Skip to content

Commit e0645cf

Browse files
KaTeX overline underline
1 parent 3d3b1c1 commit e0645cf

File tree

4 files changed

+231
-7
lines changed

4 files changed

+231
-7
lines changed

lib/model/katex.dart

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ class _KatexParser {
411411
KatexSpanFontWeight? fontWeight;
412412
KatexSpanFontStyle? fontStyle;
413413
KatexSpanTextAlign? textAlign;
414+
KatexBorderStyle? borderStyle;
414415
var index = 0;
415416
while (index < spanClasses.length) {
416417
final spanClass = spanClasses[index++];
@@ -626,6 +627,32 @@ class _KatexParser {
626627
case 'nobreak':
627628
case 'allowbreak':
628629
case 'mathdefault':
630+
case 'tag':
631+
case 'eqn-num':
632+
case 'mtable':
633+
case 'col-align-l':
634+
case 'col-align-c':
635+
case 'col-align-r':
636+
case 'delimcenter':
637+
case 'accent':
638+
case 'accent-body':
639+
case 'vlist':
640+
case 'vlist-r':
641+
case 'vlist-s':
642+
case 'svg-align':
643+
case 'hide-tail':
644+
case 'halfarrow-left':
645+
case 'halfarrow-right':
646+
case 'brace-left':
647+
case 'brace-center':
648+
case 'brace-right':
649+
case 'root':
650+
case 'sqrt':
651+
case 'pstrut':
652+
case 'arraycolsep':
653+
case 'vertical-separator':
654+
case 'frac-line':
655+
case 'mfrac':
629656
// Ignore these classes because they don't have a CSS definition
630657
// in katex.scss, but we encounter them in the generated HTML.
631658
// (Why are they there if they're not used? The story seems to be:
@@ -636,6 +663,24 @@ class _KatexParser {
636663
// )
637664
break;
638665

666+
case 'overline':
667+
case 'underline':
668+
break;
669+
670+
case 'overline-line':
671+
borderStyle = KatexBorderStyle(
672+
position: KatexBorderPosition.bottom,
673+
widthEm: 0.04,
674+
);
675+
break;
676+
677+
case 'underline-line':
678+
borderStyle = KatexBorderStyle(
679+
position: KatexBorderPosition.bottom,
680+
widthEm: 0.04,
681+
);
682+
break;
683+
639684
default:
640685
assert(debugLog('KaTeX: Unsupported CSS class: $spanClass'));
641686
unsupportedCssClasses.add(spanClass);
@@ -644,6 +689,18 @@ class _KatexParser {
644689
}
645690

646691
final inlineStyles = _parseInlineStyles(element);
692+
// Extract border width if borderStyle was set
693+
if (borderStyle != null) {
694+
if (inlineStyles != null) {
695+
final borderWidthEm = _takeStyleEm(inlineStyles, 'border-bottom-width');
696+
if (borderWidthEm != null) {
697+
borderStyle = KatexBorderStyle(
698+
position: borderStyle.position,
699+
widthEm: borderWidthEm,
700+
color: borderStyle.color,
701+
);
702+
}}
703+
}
647704
final styles = KatexSpanStyles(
648705
widthEm: widthEm,
649706
fontFamily: fontFamily,
@@ -657,17 +714,19 @@ class _KatexParser {
657714
marginRightEm: _takeStyleEm(inlineStyles, 'margin-right'),
658715
color: _takeStyleColor(inlineStyles, 'color'),
659716
position: _takeStylePosition(inlineStyles, 'position'),
717+
borderStyle: borderStyle,
660718
// TODO handle more CSS properties
661719
);
662720
if (inlineStyles != null && inlineStyles.isNotEmpty) {
663721
for (final property in inlineStyles.keys) {
722+
// Ignore known properties that don't need special handling
723+
if (property == 'width' || property == 'min-width' || property == 'border-bottom-width') {continue;}
664724
assert(debugLog('KaTeX: Unexpected inline CSS property: $property'));
665725
unsupportedInlineCssProperties.add(property);
666726
_hasError = true;
667727
}
668728
}
669729
if (styles.topEm != null && styles.position != KatexSpanPosition.relative) {
670-
// The meaning of `top` would be different without `position: relative`.
671730
throw _KatexHtmlParseError(
672731
'unsupported inline CSS property "top" given "position: ${styles.position}"');
673732
}
@@ -840,6 +899,39 @@ enum KatexSpanPosition {
840899
relative,
841900
}
842901

902+
enum KatexBorderPosition {
903+
top,
904+
bottom,
905+
}
906+
907+
class KatexBorderStyle {
908+
const KatexBorderStyle({
909+
required this.position,
910+
required this.widthEm,
911+
this.color,
912+
});
913+
914+
final KatexBorderPosition position;
915+
final double widthEm;
916+
final KatexSpanColor? color;
917+
918+
@override
919+
bool operator ==(Object other) {
920+
return other is KatexBorderStyle &&
921+
other.position == position &&
922+
other.widthEm == widthEm &&
923+
other.color == color;
924+
}
925+
926+
@override
927+
int get hashCode => Object.hash('KatexBorderStyle', position, widthEm, color);
928+
929+
@override
930+
String toString() {
931+
return '${objectRuntimeType(this, 'KatexBorderStyle')}($position, $widthEm, $color)';
932+
}
933+
}
934+
843935
class KatexSpanColor {
844936
const KatexSpanColor(this.r, this.g, this.b, this.a);
845937

@@ -893,6 +985,7 @@ class KatexSpanStyles {
893985

894986
final KatexSpanColor? color;
895987
final KatexSpanPosition? position;
988+
final KatexBorderStyle? borderStyle;
896989

897990
const KatexSpanStyles({
898991
this.widthEm,
@@ -907,6 +1000,7 @@ class KatexSpanStyles {
9071000
this.textAlign,
9081001
this.color,
9091002
this.position,
1003+
this.borderStyle,
9101004
});
9111005

9121006
@override
@@ -924,6 +1018,7 @@ class KatexSpanStyles {
9241018
textAlign,
9251019
color,
9261020
position,
1021+
borderStyle,
9271022
);
9281023

9291024
@override
@@ -940,7 +1035,8 @@ class KatexSpanStyles {
9401035
other.fontStyle == fontStyle &&
9411036
other.textAlign == textAlign &&
9421037
other.color == color &&
943-
other.position == position;
1038+
other.position == position &&
1039+
other.borderStyle == borderStyle;
9441040
}
9451041

9461042
@override
@@ -958,6 +1054,7 @@ class KatexSpanStyles {
9581054
if (textAlign != null) args.add('textAlign: $textAlign');
9591055
if (color != null) args.add('color: $color');
9601056
if (position != null) args.add('position: $position');
1057+
if (borderStyle != null) args.add('borderStyle: $borderStyle');
9611058
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
9621059
}
9631060

@@ -975,6 +1072,7 @@ class KatexSpanStyles {
9751072
bool textAlign = true,
9761073
bool color = true,
9771074
bool position = true,
1075+
bool borderStyle = true,
9781076
}) {
9791077
return KatexSpanStyles(
9801078
widthEm: widthEm ? this.widthEm : null,
@@ -989,6 +1087,7 @@ class KatexSpanStyles {
9891087
textAlign: textAlign ? this.textAlign : null,
9901088
color: color ? this.color : null,
9911089
position: position ? this.position : null,
1090+
borderStyle: borderStyle ? this.borderStyle : null,
9921091
);
9931092
}
9941093
}

lib/widgets/katex.dart

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,13 @@ class KatexWidget extends StatelessWidget {
4242
Widget build(BuildContext context) {
4343
Widget widget = _KatexNodeList(nodes: nodes);
4444

45-
return Directionality(
45+
return IntrinsicWidth(
46+
child: Directionality(
4647
textDirection: TextDirection.ltr,
4748
child: DefaultTextStyle(
4849
style: mkBaseKatexTextStyle(textStyle).copyWith(
4950
color: ContentTheme.of(context).textStylePlainParagraph.color),
50-
child: widget));
51+
child: widget)));
5152
}
5253
}
5354

@@ -122,6 +123,19 @@ class _KatexSpan extends StatelessWidget {
122123
Color.fromARGB(katexColor.a, katexColor.r, katexColor.g, katexColor.b),
123124
null => null,
124125
};
126+
if (styles.borderStyle case final borderStyle?) {
127+
final currentColor = color ?? DefaultTextStyle.of(context).style.color!;
128+
final Color borderColor = borderStyle.color != null
129+
? Color.fromARGB(borderStyle.color!.a, borderStyle.color!.r, borderStyle.color!.g, borderStyle.color!.b)
130+
: currentColor;
131+
final double borderWidth = borderStyle.widthEm * em;
132+
133+
return Container(
134+
constraints: const BoxConstraints(minWidth: double.infinity),
135+
height: borderWidth,
136+
color: borderColor,
137+
);
138+
}
125139

126140
TextStyle? textStyle;
127141
if (fontFamily != null ||
@@ -232,11 +246,11 @@ class _KatexVlist extends StatelessWidget {
232246
Widget build(BuildContext context) {
233247
final em = DefaultTextStyle.of(context).style.fontSize!;
234248

235-
return Stack(children: List.unmodifiable(node.rows.map((row) {
249+
return IntrinsicWidth(child: Stack(children: List.unmodifiable(node.rows.map((row) {
236250
return Transform.translate(
237251
offset: Offset(0, row.verticalOffsetEm * em),
238252
child: _KatexSpan(row.node));
239-
})));
253+
}))));
240254
}
241255
}
242256

test/model/katex_test.dart

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,109 @@ class KatexExample extends ContentExample {
731731
]),
732732
]),
733733
]);
734+
735+
static final overline = KatexExample.block(
736+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Saif.20KaTeX/near/2285099
737+
r'overline: \overline{AB}',
738+
r'\overline{AB}',
739+
'<p>'
740+
'<span class="katex-display"><span class="katex">'
741+
'<span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mover accent="true"><mrow><mi>A</mi><mi>B</mi></mrow><mo stretchy="true">‾</mo></mover></mrow><annotation encoding="application/x-tex">\\overline{AB}</annotation></semantics></math></span>'
742+
'<span class="katex-html" aria-hidden="true">'
743+
'<span class="base">'
744+
'<span class="strut" style="height:0.8833em;"></span>'
745+
'<span class="mord overline">'
746+
'<span class="vlist-t">'
747+
'<span class="vlist-r">'
748+
'<span class="vlist" style="height:0.8833em;">'
749+
'<span style="top:-3em;">'
750+
'<span class="pstrut" style="height:3em;"></span>'
751+
'<span class="mord">'
752+
'<span class="mord mathnormal">A</span>'
753+
'<span class="mord mathnormal" style="margin-right:0.05017em;">B</span></span></span>'
754+
'<span style="top:-3.8033em;">'
755+
'<span class="pstrut" style="height:3em;"></span>'
756+
'<span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span></span></span></span></p>',[
757+
KatexSpanNode(nodes: [
758+
KatexStrutNode(heightEm: 0.8833, verticalAlignEm: null),
759+
KatexSpanNode(nodes: [
760+
KatexVlistNode(rows: [
761+
KatexVlistRowNode(
762+
verticalOffsetEm: -3 + 3,
763+
node: KatexSpanNode(nodes: [
764+
KatexSpanNode(nodes: [
765+
KatexSpanNode(
766+
styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic),
767+
text: 'A'),
768+
KatexSpanNode(
769+
styles: KatexSpanStyles(marginRightEm: 0.05017, fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic),
770+
text: 'B'),
771+
]),
772+
])),
773+
KatexVlistRowNode(
774+
verticalOffsetEm: -3.8033 + 3,
775+
node: KatexSpanNode(nodes: [
776+
KatexSpanNode(
777+
styles: KatexSpanStyles(borderStyle: KatexBorderStyle(position: KatexBorderPosition.bottom, widthEm: 0.04, color: null)),
778+
nodes: []),
779+
])),
780+
]),
781+
]),
782+
]),
783+
]);
784+
785+
static final underline = KatexExample.block(
786+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Saif.20KaTeX/near/2285099
787+
r'underline: \underline{AB}',
788+
r'\underline{AB}',
789+
'<p>'
790+
'<span class="katex-display"><span class="katex">'
791+
'<span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><munder accentunder="true"><mrow><mi>A</mi><mi>B</mi></mrow><mo stretchy="true">‾</mo></munder></mrow><annotation encoding="application/x-tex">\\underline{AB}</annotation></semantics></math></span>'
792+
'<span class="katex-html" aria-hidden="true">'
793+
'<span class="base">'
794+
'<span class="strut" style="height:0.8833em;vertical-align:-0.2em;"></span>'
795+
'<span class="mord underline">'
796+
'<span class="vlist-t vlist-t2">'
797+
'<span class="vlist-r">'
798+
'<span class="vlist" style="height:0.6833em;">'
799+
'<span style="top:-2.84em;">'
800+
'<span class="pstrut" style="height:3em;"></span>'
801+
'<span class="underline-line" style="border-bottom-width:0.04em;"></span></span>'
802+
'<span style="top:-3em;">'
803+
'<span class="pstrut" style="height:3em;"></span>'
804+
'<span class="mord">'
805+
'<span class="mord mathnormal">A</span>'
806+
'<span class="mord mathnormal" style="margin-right:0.05017em;">B</span></span></span></span>'
807+
'<span class="vlist-s">​</span></span>'
808+
'<span class="vlist-r">'
809+
'<span class="vlist" style="height:0.2em;"><span></span></span></span></span></span></span></span></span></span></p>',[
810+
KatexSpanNode(nodes: [
811+
KatexStrutNode(heightEm: 0.8833, verticalAlignEm: -0.2),
812+
KatexSpanNode(nodes: [
813+
KatexVlistNode(rows: [
814+
KatexVlistRowNode(
815+
verticalOffsetEm: -2.84 + 3,
816+
node: KatexSpanNode(nodes: [
817+
KatexSpanNode(
818+
styles: KatexSpanStyles(borderStyle: KatexBorderStyle(position: KatexBorderPosition.bottom, widthEm: 0.04, color: null)),
819+
nodes: []),
820+
])),
821+
KatexVlistRowNode(
822+
verticalOffsetEm: -3 + 3,
823+
node: KatexSpanNode(nodes: [
824+
KatexSpanNode(nodes: [
825+
KatexSpanNode(
826+
styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic),
827+
text: 'A'),
828+
KatexSpanNode(
829+
styles: KatexSpanStyles(marginRightEm: 0.05017, fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic),
830+
text: 'B'),
831+
]),
832+
])),
833+
]),
834+
]),
835+
]),
836+
]);
734837
}
735838

736839
void main() async {
@@ -754,6 +857,8 @@ void main() async {
754857
testParseExample(KatexExample.bigOperators);
755858
testParseExample(KatexExample.colonEquals);
756859
testParseExample(KatexExample.nulldelimiter);
860+
testParseExample(KatexExample.overline);
861+
testParseExample(KatexExample.underline);
757862

758863
group('parseCssHexColor', () {
759864
const testCases = [
@@ -821,4 +926,4 @@ void main() async {
821926
}, skip: Platform.isWindows, // [intended] purely analyzes source, so
822927
// any one platform is enough; avoid dealing with Windows file paths
823928
);
824-
}
929+
}

test/widgets/katex_test.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ void main() {
8181
('a', Offset(2.47, 3.36), Size(10.88, 25.00)),
8282
('b', Offset(15.81, 3.36), Size(8.82, 25.00)),
8383
]),
84+
(KatexExample.overline, skip: false, [
85+
('A', Offset(0.0, 5.6), Size(15.4, 25.0)),
86+
]),
87+
(KatexExample.underline, skip: false, [
88+
('B', Offset(15.4, 5.6), Size(15.6, 25.0)),
89+
]),
8490
];
8591

8692
for (final testCase in testCases) {

0 commit comments

Comments
 (0)