Skip to content

Commit 3604a50

Browse files
content: Implement syntax highlighting in CodeBlock
- Implement parsing of all the pygments token types that can be emitted by pygments as span classes. - Add styles adapted from web frontend's css for the code block spans.
1 parent 3cba058 commit 3604a50

File tree

6 files changed

+659
-29
lines changed

6 files changed

+659
-29
lines changed

lib/model/code_block.dart

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
// List of all the tokens that pygments can emit for syntax highlighting
2+
// https://github.com/pygments/pygments/blob/d0acfff1121f9ee3696b01a9077ebe9990216634/pygments/token.py#L123-L214
3+
//
4+
// Note: If you update this list make sure to update the permalink
5+
// and the `tryFromString` function below.
6+
enum CodeBlockSpanType {
7+
/// A code-block span that is unrecognized by the parser.
8+
unknown,
9+
/// A run of unstyled text in a code block.
10+
text,
11+
/// A code-block span with CSS class `hll`.
12+
///
13+
/// Unlike most `CodeBlockSpanToken` values, this does not correspond to
14+
/// a Pygments "token type". See discussion:
15+
/// https://github.com/zulip/zulip-flutter/pull/242#issuecomment-1652450667
16+
highlightedLines,
17+
/// A code-block span with CSS class `w`.
18+
whitespace,
19+
/// A code-block span with CSS class `esc`.
20+
escape,
21+
/// A code-block span with CSS class `err`.
22+
error,
23+
/// A code-block span with CSS class `x`.
24+
other,
25+
/// A code-block span with CSS class `k`.
26+
keyword,
27+
/// A code-block span with CSS class `kc`.
28+
keywordConstant,
29+
/// A code-block span with CSS class `kd`.
30+
keywordDeclaration,
31+
/// A code-block span with CSS class `kn`.
32+
keywordNamespace,
33+
/// A code-block span with CSS class `kp`.
34+
keywordPseudo,
35+
/// A code-block span with CSS class `kr`.
36+
keywordReserved,
37+
/// A code-block span with CSS class `kt`.
38+
keywordType,
39+
/// A code-block span with CSS class `n`.
40+
name,
41+
/// A code-block span with CSS class `na`.
42+
nameAttribute,
43+
/// A code-block span with CSS class `nb`.
44+
nameBuiltin,
45+
/// A code-block span with CSS class `bp`.
46+
nameBuiltinPseudo,
47+
/// A code-block span with CSS class `nc`.
48+
nameClass,
49+
/// A code-block span with CSS class `no`.
50+
nameConstant,
51+
/// A code-block span with CSS class `nd`.
52+
nameDecorator,
53+
/// A code-block span with CSS class `ni`.
54+
nameEntity,
55+
/// A code-block span with CSS class `ne`.
56+
nameException,
57+
/// A code-block span with CSS class `nf`.
58+
nameFunction,
59+
/// A code-block span with CSS class `fm`.
60+
nameFunctionMagic,
61+
/// A code-block span with CSS class `py`.
62+
nameProperty,
63+
/// A code-block span with CSS class `nl`.
64+
nameLabel,
65+
/// A code-block span with CSS class `nn`.
66+
nameNamespace,
67+
/// A code-block span with CSS class `nx`.
68+
nameOther,
69+
/// A code-block span with CSS class `nt`.
70+
nameTag,
71+
/// A code-block span with CSS class `nv`.
72+
nameVariable,
73+
/// A code-block span with CSS class `vc`.
74+
nameVariableClass,
75+
/// A code-block span with CSS class `vg`.
76+
nameVariableGlobal,
77+
/// A code-block span with CSS class `vi`.
78+
nameVariableInstance,
79+
/// A code-block span with CSS class `vm`.
80+
nameVariableMagic,
81+
/// A code-block span with CSS class `l`.
82+
literal,
83+
/// A code-block span with CSS class `ld`.
84+
literalDate,
85+
/// A code-block span with CSS class `s`.
86+
string,
87+
/// A code-block span with CSS class `sa`.
88+
stringAffix,
89+
/// A code-block span with CSS class `sb`.
90+
stringBacktick,
91+
/// A code-block span with CSS class `sc`.
92+
stringChar,
93+
/// A code-block span with CSS class `dl`.
94+
stringDelimiter,
95+
/// A code-block span with CSS class `sd`.
96+
stringDoc,
97+
/// A code-block span with CSS class `s2`.
98+
stringDouble,
99+
/// A code-block span with CSS class `se`.
100+
stringEscape,
101+
/// A code-block span with CSS class `sh`.
102+
stringHeredoc,
103+
/// A code-block span with CSS class `si`.
104+
stringInterpol,
105+
/// A code-block span with CSS class `sx`.
106+
stringOther,
107+
/// A code-block span with CSS class `sr`.
108+
stringRegex,
109+
/// A code-block span with CSS class `s1`.
110+
stringSingle,
111+
/// A code-block span with CSS class `ss`.
112+
stringSymbol,
113+
/// A code-block span with CSS class `m`.
114+
number,
115+
/// A code-block span with CSS class `mb`.
116+
numberBin,
117+
/// A code-block span with CSS class `mf`.
118+
numberFloat,
119+
/// A code-block span with CSS class `mh`.
120+
numberHex,
121+
/// A code-block span with CSS class `mi`.
122+
numberInteger,
123+
/// A code-block span with CSS class `il`.
124+
numberIntegerLong,
125+
/// A code-block span with CSS class `mo`.
126+
numberOct,
127+
/// A code-block span with CSS class `o`.
128+
operator,
129+
/// A code-block span with CSS class `ow`.
130+
operatorWord,
131+
/// A code-block span with CSS class `p`.
132+
punctuation,
133+
/// A code-block span with CSS class `pm`.
134+
punctuationMarker,
135+
/// A code-block span with CSS class `c`.
136+
comment,
137+
/// A code-block span with CSS class `ch`.
138+
commentHashbang,
139+
/// A code-block span with CSS class `cm`.
140+
commentMultiline,
141+
/// A code-block span with CSS class `cp`.
142+
commentPreproc,
143+
/// A code-block span with CSS class `cpf`.
144+
commentPreprocFile,
145+
/// A code-block span with CSS class `c1`.
146+
commentSingle,
147+
/// A code-block span with CSS class `cs`.
148+
commentSpecial,
149+
/// A code-block span with CSS class `g`.
150+
generic,
151+
/// A code-block span with CSS class `gd`.
152+
genericDeleted,
153+
/// A code-block span with CSS class `ge`.
154+
genericEmph,
155+
/// A code-block span with CSS class `gr`.
156+
genericError,
157+
/// A code-block span with CSS class `gh`.
158+
genericHeading,
159+
/// A code-block span with CSS class `gi`.
160+
genericInserted,
161+
/// A code-block span with CSS class `go`.
162+
genericOutput,
163+
/// A code-block span with CSS class `gp`.
164+
genericPrompt,
165+
/// A code-block span with CSS class `gs`.
166+
genericStrong,
167+
/// A code-block span with CSS class `gu`.
168+
genericSubheading,
169+
/// A code-block span with CSS class `ges`.
170+
genericEmphStrong,
171+
/// A code-block span with CSS class `gt`.
172+
genericTraceback,
173+
}
174+
175+
CodeBlockSpanType codeBlockSpanTypeFromClassName(String className) {
176+
return switch (className) {
177+
'' => CodeBlockSpanType.text,
178+
'hll' => CodeBlockSpanType.highlightedLines,
179+
'w' => CodeBlockSpanType.whitespace,
180+
'esc' => CodeBlockSpanType.escape,
181+
'err' => CodeBlockSpanType.error,
182+
'x' => CodeBlockSpanType.other,
183+
'k' => CodeBlockSpanType.keyword,
184+
'kc' => CodeBlockSpanType.keywordConstant,
185+
'kd' => CodeBlockSpanType.keywordDeclaration,
186+
'kn' => CodeBlockSpanType.keywordNamespace,
187+
'kp' => CodeBlockSpanType.keywordPseudo,
188+
'kr' => CodeBlockSpanType.keywordReserved,
189+
'kt' => CodeBlockSpanType.keywordType,
190+
'n' => CodeBlockSpanType.name,
191+
'na' => CodeBlockSpanType.nameAttribute,
192+
'nb' => CodeBlockSpanType.nameBuiltin,
193+
'bp' => CodeBlockSpanType.nameBuiltinPseudo,
194+
'nc' => CodeBlockSpanType.nameClass,
195+
'no' => CodeBlockSpanType.nameConstant,
196+
'nd' => CodeBlockSpanType.nameDecorator,
197+
'ni' => CodeBlockSpanType.nameEntity,
198+
'ne' => CodeBlockSpanType.nameException,
199+
'nf' => CodeBlockSpanType.nameFunction,
200+
'fm' => CodeBlockSpanType.nameFunctionMagic,
201+
'py' => CodeBlockSpanType.nameProperty,
202+
'nl' => CodeBlockSpanType.nameLabel,
203+
'nn' => CodeBlockSpanType.nameNamespace,
204+
'nx' => CodeBlockSpanType.nameOther,
205+
'nt' => CodeBlockSpanType.nameTag,
206+
'nv' => CodeBlockSpanType.nameVariable,
207+
'vc' => CodeBlockSpanType.nameVariableClass,
208+
'vg' => CodeBlockSpanType.nameVariableGlobal,
209+
'vi' => CodeBlockSpanType.nameVariableInstance,
210+
'vm' => CodeBlockSpanType.nameVariableMagic,
211+
'l' => CodeBlockSpanType.literal,
212+
'ld' => CodeBlockSpanType.literalDate,
213+
's' => CodeBlockSpanType.string,
214+
'sa' => CodeBlockSpanType.stringAffix,
215+
'sb' => CodeBlockSpanType.stringBacktick,
216+
'sc' => CodeBlockSpanType.stringChar,
217+
'dl' => CodeBlockSpanType.stringDelimiter,
218+
'sd' => CodeBlockSpanType.stringDoc,
219+
's2' => CodeBlockSpanType.stringDouble,
220+
'se' => CodeBlockSpanType.stringEscape,
221+
'sh' => CodeBlockSpanType.stringHeredoc,
222+
'si' => CodeBlockSpanType.stringInterpol,
223+
'sx' => CodeBlockSpanType.stringOther,
224+
'sr' => CodeBlockSpanType.stringRegex,
225+
's1' => CodeBlockSpanType.stringSingle,
226+
'ss' => CodeBlockSpanType.stringSymbol,
227+
'm' => CodeBlockSpanType.number,
228+
'mb' => CodeBlockSpanType.numberBin,
229+
'mf' => CodeBlockSpanType.numberFloat,
230+
'mh' => CodeBlockSpanType.numberHex,
231+
'mi' => CodeBlockSpanType.numberInteger,
232+
'il' => CodeBlockSpanType.numberIntegerLong,
233+
'mo' => CodeBlockSpanType.numberOct,
234+
'o' => CodeBlockSpanType.operator,
235+
'ow' => CodeBlockSpanType.operatorWord,
236+
'p' => CodeBlockSpanType.punctuation,
237+
'pm' => CodeBlockSpanType.punctuationMarker,
238+
'c' => CodeBlockSpanType.comment,
239+
'ch' => CodeBlockSpanType.commentHashbang,
240+
'cm' => CodeBlockSpanType.commentMultiline,
241+
'cp' => CodeBlockSpanType.commentPreproc,
242+
'cpf' => CodeBlockSpanType.commentPreprocFile,
243+
'c1' => CodeBlockSpanType.commentSingle,
244+
'cs' => CodeBlockSpanType.commentSpecial,
245+
'g' => CodeBlockSpanType.generic,
246+
'gd' => CodeBlockSpanType.genericDeleted,
247+
'ge' => CodeBlockSpanType.genericEmph,
248+
'gr' => CodeBlockSpanType.genericError,
249+
'gh' => CodeBlockSpanType.genericHeading,
250+
'gi' => CodeBlockSpanType.genericInserted,
251+
'go' => CodeBlockSpanType.genericOutput,
252+
'gp' => CodeBlockSpanType.genericPrompt,
253+
'gs' => CodeBlockSpanType.genericStrong,
254+
'gu' => CodeBlockSpanType.genericSubheading,
255+
'ges' => CodeBlockSpanType.genericEmphStrong,
256+
'gt' => CodeBlockSpanType.genericTraceback,
257+
_ => CodeBlockSpanType.unknown,
258+
};
259+
}

lib/model/content.dart

+55-20
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'package:flutter/foundation.dart';
22
import 'package:html/dom.dart' as dom;
33
import 'package:html/parser.dart';
44

5+
import 'code_block.dart';
6+
57
/// A node in a parse tree for Zulip message-style content.
68
///
79
/// See [ZulipContent].
@@ -255,23 +257,35 @@ class QuotationNode extends BlockContentNode {
255257
}
256258

257259
class CodeBlockNode extends BlockContentNode {
258-
// TODO(#191) represent the code-highlighting style spans in CodeBlockNode
259-
const CodeBlockNode({super.debugHtmlNode, required this.text});
260+
const CodeBlockNode(this.spans, {super.debugHtmlNode});
261+
262+
final List<CodeBlockSpanNode> spans;
263+
264+
@override
265+
List<DiagnosticsNode> debugDescribeChildren() {
266+
return spans.map((node) => node.toDiagnosticsNode()).toList();
267+
}
268+
}
269+
270+
class CodeBlockSpanNode extends InlineContentNode {
271+
const CodeBlockSpanNode({super.debugHtmlNode, required this.text, required this.type});
260272

261273
final String text;
274+
final CodeBlockSpanType type;
262275

263276
@override
264277
bool operator ==(Object other) {
265-
return other is CodeBlockNode && other.text == text;
278+
return other is CodeBlockSpanNode && other.text == text && other.type == type;
266279
}
267280

268281
@override
269-
int get hashCode => Object.hash('CodeBlockNode', text);
282+
int get hashCode => Object.hash('CodeBlockSpanNode', text, type);
270283

271284
@override
272285
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
273286
super.debugFillProperties(properties);
274287
properties.add(StringProperty('text', text));
288+
properties.add(EnumProperty('type', type));
275289
}
276290
}
277291

@@ -658,27 +672,48 @@ class _ZulipContentParser {
658672
return UnimplementedBlockContentNode(htmlNode: divElement);
659673
}
660674

661-
final buffer = StringBuffer();
675+
final spans = <CodeBlockSpanNode>[];
662676
for (int i = 0; i < mainElement.nodes.length; i++) {
663677
final child = mainElement.nodes[i];
664-
if (child is dom.Text) {
665-
String text = child.text;
666-
if (i == mainElement.nodes.length - 1) {
667-
// The HTML tends to have a final newline here. If included in the
668-
// [Text] widget, that would make a trailing blank line. So cut it out.
669-
text = text.replaceFirst(RegExp(r'\n$'), '');
670-
}
671-
buffer.write(text);
672-
} else if (child is dom.Element && child.localName == 'span') {
673-
// TODO(#191) parse the code-highlighting spans, to style them
674-
buffer.write(child.text);
675-
} else {
676-
return UnimplementedBlockContentNode(htmlNode: divElement);
678+
679+
final CodeBlockSpanNode span;
680+
switch (child) {
681+
case dom.Text(:var text):
682+
if (i == mainElement.nodes.length - 1) {
683+
// The HTML tends to have a final newline here. If included in the
684+
// [Text] widget, that would make a trailing blank line. So cut it out.
685+
text = text.replaceFirst(RegExp(r'\n$'), '');
686+
}
687+
span = CodeBlockSpanNode(text: text, type: CodeBlockSpanType.text);
688+
689+
case dom.Element(:final text, :final classes, :final localName)
690+
when localName == 'span'
691+
&& classes.length == 1:
692+
final CodeBlockSpanType type = codeBlockSpanTypeFromClassName(classes.first);
693+
switch (type) {
694+
case CodeBlockSpanType.unknown:
695+
return UnimplementedBlockContentNode(htmlNode: divElement);
696+
case CodeBlockSpanType.highlightedLines:
697+
// TODO: Implement nesting in CodeBlockSpanNode to support hierarchically
698+
// inherited styles for `span.hll` nodes.
699+
return UnimplementedBlockContentNode(htmlNode: divElement);
700+
default:
701+
span = CodeBlockSpanNode(text: text, type: type);
702+
}
703+
704+
default:
705+
return UnimplementedBlockContentNode(htmlNode: divElement);
706+
}
707+
708+
if (span.text.isNotEmpty) {
709+
// We don't want to emit unnecessary CodeBlockSpanNode with empty text
710+
// which results in a TextSpan with empty text which results in
711+
// nothing displayed in the end anyway.
712+
spans.add(span);
677713
}
678714
}
679-
final text = buffer.toString();
680715

681-
return CodeBlockNode(text: text, debugHtmlNode: debugHtmlNode);
716+
return CodeBlockNode(spans, debugHtmlNode: debugHtmlNode);
682717
}
683718

684719
BlockContentNode parseImageNode(dom.Element divElement) {

0 commit comments

Comments
 (0)