Skip to content

Commit e78894e

Browse files
rajveermalviyagnprice
authored andcommitted
content: Handle vertical offset spans in KaTeX content
Implement handling most common types of `vlist` spans.
1 parent 1fe817c commit e78894e

File tree

5 files changed

+565
-1
lines changed

5 files changed

+565
-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: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,98 @@ 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
215298
// `strut` span, for which we emit `KatexStrutNode` separately.
216299
if (inlineStyles.verticalAlignEm != null) throw _KatexHtmlParseError();
300+
301+
// Currently, we expect `top` to only be inside a vlist, and
302+
// we handle that case separately above.
303+
if (inlineStyles.topEm != null) throw _KatexHtmlParseError();
217304
}
218305

219306
// Aggregate the CSS styles that apply, in the same order as the CSS
@@ -224,7 +311,9 @@ class _KatexParser {
224311
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
225312
// A copy of class definition (where possible) is accompanied in a comment
226313
// with each case statement to keep track of updates.
227-
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
314+
final spanClasses = element.className != ''
315+
? List<String>.unmodifiable(element.className.split(' '))
316+
: const <String>[];
228317
String? fontFamily;
229318
double? fontSizeEm;
230319
KatexSpanFontWeight? fontWeight;
@@ -492,6 +581,7 @@ class _KatexParser {
492581
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
493582
double? heightEm;
494583
double? verticalAlignEm;
584+
double? topEm;
495585
double? marginRightEm;
496586
double? marginLeftEm;
497587

@@ -510,6 +600,10 @@ class _KatexParser {
510600
verticalAlignEm = _getEm(expression);
511601
if (verticalAlignEm != null) continue;
512602

603+
case 'top':
604+
topEm = _getEm(expression);
605+
if (topEm != null) continue;
606+
513607
case 'margin-right':
514608
marginRightEm = _getEm(expression);
515609
if (marginRightEm != null) {
@@ -537,6 +631,7 @@ class _KatexParser {
537631

538632
return KatexSpanStyles(
539633
heightEm: heightEm,
634+
topEm: topEm,
540635
verticalAlignEm: verticalAlignEm,
541636
marginRightEm: marginRightEm,
542637
marginLeftEm: marginLeftEm,
@@ -578,6 +673,8 @@ class KatexSpanStyles {
578673
final double? heightEm;
579674
final double? verticalAlignEm;
580675

676+
final double? topEm;
677+
581678
final double? marginRightEm;
582679
final double? marginLeftEm;
583680

@@ -590,6 +687,7 @@ class KatexSpanStyles {
590687
const KatexSpanStyles({
591688
this.heightEm,
592689
this.verticalAlignEm,
690+
this.topEm,
593691
this.marginRightEm,
594692
this.marginLeftEm,
595693
this.fontFamily,
@@ -604,6 +702,7 @@ class KatexSpanStyles {
604702
'KatexSpanStyles',
605703
heightEm,
606704
verticalAlignEm,
705+
topEm,
607706
marginRightEm,
608707
marginLeftEm,
609708
fontFamily,
@@ -618,6 +717,7 @@ class KatexSpanStyles {
618717
return other is KatexSpanStyles &&
619718
other.heightEm == heightEm &&
620719
other.verticalAlignEm == verticalAlignEm &&
720+
other.topEm == topEm &&
621721
other.marginRightEm == marginRightEm &&
622722
other.marginLeftEm == marginLeftEm &&
623723
other.fontFamily == fontFamily &&
@@ -632,6 +732,7 @@ class KatexSpanStyles {
632732
final args = <String>[];
633733
if (heightEm != null) args.add('heightEm: $heightEm');
634734
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
735+
if (topEm != null) args.add('topEm: $topEm');
635736
if (marginRightEm != null) args.add('marginRightEm: $marginRightEm');
636737
if (marginLeftEm != null) args.add('marginLeftEm: $marginLeftEm');
637738
if (fontFamily != null) args.add('fontFamily: $fontFamily');
@@ -653,6 +754,7 @@ class KatexSpanStyles {
653754
return KatexSpanStyles(
654755
heightEm: other.heightEm ?? heightEm,
655756
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
757+
topEm: other.topEm ?? topEm,
656758
marginRightEm: other.marginRightEm ?? marginRightEm,
657759
marginLeftEm: other.marginLeftEm ?? marginLeftEm,
658760
fontFamily: other.fontFamily ?? fontFamily,
@@ -666,6 +768,7 @@ class KatexSpanStyles {
666768
KatexSpanStyles filter({
667769
bool heightEm = true,
668770
bool verticalAlignEm = true,
771+
bool topEm = true,
669772
bool marginRightEm = true,
670773
bool marginLeftEm = true,
671774
bool fontFamily = true,
@@ -677,6 +780,7 @@ class KatexSpanStyles {
677780
return KatexSpanStyles(
678781
heightEm: heightEm ? this.heightEm : null,
679782
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
783+
topEm: topEm ? this.topEm : null,
680784
marginRightEm: marginRightEm ? this.marginRightEm : null,
681785
marginLeftEm: marginLeftEm ? this.marginLeftEm : null,
682786
fontFamily: fontFamily ? this.fontFamily : null,

lib/widgets/content.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,7 @@ class _KatexNodeList extends StatelessWidget {
897897
child: switch (e) {
898898
KatexSpanNode() => _KatexSpan(e),
899899
KatexStrutNode() => _KatexStrut(e),
900+
KatexVlistNode() => _KatexVlist(e),
900901
}));
901902
}))));
902903
}
@@ -924,6 +925,10 @@ class _KatexSpan extends StatelessWidget {
924925
// So, this should always be null for non `strut` spans.
925926
assert(styles.verticalAlignEm == null);
926927

928+
// Currently, we expect `top` to be only present with the
929+
// vlist inner row span, and parser handles that explicitly.
930+
assert(styles.topEm == null);
931+
927932
final fontFamily = styles.fontFamily;
928933
final fontSize = switch (styles.fontSizeEm) {
929934
double fontSizeEm => fontSizeEm * em,
@@ -1024,6 +1029,23 @@ class _KatexStrut extends StatelessWidget {
10241029
}
10251030
}
10261031

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

0 commit comments

Comments
 (0)