fix(mobile): bound preview scanning and honor escaped backticks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-29 09:48:37 -07:00
parent 78ddb5699d
commit f538853ed1
2 changed files with 27 additions and 6 deletions

View File

@@ -145,8 +145,10 @@ String messagePreviewSource(String content, {required int maxLines}) {
} }
/// The first `maxLines + 1` lines that render, trimmed and without block /// The first `maxLines + 1` lines that render, trimmed and without block
/// syntax, read lazily so the rest of [content] is never scanned. [code] marks /// syntax, read lazily so the rest of [content] is never scanned. Lines that
/// a line inside a fenced block, and [end] is its end offset in [content]. /// don't render still cost a scan, so reading also stops after
/// [_maxScanLength] characters. [code] marks a line inside a fenced block, and
/// [end] is its end offset in [content].
Iterable<({String text, bool code, int end})> _keptLines( Iterable<({String text, bool code, int end})> _keptLines(
String content, String content,
int maxLines, int maxLines,
@@ -154,7 +156,9 @@ Iterable<({String text, bool code, int end})> _keptLines(
var kept = 0; var kept = 0;
var fenceOpen = false; var fenceOpen = false;
var start = 0; var start = 0;
while (start <= content.length && kept <= maxLines) { while (start <= content.length &&
start <= _maxScanLength &&
kept <= maxLines) {
var end = content.indexOf('\n', start); var end = content.indexOf('\n', start);
if (end < 0) end = content.length; if (end < 0) end = content.length;
// A long line wraps past the visible lines well before this length. // A long line wraps past the visible lines well before this length.
@@ -183,6 +187,8 @@ Iterable<({String text, bool code, int end})> _keptLines(
} }
const _maxLineLength = 2000; const _maxLineLength = 2000;
const _maxScanLength = 8 * _maxLineLength;
const _backslash = 0x5C;
const _backtick = 0x60; const _backtick = 0x60;
final _rule = RegExp(r'^([-*_])(?:\s*\1){2,}$'); final _rule = RegExp(r'^([-*_])(?:\s*\1){2,}$');
@@ -301,13 +307,18 @@ class _PreviewBuilder {
piece is PreviewText ? text(piece.text) : pieces.add(piece); piece is PreviewText ? text(piece.text) : pieces.add(piece);
/// One line of prose: inline code is literal, everything else is inline /// One line of prose: inline code is literal, everything else is inline
/// Markdown. A code span opens at a run of backticks and closes at the next /// Markdown. A code span opens at an unescaped run of backticks and closes
/// run of the same length; a run with no match is literal. /// at the next run of the same length; a run with no match is literal.
void line(String line) { void line(String line) {
var prose = 0; var prose = 0;
var cursor = 0; var cursor = 0;
while (cursor < line.length) { while (cursor < line.length) {
if (line.codeUnitAt(cursor) != _backtick) { final unit = line.codeUnitAt(cursor);
if (unit == _backslash) {
cursor += 2;
continue;
}
if (unit != _backtick) {
cursor++; cursor++;
continue; continue;
} }

View File

@@ -62,6 +62,7 @@ void main() {
('escaped emphasis', r'\*literal\* and \_x\_', '*literal* and _x_'), ('escaped emphasis', r'\*literal\* and \_x\_', '*literal* and _x_'),
('escaped backslash', r'a \\ b', r'a \ b'), ('escaped backslash', r'a \\ b', r'a \ b'),
('escaped link opener', r'\[docs](https://x)', '[docs](https://x)'), ('escaped link opener', r'\[docs](https://x)', '[docs](https://x)'),
('escaped backticks', r'\`literal\` and `code`', '`literal` and code'),
]) { ]) {
test(name, () => expect(_text(source), expected)); test(name, () => expect(_text(source), expected));
} }
@@ -99,6 +100,15 @@ void main() {
expect(_text('# a\n\n---\n- b\nc\nd'), 'a\nb\nc'); expect(_text('# a\n\n---\n- b\nc\nd'), 'a\nb\nc');
}); });
test('stops reading after a long run of lines that do not render', () {
final content = '\n' * 1000000 + 'late';
expect(_text(content), '');
expect(
messagePreviewSource(content, maxLines: 2).length,
lessThan(20000),
);
});
test('caps a very long line', () { test('caps a very long line', () {
expect(_text('x' * 5000).length, 2000); expect(_text('x' * 5000).length, 2000);
}); });