Skip to content

Commit d81008f

Browse files
content: Handle 'strut' span in KaTeX content
In KaTeX HTML it is used to set the baseline of the content in a span, so handle it separately here.
1 parent 0434863 commit d81008f

File tree

5 files changed

+119
-29
lines changed

5 files changed

+119
-29
lines changed

lib/model/content.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,24 @@ class KatexSpanNode extends KatexNode {
406406
}
407407
}
408408

409+
class KatexStrutNode extends KatexNode {
410+
const KatexStrutNode({
411+
required this.heightEm,
412+
required this.verticalAlignEm,
413+
super.debugHtmlNode,
414+
});
415+
416+
final double heightEm;
417+
final double? verticalAlignEm;
418+
419+
@override
420+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
421+
super.debugFillProperties(properties);
422+
properties.add(DoubleProperty('heightEm', heightEm));
423+
properties.add(DoubleProperty('verticalAlignEm', verticalAlignEm));
424+
}
425+
}
426+
409427
class MathBlockNode extends MathNode implements BlockContentNode {
410428
const MathBlockNode({
411429
super.debugHtmlNode,

lib/model/katex.dart

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,37 @@ class _KatexParser {
133133
KatexNode _parseSpan(dom.Element element) {
134134
// TODO maybe check if the sequence of ancestors matter for spans.
135135

136+
if (element.className.startsWith('strut')) {
137+
if (element.className == 'strut' && element.nodes.isEmpty) {
138+
final styles = _parseSpanInlineStyles(element);
139+
if (styles == null) throw KatexHtmlParseError();
140+
141+
final heightEm = styles.heightEm;
142+
if (heightEm == null) throw KatexHtmlParseError();
143+
final verticalAlignEm = styles.verticalAlignEm;
144+
145+
// Ensure only `height` and `vertical-align` inline styles are present.
146+
if (styles.filter(heightEm: false, verticalAlignEm: false) !=
147+
KatexSpanStyles()) {
148+
throw KatexHtmlParseError();
149+
}
150+
151+
return KatexStrutNode(
152+
heightEm: heightEm,
153+
verticalAlignEm: verticalAlignEm);
154+
} else {
155+
throw KatexHtmlParseError();
156+
}
157+
}
158+
136159
final debugHtmlNode = kDebugMode ? element : null;
137160

138161
final inlineStyles = _parseSpanInlineStyles(element);
162+
if (inlineStyles != null) {
163+
// We expect `vertical-align` inline style to be only present on a
164+
// `strut` span, for which we emit `KatexStrutNode` separately.
165+
if (inlineStyles.verticalAlignEm != null) throw KatexHtmlParseError();
166+
}
139167

140168
// Aggregate the CSS styles that apply, in the same order as the CSS
141169
// classes specified for this span, mimicking the behaviour on web.
@@ -162,8 +190,9 @@ class _KatexParser {
162190

163191
case 'strut':
164192
// .strut { ... }
165-
// Do nothing, it has properties that don't need special handling.
166-
break;
193+
// We expect the 'strut' class to be the only class in a span,
194+
// in which case we handle it separately and emit `KatexStrutNode`.
195+
throw KatexHtmlParseError();
167196

168197
case 'textbf':
169198
// .textbf { font-weight: bold; }
@@ -394,6 +423,7 @@ class _KatexParser {
394423
final stylesheet = css_parser.parse('*{$styleStr}');
395424
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
396425
double? heightEm;
426+
double? verticalAlignEm;
397427

398428
for (final declaration in rule.declarationGroup.declarations) {
399429
if (declaration case css_visitor.Declaration(
@@ -405,6 +435,10 @@ class _KatexParser {
405435
case 'height':
406436
heightEm = _getEm(expression);
407437
if (heightEm != null) continue;
438+
439+
case 'vertical-align':
440+
verticalAlignEm = _getEm(expression);
441+
if (verticalAlignEm != null) continue;
408442
}
409443

410444
// TODO handle more CSS properties
@@ -418,6 +452,7 @@ class _KatexParser {
418452

419453
return KatexSpanStyles(
420454
heightEm: heightEm,
455+
verticalAlignEm: verticalAlignEm,
421456
);
422457
} else {
423458
throw KatexHtmlParseError();
@@ -454,6 +489,7 @@ enum KatexSpanTextAlign {
454489
@immutable
455490
class KatexSpanStyles {
456491
final double? heightEm;
492+
final double? verticalAlignEm;
457493

458494
final String? fontFamily;
459495
final double? fontSizeEm;
@@ -463,6 +499,7 @@ class KatexSpanStyles {
463499

464500
const KatexSpanStyles({
465501
this.heightEm,
502+
this.verticalAlignEm,
466503
this.fontFamily,
467504
this.fontSizeEm,
468505
this.fontWeight,
@@ -474,6 +511,7 @@ class KatexSpanStyles {
474511
int get hashCode => Object.hash(
475512
'KatexSpanStyles',
476513
heightEm,
514+
verticalAlignEm,
477515
fontFamily,
478516
fontSizeEm,
479517
fontWeight,
@@ -485,6 +523,7 @@ class KatexSpanStyles {
485523
bool operator ==(Object other) {
486524
return other is KatexSpanStyles &&
487525
other.heightEm == heightEm &&
526+
other.verticalAlignEm == verticalAlignEm &&
488527
other.fontFamily == fontFamily &&
489528
other.fontSizeEm == fontSizeEm &&
490529
other.fontWeight == fontWeight &&
@@ -496,6 +535,7 @@ class KatexSpanStyles {
496535
String toString() {
497536
final args = <String>[];
498537
if (heightEm != null) args.add('heightEm: $heightEm');
538+
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
499539
if (fontFamily != null) args.add('fontFamily: $fontFamily');
500540
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
501541
if (fontWeight != null) args.add('fontWeight: $fontWeight');
@@ -507,13 +547,34 @@ class KatexSpanStyles {
507547
KatexSpanStyles merge(KatexSpanStyles other) {
508548
return KatexSpanStyles(
509549
heightEm: other.heightEm ?? heightEm,
550+
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
510551
fontFamily: other.fontFamily ?? fontFamily,
511552
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
512553
fontStyle: other.fontStyle ?? fontStyle,
513554
fontWeight: other.fontWeight ?? fontWeight,
514555
textAlign: other.textAlign ?? textAlign,
515556
);
516557
}
558+
559+
KatexSpanStyles filter({
560+
bool heightEm = true,
561+
bool verticalAlignEm = true,
562+
bool fontFamily = true,
563+
bool fontSizeEm = true,
564+
bool fontWeight = true,
565+
bool fontStyle = true,
566+
bool textAlign = true,
567+
}) {
568+
return KatexSpanStyles(
569+
heightEm: heightEm ? this.heightEm : null,
570+
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
571+
fontFamily: fontFamily ? this.fontFamily : null,
572+
fontSizeEm: fontSizeEm ? this.fontSizeEm : null,
573+
fontWeight: fontWeight ? this.fontWeight : null,
574+
fontStyle: fontStyle ? this.fontStyle : null,
575+
textAlign: textAlign ? this.textAlign : null,
576+
);
577+
}
517578
}
518579

519580
class KatexHtmlParseError extends Error {

lib/widgets/content.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,7 @@ class _KatexNodeList extends StatelessWidget {
879879
data: MediaQueryData(textScaler: TextScaler.noScaling),
880880
child: switch (e) {
881881
KatexSpanNode() => _KatexSpan(e),
882+
KatexStrutNode() => _KatexStrut(e),
882883
}));
883884
}))));
884885
}
@@ -960,6 +961,30 @@ class _KatexSpan extends StatelessWidget {
960961
}
961962
}
962963

964+
class _KatexStrut extends StatelessWidget {
965+
const _KatexStrut(this.node);
966+
967+
final KatexStrutNode node;
968+
969+
@override
970+
Widget build(BuildContext context) {
971+
final em = DefaultTextStyle.of(context).style.fontSize!;
972+
973+
final verticalAlignEm = node.verticalAlignEm;
974+
if (verticalAlignEm == null) {
975+
return SizedBox(height: node.heightEm * em);
976+
}
977+
978+
return SizedBox(
979+
height: node.heightEm * em,
980+
child: Baseline(
981+
baseline: (verticalAlignEm + node.heightEm) * em,
982+
baselineType: TextBaseline.alphabetic,
983+
child: const Text('')),
984+
);
985+
}
986+
}
987+
963988
class WebsitePreview extends StatelessWidget {
964989
const WebsitePreview({super.key, required this.node});
965990

test/model/content_test.dart

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ class ContentExample {
519519
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span></span></span></span></p>',
520520
MathInlineNode(texSource: r'\lambda', nodes: [
521521
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
522-
KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
522+
KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
523523
KatexSpanNode(
524524
styles: KatexSpanStyles(
525525
fontFamily: 'KaTeX_Math',
@@ -539,7 +539,7 @@ class ContentExample {
539539
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span></span></span></span></span></p>',
540540
[MathBlockNode(texSource: r'\lambda', nodes: [
541541
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
542-
KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
542+
KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
543543
KatexSpanNode(
544544
styles: KatexSpanStyles(
545545
fontFamily: 'KaTeX_Math',
@@ -564,7 +564,7 @@ class ContentExample {
564564
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span></span></p>', [
565565
MathBlockNode(texSource: 'a', nodes: [
566566
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
567-
KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
567+
KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
568568
KatexSpanNode(
569569
styles: KatexSpanStyles(
570570
fontFamily: 'KaTeX_Math',
@@ -575,7 +575,7 @@ class ContentExample {
575575
]),
576576
MathBlockNode(texSource: 'b', nodes: [
577577
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
578-
KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
578+
KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
579579
KatexSpanNode(
580580
styles: KatexSpanStyles(
581581
fontFamily: 'KaTeX_Math',
@@ -603,7 +603,7 @@ class ContentExample {
603603
[QuotationNode([
604604
MathBlockNode(texSource: r'\lambda', nodes: [
605605
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
606-
KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
606+
KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
607607
KatexSpanNode(
608608
styles: KatexSpanStyles(
609609
fontFamily: 'KaTeX_Math',
@@ -632,7 +632,7 @@ class ContentExample {
632632
[QuotationNode([
633633
MathBlockNode(texSource: 'a', nodes: [
634634
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
635-
KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
635+
KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
636636
KatexSpanNode(
637637
styles: KatexSpanStyles(
638638
fontFamily: 'KaTeX_Math',
@@ -643,7 +643,7 @@ class ContentExample {
643643
]),
644644
MathBlockNode(texSource: 'b', nodes: [
645645
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
646-
KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
646+
KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
647647
KatexSpanNode(
648648
styles: KatexSpanStyles(
649649
fontFamily: 'KaTeX_Math',
@@ -681,7 +681,7 @@ class ContentExample {
681681
]),
682682
MathBlockNode(texSource: 'a', nodes: [
683683
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
684-
KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306),text: null, nodes: []),
684+
KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
685685
KatexSpanNode(
686686
styles: KatexSpanStyles(
687687
fontFamily: 'KaTeX_Math',
@@ -731,10 +731,7 @@ class ContentExample {
731731
styles: KatexSpanStyles(),
732732
text: null,
733733
nodes: [
734-
KatexSpanNode(
735-
styles: KatexSpanStyles(heightEm: 1.6034),
736-
text: null,
737-
nodes: []),
734+
KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null),
738735
KatexSpanNode(
739736
styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11
740737
text: '1',
@@ -800,10 +797,7 @@ class ContentExample {
800797
styles: KatexSpanStyles(),
801798
text: null,
802799
nodes: [
803-
KatexSpanNode(
804-
styles: KatexSpanStyles(heightEm: 1.6034),
805-
text: null,
806-
nodes: []),
800+
KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null),
807801
KatexSpanNode(
808802
styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1
809803
text: null,
@@ -845,10 +839,7 @@ class ContentExample {
845839
styles: KatexSpanStyles(),
846840
text: null,
847841
nodes: [
848-
KatexSpanNode(
849-
styles: KatexSpanStyles(heightEm: 3.0),
850-
text: null,
851-
nodes: []),
842+
KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25),
852843
KatexSpanNode(
853844
styles: KatexSpanStyles(),
854845
text: '⟨',
@@ -1963,10 +1954,7 @@ void main() async {
19631954
testParseExample(ContentExample.mathBlockBetweenImages);
19641955
testParseExample(ContentExample.mathBlockKatexSizing);
19651956
testParseExample(ContentExample.mathBlockKatexNestedSizing);
1966-
// TODO: Re-enable this test after adding support for parsing
1967-
// `vertical-align` in inline styles. Currently it fails
1968-
// because `strut` span has `vertical-align`.
1969-
testParseExample(ContentExample.mathBlockKatexDelimSizing, skip: true);
1957+
testParseExample(ContentExample.mathBlockKatexDelimSizing);
19701958

19711959
testParseExample(ContentExample.imageSingle);
19721960
testParseExample(ContentExample.imageSingleNoDimensions);

test/widgets/content_test.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -671,9 +671,7 @@ void main() {
671671
fontSize: baseTextStyle.fontSize!,
672672
fontHeight: baseTextStyle.height!);
673673
}
674-
}, skip: true); // TODO: Re-enable this test after adding support for parsing
675-
// `vertical-align` in inline styles. Currently it fails
676-
// because `strut` span has `vertical-align`.
674+
});
677675
});
678676

679677
/// Make a [TargetFontSizeFinder] to pass to [checkFontSizeRatio],

0 commit comments

Comments
 (0)