From f538853ed1a99b2153152497466d7da8d6b5b4f5 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Thu, 29 Oct 2026 09:48:37 -0700 Subject: [PATCH] fix(mobile): bound preview scanning and honor escaped backticks Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tom Brow --- .../message_content/preview_text.dart | 23 ++++++++++++++----- .../message_content/preview_text_test.dart | 10 ++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/mobile/lib/features/channels/message_content/preview_text.dart b/mobile/lib/features/channels/message_content/preview_text.dart index 1fdb962c5f..dbae88023b 100644 --- a/mobile/lib/features/channels/message_content/preview_text.dart +++ b/mobile/lib/features/channels/message_content/preview_text.dart @@ -145,8 +145,10 @@ String messagePreviewSource(String content, {required int maxLines}) { } /// The first `maxLines + 1` lines that render, trimmed and without block -/// syntax, read lazily so the rest of [content] is never scanned. [code] marks -/// a line inside a fenced block, and [end] is its end offset in [content]. +/// syntax, read lazily so the rest of [content] is never scanned. Lines that +/// 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( String content, int maxLines, @@ -154,7 +156,9 @@ Iterable<({String text, bool code, int end})> _keptLines( var kept = 0; var fenceOpen = false; var start = 0; - while (start <= content.length && kept <= maxLines) { + while (start <= content.length && + start <= _maxScanLength && + kept <= maxLines) { var end = content.indexOf('\n', start); if (end < 0) end = content.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 _maxScanLength = 8 * _maxLineLength; +const _backslash = 0x5C; const _backtick = 0x60; final _rule = RegExp(r'^([-*_])(?:\s*\1){2,}$'); @@ -301,13 +307,18 @@ class _PreviewBuilder { piece is PreviewText ? text(piece.text) : pieces.add(piece); /// 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 - /// run of the same length; a run with no match is literal. + /// Markdown. A code span opens at an unescaped run of backticks and closes + /// at the next run of the same length; a run with no match is literal. void line(String line) { var prose = 0; var cursor = 0; while (cursor < line.length) { - if (line.codeUnitAt(cursor) != _backtick) { + final unit = line.codeUnitAt(cursor); + if (unit == _backslash) { + cursor += 2; + continue; + } + if (unit != _backtick) { cursor++; continue; } diff --git a/mobile/test/features/channels/message_content/preview_text_test.dart b/mobile/test/features/channels/message_content/preview_text_test.dart index 0b256b885c..970e8b44cf 100644 --- a/mobile/test/features/channels/message_content/preview_text_test.dart +++ b/mobile/test/features/channels/message_content/preview_text_test.dart @@ -62,6 +62,7 @@ void main() { ('escaped emphasis', r'\*literal\* and \_x\_', '*literal* and _x_'), ('escaped backslash', r'a \\ b', r'a \ b'), ('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)); } @@ -99,6 +100,15 @@ void main() { 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', () { expect(_text('x' * 5000).length, 2000); });