mirror of
https://github.com/block/buzz.git
synced 2026-11-08 10:21:46 +00:00
fix(mobile): keep unrestored escape stand-ins readable
gpt_markdown sometimes shows text unparsed, for example a strikethrough that spans lines, and then a private-use stand-in showed as nothing. Make each stand-in a WORD JOINER followed by the character's fullwidth form, so that fallback still reads `#` rather than losing the character. Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tom Brow <tomb@squareup.com>
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
import 'link_normalizer.dart';
|
||||
|
||||
/// Holds each backslash-escaped character as a private-use stand-in, so no
|
||||
/// link, emphasis, mention, emoji, channel or math syntax matches it: `\*x\*`
|
||||
/// stays `*x*` and `\@alice` stays text.
|
||||
/// Holds each backslash-escaped character as a stand-in, so no link,
|
||||
/// emphasis, mention, emoji, channel or math syntax matches it: `\*x\*` stays
|
||||
/// `*x*` and `\@alice` stays text.
|
||||
///
|
||||
/// A stand-in is [_marker] followed by the character shifted into the
|
||||
/// private-use area. An authored [_marker] becomes two markers, so authored
|
||||
/// private-use characters come back from [unshieldMarkdownEscapes] unchanged.
|
||||
/// A stand-in is an invisible [_marker] followed by the character's fullwidth
|
||||
/// form, so text the renderer shows without restoring it still reads right.
|
||||
/// An authored [_marker] becomes two markers, so authored text comes back from
|
||||
/// [unshieldMarkdownEscapes] unchanged.
|
||||
String shieldMarkdownEscapes(String text) => _needsShield(text)
|
||||
? text.replaceAllMapped(
|
||||
_escape,
|
||||
(m) => m[1] == null
|
||||
? '$_marker$_marker'
|
||||
: '$_marker${String.fromCharCode(_escapeBase + m[1]!.codeUnitAt(0))}',
|
||||
: '$_marker${String.fromCharCode(_fullwidthOffset + m[1]!.codeUnitAt(0))}',
|
||||
)
|
||||
: text;
|
||||
|
||||
@@ -34,18 +35,20 @@ String reescapeMarkdownEscapes(String text) => text.replaceAllMapped(
|
||||
);
|
||||
|
||||
/// Matches one stand-in created by [shieldMarkdownEscapes].
|
||||
final shieldedMarkdownEscape = RegExp('\uE000[\uE000\uE021-\uE07E]');
|
||||
final shieldedMarkdownEscape = RegExp('\u2060[\u2060\uFF01-\uFF5E]');
|
||||
|
||||
bool _needsShield(String text) => text.contains(r'\') || text.contains(_marker);
|
||||
|
||||
String _held(String standIn) {
|
||||
final unit = standIn.codeUnitAt(1);
|
||||
return unit == _escapeBase
|
||||
final held = standIn.substring(1);
|
||||
return held == _marker
|
||||
? _marker
|
||||
: String.fromCharCode(unit - _escapeBase);
|
||||
: String.fromCharCode(held.codeUnitAt(0) - _fullwidthOffset);
|
||||
}
|
||||
|
||||
const _marker = '\uE000';
|
||||
const _escapeBase = 0xE000;
|
||||
// WORD JOINER: zero-width, not whitespace, and not Markdown syntax.
|
||||
const _marker = '\u2060';
|
||||
// ASCII 0x21-0x7E to FULLWIDTH 0xFF01-0xFF5E.
|
||||
const _fullwidthOffset = 0xFEE0;
|
||||
// CommonMark: a backslash escapes any ASCII punctuation character.
|
||||
final _escape = RegExp(r'\\([!-/:-@\[-`{-~])|\uE000');
|
||||
final _escape = RegExp(r'\\([!-/:-@\[-`{-~])|\u2060');
|
||||
|
||||
@@ -512,8 +512,10 @@ void main() {
|
||||
('```\nx \\# y\n```', r'x \# y'),
|
||||
(r'code `a\#b`', r'a\#b'),
|
||||
(r'\`literal\` then `a\#b`', '`literal` then '),
|
||||
// Authored private-use characters are not mistaken for escapes.
|
||||
('\uE02A \uE000\uE02A \\#', '\uE02A \uE000\uE02A #'),
|
||||
// Authored stand-in characters are not mistaken for escapes.
|
||||
('\uFF0A \u2060\uFF0A \\#', '\uFF0A \u2060\uFF0A #'),
|
||||
// gpt_markdown shows this unparsed; the stand-in still reads as #.
|
||||
('~~a\nb \\#c~~', '\uFF03c'),
|
||||
// A bare link's trailing backslash stays outside the link.
|
||||
(r'see https://example.com/a\', 'see '),
|
||||
(r'in buzz://channel/9f18f3c6-b5e5-453d-b36d-65487913be0b.\', r'.\'),
|
||||
@@ -538,17 +540,17 @@ void main() {
|
||||
);
|
||||
}
|
||||
// No stand-in or leaked link syntax reaches text or semantics.
|
||||
if (content.contains('\uE000')) continue;
|
||||
if (content.contains('\u2060') || shown == '\uFF03c') continue;
|
||||
expect(
|
||||
find.textContaining(
|
||||
RegExp('\uE000|\\]\\(http'),
|
||||
RegExp('\u2060|\\]\\(http'),
|
||||
findRichText: true,
|
||||
),
|
||||
findsNothing,
|
||||
reason: reason,
|
||||
);
|
||||
expect(
|
||||
find.bySemanticsLabel(RegExp('\uE000')),
|
||||
find.bySemanticsLabel(RegExp('\u2060')),
|
||||
findsNothing,
|
||||
reason: reason,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user