Skip to content

Commit 5fe01af

Browse files
content: Handle vertical offset spans in KaTeX content
Implement handling most common types of `vlist` spans.
1 parent 8f94554 commit 5fe01af

File tree

5 files changed

+542
-1
lines changed

5 files changed

+542
-1
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: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,89 @@ class _KatexParser {
209209
debugHtmlNode: debugHtmlNode);
210210
}
211211

212+
if (element.className == 'vlist-t'
213+
|| element.className == 'vlist-t vlist-t2') {
214+
final vlistT = element;
215+
if (vlistT.nodes.isEmpty) throw _KatexHtmlParseError();
216+
if (vlistT.attributes.containsKey('style')) throw _KatexHtmlParseError();
217+
218+
final hasTwoVlistR = vlistT.className == 'vlist-t vlist-t2';
219+
if (!hasTwoVlistR && vlistT.nodes.length != 1) throw _KatexHtmlParseError();
220+
221+
if (hasTwoVlistR) {
222+
if (vlistT.nodes case [
223+
_,
224+
dom.Element(localName: 'span', className: 'vlist-r', nodes: [
225+
dom.Element(localName: 'span', className: 'vlist', nodes: [
226+
dom.Element(localName: 'span', className: '', nodes: []),
227+
]),
228+
]),
229+
]) {
230+
// Do nothing.
231+
} else {
232+
throw _KatexHtmlParseError();
233+
}
234+
}
235+
236+
if (vlistT.nodes.first
237+
case dom.Element(localName: 'span', className: 'vlist-r') &&
238+
final vlistR) {
239+
if (vlistR.attributes.containsKey('style')) throw _KatexHtmlParseError();
240+
241+
if (vlistR.nodes.first
242+
case dom.Element(localName: 'span', className: 'vlist') &&
243+
final vlist) {
244+
final rows = <KatexVlistRowNode>[];
245+
246+
for (final innerSpan in vlist.nodes) {
247+
if (innerSpan case dom.Element(
248+
localName: 'span',
249+
className: '',
250+
nodes: [
251+
dom.Element(localName: 'span', className: 'pstrut') &&
252+
final pstrutSpan,
253+
...final otherSpans,
254+
],
255+
)) {
256+
var styles = _parseSpanInlineStyles(innerSpan);
257+
if (styles == null) throw _KatexHtmlParseError();
258+
if (styles.verticalAlignEm != null) throw _KatexHtmlParseError();
259+
final topEm = styles.topEm ?? 0;
260+
261+
styles = styles.filter(topEm: false);
262+
263+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan);
264+
if (pstrutStyles == null) throw _KatexHtmlParseError();
265+
if (pstrutStyles.filter(heightEm: false)
266+
!= const KatexSpanStyles()) {
267+
throw _KatexHtmlParseError();
268+
}
269+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
270+
271+
rows.add(KatexVlistRowNode(
272+
verticalOffsetEm: topEm + pstrutHeight,
273+
debugHtmlNode: kDebugMode ? innerSpan : null,
274+
node: KatexSpanNode(
275+
styles: styles,
276+
text: null,
277+
nodes: _parseChildSpans(otherSpans))));
278+
} else {
279+
throw _KatexHtmlParseError();
280+
}
281+
}
282+
283+
return KatexVlistNode(
284+
rows: rows,
285+
debugHtmlNode: kDebugMode ? vlistT : null,
286+
);
287+
} else {
288+
throw _KatexHtmlParseError();
289+
}
290+
} else {
291+
throw _KatexHtmlParseError();
292+
}
293+
}
294+
212295
final inlineStyles = _parseSpanInlineStyles(element);
213296
if (inlineStyles != null) {
214297
// We expect `vertical-align` inline style to be only present on a
@@ -224,7 +307,9 @@ class _KatexParser {
224307
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
225308
// A copy of class definition (where possible) is accompanied in a comment
226309
// with each case statement to keep track of updates.
227-
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
310+
final spanClasses = element.className != ''
311+
? List<String>.unmodifiable(element.className.split(' '))
312+
: const <String>[];
228313
String? fontFamily;
229314
double? fontSizeEm;
230315
KatexSpanFontWeight? fontWeight;

lib/widgets/content.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,7 @@ class _KatexNodeList extends StatelessWidget {
893893
child: switch (e) {
894894
KatexSpanNode() => _KatexSpan(e),
895895
KatexStrutNode() => _KatexStrut(e),
896+
KatexVlistNode() => _KatexVlist(e),
896897
}));
897898
}))));
898899
}
@@ -1025,6 +1026,23 @@ class _KatexStrut extends StatelessWidget {
10251026
}
10261027
}
10271028

1029+
class _KatexVlist extends StatelessWidget {
1030+
const _KatexVlist(this.node);
1031+
1032+
final KatexVlistNode node;
1033+
1034+
@override
1035+
Widget build(BuildContext context) {
1036+
final em = DefaultTextStyle.of(context).style.fontSize!;
1037+
1038+
return Stack(children: List.unmodifiable(node.rows.map((row) {
1039+
return Transform.translate(
1040+
offset: Offset(0, row.verticalOffsetEm * em),
1041+
child: _KatexSpan(row.node));
1042+
})));
1043+
}
1044+
}
1045+
10281046
class WebsitePreview extends StatelessWidget {
10291047
const WebsitePreview({super.key, required this.node});
10301048

0 commit comments

Comments
 (0)