From 981f5b70a17f41b17fe1e88e930ae8ee6ab5d486 Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Wed, 16 Jul 2025 21:58:37 +0530 Subject: [PATCH 1/3] content: Make CodeBlockSpanNode take a list of span types But only allowing a single element for now, making the diff for content tests simpler for the later commit which implements support for multiple span types. Also better formatting for code block HTML in content tests. --- lib/model/content.dart | 20 ++++-- lib/widgets/content.dart | 2 +- test/model/content_test.dart | 136 ++++++++++++++++++++--------------- 3 files changed, 92 insertions(+), 66 deletions(-) diff --git a/lib/model/content.dart b/lib/model/content.dart index 78fc961c00..95f74e30b5 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -320,24 +320,30 @@ class CodeBlockNode extends BlockContentNode { } class CodeBlockSpanNode extends ContentNode { - const CodeBlockSpanNode({super.debugHtmlNode, required this.text, required this.type}); + const CodeBlockSpanNode({ + super.debugHtmlNode, + required this.text, + required this.spanTypes, + }) : assert(spanTypes.length == 1); final String text; - final CodeBlockSpanType type; + final List spanTypes; @override bool operator ==(Object other) { - return other is CodeBlockSpanNode && other.text == text && other.type == type; + return other is CodeBlockSpanNode && + other.text == text && + other.spanTypes == spanTypes; } @override - int get hashCode => Object.hash('CodeBlockSpanNode', text, type); + int get hashCode => Object.hash('CodeBlockSpanNode', text, spanTypes); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(StringProperty('text', text)); - properties.add(EnumProperty('type', type)); + properties.add(StringProperty('spanTypes', spanTypes.toString())); } } @@ -1247,7 +1253,7 @@ class _ZulipContentParser { if (text.isEmpty) { continue; } - span = CodeBlockSpanNode(text: text, type: CodeBlockSpanType.text); + span = CodeBlockSpanNode(text: text, spanTypes: const [CodeBlockSpanType.text]); case dom.Element(localName: 'span', :final text, :final className): // Empirically, when a Pygments node has multiple classes, the first @@ -1268,7 +1274,7 @@ class _ZulipContentParser { // inherited styles for `span.hll` nodes. return UnimplementedBlockContentNode(htmlNode: divElement); default: - span = CodeBlockSpanNode(text: text, type: spanType); + span = CodeBlockSpanNode(text: text, spanTypes: [spanType]); } default: diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index 52ab7008b8..52b2d4c69c 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -776,7 +776,7 @@ class CodeBlock extends StatelessWidget { child: Text.rich(TextSpan( style: styles.plain, children: node.spans - .map((node) => TextSpan(style: styles.forSpan(node.type), text: node.text)) + .map((node) => TextSpan(style: styles.forSpan(node.spanTypes.single), text: node.text)) .toList(growable: false)))); } } diff --git a/test/model/content_test.dart b/test/model/content_test.dart index c19e1c02a5..126188dba0 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -375,86 +375,105 @@ class ContentExample { QuotationNode([ParagraphNode(links: null, nodes: [TextNode('words')])]) ]); - static const codeBlockPlain = ContentExample( + static final codeBlockPlain = ContentExample( 'code block without syntax highlighting', "```\nverb\natim\n```", expectedText: 'verb\natim', - '
verb\natim\n
', [ + '
' + '
'
+        'verb\natim\n
', [ CodeBlockNode([ - CodeBlockSpanNode(text: 'verb\natim', type: CodeBlockSpanType.text), + CodeBlockSpanNode(text: 'verb\natim', spanTypes: [CodeBlockSpanType.text]), ]), ]); - static const codeBlockHighlightedShort = ContentExample( + static final codeBlockHighlightedShort = ContentExample( 'code block with syntax highlighting', "```dart\nclass A {}\n```", expectedText: 'class A {}', - '
'
-        'class '
-        'A {}'
-        '\n
', [ + '
' + '
'
+        ''
+          'class'
+          ' '
+          'A'
+          ' '
+          '{}\n
', [ CodeBlockNode([ - CodeBlockSpanNode(text: 'class', type: CodeBlockSpanType.keywordDeclaration), - CodeBlockSpanNode(text: ' ', type: CodeBlockSpanType.whitespace), - CodeBlockSpanNode(text: 'A', type: CodeBlockSpanType.nameClass), - CodeBlockSpanNode(text: ' ', type: CodeBlockSpanType.whitespace), - CodeBlockSpanNode(text: '{}', type: CodeBlockSpanType.punctuation), + CodeBlockSpanNode(text: 'class', spanTypes: [CodeBlockSpanType.keywordDeclaration]), + CodeBlockSpanNode(text: ' ', spanTypes: [CodeBlockSpanType.whitespace]), + CodeBlockSpanNode(text: 'A', spanTypes: [CodeBlockSpanType.nameClass]), + CodeBlockSpanNode(text: ' ', spanTypes: [CodeBlockSpanType.whitespace]), + CodeBlockSpanNode(text: '{}', spanTypes: [CodeBlockSpanType.punctuation]), ]), ]); - static const codeBlockHighlightedMultiline = ContentExample( + static final codeBlockHighlightedMultiline = ContentExample( 'code block, multiline, with syntax highlighting', '```rust\nfn main() {\n print!("Hello ");\n\n print!("world!\\n");\n}\n```', expectedText: 'fn main() {\n print!("Hello ");\n\n print!("world!\\n");\n}', - '
'
-        'fn main'
-        '() {\n'
-        '    print!('
-        '"Hello ");\n\n'
-        '    print!('
-        '"world!\\n"'
-        ');\n}\n'
-        '
', [ + '
' + '
'
+        ''
+          'fn '
+          'main'
+          '()'
+          ' '
+          '{\n'
+          '    '
+          'print!'
+          '('
+          '"Hello "'
+          ');\n\n'
+          '    '
+          'print!'
+          '('
+          '"world!'
+          '\\n'
+          '"'
+          ');\n'
+          '}\n
', [ CodeBlockNode([ - CodeBlockSpanNode(text: 'fn', type: CodeBlockSpanType.keyword), - CodeBlockSpanNode(text: ' ', type: CodeBlockSpanType.text), - CodeBlockSpanNode(text: 'main', type: CodeBlockSpanType.nameFunction), - CodeBlockSpanNode(text: '()', type: CodeBlockSpanType.punctuation), - CodeBlockSpanNode(text: ' ', type: CodeBlockSpanType.whitespace), - CodeBlockSpanNode(text: '{', type: CodeBlockSpanType.punctuation), - CodeBlockSpanNode(text: '\n', type: CodeBlockSpanType.text), - CodeBlockSpanNode(text: ' ', type: CodeBlockSpanType.whitespace), - CodeBlockSpanNode(text: 'print!', type: CodeBlockSpanType.nameFunctionMagic), - CodeBlockSpanNode(text: '(', type: CodeBlockSpanType.punctuation), - CodeBlockSpanNode(text: '"Hello "', type: CodeBlockSpanType.string), - CodeBlockSpanNode(text: ');', type: CodeBlockSpanType.punctuation), - CodeBlockSpanNode(text: '\n\n', type: CodeBlockSpanType.text), - CodeBlockSpanNode(text: ' ', type: CodeBlockSpanType.whitespace), - CodeBlockSpanNode(text: 'print!', type: CodeBlockSpanType.nameFunctionMagic), - CodeBlockSpanNode(text: '(', type: CodeBlockSpanType.punctuation), - CodeBlockSpanNode(text: '"world!', type: CodeBlockSpanType.string), - CodeBlockSpanNode(text: '\\n', type: CodeBlockSpanType.stringEscape), - CodeBlockSpanNode(text: '"', type: CodeBlockSpanType.string), - CodeBlockSpanNode(text: ');', type: CodeBlockSpanType.punctuation), - CodeBlockSpanNode(text: '\n', type: CodeBlockSpanType.text), - CodeBlockSpanNode(text: '}', type: CodeBlockSpanType.punctuation), + CodeBlockSpanNode(text: 'fn', spanTypes: [CodeBlockSpanType.keyword]), + CodeBlockSpanNode(text: ' ', spanTypes: [CodeBlockSpanType.text]), + CodeBlockSpanNode(text: 'main', spanTypes: [CodeBlockSpanType.nameFunction]), + CodeBlockSpanNode(text: '()', spanTypes: [CodeBlockSpanType.punctuation]), + CodeBlockSpanNode(text: ' ', spanTypes: [CodeBlockSpanType.whitespace]), + CodeBlockSpanNode(text: '{', spanTypes: [CodeBlockSpanType.punctuation]), + CodeBlockSpanNode(text: '\n', spanTypes: [CodeBlockSpanType.text]), + CodeBlockSpanNode(text: ' ', spanTypes: [CodeBlockSpanType.whitespace]), + CodeBlockSpanNode(text: 'print!', spanTypes: [CodeBlockSpanType.nameFunctionMagic]), + CodeBlockSpanNode(text: '(', spanTypes: [CodeBlockSpanType.punctuation]), + CodeBlockSpanNode(text: '"Hello "', spanTypes: [CodeBlockSpanType.string]), + CodeBlockSpanNode(text: ');', spanTypes: [CodeBlockSpanType.punctuation]), + CodeBlockSpanNode(text: '\n\n', spanTypes: [CodeBlockSpanType.text]), + CodeBlockSpanNode(text: ' ', spanTypes: [CodeBlockSpanType.whitespace]), + CodeBlockSpanNode(text: 'print!', spanTypes: [CodeBlockSpanType.nameFunctionMagic]), + CodeBlockSpanNode(text: '(', spanTypes: [CodeBlockSpanType.punctuation]), + CodeBlockSpanNode(text: '"world!', spanTypes: [CodeBlockSpanType.string]), + CodeBlockSpanNode(text: '\\n', spanTypes: [CodeBlockSpanType.stringEscape]), + CodeBlockSpanNode(text: '"', spanTypes: [CodeBlockSpanType.string]), + CodeBlockSpanNode(text: ');', spanTypes: [CodeBlockSpanType.punctuation]), + CodeBlockSpanNode(text: '\n', spanTypes: [CodeBlockSpanType.text]), + CodeBlockSpanNode(text: '}', spanTypes: [CodeBlockSpanType.punctuation]), ]), ]); - static const codeBlockSpansWithMultipleClasses = ContentExample( + static final codeBlockSpansWithMultipleClasses = ContentExample( 'code block spans with multiple CSS classes', '```yaml\n- item\n```', expectedText: '- item', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Greg/near/1949014 '
' - '
-'
-        ' '
-        'item\n'
-        '
', [ + '
'
+          ''
+            '-'
+            ' '
+            'item\n
', [ CodeBlockNode([ - CodeBlockSpanNode(text: "-", type: CodeBlockSpanType.punctuation), - CodeBlockSpanNode(text: " ", type: CodeBlockSpanType.whitespace), - CodeBlockSpanNode(text: "item", type: CodeBlockSpanType.literal) + CodeBlockSpanNode(text: "-", spanTypes: [CodeBlockSpanType.punctuation]), + CodeBlockSpanNode(text: " ", spanTypes: [CodeBlockSpanType.whitespace]), + CodeBlockSpanNode(text: "item", spanTypes: [CodeBlockSpanType.literal]) ]), ]); @@ -498,14 +517,15 @@ class ContentExample { '\n'), ]); - static const codeBlockFollowedByMultipleLineBreaks = ContentExample( + static final codeBlockFollowedByMultipleLineBreaks = ContentExample( 'blank text nodes after code blocks', ' code block.\n\nsome content', // https://chat.zulip.org/#narrow/stream/7-test-here/near/1774823 '
' - '
code block.\n
\n\n' + '
'
+        'code block.\n
\n\n' '

some content

', [ - CodeBlockNode([CodeBlockSpanNode(text: "code block.", type: CodeBlockSpanType.text)]), + CodeBlockNode([CodeBlockSpanNode(text: "code block.", spanTypes: [CodeBlockSpanType.text])]), ParagraphNode(links: null, nodes: [TextNode("some content")]), ]); @@ -2079,7 +2099,7 @@ void main() async { // "1. > ###### two\n > * three\n\n four" '
    \n
  1. \n
    \n
    two
    \n
      \n
    • three
    • \n' '
    \n
    \n
    '
    -        'four\n
    \n\n
  2. \n
', const [ + 'four\n\n\n\n', [ OrderedListNode(start: 1, [[ QuotationNode([ HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('two')]), @@ -2088,7 +2108,7 @@ void main() async { ]]), ]), CodeBlockNode([ - CodeBlockSpanNode(text: 'four', type: CodeBlockSpanType.text), + CodeBlockSpanNode(text: 'four', spanTypes: [CodeBlockSpanType.text]), ]), ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('\n\n')]), // TODO avoid this; it renders wrong ]]), From 7e6402c3e1481acacc6a7eefe69138b926b39091 Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Tue, 15 Jul 2025 22:24:35 +0530 Subject: [PATCH 2/3] content: Support nested code block spans The code block spans with `hll` (and `highlight` used for search keyword highlighting) classes can be nested inside other types of code block spans. So add support for parsing those types of spans. The rendered text style will be result of merging all the corresponding `TextStyle` using `TextStyle.merge`, preserving the order of those nested spans. --- lib/model/content.dart | 94 +++++++++++++++++++++--------------- lib/widgets/content.dart | 16 ++++-- test/model/content_test.dart | 40 ++++++++------- 3 files changed, 91 insertions(+), 59 deletions(-) diff --git a/lib/model/content.dart b/lib/model/content.dart index 95f74e30b5..77eddcd22b 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -324,7 +324,7 @@ class CodeBlockSpanNode extends ContentNode { super.debugHtmlNode, required this.text, required this.spanTypes, - }) : assert(spanTypes.length == 1); + }); final String text; final List spanTypes; @@ -1238,50 +1238,68 @@ class _ZulipContentParser { return UnimplementedBlockContentNode(htmlNode: divElement); } - final spans = []; + // Empirically, when a Pygments node has multiple classes, the first + // class names a standard token type and the rest are for non-standard + // token types specific to the language. Zulip web only styles the + // standard token classes and ignores the others, so we do the same. + // See: https://github.com/zulip/zulip-flutter/issues/933 + CodeBlockSpanType? parseCodeBlockSpanType(String className) { + return className.split(' ') + .map(codeBlockSpanTypeFromClassName) + .firstWhereOrNull((e) => e != CodeBlockSpanType.unknown); + } + + List spans = []; + List spanTypes = []; + bool hasFailed = false; + for (int i = 0; i < mainElement.nodes.length; i++) { final child = mainElement.nodes[i]; - final CodeBlockSpanNode span; - switch (child) { - case dom.Text(:var text): - if (i == mainElement.nodes.length - 1) { - // The HTML tends to have a final newline here. If included in the - // [Text] widget, that would make a trailing blank line. So cut it out. - text = text.replaceFirst(RegExp(r'\n$'), ''); - } - if (text.isEmpty) { - continue; - } - span = CodeBlockSpanNode(text: text, spanTypes: const [CodeBlockSpanType.text]); - - case dom.Element(localName: 'span', :final text, :final className): - // Empirically, when a Pygments node has multiple classes, the first - // class names a standard token type and the rest are for non-standard - // token types specific to the language. Zulip web only styles the - // standard token classes and ignores the others, so we do the same. - // See: https://github.com/zulip/zulip-flutter/issues/933 - final spanType = className.split(' ') - .map(codeBlockSpanTypeFromClassName) - .firstWhereOrNull((e) => e != CodeBlockSpanType.unknown); - - switch (spanType) { - case null: + void parseCodeBlockSpan(dom.Node child, bool isLastNode) { + switch (child) { + case dom.Text(:var text): + if (isLastNode) { + // The HTML tends to have a final newline here. If included in the + // [Text] widget, that would make a trailing blank line. So cut it out. + text = text.replaceFirst(RegExp(r'\n$'), ''); + } + if (text.isEmpty) { + break; + } + spans.add(CodeBlockSpanNode( + text: text, + spanTypes: spanTypes.isEmpty + ? const [CodeBlockSpanType.text] + : List.unmodifiable(spanTypes))); + + case dom.Element(localName: 'span', :final className): + final spanType = parseCodeBlockSpanType(className); + if (spanType == null) { // TODO(#194): Show these as un-syntax-highlighted code, in production. - return UnimplementedBlockContentNode(htmlNode: divElement); - case CodeBlockSpanType.highlightedLines: - // TODO: Implement nesting in CodeBlockSpanNode to support hierarchically - // inherited styles for `span.hll` nodes. - return UnimplementedBlockContentNode(htmlNode: divElement); - default: - span = CodeBlockSpanNode(text: text, spanTypes: [spanType]); - } + hasFailed = true; + return; + } - default: - return UnimplementedBlockContentNode(htmlNode: divElement); + spanTypes.add(spanType); + + for (int i = 0; i < child.nodes.length; i++) { + final grandchild = child.nodes[i]; + parseCodeBlockSpan(grandchild, + isLastNode ? i == child.nodes.length - 1 : false); + if (hasFailed) return; + } + + assert(spanTypes.removeLast() == spanType); + + default: + hasFailed = true; + return; + } } - spans.add(span); + parseCodeBlockSpan(child, i == mainElement.nodes.length - 1); + if (hasFailed) return UnimplementedBlockContentNode(htmlNode: divElement); } return CodeBlockNode(spans, debugHtmlNode: debugHtmlNode); diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index 52b2d4c69c..919e7ae34c 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -770,13 +770,23 @@ class CodeBlock extends StatelessWidget { @override Widget build(BuildContext context) { - final styles = ContentTheme.of(context).codeBlockTextStyles; + final codeBlockTextStyles = ContentTheme.of(context).codeBlockTextStyles; return _CodeBlockContainer( borderColor: Colors.transparent, child: Text.rich(TextSpan( - style: styles.plain, + style: codeBlockTextStyles.plain, children: node.spans - .map((node) => TextSpan(style: styles.forSpan(node.spanTypes.single), text: node.text)) + .map((node) { + TextStyle? style; + for (final spanType in node.spanTypes) { + final spanStyle = codeBlockTextStyles.forSpan(spanType); + if (spanStyle == null) continue; + style = style == null + ? spanStyle + : style.merge(spanStyle); + } + return TextSpan(style: style, text: node.text); + }) .toList(growable: false)))); } } diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 126188dba0..8b1d1940c8 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -375,7 +375,7 @@ class ContentExample { QuotationNode([ParagraphNode(links: null, nodes: [TextNode('words')])]) ]); - static final codeBlockPlain = ContentExample( + static const codeBlockPlain = ContentExample( 'code block without syntax highlighting', "```\nverb\natim\n```", expectedText: 'verb\natim', @@ -387,7 +387,7 @@ class ContentExample { ]), ]); - static final codeBlockHighlightedShort = ContentExample( + static const codeBlockHighlightedShort = ContentExample( 'code block with syntax highlighting', "```dart\nclass A {}\n```", expectedText: 'class A {}', @@ -408,7 +408,7 @@ class ContentExample { ]), ]); - static final codeBlockHighlightedMultiline = ContentExample( + static const codeBlockHighlightedMultiline = ContentExample( 'code block, multiline, with syntax highlighting', '```rust\nfn main() {\n print!("Hello ");\n\n print!("world!\\n");\n}\n```', expectedText: 'fn main() {\n print!("Hello ");\n\n print!("world!\\n");\n}', @@ -459,7 +459,7 @@ class ContentExample { ]), ]); - static final codeBlockSpansWithMultipleClasses = ContentExample( + static const codeBlockSpansWithMultipleClasses = ContentExample( 'code block spans with multiple CSS classes', '```yaml\n- item\n```', expectedText: '- item', @@ -492,18 +492,22 @@ class ContentExample { 'code block, with syntax highlighting and highlighted lines', '```\n::markdown hl_lines="2 4"\n# he\n## llo\n### world\n```', '
'
-        '::markdown hl_lines="2 4"\n'
-        '# he\n'
-        '## llo\n'
-        '### world\n'
-        '
', [ - // TODO: Fix this, see comment under `CodeBlockSpanType.highlightedLines` case in lib/model/content.dart. - blockUnimplemented('
'
-        '::markdown hl_lines="2 4"\n'
-        '# he\n'
-        '## llo\n'
-        '### world\n'
-        '
'), + '' + '' + '::markdown hl_lines="2 4"\n' + '' + '# he\n' + '## llo\n' + '' + '### world\n', [ + CodeBlockNode([ + CodeBlockSpanNode(text: '::markdown hl_lines="2 4"\n', spanTypes: [CodeBlockSpanType.text]), + CodeBlockSpanNode(text: '# he', spanTypes: [CodeBlockSpanType.highlightedLines, CodeBlockSpanType.genericHeading]), + CodeBlockSpanNode(text: '\n', spanTypes: [CodeBlockSpanType.highlightedLines]), + CodeBlockSpanNode(text: '## llo', spanTypes: [CodeBlockSpanType.genericSubheading]), + CodeBlockSpanNode(text: '\n', spanTypes: [CodeBlockSpanType.text]), + CodeBlockSpanNode(text: '### world', spanTypes: [CodeBlockSpanType.highlightedLines, CodeBlockSpanType.genericSubheading]), + ]), ]); static final codeBlockWithUnknownSpanType = ContentExample( @@ -517,7 +521,7 @@ class ContentExample { '\n'), ]); - static final codeBlockFollowedByMultipleLineBreaks = ContentExample( + static const codeBlockFollowedByMultipleLineBreaks = ContentExample( 'blank text nodes after code blocks', ' code block.\n\nsome content', // https://chat.zulip.org/#narrow/stream/7-test-here/near/1774823 @@ -2099,7 +2103,7 @@ void main() async { // "1. > ###### two\n > * three\n\n four" '
    \n
  1. \n
    \n
    two
    \n
      \n
    • three
    • \n' '
    \n
    \n
    '
    -        'four\n
    \n\n
  2. \n
', [ + 'four\n\n\n\n', const [ OrderedListNode(start: 1, [[ QuotationNode([ HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('two')]), From 2c8fe025c9a65b50ea9325cd33f672f7f047c85d Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Wed, 16 Jul 2025 22:38:40 +0530 Subject: [PATCH 3/3] content: Add support for search highlighting in code blocks Fixes: #1695 --- lib/model/code_block.dart | 6 +++++ lib/widgets/code_block.dart | 13 +++++++++++ test/model/content_test.dart | 45 ++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/lib/model/code_block.dart b/lib/model/code_block.dart index 222981f6e1..139c0f7e6b 100644 --- a/lib/model/code_block.dart +++ b/lib/model/code_block.dart @@ -8,6 +8,11 @@ enum CodeBlockSpanType { unknown, /// A run of unstyled text in a code block. text, + /// A code-block span with CSS class `highlight`. + /// + /// This span is emitted by server for content matching + /// used for displaying keyword search highlighting. + highlight, /// A code-block span with CSS class `hll`. /// /// Unlike most `CodeBlockSpanToken` values, this does not correspond to @@ -174,6 +179,7 @@ enum CodeBlockSpanType { CodeBlockSpanType codeBlockSpanTypeFromClassName(String className) { return switch (className) { + 'highlight' => CodeBlockSpanType.highlight, 'hll' => CodeBlockSpanType.highlightedLines, 'w' => CodeBlockSpanType.whitespace, 'esc' => CodeBlockSpanType.escape, diff --git a/lib/widgets/code_block.dart b/lib/widgets/code_block.dart index e88bb7f5a7..8c57e6ffd3 100644 --- a/lib/widgets/code_block.dart +++ b/lib/widgets/code_block.dart @@ -19,6 +19,10 @@ class CodeBlockTextStyles { height: 1.4)) .merge(weightVariableTextStyle(context)), + // .highlight { background-color: hsl(51deg 100% 79%); } + // See https://github.com/zulip/zulip/blob/f87479703/web/styles/rendered_markdown.css#L1037-L1039 + highlight: TextStyle(backgroundColor: const HSLColor.fromAHSL(1, 51, 1, 0.79).toColor()), + // .hll { background-color: hsl(60deg 100% 90%); } hll: TextStyle(backgroundColor: const HSLColor.fromAHSL(1, 60, 1, 0.90).toColor()), @@ -259,6 +263,10 @@ class CodeBlockTextStyles { height: 1.4)) .merge(weightVariableTextStyle(context)), + // .highlight { background-color: hsl(51deg 100% 23%); } + // See https://github.com/zulip/zulip/blob/f87479703/web/styles/dark_theme.css#L410-L412 + highlight: TextStyle(backgroundColor: const HSLColor.fromAHSL(1, 51, 1, 0.23).toColor()), + // .hll { background-color: #49483e; } hll: const TextStyle(backgroundColor: Color(0xff49483e)), @@ -500,6 +508,7 @@ class CodeBlockTextStyles { CodeBlockTextStyles._({ required this.plain, + required TextStyle highlight, required TextStyle hll, required TextStyle c, required TextStyle err, @@ -580,6 +589,7 @@ class CodeBlockTextStyles { required TextStyle? vm, required TextStyle il, }) : + _highlight = highlight, _hll = hll, _c = c, _err = err, @@ -663,6 +673,7 @@ class CodeBlockTextStyles { /// The baseline style that the [forSpan] styles get applied on top of. final TextStyle plain; + final TextStyle _highlight; final TextStyle _hll; final TextStyle _c; final TextStyle _err; @@ -751,6 +762,7 @@ class CodeBlockTextStyles { TextStyle? forSpan(CodeBlockSpanType type) { return switch (type) { CodeBlockSpanType.text => null, // A span with type of text is always unstyled. + CodeBlockSpanType.highlight => _highlight, CodeBlockSpanType.highlightedLines => _hll, CodeBlockSpanType.comment => _c, CodeBlockSpanType.error => _err, @@ -839,6 +851,7 @@ class CodeBlockTextStyles { return CodeBlockTextStyles._( plain: TextStyle.lerp(a.plain, b.plain, t)!, + highlight: TextStyle.lerp(a._highlight, b._highlight, t)!, hll: TextStyle.lerp(a._hll, b._hll, t)!, c: TextStyle.lerp(a._c, b._c, t)!, err: TextStyle.lerp(a._err, b._err, t)!, diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 8b1d1940c8..5af7f06d12 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -533,6 +533,49 @@ class ContentExample { ParagraphNode(links: null, nodes: [TextNode("some content")]), ]); + static const codeBlockSearchHighlight = ContentExample( + 'code block, search highlight', + '```dart\nclass A {}\n```', + '
' + '
'
+        ''
+        ''
+          ''
+            'class'
+          ' '
+          'A'
+          ' '
+          '{}\n
', [ + CodeBlockNode([ + CodeBlockSpanNode(text: 'class', spanTypes: [CodeBlockSpanType.keywordDeclaration, CodeBlockSpanType.highlight]), + CodeBlockSpanNode(text: ' ', spanTypes: [CodeBlockSpanType.whitespace]), + CodeBlockSpanNode(text: 'A', spanTypes: [CodeBlockSpanType.nameClass]), + CodeBlockSpanNode(text: ' ', spanTypes: [CodeBlockSpanType.whitespace]), + CodeBlockSpanNode(text: '{}', spanTypes: [CodeBlockSpanType.punctuation]), + ]), + ]); + + static const codeBlockSearchHighlightBetweenText = ContentExample( + 'code block, search highlight between text', + '```console\n# postgresql\nThe World\'s Most Advanced Open Source Relational Database\n```', + '
' + '
'
+        ''
+        ''
+          '# postgresql\n'
+          ''
+            'The '
+            'World'
+            '\'s Most Advanced Open Source Relational Database\n
', [ + CodeBlockNode([ + CodeBlockSpanNode(text: '# ', spanTypes: [CodeBlockSpanType.genericPrompt]), + CodeBlockSpanNode(text: "postgresql\n", spanTypes: [CodeBlockSpanType.text]), + CodeBlockSpanNode(text: 'The ', spanTypes: [CodeBlockSpanType.genericOutput]), + CodeBlockSpanNode(text: 'World', spanTypes: [CodeBlockSpanType.genericOutput, CodeBlockSpanType.highlight]), + CodeBlockSpanNode(text: '\'s Most Advanced Open Source Relational Database', spanTypes: [CodeBlockSpanType.genericOutput]), + ]), + ]); + static final mathInline = ContentExample.inline( 'inline math', r"$$ \lambda $$", @@ -2047,6 +2090,8 @@ void main() async { testParseExample(ContentExample.codeBlockWithHighlightedLines); testParseExample(ContentExample.codeBlockWithUnknownSpanType); testParseExample(ContentExample.codeBlockFollowedByMultipleLineBreaks); + testParseExample(ContentExample.codeBlockSearchHighlight); + testParseExample(ContentExample.codeBlockSearchHighlightBetweenText); testParseExample(ContentExample.mathBlock); testParseExample(ContentExample.mathBlocksMultipleInParagraph);