Skip to content

Commit f2282e2

Browse files
content: Add CodeBlockSpanNode to parse token name
1 parent 38ed6c8 commit f2282e2

File tree

3 files changed

+332
-18
lines changed

3 files changed

+332
-18
lines changed

lib/model/content.dart

+292-10
Original file line numberDiff line numberDiff line change
@@ -255,23 +255,294 @@ class QuotationNode extends BlockContentNode {
255255
}
256256

257257
class CodeBlockNode extends BlockContentNode {
258-
// TODO(#191) represent the code-highlighting style spans in CodeBlockNode
259-
const CodeBlockNode({super.debugHtmlNode, required this.text});
258+
const CodeBlockNode({super.debugHtmlNode, required this.spans});
259+
260+
final List<CodeBlockSpanNode> spans;
261+
262+
@override
263+
List<DiagnosticsNode> debugDescribeChildren() {
264+
return spans
265+
.map((node) => node.toDiagnosticsNode())
266+
.toList();
267+
}
268+
}
269+
270+
// List of all the tokens that pygments can emit for syntax highlighting
271+
// https://github.com/pygments/pygments/blob/d0acfff1121f9ee3696b01a9077ebe9990216634/pygments/token.py#L123-L214
272+
//
273+
// Note: If you update this list make sure to update the permalink
274+
// and the `tryFromString` function below.
275+
enum CodeBlockSpanToken {
276+
/// No styles applied
277+
text,
278+
// 'hll'
279+
highlightedLines,
280+
/// 'w'
281+
whitespace,
282+
/// 'esc'
283+
escape,
284+
/// 'err'
285+
error,
286+
/// 'x'
287+
other,
288+
/// 'k'
289+
keyword,
290+
/// 'kc'
291+
keywordConstant,
292+
/// 'kd'
293+
keywordDeclaration,
294+
/// 'kn'
295+
keywordNamespace,
296+
/// 'kp'
297+
keywordPseudo,
298+
/// 'kr'
299+
keywordReserved,
300+
/// 'kt'
301+
keywordType,
302+
/// 'n'
303+
name,
304+
/// 'na'
305+
nameAttribute,
306+
/// 'nb'
307+
nameBuiltin,
308+
/// 'bp'
309+
nameBuiltinPseudo,
310+
/// 'nc'
311+
nameClass,
312+
/// 'no'
313+
nameConstant,
314+
/// 'nd'
315+
nameDecorator,
316+
/// 'ni'
317+
nameEntity,
318+
/// 'ne'
319+
nameException,
320+
/// 'nf'
321+
nameFunction,
322+
/// 'fm'
323+
nameFunctionMagic,
324+
/// 'py'
325+
nameProperty,
326+
/// 'nl'
327+
nameLabel,
328+
/// 'nn'
329+
nameNamespace,
330+
/// 'nx'
331+
nameOther,
332+
/// 'nt'
333+
nameTag,
334+
/// 'nv'
335+
nameVariable,
336+
/// 'vc'
337+
nameVariableClass,
338+
/// 'vg'
339+
nameVariableGlobal,
340+
/// 'vi'
341+
nameVariableInstance,
342+
/// 'vm'
343+
nameVariableMagic,
344+
/// 'l'
345+
literal,
346+
/// 'ld'
347+
literalDate,
348+
/// 's'
349+
string,
350+
/// 'sa'
351+
stringAffix,
352+
/// 'sb'
353+
stringBacktick,
354+
/// 'sc'
355+
stringChar,
356+
/// 'dl'
357+
stringDelimiter,
358+
/// 'sd'
359+
stringDoc,
360+
/// 's2'
361+
stringDouble,
362+
/// 'se'
363+
stringEscape,
364+
/// 'sh'
365+
stringHeredoc,
366+
/// 'si'
367+
stringInterpol,
368+
/// 'sx'
369+
stringOther,
370+
/// 'sr'
371+
stringRegex,
372+
/// 's1'
373+
stringSingle,
374+
/// 'ss'
375+
stringSymbol,
376+
/// 'm'
377+
number,
378+
/// 'mb'
379+
numberBin,
380+
/// 'mf'
381+
numberFloat,
382+
/// 'mh'
383+
numberHex,
384+
/// 'mi'
385+
numberInteger,
386+
/// 'il'
387+
numberIntegerLong,
388+
/// 'mo'
389+
numberOct,
390+
/// 'o'
391+
operator,
392+
/// 'ow'
393+
operatorWord,
394+
/// 'p'
395+
punctuation,
396+
/// 'pm'
397+
punctuationMarker,
398+
/// 'c'
399+
comment,
400+
/// 'ch'
401+
commentHashbang,
402+
/// 'cm'
403+
commentMultiline,
404+
/// 'cp'
405+
commentPreproc,
406+
/// 'cpf'
407+
commentPreprocFile,
408+
/// 'c1'
409+
commentSingle,
410+
/// 'cs'
411+
commentSpecial,
412+
/// 'g'
413+
generic,
414+
/// 'gd'
415+
genericDeleted,
416+
/// 'ge'
417+
genericEmph,
418+
/// 'gr'
419+
genericError,
420+
/// 'gh'
421+
genericHeading,
422+
/// 'gi'
423+
genericInserted,
424+
/// 'go'
425+
genericOutput,
426+
/// 'gp'
427+
genericPrompt,
428+
/// 'gs'
429+
genericStrong,
430+
/// 'gu'
431+
genericSubheading,
432+
/// 'ges'
433+
genericEmphStrong,
434+
/// 'gt'
435+
genericTraceback,
436+
}
437+
438+
extension CodeBlockSpanTokenExt on CodeBlockSpanToken {
439+
static CodeBlockSpanToken tryFromClassName(String className) {
440+
return switch (className) {
441+
'hll' => CodeBlockSpanToken.highlightedLines,
442+
'w' => CodeBlockSpanToken.whitespace,
443+
'esc' => CodeBlockSpanToken.escape,
444+
'err' => CodeBlockSpanToken.error,
445+
'x' => CodeBlockSpanToken.other,
446+
'k' => CodeBlockSpanToken.keyword,
447+
'kc' => CodeBlockSpanToken.keywordConstant,
448+
'kd' => CodeBlockSpanToken.keywordDeclaration,
449+
'kn' => CodeBlockSpanToken.keywordNamespace,
450+
'kp' => CodeBlockSpanToken.keywordPseudo,
451+
'kr' => CodeBlockSpanToken.keywordReserved,
452+
'kt' => CodeBlockSpanToken.keywordType,
453+
'n' => CodeBlockSpanToken.name,
454+
'na' => CodeBlockSpanToken.nameAttribute,
455+
'nb' => CodeBlockSpanToken.nameBuiltin,
456+
'bp' => CodeBlockSpanToken.nameBuiltinPseudo,
457+
'nc' => CodeBlockSpanToken.nameClass,
458+
'no' => CodeBlockSpanToken.nameConstant,
459+
'nd' => CodeBlockSpanToken.nameDecorator,
460+
'ni' => CodeBlockSpanToken.nameEntity,
461+
'ne' => CodeBlockSpanToken.nameException,
462+
'nf' => CodeBlockSpanToken.nameFunction,
463+
'fm' => CodeBlockSpanToken.nameFunctionMagic,
464+
'py' => CodeBlockSpanToken.nameProperty,
465+
'nl' => CodeBlockSpanToken.nameLabel,
466+
'nn' => CodeBlockSpanToken.nameNamespace,
467+
'nx' => CodeBlockSpanToken.nameOther,
468+
'nt' => CodeBlockSpanToken.nameTag,
469+
'nv' => CodeBlockSpanToken.nameVariable,
470+
'vc' => CodeBlockSpanToken.nameVariableClass,
471+
'vg' => CodeBlockSpanToken.nameVariableGlobal,
472+
'vi' => CodeBlockSpanToken.nameVariableInstance,
473+
'vm' => CodeBlockSpanToken.nameVariableMagic,
474+
'l' => CodeBlockSpanToken.literal,
475+
'ld' => CodeBlockSpanToken.literalDate,
476+
's' => CodeBlockSpanToken.string,
477+
'sa' => CodeBlockSpanToken.stringAffix,
478+
'sb' => CodeBlockSpanToken.stringBacktick,
479+
'sc' => CodeBlockSpanToken.stringChar,
480+
'dl' => CodeBlockSpanToken.stringDelimiter,
481+
'sd' => CodeBlockSpanToken.stringDoc,
482+
's2' => CodeBlockSpanToken.stringDouble,
483+
'se' => CodeBlockSpanToken.stringEscape,
484+
'sh' => CodeBlockSpanToken.stringHeredoc,
485+
'si' => CodeBlockSpanToken.stringInterpol,
486+
'sx' => CodeBlockSpanToken.stringOther,
487+
'sr' => CodeBlockSpanToken.stringRegex,
488+
's1' => CodeBlockSpanToken.stringSingle,
489+
'ss' => CodeBlockSpanToken.stringSymbol,
490+
'm' => CodeBlockSpanToken.number,
491+
'mb' => CodeBlockSpanToken.numberBin,
492+
'mf' => CodeBlockSpanToken.numberFloat,
493+
'mh' => CodeBlockSpanToken.numberHex,
494+
'mi' => CodeBlockSpanToken.numberInteger,
495+
'il' => CodeBlockSpanToken.numberIntegerLong,
496+
'mo' => CodeBlockSpanToken.numberOct,
497+
'o' => CodeBlockSpanToken.operator,
498+
'ow' => CodeBlockSpanToken.operatorWord,
499+
'p' => CodeBlockSpanToken.punctuation,
500+
'pm' => CodeBlockSpanToken.punctuationMarker,
501+
'c' => CodeBlockSpanToken.comment,
502+
'ch' => CodeBlockSpanToken.commentHashbang,
503+
'cm' => CodeBlockSpanToken.commentMultiline,
504+
'cp' => CodeBlockSpanToken.commentPreproc,
505+
'cpf' => CodeBlockSpanToken.commentPreprocFile,
506+
'c1' => CodeBlockSpanToken.commentSingle,
507+
'cs' => CodeBlockSpanToken.commentSpecial,
508+
'g' => CodeBlockSpanToken.generic,
509+
'gd' => CodeBlockSpanToken.genericDeleted,
510+
'ge' => CodeBlockSpanToken.genericEmph,
511+
'gr' => CodeBlockSpanToken.genericError,
512+
'gh' => CodeBlockSpanToken.genericHeading,
513+
'gi' => CodeBlockSpanToken.genericInserted,
514+
'go' => CodeBlockSpanToken.genericOutput,
515+
'gp' => CodeBlockSpanToken.genericPrompt,
516+
'gs' => CodeBlockSpanToken.genericStrong,
517+
'gu' => CodeBlockSpanToken.genericSubheading,
518+
'ges' => CodeBlockSpanToken.genericEmphStrong,
519+
'gt' => CodeBlockSpanToken.genericTraceback,
520+
_ => CodeBlockSpanToken.text,
521+
};
522+
}
523+
}
524+
525+
class CodeBlockSpanNode extends BlockContentNode {
526+
const CodeBlockSpanNode({super.debugHtmlNode, required this.text, required this.tokenType});
260527

261528
final String text;
529+
final CodeBlockSpanToken tokenType;
262530

263531
@override
264532
bool operator ==(Object other) {
265-
return other is CodeBlockNode && other.text == text;
533+
return other is CodeBlockSpanNode
534+
&& other.text == text
535+
&& other.tokenType == tokenType;
266536
}
267537

268538
@override
269-
int get hashCode => Object.hash('CodeBlockNode', text);
539+
int get hashCode => Object.hash('CodeBlockSpanNode', text, tokenType);
270540

271541
@override
272542
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
273543
super.debugFillProperties(properties);
274544
properties.add(StringProperty('text', text));
545+
properties.add(EnumProperty('tokenType', tokenType));
275546
}
276547
}
277548

@@ -658,7 +929,7 @@ class _ZulipContentParser {
658929
return UnimplementedBlockContentNode(htmlNode: divElement);
659930
}
660931

661-
final buffer = StringBuffer();
932+
final spans = <CodeBlockSpanNode>[];
662933
for (int i = 0; i < mainElement.nodes.length; i++) {
663934
final child = mainElement.nodes[i];
664935
if (child is dom.Text) {
@@ -668,17 +939,28 @@ class _ZulipContentParser {
668939
// [Text] widget, that would make a trailing blank line. So cut it out.
669940
text = text.replaceFirst(RegExp(r'\n$'), '');
670941
}
671-
buffer.write(text);
942+
if (text.isNotEmpty) {
943+
spans.add(CodeBlockSpanNode(text: text, tokenType: CodeBlockSpanToken.text));
944+
}
672945
} 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);
946+
if (child.className.isEmpty) {
947+
spans.add(CodeBlockSpanNode(text: child.text, tokenType: CodeBlockSpanToken.text));
948+
} else {
949+
// Further code is based on an assumption that there will
950+
// be only single class associated with the span element.
951+
if (child.className.split(' ').length > 1) {
952+
return UnimplementedBlockContentNode(htmlNode: divElement);
953+
}
954+
955+
final spanClass = CodeBlockSpanTokenExt.tryFromClassName(child.className);
956+
spans.add(CodeBlockSpanNode(text: child.text, tokenType: spanClass));
957+
}
675958
} else {
676959
return UnimplementedBlockContentNode(htmlNode: divElement);
677960
}
678961
}
679-
final text = buffer.toString();
680962

681-
return CodeBlockNode(text: text, debugHtmlNode: debugHtmlNode);
963+
return CodeBlockNode(spans: spans, debugHtmlNode: debugHtmlNode);
682964
}
683965

684966
BlockContentNode parseImageNode(dom.Element divElement) {

lib/widgets/content.dart

+27-5
Original file line numberDiff line numberDiff line change
@@ -255,18 +255,40 @@ class CodeBlock extends StatelessWidget {
255255

256256
@override
257257
Widget build(BuildContext context) {
258-
final text = node.text;
259-
260258
return Container(
261-
padding: const EdgeInsets.fromLTRB(7, 5, 7, 3),
262259
decoration: BoxDecoration(
263260
color: Colors.white,
264261
border: Border.all(
265262
width: 1,
266-
color: const HSLColor.fromAHSL(0.15, 0, 0, 0).toColor())),
263+
color: const HSLColor.fromAHSL(0.15, 0, 0, 0).toColor()),
264+
borderRadius: BorderRadius.circular(4)),
267265
child: SingleChildScrollViewWithScrollbar(
268266
scrollDirection: Axis.horizontal,
269-
child: Text(text, style: _kCodeBlockStyle)));
267+
child: Padding(
268+
padding: const EdgeInsets.fromLTRB(7, 5, 7, 3),
269+
child: _buildText(),
270+
)));
271+
}
272+
273+
Widget _buildText() {
274+
if (node.spans.isEmpty) {
275+
return Text('', style: _kCodeBlockStyle);
276+
} else if (node.spans.length == 1 && node.spans.first.tokenType == CodeBlockSpanToken.text){
277+
return Text(node.spans.first.text, style: _kCodeBlockStyle);
278+
} else {
279+
return Text.rich(_buildNodes(node.spans, style: _kCodeBlockStyle));
280+
}
281+
}
282+
283+
InlineSpan _buildNodes(List<CodeBlockSpanNode> nodes, {required TextStyle? style}) {
284+
return TextSpan(
285+
style: style,
286+
children: nodes.map(_buildNode).toList(growable: false));
287+
}
288+
289+
InlineSpan _buildNode(CodeBlockSpanNode node) {
290+
// TODO: generate text styles for each node based on token type
291+
return TextSpan(text: node.text);
270292
}
271293
}
272294

0 commit comments

Comments
 (0)