Skip to content

Commit 345e4e7

Browse files
KaTeX overline underline
1 parent 3d3b1c1 commit 345e4e7

File tree

4 files changed

+246
-6
lines changed

4 files changed

+246
-6
lines changed

lib/model/katex.dart

Lines changed: 116 additions & 1 deletion
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,19 +714,37 @@ 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) {
670730
// The meaning of `top` would be different without `position: relative`.
731+
// Allow 'top' without 'position: relative' inside vlist structures
732+
var parent = element.parent;
733+
var isInVlist = false;
734+
735+
while (parent != null) {
736+
if (parent.localName == 'span') {
737+
final className = parent.className;
738+
if (className.contains('vlist') || className.contains('overline') || className.contains('underline')) {
739+
isInVlist = true;
740+
break;
741+
}}
742+
parent = parent.parent;
743+
}
744+
if (!isInVlist) {
671745
throw _KatexHtmlParseError(
672746
'unsupported inline CSS property "top" given "position: ${styles.position}"');
747+
}
673748
}
674749

675750
String? text;
@@ -840,6 +915,39 @@ enum KatexSpanPosition {
840915
relative,
841916
}
842917

918+
enum KatexBorderPosition {
919+
top,
920+
bottom,
921+
}
922+
923+
class KatexBorderStyle {
924+
const KatexBorderStyle({
925+
required this.position,
926+
required this.widthEm,
927+
this.color,
928+
});
929+
930+
final KatexBorderPosition position;
931+
final double widthEm;
932+
final KatexSpanColor? color;
933+
934+
@override
935+
bool operator ==(Object other) {
936+
return other is KatexBorderStyle &&
937+
other.position == position &&
938+
other.widthEm == widthEm &&
939+
other.color == color;
940+
}
941+
942+
@override
943+
int get hashCode => Object.hash('KatexBorderStyle', position, widthEm, color);
944+
945+
@override
946+
String toString() {
947+
return '${objectRuntimeType(this, 'KatexBorderStyle')}($position, $widthEm, $color)';
948+
}
949+
}
950+
843951
class KatexSpanColor {
844952
const KatexSpanColor(this.r, this.g, this.b, this.a);
845953

@@ -893,6 +1001,7 @@ class KatexSpanStyles {
8931001

8941002
final KatexSpanColor? color;
8951003
final KatexSpanPosition? position;
1004+
final KatexBorderStyle? borderStyle;
8961005

8971006
const KatexSpanStyles({
8981007
this.widthEm,
@@ -907,6 +1016,7 @@ class KatexSpanStyles {
9071016
this.textAlign,
9081017
this.color,
9091018
this.position,
1019+
this.borderStyle,
9101020
});
9111021

9121022
@override
@@ -924,6 +1034,7 @@ class KatexSpanStyles {
9241034
textAlign,
9251035
color,
9261036
position,
1037+
borderStyle,
9271038
);
9281039

9291040
@override
@@ -940,7 +1051,8 @@ class KatexSpanStyles {
9401051
other.fontStyle == fontStyle &&
9411052
other.textAlign == textAlign &&
9421053
other.color == color &&
943-
other.position == position;
1054+
other.position == position &&
1055+
other.borderStyle == borderStyle;
9441056
}
9451057

9461058
@override
@@ -958,6 +1070,7 @@ class KatexSpanStyles {
9581070
if (textAlign != null) args.add('textAlign: $textAlign');
9591071
if (color != null) args.add('color: $color');
9601072
if (position != null) args.add('position: $position');
1073+
if (borderStyle != null) args.add('borderStyle: $borderStyle');
9611074
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
9621075
}
9631076

@@ -975,6 +1088,7 @@ class KatexSpanStyles {
9751088
bool textAlign = true,
9761089
bool color = true,
9771090
bool position = true,
1091+
bool borderStyle = true,
9781092
}) {
9791093
return KatexSpanStyles(
9801094
widthEm: widthEm ? this.widthEm : null,
@@ -989,6 +1103,7 @@ class KatexSpanStyles {
9891103
textAlign: textAlign ? this.textAlign : null,
9901104
color: color ? this.color : null,
9911105
position: position ? this.position : null,
1106+
borderStyle: borderStyle ? this.borderStyle : null,
9921107
);
9931108
}
9941109
}

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+
}

0 commit comments

Comments
 (0)