Skip to content

Commit 9cc6700

Browse files
rajveermalviyagnprice
authored andcommitted
katex: Handle position & top property in span inline styles
Allowing support for handling KaTeX HTML for big operators. Fixes: #1671
1 parent db4e843 commit 9cc6700

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
@@ -637,6 +637,7 @@ class _KatexParser {
637637
marginLeftEm: _takeStyleEm(inlineStyles, 'margin-left'),
638638
marginRightEm: _takeStyleEm(inlineStyles, 'margin-right'),
639639
color: _takeStyleColor(inlineStyles, 'color'),
640+
position: _takeStylePosition(inlineStyles, 'position'),
640641
// TODO handle more CSS properties
641642
);
642643
if (inlineStyles != null && inlineStyles.isNotEmpty) {
@@ -646,10 +647,10 @@ class _KatexParser {
646647
_hasError = true;
647648
}
648649
}
649-
// Currently, we expect `top` to only be inside a vlist, and
650-
// we handle that case separately above.
651-
if (styles.topEm != null) {
652-
throw _KatexHtmlParseError('unsupported inline CSS property: top');
650+
if (styles.topEm != null && styles.position != KatexSpanPosition.relative) {
651+
// The meaning of `top` would be different without `position: relative`.
652+
throw _KatexHtmlParseError(
653+
'unsupported inline CSS property "top" given "position: ${styles.position}"');
653654
}
654655

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

776805
enum KatexSpanFontWeight {
@@ -788,6 +817,10 @@ enum KatexSpanTextAlign {
788817
right,
789818
}
790819

820+
enum KatexSpanPosition {
821+
relative,
822+
}
823+
791824
class KatexSpanColor {
792825
const KatexSpanColor(this.r, this.g, this.b, this.a);
793826

@@ -840,6 +873,7 @@ class KatexSpanStyles {
840873
final KatexSpanTextAlign? textAlign;
841874

842875
final KatexSpanColor? color;
876+
final KatexSpanPosition? position;
843877

844878
const KatexSpanStyles({
845879
this.widthEm,
@@ -853,6 +887,7 @@ class KatexSpanStyles {
853887
this.fontStyle,
854888
this.textAlign,
855889
this.color,
890+
this.position,
856891
});
857892

858893
@override
@@ -869,6 +904,7 @@ class KatexSpanStyles {
869904
fontStyle,
870905
textAlign,
871906
color,
907+
position,
872908
);
873909

874910
@override
@@ -884,7 +920,8 @@ class KatexSpanStyles {
884920
other.fontWeight == fontWeight &&
885921
other.fontStyle == fontStyle &&
886922
other.textAlign == textAlign &&
887-
other.color == color;
923+
other.color == color &&
924+
other.position == position;
888925
}
889926

890927
@override
@@ -901,6 +938,7 @@ class KatexSpanStyles {
901938
if (fontStyle != null) args.add('fontStyle: $fontStyle');
902939
if (textAlign != null) args.add('textAlign: $textAlign');
903940
if (color != null) args.add('color: $color');
941+
if (position != null) args.add('position: $position');
904942
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
905943
}
906944

@@ -917,6 +955,7 @@ class KatexSpanStyles {
917955
bool fontStyle = true,
918956
bool textAlign = true,
919957
bool color = true,
958+
bool position = true,
920959
}) {
921960
return KatexSpanStyles(
922961
widthEm: widthEm ? this.widthEm : null,
@@ -930,6 +969,7 @@ class KatexSpanStyles {
930969
fontStyle: fontStyle ? this.fontStyle : null,
931970
textAlign: textAlign ? this.textAlign : null,
932971
color: color ? this.color : null,
972+
position: position ? this.position : null,
933973
);
934974
}
935975
}

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) {
@@ -183,6 +183,18 @@ class _KatexSpan extends StatelessWidget {
183183
widget = Padding(padding: margin, child: widget);
184184
}
185185

186+
switch (styles.position) {
187+
case KatexSpanPosition.relative:
188+
if (styles.topEm case final topEm?) {
189+
widget = Transform.translate(
190+
offset: Offset(0, topEm * em),
191+
child: widget);
192+
}
193+
194+
case null:
195+
break;
196+
}
197+
186198
return widget;
187199
}
188200
}

test/model/katex_test.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,62 @@ class KatexExample extends ContentExample {
644644
]),
645645
]);
646646

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+
]);
702+
647703
static final nulldelimiter = KatexExample.block(
648704
r'null delimiters, like `\left.`',
649705
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2205534
@@ -695,6 +751,8 @@ void main() async {
695751
testParseExample(KatexExample.textColor);
696752
testParseExample(KatexExample.customColorMacro);
697753
testParseExample(KatexExample.phantom);
754+
testParseExample(KatexExample.bigOperators);
755+
testParseExample(KatexExample.colonEquals);
698756
testParseExample(KatexExample.nulldelimiter);
699757

700758
group('parseCssHexColor', () {

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
(KatexExample.nulldelimiter, skip: false, [
7781
('a', Offset(2.47, 3.36), Size(10.88, 25.00)),
7882
('b', Offset(15.81, 3.36), Size(8.82, 25.00)),

0 commit comments

Comments
 (0)