mirror of
https://github.com/block/buzz.git
synced 2026-11-02 07:22:55 +00:00
## Summary - replace the mobile composer sheet with a compact expanding capsule - add vertically stacked attachment actions and an inline camera preview - align suggestion, formatting, and send treatments with desktop ## Validation - `just mobile-check` - `just mobile-test` (541 passed, 1 skipped) --------- Signed-off-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz> Co-authored-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz>
250 lines
6.7 KiB
Dart
250 lines
6.7 KiB
Dart
part of '../compose_bar.dart';
|
|
|
|
enum _MarkdownStyle { codeBlock, inlineCode, bold, italic, strikethrough }
|
|
|
|
class _MarkdownRule {
|
|
final RegExp pattern;
|
|
final _MarkdownStyle style;
|
|
final bool parsesNestedFormatting;
|
|
|
|
_MarkdownRule(
|
|
String pattern,
|
|
this.style, {
|
|
this.parsesNestedFormatting = true,
|
|
}) : pattern = RegExp(pattern);
|
|
}
|
|
|
|
class _MarkdownEditingController extends TextEditingController {
|
|
static final _rules = [
|
|
_MarkdownRule(
|
|
r'```(?:\r?\n)?([\s\S]*?)(?:\r?\n)?```',
|
|
_MarkdownStyle.codeBlock,
|
|
parsesNestedFormatting: false,
|
|
),
|
|
_MarkdownRule(
|
|
r'`([^`\n]*?)`',
|
|
_MarkdownStyle.inlineCode,
|
|
parsesNestedFormatting: false,
|
|
),
|
|
_MarkdownRule(r'\*\*([^\n]*?)\*\*', _MarkdownStyle.bold),
|
|
_MarkdownRule(r'~~([^\n]*?)~~', _MarkdownStyle.strikethrough),
|
|
_MarkdownRule(r'_([^_\n]*?)_', _MarkdownStyle.italic),
|
|
];
|
|
|
|
@override
|
|
TextSpan buildTextSpan({
|
|
required BuildContext context,
|
|
TextStyle? style,
|
|
required bool withComposing,
|
|
}) {
|
|
final baseStyle = style ?? const TextStyle();
|
|
final composingRange =
|
|
withComposing && value.composing.isValid && !value.composing.isCollapsed
|
|
? value.composing
|
|
: TextRange.empty;
|
|
return TextSpan(
|
|
style: baseStyle,
|
|
children: _buildMarkdownSpans(
|
|
context,
|
|
text,
|
|
baseStyle,
|
|
sourceOffset: 0,
|
|
composingRange: composingRange,
|
|
),
|
|
);
|
|
}
|
|
|
|
List<InlineSpan> _buildMarkdownSpans(
|
|
BuildContext context,
|
|
String source,
|
|
TextStyle inheritedStyle, {
|
|
required int sourceOffset,
|
|
required TextRange composingRange,
|
|
}) {
|
|
final spans = <InlineSpan>[];
|
|
var offset = 0;
|
|
|
|
while (offset < source.length) {
|
|
final tail = source.substring(offset);
|
|
_MarkdownRule? nextRule;
|
|
RegExpMatch? nextMatch;
|
|
|
|
for (final rule in _rules) {
|
|
final match = rule.pattern.firstMatch(tail);
|
|
if (match == null) continue;
|
|
if (nextMatch == null || match.start < nextMatch.start) {
|
|
nextRule = rule;
|
|
nextMatch = match;
|
|
}
|
|
}
|
|
|
|
if (nextRule == null || nextMatch == null) {
|
|
spans.addAll(
|
|
_buildTextSpans(
|
|
source.substring(offset),
|
|
inheritedStyle,
|
|
sourceOffset + offset,
|
|
composingRange,
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
|
|
if (nextMatch.start > 0) {
|
|
spans.addAll(
|
|
_buildTextSpans(
|
|
tail.substring(0, nextMatch.start),
|
|
inheritedStyle,
|
|
sourceOffset + offset,
|
|
composingRange,
|
|
),
|
|
);
|
|
}
|
|
|
|
final fullMatch = nextMatch.group(0)!;
|
|
final content = nextMatch.group(1)!;
|
|
final (contentStart, contentEnd) = _contentBounds(
|
|
fullMatch,
|
|
nextRule.style,
|
|
);
|
|
final contentStyle = _styleFor(context, inheritedStyle, nextRule.style);
|
|
final matchOffset = sourceOffset + offset + nextMatch.start;
|
|
|
|
spans.add(
|
|
TextSpan(
|
|
text: fullMatch.substring(0, contentStart),
|
|
style: _hiddenSyntaxStyle(inheritedStyle),
|
|
),
|
|
);
|
|
if (nextRule.parsesNestedFormatting) {
|
|
spans.addAll(
|
|
_buildMarkdownSpans(
|
|
context,
|
|
content,
|
|
contentStyle,
|
|
sourceOffset: matchOffset + contentStart,
|
|
composingRange: composingRange,
|
|
),
|
|
);
|
|
} else {
|
|
spans.addAll(
|
|
_buildTextSpans(
|
|
content,
|
|
contentStyle,
|
|
matchOffset + contentStart,
|
|
composingRange,
|
|
),
|
|
);
|
|
}
|
|
spans.add(
|
|
TextSpan(
|
|
text: fullMatch.substring(contentEnd),
|
|
style: _hiddenSyntaxStyle(inheritedStyle),
|
|
),
|
|
);
|
|
|
|
offset += nextMatch.end;
|
|
}
|
|
|
|
return spans;
|
|
}
|
|
|
|
List<InlineSpan> _buildTextSpans(
|
|
String source,
|
|
TextStyle style,
|
|
int sourceOffset,
|
|
TextRange composingRange,
|
|
) {
|
|
if (source.isEmpty) return const [];
|
|
final localStart = (composingRange.start - sourceOffset)
|
|
.clamp(0, source.length)
|
|
.toInt();
|
|
final localEnd = (composingRange.end - sourceOffset)
|
|
.clamp(0, source.length)
|
|
.toInt();
|
|
if (!composingRange.isValid ||
|
|
composingRange.isCollapsed ||
|
|
localStart >= localEnd) {
|
|
return [TextSpan(text: source, style: style)];
|
|
}
|
|
|
|
final composingDecorations = [
|
|
if (style.decoration != null && style.decoration != TextDecoration.none)
|
|
style.decoration!,
|
|
TextDecoration.underline,
|
|
];
|
|
final composingStyle = style.copyWith(
|
|
decoration: TextDecoration.combine(composingDecorations),
|
|
);
|
|
return [
|
|
if (localStart > 0)
|
|
TextSpan(text: source.substring(0, localStart), style: style),
|
|
TextSpan(
|
|
text: source.substring(localStart, localEnd),
|
|
style: composingStyle,
|
|
),
|
|
if (localEnd < source.length)
|
|
TextSpan(text: source.substring(localEnd), style: style),
|
|
];
|
|
}
|
|
|
|
(int, int) _contentBounds(String fullMatch, _MarkdownStyle markdownStyle) {
|
|
final delimiterLength = switch (markdownStyle) {
|
|
_MarkdownStyle.bold || _MarkdownStyle.strikethrough => 2,
|
|
_MarkdownStyle.italic || _MarkdownStyle.inlineCode => 1,
|
|
_MarkdownStyle.codeBlock => 3,
|
|
};
|
|
if (markdownStyle != _MarkdownStyle.codeBlock) {
|
|
return (delimiterLength, fullMatch.length - delimiterLength);
|
|
}
|
|
|
|
final openingLength = fullMatch.startsWith('```\r\n')
|
|
? 5
|
|
: fullMatch.startsWith('```\n')
|
|
? 4
|
|
: delimiterLength;
|
|
final closingLength = fullMatch.endsWith('\r\n```')
|
|
? 5
|
|
: fullMatch.endsWith('\n```')
|
|
? 4
|
|
: delimiterLength;
|
|
return (openingLength, fullMatch.length - closingLength);
|
|
}
|
|
|
|
TextStyle _styleFor(
|
|
BuildContext context,
|
|
TextStyle inheritedStyle,
|
|
_MarkdownStyle markdownStyle,
|
|
) {
|
|
return switch (markdownStyle) {
|
|
_MarkdownStyle.bold => inheritedStyle.merge(
|
|
const TextStyle(fontWeight: FontWeight.w700),
|
|
),
|
|
_MarkdownStyle.italic => inheritedStyle.merge(
|
|
const TextStyle(fontStyle: FontStyle.italic),
|
|
),
|
|
_MarkdownStyle.strikethrough => inheritedStyle.merge(
|
|
const TextStyle(decoration: TextDecoration.lineThrough),
|
|
),
|
|
_MarkdownStyle.inlineCode ||
|
|
_MarkdownStyle.codeBlock => inheritedStyle.merge(
|
|
TextStyle(
|
|
fontFamily: 'GeistMono',
|
|
color: context.colors.onSurface,
|
|
backgroundColor: context.colors.surface,
|
|
),
|
|
),
|
|
};
|
|
}
|
|
|
|
TextStyle _hiddenSyntaxStyle(TextStyle inheritedStyle) {
|
|
return inheritedStyle.copyWith(
|
|
color: Colors.transparent,
|
|
fontSize: 0.01,
|
|
height: 0.01,
|
|
letterSpacing: 0,
|
|
decoration: TextDecoration.none,
|
|
);
|
|
}
|
|
}
|