Skip to content

Commit e509efe

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

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
@@ -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
});
@@ -879,6 +880,7 @@ class _KatexNodeList extends StatelessWidget {
879880
child: switch (e) {
880881
KatexSpanNode() => _KatexSpan(e),
881882
KatexStrutNode() => _KatexStrut(e),
883+
KatexVlistNode() => _KatexVlist(e),
882884
}));
883885
}))));
884886
}
@@ -1010,6 +1012,23 @@ class _KatexStrut extends StatelessWidget {
10101012
}
10111013
}
10121014

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

@@ -1328,7 +1347,7 @@ class _InlineContentBuilder {
13281347
: WidgetSpan(
13291348
alignment: PlaceholderAlignment.baseline,
13301349
baseline: TextBaseline.alphabetic,
1331-
child: _Katex(textStyle: widget.style, nodes: nodes));
1350+
child: Katex(textStyle: widget.style, nodes: nodes));
13321351

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

0 commit comments

Comments
 (0)