Skip to content

Commit ee3a00b

Browse files
katex: Handle position & top property in span inline styles
Allowing support for handling KaTeX HTML for big operators. Fixes: #1671
1 parent 6b7ba52 commit ee3a00b

File tree

4 files changed

+123
-9
lines changed

4 files changed

+123
-9
lines changed

lib/model/katex.dart

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ class _KatexParser {
631631
marginLeftEm: _takeStyleEm(inlineStyles, 'margin-left'),
632632
marginRightEm: _takeStyleEm(inlineStyles, 'margin-right'),
633633
color: _takeStyleColor(inlineStyles, 'color'),
634+
position: _takeStylePosition(inlineStyles, 'position'),
634635
// TODO handle more CSS properties
635636
);
636637
if (inlineStyles != null && inlineStyles.isNotEmpty) {
@@ -640,10 +641,10 @@ class _KatexParser {
640641
_hasError = true;
641642
}
642643
}
643-
// Currently, we expect `top` to only be inside a vlist, and
644-
// we handle that case separately above.
645-
if (styles.topEm != null) {
646-
throw _KatexHtmlParseError('unsupported inline CSS property: top');
644+
if (styles.topEm != null && styles.position != KatexSpanPosition.relative) {
645+
// The meaning of `top` would be different without `position: relative`.
646+
throw _KatexHtmlParseError(
647+
'unsupported inline CSS property "top" given "position: ${styles.position}"');
647648
}
648649

649650
String? text;
@@ -765,6 +766,34 @@ class _KatexParser {
765766
_hasError = true;
766767
return null;
767768
}
769+
770+
/// Remove the given property from the given style map,
771+
/// and parse as a CSS position value.
772+
///
773+
/// If the property is present but is not a valid CSS position value,
774+
/// record an error and return null.
775+
///
776+
/// If the property is absent, return null with no error.
777+
///
778+
/// If the map is null, treat it as empty.
779+
///
780+
/// To produce the map this method expects, see [_parseInlineStyles].
781+
KatexSpanPosition? _takeStylePosition(Map<String, css_visitor.Expression>? styles, String property) {
782+
final expression = styles?.remove(property);
783+
if (expression == null) return null;
784+
if (expression case css_visitor.LiteralTerm(:final value)) {
785+
if (value case css_visitor.Identifier(:final name)) {
786+
if (name == 'relative') {
787+
return KatexSpanPosition.relative;
788+
}
789+
}
790+
}
791+
assert(debugLog('KaTeX: Unsupported value for CSS property $property,'
792+
' expected a CSS position value: ${expression.toDebugString()}'));
793+
unsupportedInlineCssProperties.add(property);
794+
_hasError = true;
795+
return null;
796+
}
768797
}
769798

770799
enum KatexSpanFontWeight {
@@ -782,6 +811,10 @@ enum KatexSpanTextAlign {
782811
right,
783812
}
784813

814+
enum KatexSpanPosition {
815+
relative,
816+
}
817+
785818
class KatexSpanColor {
786819
const KatexSpanColor(this.r, this.g, this.b, this.a);
787820

@@ -832,6 +865,7 @@ class KatexSpanStyles {
832865
final KatexSpanTextAlign? textAlign;
833866

834867
final KatexSpanColor? color;
868+
final KatexSpanPosition? position;
835869

836870
const KatexSpanStyles({
837871
this.heightEm,
@@ -844,6 +878,7 @@ class KatexSpanStyles {
844878
this.fontStyle,
845879
this.textAlign,
846880
this.color,
881+
this.position,
847882
});
848883

849884
@override
@@ -859,6 +894,7 @@ class KatexSpanStyles {
859894
fontStyle,
860895
textAlign,
861896
color,
897+
position,
862898
);
863899

864900
@override
@@ -873,7 +909,8 @@ class KatexSpanStyles {
873909
other.fontWeight == fontWeight &&
874910
other.fontStyle == fontStyle &&
875911
other.textAlign == textAlign &&
876-
other.color == color;
912+
other.color == color &&
913+
other.position == position;
877914
}
878915

879916
@override
@@ -889,6 +926,7 @@ class KatexSpanStyles {
889926
if (fontStyle != null) args.add('fontStyle: $fontStyle');
890927
if (textAlign != null) args.add('textAlign: $textAlign');
891928
if (color != null) args.add('color: $color');
929+
if (position != null) args.add('position: $position');
892930
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
893931
}
894932

@@ -904,6 +942,7 @@ class KatexSpanStyles {
904942
bool fontStyle = true,
905943
bool textAlign = true,
906944
bool color = true,
945+
bool position = true,
907946
}) {
908947
return KatexSpanStyles(
909948
heightEm: heightEm ? this.heightEm : null,
@@ -916,6 +955,7 @@ class KatexSpanStyles {
916955
fontStyle: fontStyle ? this.fontStyle : null,
917956
textAlign: textAlign ? this.textAlign : null,
918957
color: color ? this.color : null,
958+
position: position ? this.position : null,
919959
);
920960
}
921961
}

lib/widgets/katex.dart

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ class _KatexSpan extends StatelessWidget {
9696
}
9797

9898
final styles = node.styles;
99-
100-
// Currently, we expect `top` to be only present with the
101-
// vlist inner row span, and parser handles that explicitly.
102-
assert(styles.topEm == null);
99+
if (styles.topEm != null) {
100+
// The meaning of `top` would be different without `position: relative`.
101+
assert(styles.position == KatexSpanPosition.relative);
102+
}
103103

104104
final fontFamily = styles.fontFamily;
105105
final fontSize = switch (styles.fontSizeEm) {
@@ -180,6 +180,18 @@ class _KatexSpan extends StatelessWidget {
180180
widget = Padding(padding: margin, child: widget);
181181
}
182182

183+
switch (styles.position) {
184+
case KatexSpanPosition.relative:
185+
if (styles.topEm case final topEm?) {
186+
widget = Transform.translate(
187+
offset: Offset(0, topEm * em),
188+
child: widget);
189+
}
190+
191+
case null:
192+
break;
193+
}
194+
183195
return widget;
184196
}
185197
}

test/model/katex_test.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,62 @@ class KatexExample extends ContentExample {
643643
text: '∗'),
644644
]),
645645
]);
646+
647+
static final bigOperators = KatexExample.block(
648+
r'big operators: \int',
649+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2240766
650+
r'\int',
651+
'<p>'
652+
'<span class="katex-display"><span class="katex">'
653+
'<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">\\int</annotation></semantics></math></span>'
654+
'<span class="katex-html" aria-hidden="true">'
655+
'<span class="base">'
656+
'<span class="strut" style="height:2.2222em;vertical-align:-0.8622em;"></span>'
657+
'<span class="mop op-symbol large-op" style="margin-right:0.44445em;position:relative;top:-0.0011em;">∫</span></span></span></span></span></p>', [
658+
KatexSpanNode(nodes: [
659+
KatexStrutNode(heightEm: 2.2222, verticalAlignEm: -0.8622),
660+
KatexSpanNode(
661+
styles: KatexSpanStyles(
662+
topEm: -0.0011,
663+
marginRightEm: 0.44445,
664+
fontFamily: 'KaTeX_Size2',
665+
position: KatexSpanPosition.relative),
666+
text: '∫'),
667+
]),
668+
]);
669+
670+
static final colonEquals = KatexExample.block(
671+
r'\colonequals relation',
672+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2244936
673+
r'\colonequals',
674+
'<p>'
675+
'<span class="katex-display"><span class="katex">'
676+
'<span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mo><mi mathvariant="normal">≔</mi></mo></mrow><annotation encoding="application/x-tex">\\colonequals</annotation></semantics></math></span>'
677+
'<span class="katex-html" aria-hidden="true">'
678+
'<span class="base">'
679+
'<span class="strut" style="height:0.4306em;"></span>'
680+
'<span class="mrel">'
681+
'<span class="mrel">'
682+
'<span class="mop" style="position:relative;top:-0.0347em;">:</span></span>'
683+
'<span class="mrel">'
684+
'<span class="mspace" style="margin-right:-0.0667em;"></span></span>'
685+
'<span class="mrel">=</span></span></span></span></span></span></p>', [
686+
KatexSpanNode(nodes: [
687+
KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
688+
KatexSpanNode(nodes: [
689+
KatexSpanNode(nodes: [
690+
KatexSpanNode(
691+
styles: KatexSpanStyles(topEm: -0.0347, position: KatexSpanPosition.relative),
692+
text: ':'),
693+
]),
694+
KatexSpanNode(nodes: [
695+
KatexSpanNode(nodes: []),
696+
KatexNegativeMarginNode(leftOffsetEm: -0.0667, nodes: []),
697+
]),
698+
KatexSpanNode(text: '='),
699+
]),
700+
]),
701+
]);
646702
}
647703

648704
void main() async {
@@ -663,6 +719,8 @@ void main() async {
663719
testParseExample(KatexExample.textColor);
664720
testParseExample(KatexExample.customColorMacro);
665721
testParseExample(KatexExample.phantom);
722+
testParseExample(KatexExample.bigOperators);
723+
testParseExample(KatexExample.colonEquals);
666724

667725
group('parseCssHexColor', () {
668726
const testCases = [

test/widgets/katex_test.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ void main() {
7373
('X', Offset(0.00, 7.04), Size(17.03, 25.00)),
7474
('n', Offset(17.03, 15.90), Size(8.63, 17.00)),
7575
]),
76+
(KatexExample.colonEquals, skip: false, [
77+
(':', Offset(0.00, 3.45), Size(5.72, 25.00)),
78+
('=', Offset(5.72, 3.92), Size(16.00, 25.00)),
79+
]),
7680
];
7781

7882
for (final testCase in testCases) {

0 commit comments

Comments
 (0)