Skip to content

Commit 7fc2368

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

File tree

3 files changed

+84
-7
lines changed

3 files changed

+84
-7
lines changed

lib/model/katex.dart

Lines changed: 43 additions & 3 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) {
@@ -642,8 +643,8 @@ class _KatexParser {
642643
}
643644
// Currently, we expect `top` to only be inside a vlist, and
644645
// we handle that case separately above.
645-
if (styles.topEm != null) {
646-
throw _KatexHtmlParseError('unsupported inline CSS property: top');
646+
if (styles.topEm != null && styles.position != KatexSpanPosition.relative) {
647+
throw _KatexHtmlParseError('unsupported inline CSS property "top", when "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 literal value of CSS position attribute.
772+
///
773+
/// If the property is present but is not a valid literal value of
774+
/// position attribute, 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 position literal 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: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,6 @@ class _KatexSpan extends StatelessWidget {
9797

9898
final styles = node.styles;
9999

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);
103-
104100
final fontFamily = styles.fontFamily;
105101
final fontSize = switch (styles.fontSizeEm) {
106102
double fontSizeEm => fontSizeEm * em,
@@ -180,6 +176,23 @@ class _KatexSpan extends StatelessWidget {
180176
widget = Padding(padding: margin, child: widget);
181177
}
182178

179+
switch (styles.position) {
180+
case KatexSpanPosition.relative:
181+
final offset = switch (styles.topEm) {
182+
final topEm? => Offset(0, topEm * em),
183+
null => null,
184+
};
185+
186+
if (offset != null) {
187+
widget = Transform.translate(
188+
offset: offset,
189+
child: widget);
190+
}
191+
192+
case null:
193+
break;
194+
}
195+
183196
return widget;
184197
}
185198
}

test/model/katex_test.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,29 @@ 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+
]);
646669
}
647670

648671
void main() async {
@@ -663,6 +686,7 @@ void main() async {
663686
testParseExample(KatexExample.textColor);
664687
testParseExample(KatexExample.customColorMacro);
665688
testParseExample(KatexExample.phantom);
689+
testParseExample(KatexExample.bigOperators);
666690

667691
group('parseCssHexColor', () {
668692
const testCases = [

0 commit comments

Comments
 (0)