Skip to content

Commit ab86277

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 7bf54a7 commit ab86277

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; }
@@ -373,6 +402,7 @@ class _KatexParser {
373402
final stylesheet = css_parser.parse('*{$styleStr}');
374403
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
375404
double? heightEm;
405+
double? verticalAlignEm;
376406

377407
for (final declaration in rule.declarationGroup.declarations) {
378408
if (declaration case css_visitor.Declaration(
@@ -384,6 +414,10 @@ class _KatexParser {
384414
case 'height':
385415
heightEm = _getEm(expression);
386416
if (heightEm != null) continue;
417+
418+
case 'vertical-align':
419+
verticalAlignEm = _getEm(expression);
420+
if (verticalAlignEm != null) continue;
387421
}
388422

389423
// TODO handle more CSS properties
@@ -397,6 +431,7 @@ class _KatexParser {
397431

398432
return KatexSpanStyles(
399433
heightEm: heightEm,
434+
verticalAlignEm: verticalAlignEm,
400435
);
401436
} else {
402437
throw KatexHtmlParseError();
@@ -433,6 +468,7 @@ enum KatexSpanTextAlign {
433468
@immutable
434469
class KatexSpanStyles {
435470
final double? heightEm;
471+
final double? verticalAlignEm;
436472

437473
final String? fontFamily;
438474
final double? fontSizeEm;
@@ -442,6 +478,7 @@ class KatexSpanStyles {
442478

443479
const KatexSpanStyles({
444480
this.heightEm,
481+
this.verticalAlignEm,
445482
this.fontFamily,
446483
this.fontSizeEm,
447484
this.fontWeight,
@@ -453,6 +490,7 @@ class KatexSpanStyles {
453490
int get hashCode => Object.hash(
454491
'KatexSpanStyles',
455492
heightEm,
493+
verticalAlignEm,
456494
fontFamily,
457495
fontSizeEm,
458496
fontWeight,
@@ -464,6 +502,7 @@ class KatexSpanStyles {
464502
bool operator ==(Object other) {
465503
return other is KatexSpanStyles &&
466504
other.heightEm == heightEm &&
505+
other.verticalAlignEm == verticalAlignEm &&
467506
other.fontFamily == fontFamily &&
468507
other.fontSizeEm == fontSizeEm &&
469508
other.fontWeight == fontWeight &&
@@ -475,6 +514,7 @@ class KatexSpanStyles {
475514
String toString() {
476515
final args = <String>[];
477516
if (heightEm != null) args.add('heightEm: $heightEm');
517+
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
478518
if (fontFamily != null) args.add('fontFamily: $fontFamily');
479519
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
480520
if (fontWeight != null) args.add('fontWeight: $fontWeight');
@@ -486,13 +526,34 @@ class KatexSpanStyles {
486526
KatexSpanStyles merge(KatexSpanStyles other) {
487527
return KatexSpanStyles(
488528
heightEm: other.heightEm ?? heightEm,
529+
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
489530
fontFamily: other.fontFamily ?? fontFamily,
490531
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
491532
fontStyle: other.fontStyle ?? fontStyle,
492533
fontWeight: other.fontWeight ?? fontWeight,
493534
textAlign: other.textAlign ?? textAlign,
494535
);
495536
}
537+
538+
KatexSpanStyles filter({
539+
bool heightEm = true,
540+
bool verticalAlignEm = true,
541+
bool fontFamily = true,
542+
bool fontSizeEm = true,
543+
bool fontWeight = true,
544+
bool fontStyle = true,
545+
bool textAlign = true,
546+
}) {
547+
return KatexSpanStyles(
548+
heightEm: heightEm ? this.heightEm : null,
549+
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
550+
fontFamily: fontFamily ? this.fontFamily : null,
551+
fontSizeEm: fontSizeEm ? this.fontSizeEm : null,
552+
fontWeight: fontWeight ? this.fontWeight : null,
553+
fontStyle: fontStyle ? this.fontStyle : null,
554+
textAlign: textAlign ? this.textAlign : null,
555+
);
556+
}
496557
}
497558

498559
class KatexHtmlParseError extends Error {

lib/widgets/content.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,7 @@ class _KatexNodeList extends StatelessWidget {
886886
data: MediaQueryData(textScaler: TextScaler.noScaling),
887887
child: switch (e) {
888888
KatexSpanNode() => _KatexSpan(e),
889+
KatexStrutNode() => _KatexStrut(e),
889890
}));
890891
}))));
891892
}
@@ -967,6 +968,30 @@ class _KatexSpan extends StatelessWidget {
967968
}
968969
}
969970

971+
class _KatexStrut extends StatelessWidget {
972+
const _KatexStrut(this.node);
973+
974+
final KatexStrutNode node;
975+
976+
@override
977+
Widget build(BuildContext context) {
978+
final em = DefaultTextStyle.of(context).style.fontSize!;
979+
980+
final verticalAlignEm = node.verticalAlignEm;
981+
if (verticalAlignEm == null) {
982+
return SizedBox(height: node.heightEm * em);
983+
}
984+
985+
return SizedBox(
986+
height: node.heightEm * em,
987+
child: Baseline(
988+
baseline: (verticalAlignEm + node.heightEm) * em,
989+
baselineType: TextBaseline.alphabetic,
990+
child: const Text('')),
991+
);
992+
}
993+
}
994+
970995
class WebsitePreview extends StatelessWidget {
971996
const WebsitePreview({super.key, required this.node});
972997

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)