Skip to content

Commit 85f4ecf

Browse files
rajveermalviyagnprice
authored andcommitted
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 0ec1641 commit 85f4ecf

File tree

5 files changed

+127
-35
lines changed

5 files changed

+127
-35
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: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,34 @@ class _KatexParser {
187187

188188
final debugHtmlNode = kDebugMode ? element : null;
189189

190+
if (element.className == 'strut') {
191+
if (element.nodes.isNotEmpty) throw _KatexHtmlParseError();
192+
193+
final styles = _parseSpanInlineStyles(element);
194+
if (styles == null) throw _KatexHtmlParseError();
195+
196+
final heightEm = styles.heightEm;
197+
if (heightEm == null) throw _KatexHtmlParseError();
198+
final verticalAlignEm = styles.verticalAlignEm;
199+
200+
// Ensure only `height` and `vertical-align` inline styles are present.
201+
if (styles.filter(heightEm: false, verticalAlignEm: false)
202+
!= const KatexSpanStyles()) {
203+
throw _KatexHtmlParseError();
204+
}
205+
206+
return KatexStrutNode(
207+
heightEm: heightEm,
208+
verticalAlignEm: verticalAlignEm,
209+
debugHtmlNode: debugHtmlNode);
210+
}
211+
190212
final inlineStyles = _parseSpanInlineStyles(element);
213+
if (inlineStyles != null) {
214+
// We expect `vertical-align` inline style to be only present on a
215+
// `strut` span, for which we emit `KatexStrutNode` separately.
216+
if (inlineStyles.verticalAlignEm != null) throw _KatexHtmlParseError();
217+
}
191218

192219
// Aggregate the CSS styles that apply, in the same order as the CSS
193220
// classes specified for this span, mimicking the behaviour on web.
@@ -214,8 +241,9 @@ class _KatexParser {
214241

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

220248
case 'textbf':
221249
// .textbf { font-weight: bold; }
@@ -463,6 +491,7 @@ class _KatexParser {
463491
final stylesheet = css_parser.parse('*{$styleStr}');
464492
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
465493
double? heightEm;
494+
double? verticalAlignEm;
466495

467496
for (final declaration in rule.declarationGroup.declarations) {
468497
if (declaration case css_visitor.Declaration(
@@ -474,6 +503,10 @@ class _KatexParser {
474503
case 'height':
475504
heightEm = _getEm(expression);
476505
if (heightEm != null) continue;
506+
507+
case 'vertical-align':
508+
verticalAlignEm = _getEm(expression);
509+
if (verticalAlignEm != null) continue;
477510
}
478511

479512
// TODO handle more CSS properties
@@ -488,6 +521,7 @@ class _KatexParser {
488521

489522
return KatexSpanStyles(
490523
heightEm: heightEm,
524+
verticalAlignEm: verticalAlignEm,
491525
);
492526
} else {
493527
throw _KatexHtmlParseError();
@@ -524,6 +558,7 @@ enum KatexSpanTextAlign {
524558
@immutable
525559
class KatexSpanStyles {
526560
final double? heightEm;
561+
final double? verticalAlignEm;
527562

528563
final String? fontFamily;
529564
final double? fontSizeEm;
@@ -533,6 +568,7 @@ class KatexSpanStyles {
533568

534569
const KatexSpanStyles({
535570
this.heightEm,
571+
this.verticalAlignEm,
536572
this.fontFamily,
537573
this.fontSizeEm,
538574
this.fontWeight,
@@ -544,6 +580,7 @@ class KatexSpanStyles {
544580
int get hashCode => Object.hash(
545581
'KatexSpanStyles',
546582
heightEm,
583+
verticalAlignEm,
547584
fontFamily,
548585
fontSizeEm,
549586
fontWeight,
@@ -555,6 +592,7 @@ class KatexSpanStyles {
555592
bool operator ==(Object other) {
556593
return other is KatexSpanStyles &&
557594
other.heightEm == heightEm &&
595+
other.verticalAlignEm == verticalAlignEm &&
558596
other.fontFamily == fontFamily &&
559597
other.fontSizeEm == fontSizeEm &&
560598
other.fontWeight == fontWeight &&
@@ -566,6 +604,7 @@ class KatexSpanStyles {
566604
String toString() {
567605
final args = <String>[];
568606
if (heightEm != null) args.add('heightEm: $heightEm');
607+
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
569608
if (fontFamily != null) args.add('fontFamily: $fontFamily');
570609
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
571610
if (fontWeight != null) args.add('fontWeight: $fontWeight');
@@ -584,13 +623,34 @@ class KatexSpanStyles {
584623
KatexSpanStyles merge(KatexSpanStyles other) {
585624
return KatexSpanStyles(
586625
heightEm: other.heightEm ?? heightEm,
626+
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
587627
fontFamily: other.fontFamily ?? fontFamily,
588628
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
589629
fontStyle: other.fontStyle ?? fontStyle,
590630
fontWeight: other.fontWeight ?? fontWeight,
591631
textAlign: other.textAlign ?? textAlign,
592632
);
593633
}
634+
635+
KatexSpanStyles filter({
636+
bool heightEm = true,
637+
bool verticalAlignEm = true,
638+
bool fontFamily = true,
639+
bool fontSizeEm = true,
640+
bool fontWeight = true,
641+
bool fontStyle = true,
642+
bool textAlign = true,
643+
}) {
644+
return KatexSpanStyles(
645+
heightEm: heightEm ? this.heightEm : null,
646+
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
647+
fontFamily: fontFamily ? this.fontFamily : null,
648+
fontSizeEm: fontSizeEm ? this.fontSizeEm : null,
649+
fontWeight: fontWeight ? this.fontWeight : null,
650+
fontStyle: fontStyle ? this.fontStyle : null,
651+
textAlign: textAlign ? this.textAlign : null,
652+
);
653+
}
594654
}
595655

596656
class _KatexHtmlParseError extends Error {

lib/widgets/content.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,7 @@ class _KatexNodeList extends StatelessWidget {
896896
data: MediaQueryData(textScaler: TextScaler.noScaling),
897897
child: switch (e) {
898898
KatexSpanNode() => _KatexSpan(e),
899+
KatexStrutNode() => _KatexStrut(e),
899900
}));
900901
}))));
901902
}
@@ -918,6 +919,10 @@ class _KatexSpan extends StatelessWidget {
918919
}
919920

920921
final styles = node.styles;
922+
// We expect vertical-align to be only present with the
923+
// `strut` span, for which parser explicitly emits `KatexStrutNode`.
924+
// So, this should always be null for non `strut` spans.
925+
assert(styles.verticalAlignEm == null);
921926

922927
final fontFamily = styles.fontFamily;
923928
final fontSize = switch (styles.fontSizeEm) {
@@ -976,6 +981,30 @@ class _KatexSpan extends StatelessWidget {
976981
}
977982
}
978983

984+
class _KatexStrut extends StatelessWidget {
985+
const _KatexStrut(this.node);
986+
987+
final KatexStrutNode node;
988+
989+
@override
990+
Widget build(BuildContext context) {
991+
final em = DefaultTextStyle.of(context).style.fontSize!;
992+
993+
final verticalAlignEm = node.verticalAlignEm;
994+
if (verticalAlignEm == null) {
995+
return SizedBox(height: node.heightEm * em);
996+
}
997+
998+
return SizedBox(
999+
height: node.heightEm * em,
1000+
child: Baseline(
1001+
baseline: (verticalAlignEm + node.heightEm) * em,
1002+
baselineType: TextBaseline.alphabetic,
1003+
child: const Text('')),
1004+
);
1005+
}
1006+
}
1007+
9791008
class WebsitePreview extends StatelessWidget {
9801009
const WebsitePreview({super.key, required this.node});
9811010

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: '⟨',
@@ -1981,10 +1972,7 @@ void main() async {
19811972
testParseExample(ContentExample.mathBlockBetweenImages);
19821973
testParseExample(ContentExample.mathBlockKatexSizing);
19831974
testParseExample(ContentExample.mathBlockKatexNestedSizing);
1984-
// TODO: Re-enable this test after adding support for parsing
1985-
// `vertical-align` in inline styles. Currently it fails
1986-
// because `strut` span has `vertical-align`.
1987-
testParseExample(ContentExample.mathBlockKatexDelimSizing, skip: true);
1975+
testParseExample(ContentExample.mathBlockKatexDelimSizing);
19881976

19891977
testParseExample(ContentExample.imageSingle);
19901978
testParseExample(ContentExample.imageSingleNoDimensions);

test/widgets/content_test.dart

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -591,14 +591,11 @@ void main() {
591591
('1', Offset(0.00, 40.24), Size(5.14, 12.00)),
592592
('2', Offset(5.14, 2.80), Size(25.59, 61.00)),
593593
]),
594-
// TODO: Re-enable this test after adding support for parsing
595-
// `vertical-align` in inline styles. Currently it fails
596-
// because `strut` span has `vertical-align`.
597-
(ContentExample.mathBlockKatexDelimSizing, skip: true, [
598-
('(', Offset(8.00, 46.36), Size(9.42, 25.00)),
599-
('[', Offset(17.42, 46.36), Size(9.71, 25.00)),
600-
('⌈', Offset(27.12, 46.36), Size(11.99, 25.00)),
601-
('⌊', Offset(39.11, 46.36), Size(13.14, 25.00)),
594+
(ContentExample.mathBlockKatexDelimSizing, skip: false, [
595+
('(', Offset(8.00, 20.14), Size(9.42, 25.00)),
596+
('[', Offset(17.42, 20.14), Size(9.71, 25.00)),
597+
('⌈', Offset(27.12, 20.14), Size(11.99, 25.00)),
598+
('⌊', Offset(39.11, 20.14), Size(13.14, 25.00)),
602599
]),
603600
];
604601

@@ -629,7 +626,7 @@ void main() {
629626
check(size)
630627
.within(distance: 0.05, from: expectedSize);
631628
}
632-
}, skip: testCase.skip);
629+
});
633630
}
634631
});
635632
});

0 commit comments

Comments
 (0)