Skip to content

Commit ec14306

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

File tree

5 files changed

+639
-5
lines changed

5 files changed

+639
-5
lines changed

lib/model/content.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,36 @@ class KatexSpanNode extends KatexNode {
406406
}
407407
}
408408

409+
class KatexVlistNode extends KatexNode {
410+
const KatexVlistNode({
411+
required this.rows,
412+
super.debugHtmlNode,
413+
});
414+
415+
final List<KatexVlistRowNode> rows;
416+
417+
@override
418+
List<DiagnosticsNode> debugDescribeChildren() {
419+
return rows.map((row) => row.toDiagnosticsNode()).toList();
420+
}
421+
}
422+
423+
class KatexVlistRowNode extends ContentNode {
424+
const KatexVlistRowNode({
425+
required this.verticalOffsetEm,
426+
required this.node,
427+
});
428+
429+
final double verticalOffsetEm;
430+
final KatexSpanNode node;
431+
432+
@override
433+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
434+
super.debugFillProperties(properties);
435+
properties.add(DoubleProperty('verticalOffsetEm', verticalOffsetEm));
436+
}
437+
}
438+
409439
class MathBlockNode extends MathNode implements BlockContentNode {
410440
const MathBlockNode({
411441
super.debugHtmlNode,

lib/model/katex.dart

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,83 @@ class _KatexParser {
133133
KatexNode _parseSpan(dom.Element element) {
134134
// TODO maybe check if the sequence of ancestors matter for spans.
135135

136+
if (element.className.startsWith('vlist')) {
137+
if (element case dom.Element(
138+
localName: 'span',
139+
className: 'vlist-t' || 'vlist-t vlist-t2',
140+
nodes: [...],
141+
) && final vlistT) {
142+
if (vlistT.attributes.containsKey('style')) throw KatexHtmlParseError();
143+
144+
final hasTwoVlistR = vlistT.className == 'vlist-t vlist-t2';
145+
if (!hasTwoVlistR && vlistT.nodes.length != 1) throw KatexHtmlParseError();
146+
147+
if (hasTwoVlistR) {
148+
if (vlistT.nodes case [
149+
_,
150+
dom.Element(localName: 'span', className: 'vlist-r', nodes: [
151+
dom.Element(localName: 'span', className: 'vlist', nodes: [
152+
dom.Element(localName: 'span', className: '', nodes: []),
153+
]),
154+
]),
155+
]) {
156+
// Do nothing.
157+
} else {
158+
throw KatexHtmlParseError();
159+
}
160+
}
161+
162+
if (vlistT.nodes.first
163+
case dom.Element(localName: 'span', className: 'vlist-r') &&
164+
final vlistR) {
165+
if (vlistR.attributes.containsKey('style')) throw KatexHtmlParseError();
166+
167+
if (vlistR.nodes.first
168+
case dom.Element(localName: 'span', className: 'vlist') &&
169+
final vlist) {
170+
final rows = <KatexVlistRowNode>[];
171+
172+
for (final innerSpan in vlist.nodes) {
173+
if (innerSpan case dom.Element(
174+
localName: 'span',
175+
className: '',
176+
nodes: [
177+
dom.Element(localName: 'span', className: 'pstrut') &&
178+
final pstrutSpan,
179+
...final otherSpans,
180+
],
181+
)) {
182+
var styles = _parseSpanInlineStyles(innerSpan)!;
183+
final topEm = styles.topEm ?? 0;
184+
185+
styles = styles.filter(topEm: false);
186+
187+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
188+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
189+
190+
rows.add(KatexVlistRowNode(
191+
verticalOffsetEm: topEm + pstrutHeight,
192+
node: KatexSpanNode(
193+
styles: styles,
194+
text: null,
195+
nodes: _parseChildSpans(otherSpans))));
196+
} else {
197+
throw KatexHtmlParseError();
198+
}
199+
}
200+
201+
return KatexVlistNode(rows: rows);
202+
} else {
203+
throw KatexHtmlParseError();
204+
}
205+
} else {
206+
throw KatexHtmlParseError();
207+
}
208+
} else {
209+
throw KatexHtmlParseError();
210+
}
211+
}
212+
136213
// Aggregate the CSS styles that apply, in the same order as the CSS
137214
// classes specified for this span, mimicking the behaviour on web.
138215
//
@@ -141,7 +218,9 @@ class _KatexParser {
141218
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
142219
// A copy of class definition (where possible) is accompanied in a comment
143220
// with each case statement to keep track of updates.
144-
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
221+
final spanClasses = element.className != ''
222+
? List<String>.unmodifiable(element.className.split(' '))
223+
: const <String>[];
145224
String? fontFamily;
146225
double? fontSizeEm;
147226
KatexSpanFontWeight? fontWeight;
@@ -567,6 +646,32 @@ class KatexSpanStyles {
567646
textAlign: other.textAlign ?? textAlign,
568647
);
569648
}
649+
650+
KatexSpanStyles filter({
651+
bool heightEm = true,
652+
bool verticalAlignEm = true,
653+
bool topEm = true,
654+
bool marginRightEm = true,
655+
bool marginLeftEm = true,
656+
bool fontFamily = true,
657+
bool fontSizeEm = true,
658+
bool fontWeight = true,
659+
bool fontStyle = true,
660+
bool textAlign = true,
661+
}) {
662+
return KatexSpanStyles(
663+
heightEm: heightEm ? this.heightEm : null,
664+
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
665+
topEm: topEm ? this.topEm : null,
666+
marginRightEm: marginRightEm ? this.marginRightEm : null,
667+
marginLeftEm: marginLeftEm ? this.marginLeftEm : null,
668+
fontFamily: fontFamily ? this.fontFamily : null,
669+
fontSizeEm: fontSizeEm ? this.fontSizeEm : null,
670+
fontWeight: fontWeight ? this.fontWeight : null,
671+
fontStyle: fontStyle ? this.fontStyle : null,
672+
textAlign: textAlign ? this.textAlign : null,
673+
);
674+
}
570675
}
571676

572677
class KatexHtmlParseError extends Error {

lib/widgets/content.dart

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ class MathBlock extends StatelessWidget {
820820
return Center(
821821
child: SingleChildScrollViewWithScrollbar(
822822
scrollDirection: Axis.horizontal,
823-
child: _Katex(
823+
child: Katex(
824824
textStyle: ContentTheme.of(context).textStylePlainParagraph,
825825
nodes: nodes)));
826826
}
@@ -841,8 +841,9 @@ TextStyle mkBaseKatexTextStyle(TextStyle style) {
841841
fontStyle: FontStyle.normal);
842842
}
843843

844-
class _Katex extends StatelessWidget {
845-
const _Katex({
844+
class Katex extends StatelessWidget {
845+
const Katex({
846+
super.key,
846847
required this.textStyle,
847848
required this.nodes,
848849
});
@@ -876,6 +877,7 @@ class _KatexNodeList extends StatelessWidget {
876877
baseline: TextBaseline.alphabetic,
877878
child: switch (e) {
878879
KatexSpanNode() => _KatexSpan(e),
880+
KatexVlistNode() => _KatexVlist(e),
879881
});
880882
}))));
881883
}
@@ -993,6 +995,23 @@ class _KatexSpan extends StatelessWidget {
993995
}
994996
}
995997

998+
class _KatexVlist extends StatelessWidget {
999+
const _KatexVlist(this.node);
1000+
1001+
final KatexVlistNode node;
1002+
1003+
@override
1004+
Widget build(BuildContext context) {
1005+
final em = DefaultTextStyle.of(context).style.fontSize!;
1006+
1007+
return Stack(children: List.unmodifiable(node.rows.map((row) {
1008+
return Transform.translate(
1009+
offset: Offset(0, row.verticalOffsetEm * em),
1010+
child: _KatexSpan(row.node));
1011+
})));
1012+
}
1013+
}
1014+
9961015
class WebsitePreview extends StatelessWidget {
9971016
const WebsitePreview({super.key, required this.node});
9981017

@@ -1311,7 +1330,7 @@ class _InlineContentBuilder {
13111330
: WidgetSpan(
13121331
alignment: PlaceholderAlignment.baseline,
13131332
baseline: TextBaseline.alphabetic,
1314-
child: _Katex(textStyle: widget.style, nodes: nodes));
1333+
child: Katex(textStyle: widget.style, nodes: nodes));
13151334

13161335
case GlobalTimeNode():
13171336
return WidgetSpan(alignment: PlaceholderAlignment.middle,

0 commit comments

Comments
 (0)