mirror of
https://github.com/block/buzz.git
synced 2026-11-03 16:01:35 +00:00
fix(mobile): leave tilde fences and autolink backslashes literal
Treat `~~~` fences as code when shielding escapes, as bubble segments already do, so their backslashes stay literal. Encode backslashes in an `<url>` autolink as `\` in its label and `%5C` in its destination, so none escapes the synthesized `]` or `)`. 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:
@@ -14,6 +14,7 @@ final _bareLinkPattern = RegExp(
|
||||
);
|
||||
final _trailingPunctuationPattern = RegExp(r'[.,!?:;]+$');
|
||||
final _backtickRunPattern = RegExp(r'`+');
|
||||
final _codeRunPattern = RegExp(r'`+|~{3,}');
|
||||
|
||||
/// Converts supported Buzz and HTTP(S) autolinks and bare links into Markdown
|
||||
/// links while leaving inline and fenced code untouched. Punctuation peeling
|
||||
@@ -22,7 +23,7 @@ String normalizeBareLinks(String content) =>
|
||||
mapMarkdownProse(content, _normalizeLinkSegment);
|
||||
|
||||
/// Applies [transform] to each run of [content] outside inline and fenced
|
||||
/// code, leaving code untouched.
|
||||
/// code, leaving code untouched. Fences may use backticks or tildes.
|
||||
String mapMarkdownProse(String content, String Function(String) transform) {
|
||||
final buffer = StringBuffer();
|
||||
var offset = 0;
|
||||
@@ -30,27 +31,38 @@ String mapMarkdownProse(String content, String Function(String) transform) {
|
||||
var codeStart = 0;
|
||||
var inlineDelimiterLength = 0;
|
||||
var fenceDelimiterLength = 0;
|
||||
var fenceChar = '';
|
||||
|
||||
while (offset < content.length) {
|
||||
final run = _backtickRunPattern.matchAsPrefix(content, offset);
|
||||
final run = _codeRunPattern.matchAsPrefix(content, offset);
|
||||
if (run == null) {
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
final runLength = run.end - run.start;
|
||||
final runChar = content[run.start];
|
||||
if (fenceDelimiterLength > 0) {
|
||||
if (_isClosingFence(content, run.start, run.end, fenceDelimiterLength)) {
|
||||
if (runChar == fenceChar &&
|
||||
_isClosingFence(content, run.start, run.end, fenceDelimiterLength)) {
|
||||
buffer.write(content.substring(codeStart, run.end));
|
||||
fenceDelimiterLength = 0;
|
||||
proseStart = run.end;
|
||||
}
|
||||
} else if (inlineDelimiterLength > 0) {
|
||||
if (runLength == inlineDelimiterLength) {
|
||||
if (runChar == '`' && runLength == inlineDelimiterLength) {
|
||||
buffer.write(content.substring(codeStart, run.end));
|
||||
inlineDelimiterLength = 0;
|
||||
proseStart = run.end;
|
||||
}
|
||||
} else if (runChar == '~') {
|
||||
// Tildes only fence code; shorter runs are strikethrough.
|
||||
if (_isOpeningFence(content, run.start, runLength)) {
|
||||
buffer.write(transform(content.substring(proseStart, run.start)));
|
||||
codeStart = run.start;
|
||||
fenceDelimiterLength = runLength;
|
||||
fenceChar = runChar;
|
||||
}
|
||||
} else if (_isEscaped(content, run.start)) {
|
||||
// An escaped backtick is literal; the rest of its run may still open.
|
||||
offset = run.start + 1;
|
||||
@@ -65,6 +77,7 @@ String mapMarkdownProse(String content, String Function(String) transform) {
|
||||
buffer.write(transform(content.substring(proseStart, run.start)));
|
||||
codeStart = run.start;
|
||||
fenceDelimiterLength = runLength;
|
||||
fenceChar = runChar;
|
||||
}
|
||||
|
||||
offset = run.end;
|
||||
@@ -276,9 +289,8 @@ String _normalizeProseLinks(String segment) {
|
||||
// An escaped `\<` is literal, not an autolink; its URL stays a bare link.
|
||||
var normalized = segment.replaceAllMapped(
|
||||
_autolinkPattern,
|
||||
(match) => _isEscaped(segment, match.start)
|
||||
? match[0]!
|
||||
: '[${match[1]}](${match[1]})',
|
||||
(match) =>
|
||||
_isEscaped(segment, match.start) ? match[0]! : _autolink(match[1]!),
|
||||
);
|
||||
normalized = normalized.replaceAllMapped(
|
||||
_bareLinkPattern,
|
||||
@@ -287,6 +299,11 @@ String _normalizeProseLinks(String segment) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// An autolink keeps its backslashes literally. Encode them so none is read
|
||||
// as an escape of the synthesized `]` or `)`.
|
||||
String _autolink(String url) =>
|
||||
'[${url.replaceAll(r'\', '\')}](${url.replaceAll(r'\', '%5C')})';
|
||||
|
||||
String _normalizeBareLink(String segment, Match match) {
|
||||
final matched = match[0]!;
|
||||
var url = matched;
|
||||
|
||||
@@ -514,6 +514,7 @@ void main() {
|
||||
(r'\`literal\` then `a\#b`', '`literal` then '),
|
||||
// Authored stand-in characters are not mistaken for escapes.
|
||||
('\uFF0A \u2060\uFF0A \\#', '\uFF0A \u2060\uFF0A #'),
|
||||
('~~~\nx \\# y\n~~~', r'x \# y'),
|
||||
// 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.
|
||||
@@ -521,6 +522,7 @@ void main() {
|
||||
(r'in buzz://channel/9f18f3c6-b5e5-453d-b36d-65487913be0b.\', r'.\'),
|
||||
(r'*https://example.com/a\**', '*'),
|
||||
(r'see \<https://example.com/b>', 'see <'),
|
||||
(r'see <https://example.com/a\>', r'https://example.com/a\'),
|
||||
(
|
||||
'\n'
|
||||
'',
|
||||
|
||||
Reference in New Issue
Block a user