Skip to content

Commit 1e20472

Browse files
content: Handle 'position' & 'top' property in KaTeX span inline style
Allowing support for handling KaTeX HTML for big operators. Fixes: #1671
1 parent 280bf3c commit 1e20472

File tree

3 files changed

+80
-12
lines changed

3 files changed

+80
-12
lines changed

lib/model/katex.dart

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,6 @@ class _KatexParser {
320320
// We expect `vertical-align` inline style to be only present on a
321321
// `strut` span, for which we emit `KatexStrutNode` separately.
322322
if (inlineStyles.verticalAlignEm != null) throw _KatexHtmlParseError();
323-
324-
// Currently, we expect `top` to only be inside a vlist, and
325-
// we handle that case separately above.
326-
if (inlineStyles.topEm != null) throw _KatexHtmlParseError();
327323
}
328324

329325
// Aggregate the CSS styles that apply, in the same order as the CSS
@@ -586,10 +582,21 @@ class _KatexParser {
586582
}
587583
if (text == null && spans == null) throw _KatexHtmlParseError();
588584

585+
final mergedStyles = inlineStyles != null
586+
? styles.merge(inlineStyles)
587+
: styles;
588+
589+
// We expect `top` style to be only present if `position: relative`
590+
// is also present. As both are non-inherited CSS attributes and
591+
// should only ever be present together.
592+
// TODO account for other sides (left, right, bottom).
593+
if (mergedStyles.topEm != null
594+
&& mergedStyles.position != KatexSpanPosition.relative) {
595+
throw _KatexHtmlParseError();
596+
}
597+
589598
return KatexSpanNode(
590-
styles: inlineStyles != null
591-
? styles.merge(inlineStyles)
592-
: styles,
599+
styles: mergedStyles,
593600
text: text,
594601
nodes: spans,
595602
debugHtmlNode: debugHtmlNode);
@@ -607,6 +614,7 @@ class _KatexParser {
607614
double? topEm;
608615
double? marginRightEm;
609616
double? marginLeftEm;
617+
KatexSpanPosition? position;
610618

611619
for (final declaration in rule.declarationGroup.declarations) {
612620
if (declaration case css_visitor.Declaration(
@@ -640,6 +648,13 @@ class _KatexParser {
640648
if (marginLeftEm < 0) throw _KatexHtmlParseError();
641649
continue;
642650
}
651+
652+
case 'position':
653+
position = switch (_getLiteral(expression)) {
654+
'relative' => KatexSpanPosition.relative,
655+
_ => null,
656+
};
657+
if (position != null) continue;
643658
}
644659

645660
// TODO handle more CSS properties
@@ -658,6 +673,7 @@ class _KatexParser {
658673
verticalAlignEm: verticalAlignEm,
659674
marginRightEm: marginRightEm,
660675
marginLeftEm: marginLeftEm,
676+
position: position,
661677
);
662678
} else {
663679
throw _KatexHtmlParseError();
@@ -674,6 +690,17 @@ class _KatexParser {
674690
}
675691
return null;
676692
}
693+
694+
/// Returns the CSS literal string value if the given [expression] is
695+
/// actually a literal expression, else returns null.
696+
String? _getLiteral(css_visitor.Expression expression) {
697+
if (expression case css_visitor.LiteralTerm(:final value)) {
698+
if (value case css_visitor.Identifier(:final name)) {
699+
return name;
700+
}
701+
}
702+
return null;
703+
}
677704
}
678705

679706
enum KatexSpanFontWeight {
@@ -691,6 +718,10 @@ enum KatexSpanTextAlign {
691718
right,
692719
}
693720

721+
enum KatexSpanPosition {
722+
relative,
723+
}
724+
694725
@immutable
695726
class KatexSpanStyles {
696727
final double? heightEm;
@@ -707,6 +738,8 @@ class KatexSpanStyles {
707738
final KatexSpanFontStyle? fontStyle;
708739
final KatexSpanTextAlign? textAlign;
709740

741+
final KatexSpanPosition? position;
742+
710743
const KatexSpanStyles({
711744
this.heightEm,
712745
this.verticalAlignEm,
@@ -718,6 +751,7 @@ class KatexSpanStyles {
718751
this.fontWeight,
719752
this.fontStyle,
720753
this.textAlign,
754+
this.position,
721755
});
722756

723757
@override
@@ -733,6 +767,7 @@ class KatexSpanStyles {
733767
fontWeight,
734768
fontStyle,
735769
textAlign,
770+
position,
736771
);
737772

738773
@override
@@ -747,7 +782,8 @@ class KatexSpanStyles {
747782
other.fontSizeEm == fontSizeEm &&
748783
other.fontWeight == fontWeight &&
749784
other.fontStyle == fontStyle &&
750-
other.textAlign == textAlign;
785+
other.textAlign == textAlign &&
786+
other.position == position;
751787
}
752788

753789
@override
@@ -763,6 +799,7 @@ class KatexSpanStyles {
763799
if (fontWeight != null) args.add('fontWeight: $fontWeight');
764800
if (fontStyle != null) args.add('fontStyle: $fontStyle');
765801
if (textAlign != null) args.add('textAlign: $textAlign');
802+
if (position != null) args.add('position: $position');
766803
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
767804
}
768805

@@ -785,6 +822,7 @@ class KatexSpanStyles {
785822
fontStyle: other.fontStyle ?? fontStyle,
786823
fontWeight: other.fontWeight ?? fontWeight,
787824
textAlign: other.textAlign ?? textAlign,
825+
position: other.position ?? position,
788826
);
789827
}
790828

@@ -799,6 +837,7 @@ class KatexSpanStyles {
799837
bool fontWeight = true,
800838
bool fontStyle = true,
801839
bool textAlign = true,
840+
bool position = true,
802841
}) {
803842
return KatexSpanStyles(
804843
heightEm: heightEm ? this.heightEm : null,
@@ -811,6 +850,7 @@ class KatexSpanStyles {
811850
fontWeight: fontWeight ? this.fontWeight : null,
812851
fontStyle: fontStyle ? this.fontStyle : null,
813852
textAlign: textAlign ? this.textAlign : null,
853+
position: position ? this.position : null,
814854
);
815855
}
816856
}

lib/widgets/content.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -925,10 +925,6 @@ class _KatexSpan extends StatelessWidget {
925925
// So, this should always be null for non `strut` spans.
926926
assert(styles.verticalAlignEm == null);
927927

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-
932928
final fontFamily = styles.fontFamily;
933929
final fontSize = switch (styles.fontSizeEm) {
934930
double fontSizeEm => fontSizeEm * em,
@@ -1001,6 +997,13 @@ class _KatexSpan extends StatelessWidget {
1001997
widget = Padding(padding: margin, child: widget);
1002998
}
1003999

1000+
if (styles.topEm != null) {
1001+
assert(styles.position == KatexSpanPosition.relative);
1002+
widget = Transform.translate(
1003+
offset: Offset(0, styles.topEm! * em),
1004+
child: widget);
1005+
}
1006+
10041007
return widget;
10051008
}
10061009
}

test/model/content_test.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,6 +1165,30 @@ class ContentExample {
11651165
]),
11661166
]);
11671167

1168+
static const mathBlockKatexBigOperators = ContentExample(
1169+
'math block katex big operators',
1170+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2203220
1171+
'```math\n\\bigsqcup\n```',
1172+
'<p>'
1173+
'<span class="katex-display"><span class="katex">'
1174+
'<span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mo>⨆</mo></mrow><annotation encoding="application/x-tex">\\bigsqcup</annotation></semantics></math></span>'
1175+
'<span class="katex-html" aria-hidden="true">'
1176+
'<span class="base">'
1177+
'<span class="strut" style="height:1.6em;vertical-align:-0.55em;"></span>'
1178+
'<span class="mop op-symbol large-op" style="position:relative;top:0em;">⨆</span></span></span></span></span></p>', [
1179+
MathBlockNode(texSource: '\\bigsqcup', nodes: [
1180+
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
1181+
KatexStrutNode(heightEm: 1.6, verticalAlignEm: -0.55),
1182+
KatexSpanNode(
1183+
styles: KatexSpanStyles(
1184+
topEm: 0.0,
1185+
fontFamily: 'KaTeX_Size2',
1186+
position: KatexSpanPosition.relative),
1187+
text: '⨆', nodes: null),
1188+
]),
1189+
]),
1190+
]);
1191+
11681192
static const imageSingle = ContentExample(
11691193
'single image',
11701194
// https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103
@@ -2259,6 +2283,7 @@ void main() async {
22592283
testParseExample(ContentExample.mathBlockKatexSubscript);
22602284
testParseExample(ContentExample.mathBlockKatexSubSuperScript);
22612285
testParseExample(ContentExample.mathBlockKatexRaisebox);
2286+
testParseExample(ContentExample.mathBlockKatexBigOperators);
22622287

22632288
testParseExample(ContentExample.imageSingle);
22642289
testParseExample(ContentExample.imageSingleNoDimensions);

0 commit comments

Comments
 (0)