Skip to content

Commit 832d1b2

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 c4628c7 commit 832d1b2

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
@@ -411,6 +411,24 @@ class KatexSpanNode extends KatexNode {
411411
}
412412
}
413413

414+
class KatexStrutNode extends KatexNode {
415+
const KatexStrutNode({
416+
required this.heightEm,
417+
required this.verticalAlignEm,
418+
super.debugHtmlNode,
419+
});
420+
421+
final double heightEm;
422+
final double? verticalAlignEm;
423+
424+
@override
425+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
426+
super.debugFillProperties(properties);
427+
properties.add(DoubleProperty('heightEm', heightEm));
428+
properties.add(DoubleProperty('verticalAlignEm', verticalAlignEm));
429+
}
430+
}
431+
414432
class MathBlockNode extends MathNode implements BlockContentNode {
415433
const MathBlockNode({
416434
super.debugHtmlNode,

lib/model/katex.dart

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

188+
if (element.className.startsWith('strut')) {
189+
if (element.className == 'strut' && element.nodes.isEmpty) {
190+
final styles = _parseSpanInlineStyles(element);
191+
if (styles == null) throw _KatexHtmlParseError();
192+
193+
final heightEm = styles.heightEm;
194+
if (heightEm == null) throw _KatexHtmlParseError();
195+
final verticalAlignEm = styles.verticalAlignEm;
196+
197+
// Ensure only `height` and `vertical-align` inline styles are present.
198+
if (styles.filter(heightEm: false, verticalAlignEm: false) !=
199+
KatexSpanStyles()) {
200+
throw _KatexHtmlParseError();
201+
}
202+
203+
return KatexStrutNode(
204+
heightEm: heightEm,
205+
verticalAlignEm: verticalAlignEm);
206+
} else {
207+
throw _KatexHtmlParseError();
208+
}
209+
}
210+
188211
final debugHtmlNode = kDebugMode ? element : null;
189212

190213
final inlineStyles = _parseSpanInlineStyles(element);
214+
if (inlineStyles != null) {
215+
// We expect `vertical-align` inline style to be only present on a
216+
// `strut` span, for which we emit `KatexStrutNode` separately.
217+
if (inlineStyles.verticalAlignEm != null) throw _KatexHtmlParseError();
218+
}
191219

192220
// Aggregate the CSS styles that apply, in the same order as the CSS
193221
// classes specified for this span, mimicking the behaviour on web.
@@ -214,8 +242,9 @@ class _KatexParser {
214242

215243
case 'strut':
216244
// .strut { ... }
217-
// Do nothing, it has properties that don't need special handling.
218-
break;
245+
// We expect the 'strut' class to be the only class in a span,
246+
// in which case we handle it separately and emit `KatexStrutNode`.
247+
throw _KatexHtmlParseError();
219248

220249
case 'textbf':
221250
// .textbf { font-weight: bold; }
@@ -463,6 +492,7 @@ class _KatexParser {
463492
final stylesheet = css_parser.parse('*{$styleStr}');
464493
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
465494
double? heightEm;
495+
double? verticalAlignEm;
466496

467497
for (final declaration in rule.declarationGroup.declarations) {
468498
if (declaration case css_visitor.Declaration(
@@ -474,6 +504,10 @@ class _KatexParser {
474504
case 'height':
475505
heightEm = _getEm(expression);
476506
if (heightEm != null) continue;
507+
508+
case 'vertical-align':
509+
verticalAlignEm = _getEm(expression);
510+
if (verticalAlignEm != null) continue;
477511
}
478512

479513
// TODO handle more CSS properties
@@ -488,6 +522,7 @@ class _KatexParser {
488522

489523
return KatexSpanStyles(
490524
heightEm: heightEm,
525+
verticalAlignEm: verticalAlignEm,
491526
);
492527
} else {
493528
throw _KatexHtmlParseError();
@@ -524,6 +559,7 @@ enum KatexSpanTextAlign {
524559
@immutable
525560
class KatexSpanStyles {
526561
final double? heightEm;
562+
final double? verticalAlignEm;
527563

528564
final String? fontFamily;
529565
final double? fontSizeEm;
@@ -533,6 +569,7 @@ class KatexSpanStyles {
533569

534570
const KatexSpanStyles({
535571
this.heightEm,
572+
this.verticalAlignEm,
536573
this.fontFamily,
537574
this.fontSizeEm,
538575
this.fontWeight,
@@ -544,6 +581,7 @@ class KatexSpanStyles {
544581
int get hashCode => Object.hash(
545582
'KatexSpanStyles',
546583
heightEm,
584+
verticalAlignEm,
547585
fontFamily,
548586
fontSizeEm,
549587
fontWeight,
@@ -555,6 +593,7 @@ class KatexSpanStyles {
555593
bool operator ==(Object other) {
556594
return other is KatexSpanStyles &&
557595
other.heightEm == heightEm &&
596+
other.verticalAlignEm == verticalAlignEm &&
558597
other.fontFamily == fontFamily &&
559598
other.fontSizeEm == fontSizeEm &&
560599
other.fontWeight == fontWeight &&
@@ -566,6 +605,7 @@ class KatexSpanStyles {
566605
String toString() {
567606
final args = <String>[];
568607
if (heightEm != null) args.add('heightEm: $heightEm');
608+
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
569609
if (fontFamily != null) args.add('fontFamily: $fontFamily');
570610
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
571611
if (fontWeight != null) args.add('fontWeight: $fontWeight');
@@ -584,13 +624,34 @@ class KatexSpanStyles {
584624
KatexSpanStyles merge(KatexSpanStyles other) {
585625
return KatexSpanStyles(
586626
heightEm: other.heightEm ?? heightEm,
627+
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
587628
fontFamily: other.fontFamily ?? fontFamily,
588629
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
589630
fontStyle: other.fontStyle ?? fontStyle,
590631
fontWeight: other.fontWeight ?? fontWeight,
591632
textAlign: other.textAlign ?? textAlign,
592633
);
593634
}
635+
636+
KatexSpanStyles filter({
637+
bool heightEm = true,
638+
bool verticalAlignEm = true,
639+
bool fontFamily = true,
640+
bool fontSizeEm = true,
641+
bool fontWeight = true,
642+
bool fontStyle = true,
643+
bool textAlign = true,
644+
}) {
645+
return KatexSpanStyles(
646+
heightEm: heightEm ? this.heightEm : null,
647+
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
648+
fontFamily: fontFamily ? this.fontFamily : null,
649+
fontSizeEm: fontSizeEm ? this.fontSizeEm : null,
650+
fontWeight: fontWeight ? this.fontWeight : null,
651+
fontStyle: fontStyle ? this.fontStyle : null,
652+
textAlign: textAlign ? this.textAlign : null,
653+
);
654+
}
594655
}
595656

596657
class _KatexHtmlParseError extends Error {

lib/widgets/content.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@ class _KatexNodeList extends StatelessWidget {
881881
data: MediaQueryData(textScaler: TextScaler.noScaling),
882882
child: switch (e) {
883883
KatexSpanNode() => _KatexSpan(e),
884+
KatexStrutNode() => _KatexStrut(e),
884885
}));
885886
}))));
886887
}
@@ -961,6 +962,30 @@ class _KatexSpan extends StatelessWidget {
961962
}
962963
}
963964

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

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)