Skip to content

Commit 9827271

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

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
@@ -429,6 +429,37 @@ class KatexStrutNode extends KatexNode {
429429
}
430430
}
431431

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

lib/model/katex.dart

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,87 @@ class _KatexParser {
208208
}
209209
}
210210

211+
if (element.className.startsWith('vlist')) {
212+
if (element case dom.Element(
213+
localName: 'span',
214+
className: 'vlist-t' || 'vlist-t vlist-t2',
215+
nodes: [...],
216+
) && final vlistT) {
217+
if (vlistT.attributes.containsKey('style')) throw _KatexHtmlParseError();
218+
219+
final hasTwoVlistR = vlistT.className == 'vlist-t vlist-t2';
220+
if (!hasTwoVlistR && vlistT.nodes.length != 1) throw _KatexHtmlParseError();
221+
222+
if (hasTwoVlistR) {
223+
if (vlistT.nodes case [
224+
_,
225+
dom.Element(localName: 'span', className: 'vlist-r', nodes: [
226+
dom.Element(localName: 'span', className: 'vlist', nodes: [
227+
dom.Element(localName: 'span', className: '', nodes: []),
228+
]),
229+
]),
230+
]) {
231+
// Do nothing.
232+
} else {
233+
throw _KatexHtmlParseError();
234+
}
235+
}
236+
237+
if (vlistT.nodes.first
238+
case dom.Element(localName: 'span', className: 'vlist-r') &&
239+
final vlistR) {
240+
if (vlistR.attributes.containsKey('style')) throw _KatexHtmlParseError();
241+
242+
if (vlistR.nodes.first
243+
case dom.Element(localName: 'span', className: 'vlist') &&
244+
final vlist) {
245+
final rows = <KatexVlistRowNode>[];
246+
247+
for (final innerSpan in vlist.nodes) {
248+
if (innerSpan case dom.Element(
249+
localName: 'span',
250+
className: '',
251+
nodes: [
252+
dom.Element(localName: 'span', className: 'pstrut') &&
253+
final pstrutSpan,
254+
...final otherSpans,
255+
],
256+
)) {
257+
var styles = _parseSpanInlineStyles(innerSpan)!;
258+
final topEm = styles.topEm ?? 0;
259+
260+
styles = styles.filter(topEm: false);
261+
262+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
263+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
264+
265+
rows.add(KatexVlistRowNode(
266+
verticalOffsetEm: topEm + pstrutHeight,
267+
debugHtmlNode: kDebugMode ? innerSpan : null,
268+
node: KatexSpanNode(
269+
styles: styles,
270+
text: null,
271+
nodes: _parseChildSpans(otherSpans))));
272+
} else {
273+
throw _KatexHtmlParseError();
274+
}
275+
}
276+
277+
return KatexVlistNode(
278+
rows: rows,
279+
debugHtmlNode: kDebugMode ? vlistT : null,
280+
);
281+
} else {
282+
throw _KatexHtmlParseError();
283+
}
284+
} else {
285+
throw _KatexHtmlParseError();
286+
}
287+
} else {
288+
throw _KatexHtmlParseError();
289+
}
290+
}
291+
211292
final debugHtmlNode = kDebugMode ? element : null;
212293

213294
final inlineStyles = _parseSpanInlineStyles(element);
@@ -225,7 +306,9 @@ class _KatexParser {
225306
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
226307
// A copy of class definition (where possible) is accompanied in a comment
227308
// with each case statement to keep track of updates.
228-
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
309+
final spanClasses = element.className != ''
310+
? List<String>.unmodifiable(element.className.split(' '))
311+
: const <String>[];
229312
String? fontFamily;
230313
double? fontSizeEm;
231314
KatexSpanFontWeight? fontWeight;

lib/widgets/content.dart

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ class MathBlock extends StatelessWidget {
823823
return Center(
824824
child: SingleChildScrollViewWithScrollbar(
825825
scrollDirection: Axis.horizontal,
826-
child: _Katex(
826+
child: Katex(
827827
textStyle: ContentTheme.of(context).textStylePlainParagraph,
828828
nodes: nodes)));
829829
}
@@ -844,8 +844,9 @@ TextStyle mkBaseKatexTextStyle(TextStyle style) {
844844
fontStyle: FontStyle.normal);
845845
}
846846

847-
class _Katex extends StatelessWidget {
848-
const _Katex({
847+
class Katex extends StatelessWidget {
848+
const Katex({
849+
super.key,
849850
required this.textStyle,
850851
required this.nodes,
851852
});
@@ -882,6 +883,7 @@ class _KatexNodeList extends StatelessWidget {
882883
child: switch (e) {
883884
KatexSpanNode() => _KatexSpan(e),
884885
KatexStrutNode() => _KatexStrut(e),
886+
KatexVlistNode() => _KatexVlist(e),
885887
}));
886888
}))));
887889
}
@@ -1012,6 +1014,23 @@ class _KatexStrut extends StatelessWidget {
10121014
}
10131015
}
10141016

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

@@ -1330,7 +1349,7 @@ class _InlineContentBuilder {
13301349
: WidgetSpan(
13311350
alignment: PlaceholderAlignment.baseline,
13321351
baseline: TextBaseline.alphabetic,
1333-
child: _Katex(textStyle: widget.style, nodes: nodes));
1352+
child: Katex(textStyle: widget.style, nodes: nodes));
13341353

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

0 commit comments

Comments
 (0)