Skip to content

Commit 87355b7

Browse files
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.
1 parent 132519e commit 87355b7

File tree

3 files changed

+91
-59
lines changed

3 files changed

+91
-59
lines changed

lib/model/content.dart

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ class CodeBlockSpanNode extends ContentNode {
324324
super.debugHtmlNode,
325325
required this.text,
326326
required this.spanTypes,
327-
}) : assert(spanTypes.length == 1);
327+
});
328328

329329
final String text;
330330
final List<CodeBlockSpanType> spanTypes;
@@ -1228,50 +1228,68 @@ class _ZulipContentParser {
12281228
return UnimplementedBlockContentNode(htmlNode: divElement);
12291229
}
12301230

1231-
final spans = <CodeBlockSpanNode>[];
1231+
// Empirically, when a Pygments node has multiple classes, the first
1232+
// class names a standard token type and the rest are for non-standard
1233+
// token types specific to the language. Zulip web only styles the
1234+
// standard token classes and ignores the others, so we do the same.
1235+
// See: https://github.com/zulip/zulip-flutter/issues/933
1236+
CodeBlockSpanType? parseCodeBlockSpanType(String className) {
1237+
return className.split(' ')
1238+
.map(codeBlockSpanTypeFromClassName)
1239+
.firstWhereOrNull((e) => e != CodeBlockSpanType.unknown);
1240+
}
1241+
1242+
List<CodeBlockSpanNode> spans = [];
1243+
List<CodeBlockSpanType> spanTypes = [];
1244+
bool hasFailed = false;
1245+
12321246
for (int i = 0; i < mainElement.nodes.length; i++) {
12331247
final child = mainElement.nodes[i];
12341248

1235-
final CodeBlockSpanNode span;
1236-
switch (child) {
1237-
case dom.Text(:var text):
1238-
if (i == mainElement.nodes.length - 1) {
1239-
// The HTML tends to have a final newline here. If included in the
1240-
// [Text] widget, that would make a trailing blank line. So cut it out.
1241-
text = text.replaceFirst(RegExp(r'\n$'), '');
1242-
}
1243-
if (text.isEmpty) {
1244-
continue;
1245-
}
1246-
span = CodeBlockSpanNode(text: text, spanTypes: const [CodeBlockSpanType.text]);
1247-
1248-
case dom.Element(localName: 'span', :final text, :final className):
1249-
// Empirically, when a Pygments node has multiple classes, the first
1250-
// class names a standard token type and the rest are for non-standard
1251-
// token types specific to the language. Zulip web only styles the
1252-
// standard token classes and ignores the others, so we do the same.
1253-
// See: https://github.com/zulip/zulip-flutter/issues/933
1254-
final spanType = className.split(' ')
1255-
.map(codeBlockSpanTypeFromClassName)
1256-
.firstWhereOrNull((e) => e != CodeBlockSpanType.unknown);
1257-
1258-
switch (spanType) {
1259-
case null:
1249+
void parseCodeBlockSpan(dom.Node child, bool isLastNode) {
1250+
switch (child) {
1251+
case dom.Text(:var text):
1252+
if (isLastNode) {
1253+
// The HTML tends to have a final newline here. If included in the
1254+
// [Text] widget, that would make a trailing blank line. So cut it out.
1255+
text = text.replaceFirst(RegExp(r'\n$'), '');
1256+
}
1257+
if (text.isEmpty) {
1258+
break;
1259+
}
1260+
spans.add(CodeBlockSpanNode(
1261+
text: text,
1262+
spanTypes: spanTypes.isEmpty
1263+
? const [CodeBlockSpanType.text]
1264+
: List.unmodifiable(spanTypes)));
1265+
1266+
case dom.Element(localName: 'span', :final className):
1267+
final spanType = parseCodeBlockSpanType(className);
1268+
if (spanType == null) {
12601269
// TODO(#194): Show these as un-syntax-highlighted code, in production.
1261-
return UnimplementedBlockContentNode(htmlNode: divElement);
1262-
case CodeBlockSpanType.highlightedLines:
1263-
// TODO: Implement nesting in CodeBlockSpanNode to support hierarchically
1264-
// inherited styles for `span.hll` nodes.
1265-
return UnimplementedBlockContentNode(htmlNode: divElement);
1266-
default:
1267-
span = CodeBlockSpanNode(text: text, spanTypes: [spanType]);
1268-
}
1270+
hasFailed = true;
1271+
return;
1272+
}
12691273

1270-
default:
1271-
return UnimplementedBlockContentNode(htmlNode: divElement);
1274+
spanTypes.add(spanType);
1275+
1276+
for (int i = 0; i < child.nodes.length; i++) {
1277+
final grandchild = child.nodes[i];
1278+
parseCodeBlockSpan(grandchild,
1279+
isLastNode ? i == child.nodes.length - 1 : false);
1280+
if (hasFailed) return;
1281+
}
1282+
1283+
assert(spanTypes.removeLast() == spanType);
1284+
1285+
default:
1286+
hasFailed = true;
1287+
return;
1288+
}
12721289
}
12731290

1274-
spans.add(span);
1291+
parseCodeBlockSpan(child, i == mainElement.nodes.length - 1);
1292+
if (hasFailed) return UnimplementedBlockContentNode(htmlNode: divElement);
12751293
}
12761294

12771295
return CodeBlockNode(spans, debugHtmlNode: debugHtmlNode);

lib/widgets/content.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -778,13 +778,23 @@ class CodeBlock extends StatelessWidget {
778778

779779
@override
780780
Widget build(BuildContext context) {
781-
final styles = ContentTheme.of(context).codeBlockTextStyles;
781+
final codeBlockTextStyles = ContentTheme.of(context).codeBlockTextStyles;
782782
return _CodeBlockContainer(
783783
borderColor: Colors.transparent,
784784
child: Text.rich(TextSpan(
785-
style: styles.plain,
785+
style: codeBlockTextStyles.plain,
786786
children: node.spans
787-
.map((node) => TextSpan(style: styles.forSpan(node.spanTypes.single), text: node.text))
787+
.map((node) {
788+
TextStyle? style;
789+
for (final spanType in node.spanTypes) {
790+
final spanStyle = codeBlockTextStyles.forSpan(spanType);
791+
if (spanStyle == null) continue;
792+
style = style == null
793+
? spanStyle
794+
: style.merge(spanStyle);
795+
}
796+
return TextSpan(style: style, text: node.text);
797+
})
788798
.toList(growable: false))));
789799
}
790800
}

test/model/content_test.dart

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ class ContentExample {
382382
QuotationNode([ParagraphNode(links: null, nodes: [TextNode('words')])])
383383
]);
384384

385-
static final codeBlockPlain = ContentExample(
385+
static const codeBlockPlain = ContentExample(
386386
'code block without syntax highlighting',
387387
"```\nverb\natim\n```",
388388
expectedText: 'verb\natim',
@@ -394,7 +394,7 @@ class ContentExample {
394394
]),
395395
]);
396396

397-
static final codeBlockHighlightedShort = ContentExample(
397+
static const codeBlockHighlightedShort = ContentExample(
398398
'code block with syntax highlighting',
399399
"```dart\nclass A {}\n```",
400400
expectedText: 'class A {}',
@@ -415,7 +415,7 @@ class ContentExample {
415415
]),
416416
]);
417417

418-
static final codeBlockHighlightedMultiline = ContentExample(
418+
static const codeBlockHighlightedMultiline = ContentExample(
419419
'code block, multiline, with syntax highlighting',
420420
'```rust\nfn main() {\n print!("Hello ");\n\n print!("world!\\n");\n}\n```',
421421
expectedText: 'fn main() {\n print!("Hello ");\n\n print!("world!\\n");\n}',
@@ -466,7 +466,7 @@ class ContentExample {
466466
]),
467467
]);
468468

469-
static final codeBlockSpansWithMultipleClasses = ContentExample(
469+
static const codeBlockSpansWithMultipleClasses = ContentExample(
470470
'code block spans with multiple CSS classes',
471471
'```yaml\n- item\n```',
472472
expectedText: '- item',
@@ -499,18 +499,22 @@ class ContentExample {
499499
'code block, with syntax highlighting and highlighted lines',
500500
'```\n::markdown hl_lines="2 4"\n# he\n## llo\n### world\n```',
501501
'<div class="codehilite"><pre>'
502-
'<span></span><code>::markdown hl_lines=&quot;2 4&quot;\n'
503-
'<span class="hll"><span class="gh"># he</span>\n'
504-
'</span><span class="gu">## llo</span>\n'
505-
'<span class="hll"><span class="gu">### world</span>\n'
506-
'</span></code></pre></div>', [
507-
// TODO: Fix this, see comment under `CodeBlockSpanType.highlightedLines` case in lib/model/content.dart.
508-
blockUnimplemented('<div class="codehilite"><pre>'
509-
'<span></span><code>::markdown hl_lines=&quot;2 4&quot;\n'
510-
'<span class="hll"><span class="gh"># he</span>\n'
511-
'</span><span class="gu">## llo</span>\n'
512-
'<span class="hll"><span class="gu">### world</span>\n'
513-
'</span></code></pre></div>'),
502+
'<span></span>'
503+
'<code>'
504+
'::markdown hl_lines=&quot;2 4&quot;\n'
505+
'<span class="hll">'
506+
'<span class="gh"># he</span>\n</span>'
507+
'<span class="gu">## llo</span>\n'
508+
'<span class="hll">'
509+
'<span class="gu">### world</span>\n</span></code></pre></div>', [
510+
CodeBlockNode([
511+
CodeBlockSpanNode(text: '::markdown hl_lines="2 4"\n', spanTypes: [CodeBlockSpanType.text]),
512+
CodeBlockSpanNode(text: '# he', spanTypes: [CodeBlockSpanType.highlightedLines, CodeBlockSpanType.genericHeading]),
513+
CodeBlockSpanNode(text: '\n', spanTypes: [CodeBlockSpanType.highlightedLines]),
514+
CodeBlockSpanNode(text: '## llo', spanTypes: [CodeBlockSpanType.genericSubheading]),
515+
CodeBlockSpanNode(text: '\n', spanTypes: [CodeBlockSpanType.text]),
516+
CodeBlockSpanNode(text: '### world', spanTypes: [CodeBlockSpanType.highlightedLines, CodeBlockSpanType.genericSubheading]),
517+
]),
514518
]);
515519

516520
static final codeBlockWithUnknownSpanType = ContentExample(
@@ -524,7 +528,7 @@ class ContentExample {
524528
'\n</code></pre></div>'),
525529
]);
526530

527-
static final codeBlockFollowedByMultipleLineBreaks = ContentExample(
531+
static const codeBlockFollowedByMultipleLineBreaks = ContentExample(
528532
'blank text nodes after code blocks',
529533
' code block.\n\nsome content',
530534
// https://chat.zulip.org/#narrow/stream/7-test-here/near/1774823
@@ -2060,7 +2064,7 @@ void main() async {
20602064
// "1. > ###### two\n > * three\n\n four"
20612065
'<ol>\n<li>\n<blockquote>\n<h6>two</h6>\n<ul>\n<li>three</li>\n'
20622066
'</ul>\n</blockquote>\n<div class="codehilite"><pre><span></span>'
2063-
'<code>four\n</code></pre></div>\n\n</li>\n</ol>', [
2067+
'<code>four\n</code></pre></div>\n\n</li>\n</ol>', const [
20642068
OrderedListNode(start: 1, [[
20652069
QuotationNode([
20662070
HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('two')]),

0 commit comments

Comments
 (0)