Skip to content

Commit c97467e

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

File tree

5 files changed

+623
-5
lines changed

5 files changed

+623
-5
lines changed

lib/model/content.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,37 @@ class KatexStrutNode extends KatexNode {
424424
}
425425
}
426426

427+
class KatexVlistNode extends KatexNode {
428+
const KatexVlistNode({
429+
required this.rows,
430+
super.debugHtmlNode,
431+
});
432+
433+
final List<KatexVlistRowNode> rows;
434+
435+
@override
436+
List<DiagnosticsNode> debugDescribeChildren() {
437+
return rows.map((row) => row.toDiagnosticsNode()).toList();
438+
}
439+
}
440+
441+
class KatexVlistRowNode extends ContentNode {
442+
const KatexVlistRowNode({
443+
required this.verticalOffsetEm,
444+
required this.node,
445+
super.debugHtmlNode,
446+
});
447+
448+
final double verticalOffsetEm;
449+
final KatexSpanNode node;
450+
451+
@override
452+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
453+
super.debugFillProperties(properties);
454+
properties.add(DoubleProperty('verticalOffsetEm', verticalOffsetEm));
455+
}
456+
}
457+
427458
class MathBlockNode extends MathNode implements BlockContentNode {
428459
const MathBlockNode({
429460
super.debugHtmlNode,

lib/model/katex.dart

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,87 @@ class _KatexParser {
156156
}
157157
}
158158

159+
if (element.className.startsWith('vlist')) {
160+
if (element case dom.Element(
161+
localName: 'span',
162+
className: 'vlist-t' || 'vlist-t vlist-t2',
163+
nodes: [...],
164+
) && final vlistT) {
165+
if (vlistT.attributes.containsKey('style')) throw KatexHtmlParseError();
166+
167+
final hasTwoVlistR = vlistT.className == 'vlist-t vlist-t2';
168+
if (!hasTwoVlistR && vlistT.nodes.length != 1) throw KatexHtmlParseError();
169+
170+
if (hasTwoVlistR) {
171+
if (vlistT.nodes case [
172+
_,
173+
dom.Element(localName: 'span', className: 'vlist-r', nodes: [
174+
dom.Element(localName: 'span', className: 'vlist', nodes: [
175+
dom.Element(localName: 'span', className: '', nodes: []),
176+
]),
177+
]),
178+
]) {
179+
// Do nothing.
180+
} else {
181+
throw KatexHtmlParseError();
182+
}
183+
}
184+
185+
if (vlistT.nodes.first
186+
case dom.Element(localName: 'span', className: 'vlist-r') &&
187+
final vlistR) {
188+
if (vlistR.attributes.containsKey('style')) throw KatexHtmlParseError();
189+
190+
if (vlistR.nodes.first
191+
case dom.Element(localName: 'span', className: 'vlist') &&
192+
final vlist) {
193+
final rows = <KatexVlistRowNode>[];
194+
195+
for (final innerSpan in vlist.nodes) {
196+
if (innerSpan case dom.Element(
197+
localName: 'span',
198+
className: '',
199+
nodes: [
200+
dom.Element(localName: 'span', className: 'pstrut') &&
201+
final pstrutSpan,
202+
...final otherSpans,
203+
],
204+
)) {
205+
var styles = _parseSpanInlineStyles(innerSpan)!;
206+
final topEm = styles.topEm ?? 0;
207+
208+
styles = styles.filter(topEm: false);
209+
210+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
211+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
212+
213+
rows.add(KatexVlistRowNode(
214+
verticalOffsetEm: topEm + pstrutHeight,
215+
debugHtmlNode: kDebugMode ? innerSpan : null,
216+
node: KatexSpanNode(
217+
styles: styles,
218+
text: null,
219+
nodes: _parseChildSpans(otherSpans))));
220+
} else {
221+
throw KatexHtmlParseError();
222+
}
223+
}
224+
225+
return KatexVlistNode(
226+
rows: rows,
227+
debugHtmlNode: kDebugMode ? vlistT : null,
228+
);
229+
} else {
230+
throw KatexHtmlParseError();
231+
}
232+
} else {
233+
throw KatexHtmlParseError();
234+
}
235+
} else {
236+
throw KatexHtmlParseError();
237+
}
238+
}
239+
159240
final debugHtmlNode = kDebugMode ? element : null;
160241

161242
final inlineStyles = _parseSpanInlineStyles(element);
@@ -173,7 +254,9 @@ class _KatexParser {
173254
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
174255
// A copy of class definition (where possible) is accompanied in a comment
175256
// with each case statement to keep track of updates.
176-
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
257+
final spanClasses = element.className != ''
258+
? List<String>.unmodifiable(element.className.split(' '))
259+
: const <String>[];
177260
String? fontFamily;
178261
double? fontSizeEm;
179262
KatexSpanFontWeight? fontWeight;

lib/widgets/content.dart

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

845-
class _Katex extends StatelessWidget {
846-
const _Katex({
845+
class Katex extends StatelessWidget {
846+
const Katex({
847+
super.key,
847848
required this.textStyle,
848849
required this.nodes,
849850
});
@@ -880,6 +881,7 @@ class _KatexNodeList extends StatelessWidget {
880881
child: switch (e) {
881882
KatexSpanNode() => _KatexSpan(e),
882883
KatexStrutNode() => _KatexStrut(e),
884+
KatexVlistNode() => _KatexVlist(e),
883885
}));
884886
}))));
885887
}
@@ -1011,6 +1013,23 @@ class _KatexStrut extends StatelessWidget {
10111013
}
10121014
}
10131015

1016+
class _KatexVlist extends StatelessWidget {
1017+
const _KatexVlist(this.node);
1018+
1019+
final KatexVlistNode node;
1020+
1021+
@override
1022+
Widget build(BuildContext context) {
1023+
final em = DefaultTextStyle.of(context).style.fontSize!;
1024+
1025+
return Stack(children: List.unmodifiable(node.rows.map((row) {
1026+
return Transform.translate(
1027+
offset: Offset(0, row.verticalOffsetEm * em),
1028+
child: _KatexSpan(row.node));
1029+
})));
1030+
}
1031+
}
1032+
10141033
class WebsitePreview extends StatelessWidget {
10151034
const WebsitePreview({super.key, required this.node});
10161035

@@ -1329,7 +1348,7 @@ class _InlineContentBuilder {
13291348
: WidgetSpan(
13301349
alignment: PlaceholderAlignment.baseline,
13311350
baseline: TextBaseline.alphabetic,
1332-
child: _Katex(textStyle: widget.style, nodes: nodes));
1351+
child: Katex(textStyle: widget.style, nodes: nodes));
13331352

13341353
case GlobalTimeNode():
13351354
return WidgetSpan(alignment: PlaceholderAlignment.middle,

0 commit comments

Comments
 (0)