Skip to content

Commit 7ff4f9e

Browse files
content: Handle vertical offset spans in KaTeX content
Implement handling most common types of `vlist` spans.
1 parent ad10be1 commit 7ff4f9e

File tree

4 files changed

+261
-17
lines changed

4 files changed

+261
-17
lines changed

lib/model/content.dart

+35
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,41 @@ class KatexNegativeMarginNode extends KatexNode {
415415
}
416416
}
417417

418+
class KatexVlistNode extends KatexNode {
419+
const KatexVlistNode({
420+
required this.rows,
421+
super.debugHtmlNode,
422+
});
423+
424+
final List<KatexVlistRowNode> rows;
425+
426+
@override
427+
List<DiagnosticsNode> debugDescribeChildren() {
428+
return rows.map((row) => row.toDiagnosticsNode()).toList();
429+
}
430+
}
431+
432+
class KatexVlistRowNode extends ContentNode {
433+
const KatexVlistRowNode({
434+
required this.verticalOffsetEm,
435+
this.nodes = const [],
436+
});
437+
438+
final double verticalOffsetEm;
439+
final List<KatexNode> nodes;
440+
441+
@override
442+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
443+
super.debugFillProperties(properties);
444+
properties.add(StringProperty('verticalOffsetEm', '$verticalOffsetEm'));
445+
}
446+
447+
@override
448+
List<DiagnosticsNode> debugDescribeChildren() {
449+
return nodes.map((node) => node.toDiagnosticsNode()).toList();
450+
}
451+
}
452+
418453
class ImageNodeList extends BlockContentNode {
419454
const ImageNodeList(this.images, {super.debugHtmlNode});
420455

lib/model/katex.dart

+149-9
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ class KatexParser {
2020
final span = _parseSpan(node);
2121
resultSpans.add(span);
2222

23-
final marginRightEm = span.styles.marginRightEm;
24-
if (marginRightEm != null && marginRightEm.isNegative) {
25-
final previousSpansReversed =
26-
resultSpans.reversed.toList(growable: false);
27-
resultSpans = [];
28-
resultSpans.add(KatexNegativeMarginNode(
29-
marginRightEm: marginRightEm,
30-
nodes: previousSpansReversed));
23+
if (span is KatexSpanNode) {
24+
final marginRightEm = span.styles.marginRightEm;
25+
if (marginRightEm != null && marginRightEm.isNegative) {
26+
final previousSpansReversed =
27+
resultSpans.reversed.toList(growable: false);
28+
resultSpans = [];
29+
resultSpans.add(KatexNegativeMarginNode(
30+
marginRightEm: marginRightEm,
31+
nodes: previousSpansReversed));
32+
}
3133
}
3234
}
3335

@@ -37,9 +39,111 @@ class KatexParser {
3739
static final _resetSizeClassRegExp = RegExp(r'^reset-size(\d\d?)$');
3840
static final _sizeClassRegExp = RegExp(r'^size(\d\d?)$');
3941

40-
KatexSpanNode _parseSpan(dom.Element element) {
42+
KatexNode _parseSpan(dom.Element element) {
4143
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
4244

45+
if (element case dom.Element(localName: 'span', :final className)
46+
when className.startsWith('vlist')) {
47+
switch (element) {
48+
case dom.Element(
49+
localName: 'span',
50+
className: 'vlist-t',
51+
attributes: final attributesVlistT,
52+
nodes: [
53+
dom.Element(
54+
localName: 'span',
55+
className: 'vlist-r',
56+
attributes: final attributesVlistR,
57+
nodes: [
58+
dom.Element(
59+
localName: 'span',
60+
className: 'vlist',
61+
nodes: [
62+
dom.Element(
63+
localName: 'span',
64+
className: '',
65+
nodes: [
66+
dom.Element(localName: 'span', className: 'pstrut')
67+
&& final pstrutSpan,
68+
...,
69+
]) && final innerSpan,
70+
]),
71+
]),
72+
])
73+
when !attributesVlistT.containsKey('style') &&
74+
!attributesVlistR.containsKey('style'):
75+
// TODO vlist element should only have `height` style, which we ignore.
76+
77+
var styles = _parseSpanInlineStyles(innerSpan)!;
78+
final topEm = styles.topEm ?? 0;
79+
80+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
81+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
82+
83+
// TODO handle negative right-margin inline style on row nodes.
84+
return KatexVlistNode(rows: [
85+
KatexVlistRowNode(
86+
verticalOffsetEm: topEm + pstrutHeight,
87+
nodes: _parseChildSpans(innerSpan)),
88+
]);
89+
90+
case dom.Element(
91+
localName: 'span',
92+
className: 'vlist-t vlist-t2',
93+
attributes: final attributesVlistT,
94+
nodes: [
95+
dom.Element(
96+
localName: 'span',
97+
className: 'vlist-r',
98+
attributes: final attributesVlistR,
99+
nodes: [
100+
dom.Element(
101+
localName: 'span',
102+
className: 'vlist',
103+
nodes: [...]) && final vlist1,
104+
dom.Element(localName: 'span', className: 'vlist-s'),
105+
]),
106+
dom.Element(localName: 'span', className: 'vlist-r', nodes: [
107+
dom.Element(localName: 'span', className: 'vlist', nodes: [
108+
dom.Element(localName: 'span', className: '', nodes: []),
109+
])
110+
]),
111+
])
112+
when !attributesVlistT.containsKey('style') &&
113+
!attributesVlistR.containsKey('style'):
114+
// TODO Ensure both should only have a `height` style.
115+
116+
final rows = <KatexVlistRowNode>[];
117+
118+
for (final innerSpan in vlist1.nodes) {
119+
if (innerSpan case dom.Element(
120+
localName: 'span',
121+
className: '',
122+
nodes: [
123+
dom.Element(localName: 'span', className: 'pstrut') &&
124+
final pstrutSpan,
125+
...,
126+
])) {
127+
final styles = _parseSpanInlineStyles(innerSpan)!;
128+
final topEm = styles.topEm ?? 0;
129+
130+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
131+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
132+
133+
// TODO handle negative right-margin inline style on row nodes.
134+
rows.add(KatexVlistRowNode(
135+
verticalOffsetEm: topEm + pstrutHeight,
136+
nodes: _parseChildSpans(innerSpan)));
137+
}
138+
}
139+
140+
return KatexVlistNode(rows: rows);
141+
142+
default:
143+
throw KatexHtmlParseError();
144+
}
145+
}
146+
43147
var styles = KatexSpanStyles();
44148
var index = 0;
45149
while (index < spanClasses.length) {
@@ -261,9 +365,12 @@ class KatexParser {
261365
if (topLevel is! css_visitor.RuleSet) throw KatexHtmlParseError();
262366
final rule = topLevel;
263367

368+
double? heightEm;
264369
double? marginLeftEm;
265370
double? marginRightEm;
266371
double? paddingLeftEm;
372+
double? topEm;
373+
double? widthEm;
267374

268375
for (final declaration in rule.declarationGroup.declarations) {
269376
if (declaration is! css_visitor.Declaration) throw KatexHtmlParseError();
@@ -275,6 +382,10 @@ class KatexParser {
275382
final expression = expressions.expressions.single;
276383

277384
switch (property) {
385+
case 'height':
386+
heightEm = _getEm(expression);
387+
if (heightEm != null) continue;
388+
278389
case 'margin-left':
279390
marginLeftEm = _getEm(expression);
280391
if (marginLeftEm != null) continue;
@@ -287,16 +398,27 @@ class KatexParser {
287398
paddingLeftEm = _getEm(expression);
288399
if (paddingLeftEm != null) continue;
289400

401+
case 'top':
402+
topEm = _getEm(expression);
403+
if (topEm != null) continue;
404+
405+
case 'width':
406+
widthEm = _getEm(expression);
407+
if (widthEm != null) continue;
408+
290409
default:
291410
// TODO handle more CSS properties
292411
assert(debugLog('Unsupported CSS property: $property of type ${expression.runtimeType}'));
293412
}
294413
}
295414

296415
return KatexSpanStyles(
416+
heightEm: heightEm,
297417
marginLeftEm: marginLeftEm,
298418
marginRightEm: marginRightEm,
299419
paddingLeftEm: paddingLeftEm,
420+
topEm: topEm,
421+
widthEm: widthEm,
300422
);
301423
}
302424
return null;
@@ -326,9 +448,12 @@ enum KatexSpanTextAlign {
326448
}
327449

328450
class KatexSpanStyles {
451+
double? heightEm;
329452
double? marginLeftEm;
330453
double? marginRightEm;
331454
double? paddingLeftEm;
455+
double? topEm;
456+
double? widthEm;
332457

333458
String? fontFamily;
334459
double? fontSizeEm;
@@ -337,9 +462,12 @@ class KatexSpanStyles {
337462
KatexSpanTextAlign? textAlign;
338463

339464
KatexSpanStyles({
465+
this.heightEm,
340466
this.marginLeftEm,
341467
this.marginRightEm,
342468
this.paddingLeftEm,
469+
this.topEm,
470+
this.widthEm,
343471
this.fontFamily,
344472
this.fontSizeEm,
345473
this.fontStyle,
@@ -350,9 +478,12 @@ class KatexSpanStyles {
350478
@override
351479
int get hashCode => Object.hash(
352480
'KatexSpanStyles',
481+
heightEm,
353482
marginLeftEm,
354483
marginRightEm,
355484
paddingLeftEm,
485+
topEm,
486+
widthEm,
356487
fontFamily,
357488
fontSizeEm,
358489
fontStyle,
@@ -363,9 +494,12 @@ class KatexSpanStyles {
363494
@override
364495
bool operator ==(Object other) {
365496
return other is KatexSpanStyles &&
497+
other.heightEm == heightEm &&
366498
other.marginLeftEm == marginLeftEm &&
367499
other.marginRightEm == marginRightEm &&
368500
other.paddingLeftEm == paddingLeftEm &&
501+
other.topEm == topEm &&
502+
other.widthEm == widthEm &&
369503
other.fontFamily == fontFamily &&
370504
other.fontSizeEm == fontSizeEm &&
371505
other.fontStyle == fontStyle &&
@@ -380,9 +514,12 @@ class KatexSpanStyles {
380514
if (this == _zero) return '${objectRuntimeType(this, 'KatexSpanStyles')}()';
381515

382516
final args = <String>[];
517+
if (heightEm != null) args.add('heightEm: $heightEm');
383518
if (marginLeftEm != null) args.add('marginLeftEm: $marginLeftEm');
384519
if (marginRightEm != null) args.add('marginRightEm: $marginRightEm');
385520
if (paddingLeftEm != null) args.add('paddingLeftEm: $paddingLeftEm');
521+
if (topEm != null) args.add('topEm: $topEm');
522+
if (widthEm != null) args.add('width: $widthEm');
386523
if (fontFamily != null) args.add('fontFamily: $fontFamily');
387524
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
388525
if (fontStyle != null) args.add('fontStyle: $fontStyle');
@@ -393,9 +530,12 @@ class KatexSpanStyles {
393530

394531
KatexSpanStyles merge(KatexSpanStyles other) {
395532
return KatexSpanStyles(
533+
heightEm: other.heightEm ?? heightEm,
396534
marginLeftEm: other.marginLeftEm ?? marginLeftEm,
397535
marginRightEm: other.marginRightEm ?? marginRightEm,
398536
paddingLeftEm: other.paddingLeftEm ?? paddingLeftEm,
537+
topEm: other.topEm ?? topEm,
538+
widthEm: other.widthEm ?? widthEm,
399539
fontFamily: other.fontFamily ?? fontFamily,
400540
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
401541
fontStyle: other.fontStyle ?? fontStyle,

lib/widgets/content.dart

+45
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,7 @@ class _Katex extends StatelessWidget {
843843
child: switch (e) {
844844
KatexSpanNode() => _KatexSpan(e),
845845
KatexNegativeMarginNode() => _KatexNegativeMargin(e),
846+
KatexVlistNode() => _KatexVlist(e),
846847
});
847848
}))));
848849

@@ -886,6 +887,7 @@ class _KatexSpan extends StatelessWidget {
886887
child: switch (e) {
887888
KatexSpanNode() => _KatexSpan(e),
888889
KatexNegativeMarginNode() => _KatexNegativeMargin(e),
890+
KatexVlistNode() => _KatexVlist(e),
889891
});
890892
}))));
891893
}
@@ -939,6 +941,11 @@ class _KatexSpan extends StatelessWidget {
939941
padding += EdgeInsets.only(right: paddingLeftEm * em);
940942
}
941943

944+
var offset = Offset.zero;
945+
if (styles.topEm != null) {
946+
offset += Offset(0, styles.topEm! * em);
947+
}
948+
942949
if (textStyle != null || textAlign != null) {
943950
widget = DefaultTextStyle.merge(
944951
style: textStyle,
@@ -948,6 +955,15 @@ class _KatexSpan extends StatelessWidget {
948955
return Container(
949956
margin: margin != EdgeInsets.zero ? margin : null,
950957
padding: padding != EdgeInsets.zero ? padding : null,
958+
transform: offset != Offset.zero
959+
? Matrix4.translationValues(offset.dx, offset.dy, 0)
960+
: null,
961+
height: styles.heightEm != null
962+
? styles.heightEm! * em
963+
: null,
964+
width: styles.widthEm != null
965+
? styles.widthEm! * em
966+
: null,
951967
child: widget,
952968
);
953969
}
@@ -972,6 +988,7 @@ class _KatexNegativeMargin extends StatelessWidget {
972988
child: switch (e) {
973989
KatexSpanNode() => _KatexSpan(e),
974990
KatexNegativeMarginNode() => _KatexNegativeMargin(e),
991+
KatexVlistNode() => _KatexVlist(e),
975992
});
976993
}))));
977994

@@ -984,6 +1001,34 @@ class _KatexNegativeMargin extends StatelessWidget {
9841001
}
9851002
}
9861003

1004+
class _KatexVlist extends StatelessWidget {
1005+
const _KatexVlist(this.node);
1006+
1007+
final KatexVlistNode node;
1008+
1009+
@override
1010+
Widget build(BuildContext context) {
1011+
final em = DefaultTextStyle.of(context).style.fontSize!;
1012+
1013+
return Stack(
1014+
children: List.unmodifiable(node.rows.map((row) {
1015+
return Transform.translate(
1016+
offset: Offset(0, row.verticalOffsetEm * em),
1017+
child: RichText(text: TextSpan(
1018+
children: List.unmodifiable(row.nodes.map((e) {
1019+
return WidgetSpan(
1020+
alignment: PlaceholderAlignment.baseline,
1021+
baseline: TextBaseline.alphabetic,
1022+
child: switch (e) {
1023+
KatexSpanNode() => _KatexSpan(e),
1024+
KatexNegativeMarginNode() => _KatexNegativeMargin(e),
1025+
KatexVlistNode() => _KatexVlist(e),
1026+
});
1027+
})))));
1028+
})));
1029+
}
1030+
}
1031+
9871032
class WebsitePreview extends StatelessWidget {
9881033
const WebsitePreview({super.key, required this.node});
9891034

0 commit comments

Comments
 (0)