Skip to content

Commit 7e6402c

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 981f5b7 commit 7e6402c

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;
@@ -1238,50 +1238,68 @@ class _ZulipContentParser {
12381238
return UnimplementedBlockContentNode(htmlNode: divElement);
12391239
}
12401240

1241-
final spans = <CodeBlockSpanNode>[];
1241+
// Empirically, when a Pygments node has multiple classes, the first
1242+
// class names a standard token type and the rest are for non-standard
1243+
// token types specific to the language. Zulip web only styles the
1244+
// standard token classes and ignores the others, so we do the same.
1245+
// See: https://github.com/zulip/zulip-flutter/issues/933
1246+
CodeBlockSpanType? parseCodeBlockSpanType(String className) {
1247+
return className.split(' ')
1248+
.map(codeBlockSpanTypeFromClassName)
1249+
.firstWhereOrNull((e) => e != CodeBlockSpanType.unknown);
1250+
}
1251+
1252+
List<CodeBlockSpanNode> spans = [];
1253+
List<CodeBlockSpanType> spanTypes = [];
1254+
bool hasFailed = false;
1255+
12421256
for (int i = 0; i < mainElement.nodes.length; i++) {
12431257
final child = mainElement.nodes[i];
12441258

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

1280-
default:
1281-
return UnimplementedBlockContentNode(htmlNode: divElement);
1284+
spanTypes.add(spanType);
1285+
1286+
for (int i = 0; i < child.nodes.length; i++) {
1287+
final grandchild = child.nodes[i];
1288+
parseCodeBlockSpan(grandchild,
1289+
isLastNode ? i == child.nodes.length - 1 : false);
1290+
if (hasFailed) return;
1291+
}
1292+
1293+
assert(spanTypes.removeLast() == spanType);
1294+
1295+
default:
1296+
hasFailed = true;
1297+
return;
1298+
}
12821299
}
12831300

1284-
spans.add(span);
1301+
parseCodeBlockSpan(child, i == mainElement.nodes.length - 1);
1302+
if (hasFailed) return UnimplementedBlockContentNode(htmlNode: divElement);
12851303
}
12861304

12871305
return CodeBlockNode(spans, debugHtmlNode: debugHtmlNode);

lib/widgets/content.dart

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

771771
@override
772772
Widget build(BuildContext context) {
773-
final styles = ContentTheme.of(context).codeBlockTextStyles;
773+
final codeBlockTextStyles = ContentTheme.of(context).codeBlockTextStyles;
774774
return _CodeBlockContainer(
775775
borderColor: Colors.transparent,
776776
child: Text.rich(TextSpan(
777-
style: styles.plain,
777+
style: codeBlockTextStyles.plain,
778778
children: node.spans
779-
.map((node) => TextSpan(style: styles.forSpan(node.spanTypes.single), text: node.text))
779+
.map((node) {
780+
TextStyle? style;
781+
for (final spanType in node.spanTypes) {
782+
final spanStyle = codeBlockTextStyles.forSpan(spanType);
783+
if (spanStyle == null) continue;
784+
style = style == null
785+
? spanStyle
786+
: style.merge(spanStyle);
787+
}
788+
return TextSpan(style: style, text: node.text);
789+
})
780790
.toList(growable: false))));
781791
}
782792
}

test/model/content_test.dart

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

378-
static final codeBlockPlain = ContentExample(
378+
static const codeBlockPlain = ContentExample(
379379
'code block without syntax highlighting',
380380
"```\nverb\natim\n```",
381381
expectedText: 'verb\natim',
@@ -387,7 +387,7 @@ class ContentExample {
387387
]),
388388
]);
389389

390-
static final codeBlockHighlightedShort = ContentExample(
390+
static const codeBlockHighlightedShort = ContentExample(
391391
'code block with syntax highlighting',
392392
"```dart\nclass A {}\n```",
393393
expectedText: 'class A {}',
@@ -408,7 +408,7 @@ class ContentExample {
408408
]),
409409
]);
410410

411-
static final codeBlockHighlightedMultiline = ContentExample(
411+
static const codeBlockHighlightedMultiline = ContentExample(
412412
'code block, multiline, with syntax highlighting',
413413
'```rust\nfn main() {\n print!("Hello ");\n\n print!("world!\\n");\n}\n```',
414414
expectedText: 'fn main() {\n print!("Hello ");\n\n print!("world!\\n");\n}',
@@ -459,7 +459,7 @@ class ContentExample {
459459
]),
460460
]);
461461

462-
static final codeBlockSpansWithMultipleClasses = ContentExample(
462+
static const codeBlockSpansWithMultipleClasses = ContentExample(
463463
'code block spans with multiple CSS classes',
464464
'```yaml\n- item\n```',
465465
expectedText: '- item',
@@ -492,18 +492,22 @@ class ContentExample {
492492
'code block, with syntax highlighting and highlighted lines',
493493
'```\n::markdown hl_lines="2 4"\n# he\n## llo\n### world\n```',
494494
'<div class="codehilite"><pre>'
495-
'<span></span><code>::markdown hl_lines=&quot;2 4&quot;\n'
496-
'<span class="hll"><span class="gh"># he</span>\n'
497-
'</span><span class="gu">## llo</span>\n'
498-
'<span class="hll"><span class="gu">### world</span>\n'
499-
'</span></code></pre></div>', [
500-
// TODO: Fix this, see comment under `CodeBlockSpanType.highlightedLines` case in lib/model/content.dart.
501-
blockUnimplemented('<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>'),
495+
'<span></span>'
496+
'<code>'
497+
'::markdown hl_lines=&quot;2 4&quot;\n'
498+
'<span class="hll">'
499+
'<span class="gh"># he</span>\n</span>'
500+
'<span class="gu">## llo</span>\n'
501+
'<span class="hll">'
502+
'<span class="gu">### world</span>\n</span></code></pre></div>', [
503+
CodeBlockNode([
504+
CodeBlockSpanNode(text: '::markdown hl_lines="2 4"\n', spanTypes: [CodeBlockSpanType.text]),
505+
CodeBlockSpanNode(text: '# he', spanTypes: [CodeBlockSpanType.highlightedLines, CodeBlockSpanType.genericHeading]),
506+
CodeBlockSpanNode(text: '\n', spanTypes: [CodeBlockSpanType.highlightedLines]),
507+
CodeBlockSpanNode(text: '## llo', spanTypes: [CodeBlockSpanType.genericSubheading]),
508+
CodeBlockSpanNode(text: '\n', spanTypes: [CodeBlockSpanType.text]),
509+
CodeBlockSpanNode(text: '### world', spanTypes: [CodeBlockSpanType.highlightedLines, CodeBlockSpanType.genericSubheading]),
510+
]),
507511
]);
508512

509513
static final codeBlockWithUnknownSpanType = ContentExample(
@@ -517,7 +521,7 @@ class ContentExample {
517521
'\n</code></pre></div>'),
518522
]);
519523

520-
static final codeBlockFollowedByMultipleLineBreaks = ContentExample(
524+
static const codeBlockFollowedByMultipleLineBreaks = ContentExample(
521525
'blank text nodes after code blocks',
522526
' code block.\n\nsome content',
523527
// https://chat.zulip.org/#narrow/stream/7-test-here/near/1774823
@@ -2099,7 +2103,7 @@ void main() async {
20992103
// "1. > ###### two\n > * three\n\n four"
21002104
'<ol>\n<li>\n<blockquote>\n<h6>two</h6>\n<ul>\n<li>three</li>\n'
21012105
'</ul>\n</blockquote>\n<div class="codehilite"><pre><span></span>'
2102-
'<code>four\n</code></pre></div>\n\n</li>\n</ol>', [
2106+
'<code>four\n</code></pre></div>\n\n</li>\n</ol>', const [
21032107
OrderedListNode(start: 1, [[
21042108
QuotationNode([
21052109
HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('two')]),

0 commit comments

Comments
 (0)