Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion lib/model/katex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ class _KatexParser {
KatexSpanFontWeight? fontWeight;
KatexSpanFontStyle? fontStyle;
KatexSpanTextAlign? textAlign;
KatexBorderStyle? borderStyle;
var index = 0;
while (index < spanClasses.length) {
final spanClass = spanClasses[index++];
Expand Down Expand Up @@ -636,6 +637,29 @@ class _KatexParser {
// )
break;

case 'overline':
case 'underline':
// Wrapper spans for overline/underline, the actual border is on -line child
break;

case 'overline-line':
// .overline-line { display: inline-block; width: 100%; border-bottom-style: solid; }
// Border applied via inline style: border-top-width: 0.04em;
borderStyle = KatexBorderStyle(
position: KatexBorderPosition.bottom,
widthEm: 0.04,
);
break;

case 'underline-line':
// .underline-line { display: inline-block; width: 100%; border-bottom-style: solid; }
// Border applied via inline style: border-bottom-width: 0.04em;
borderStyle = KatexBorderStyle(
position: KatexBorderPosition.bottom,
widthEm: 0.04,
);
break;

default:
assert(debugLog('KaTeX: Unsupported CSS class: $spanClass'));
unsupportedCssClasses.add(spanClass);
Expand All @@ -644,6 +668,18 @@ class _KatexParser {
}

final inlineStyles = _parseInlineStyles(element);
// Extract border width if borderStyle was set
if (borderStyle != null) {
if (inlineStyles != null) {
final borderWidthEm = _takeStyleEm(inlineStyles, 'border-bottom-width');
if (borderWidthEm != null) {
borderStyle = KatexBorderStyle(
position: borderStyle.position,
widthEm: borderWidthEm,
color: borderStyle.color,
);
}}
}
final styles = KatexSpanStyles(
widthEm: widthEm,
fontFamily: fontFamily,
Expand All @@ -657,10 +693,13 @@ class _KatexParser {
marginRightEm: _takeStyleEm(inlineStyles, 'margin-right'),
color: _takeStyleColor(inlineStyles, 'color'),
position: _takeStylePosition(inlineStyles, 'position'),
borderStyle: borderStyle,
// TODO handle more CSS properties
);
if (inlineStyles != null && inlineStyles.isNotEmpty) {
for (final property in inlineStyles.keys) {
// Ignore known properties that don't need special handling
if (property == 'width' || property == 'min-width' || property == 'border-bottom-width') {continue;}
assert(debugLog('KaTeX: Unexpected inline CSS property: $property'));
unsupportedInlineCssProperties.add(property);
_hasError = true;
Expand Down Expand Up @@ -840,6 +879,39 @@ enum KatexSpanPosition {
relative,
}

enum KatexBorderPosition {
top,
bottom,
}

class KatexBorderStyle {
const KatexBorderStyle({
required this.position,
required this.widthEm,
this.color,
});

final KatexBorderPosition position;
final double widthEm;
final KatexSpanColor? color;

@override
bool operator ==(Object other) {
return other is KatexBorderStyle &&
other.position == position &&
other.widthEm == widthEm &&
other.color == color;
}

@override
int get hashCode => Object.hash('KatexBorderStyle', position, widthEm, color);

@override
String toString() {
return '${objectRuntimeType(this, 'KatexBorderStyle')}($position, $widthEm, $color)';
}
}

class KatexSpanColor {
const KatexSpanColor(this.r, this.g, this.b, this.a);

Expand Down Expand Up @@ -893,6 +965,7 @@ class KatexSpanStyles {

final KatexSpanColor? color;
final KatexSpanPosition? position;
final KatexBorderStyle? borderStyle;

const KatexSpanStyles({
this.widthEm,
Expand All @@ -907,6 +980,7 @@ class KatexSpanStyles {
this.textAlign,
this.color,
this.position,
this.borderStyle,
});

@override
Expand All @@ -924,6 +998,7 @@ class KatexSpanStyles {
textAlign,
color,
position,
borderStyle,
);

@override
Expand All @@ -940,7 +1015,8 @@ class KatexSpanStyles {
other.fontStyle == fontStyle &&
other.textAlign == textAlign &&
other.color == color &&
other.position == position;
other.position == position &&
other.borderStyle == borderStyle;
}

@override
Expand All @@ -958,6 +1034,7 @@ class KatexSpanStyles {
if (textAlign != null) args.add('textAlign: $textAlign');
if (color != null) args.add('color: $color');
if (position != null) args.add('position: $position');
if (borderStyle != null) args.add('borderStyle: $borderStyle');
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
}

Expand All @@ -975,6 +1052,7 @@ class KatexSpanStyles {
bool textAlign = true,
bool color = true,
bool position = true,
bool borderStyle = true,
}) {
return KatexSpanStyles(
widthEm: widthEm ? this.widthEm : null,
Expand All @@ -989,6 +1067,7 @@ class KatexSpanStyles {
textAlign: textAlign ? this.textAlign : null,
color: color ? this.color : null,
position: position ? this.position : null,
borderStyle: borderStyle ? this.borderStyle : null,
);
}
}
Expand Down
40 changes: 29 additions & 11 deletions lib/widgets/katex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ class KatexWidget extends StatelessWidget {
Widget build(BuildContext context) {
Widget widget = _KatexNodeList(nodes: nodes);

return Directionality(
textDirection: TextDirection.ltr,
child: DefaultTextStyle(
style: mkBaseKatexTextStyle(textStyle).copyWith(
color: ContentTheme.of(context).textStylePlainParagraph.color),
child: widget));
return IntrinsicWidth(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the reasoning behind this change? If it is needed, then it should have it's own separate commit.

Copy link
Author

@MdSaifAliMolla MdSaifAliMolla Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without IntrinsicWidth the line takes up width of minWidth: double.infinity, so it is used to restrict the width of over/underline. Also similar thing is already in use inside RenderNegativePadding like here -computeMaxIntrinsicWidth , computeMinIntrinsicHeight, and computeMaxIntrinsicHeight,
so I thought it is as per coding convention.

Screenshot_1761587255

child: Directionality(
textDirection: TextDirection.ltr,
child: DefaultTextStyle(
style: mkBaseKatexTextStyle(textStyle).copyWith(
color: ContentTheme.of(context).textStylePlainParagraph.color),
child: widget)),
);
}
}

Expand Down Expand Up @@ -122,6 +124,20 @@ class _KatexSpan extends StatelessWidget {
Color.fromARGB(katexColor.a, katexColor.r, katexColor.g, katexColor.b),
null => null,
};
if (styles.borderStyle case final borderStyle?) {
final currentColor = color ?? DefaultTextStyle.of(context).style.color!;
final Color borderColor = borderStyle.color != null
? Color.fromARGB(borderStyle.color!.a, borderStyle.color!.r, borderStyle.color!.g, borderStyle.color!.b)
: currentColor;
final double borderWidth = borderStyle.widthEm * em;

widget = Container(
constraints: const BoxConstraints(minWidth: double.infinity),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: borderColor, width: borderWidth))),
child: widget,
);
}

TextStyle? textStyle;
if (fontFamily != null ||
Expand Down Expand Up @@ -232,11 +248,13 @@ class _KatexVlist extends StatelessWidget {
Widget build(BuildContext context) {
final em = DefaultTextStyle.of(context).style.fontSize!;

return Stack(children: List.unmodifiable(node.rows.map((row) {
return Transform.translate(
offset: Offset(0, row.verticalOffsetEm * em),
child: _KatexSpan(row.node));
})));
return IntrinsicWidth(
child: Stack(children: List.unmodifiable(node.rows.map((row) {
return Transform.translate(
offset: Offset(0, row.verticalOffsetEm * em),
child: _KatexSpan(row.node));
}))),
);
}
}

Expand Down
105 changes: 105 additions & 0 deletions test/model/katex_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,109 @@ class KatexExample extends ContentExample {
]),
]),
]);

static final overline = KatexExample.block(
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Saif.20KaTeX/near/2285099
r'overline: \overline{AB}',
r'\overline{AB}',
'<p>'
'<span class="katex-display"><span class="katex">'
'<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>'
'<span class="katex-html" aria-hidden="true">'
'<span class="base">'
'<span class="strut" style="height:0.8833em;"></span>'
'<span class="mord overline">'
'<span class="vlist-t">'
'<span class="vlist-r">'
'<span class="vlist" style="height:0.8833em;">'
'<span style="top:-3em;">'
'<span class="pstrut" style="height:3em;"></span>'
'<span class="mord">'
'<span class="mord mathnormal">A</span>'
'<span class="mord mathnormal" style="margin-right:0.05017em;">B</span></span></span>'
'<span style="top:-3.8033em;">'
'<span class="pstrut" style="height:3em;"></span>'
'<span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span></span></span></span></p>',[
KatexSpanNode(nodes: [
KatexStrutNode(heightEm: 0.8833, verticalAlignEm: null),
KatexSpanNode(nodes: [
KatexVlistNode(rows: [
KatexVlistRowNode(
verticalOffsetEm: -3 + 3,
node: KatexSpanNode(nodes: [
KatexSpanNode(nodes: [
KatexSpanNode(
styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic),
text: 'A'),
KatexSpanNode(
styles: KatexSpanStyles(marginRightEm: 0.05017, fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic),
text: 'B'),
]),
])),
KatexVlistRowNode(
verticalOffsetEm: -3.8033 + 3,
node: KatexSpanNode(nodes: [
KatexSpanNode(
styles: KatexSpanStyles(borderStyle: KatexBorderStyle(position: KatexBorderPosition.bottom, widthEm: 0.04, color: null)),
nodes: []),
])),
]),
]),
]),
]);

static final underline = KatexExample.block(
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Saif.20KaTeX/near/2285099
r'underline: \underline{AB}',
r'\underline{AB}',
'<p>'
'<span class="katex-display"><span class="katex">'
'<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>'
'<span class="katex-html" aria-hidden="true">'
'<span class="base">'
'<span class="strut" style="height:0.8833em;vertical-align:-0.2em;"></span>'
'<span class="mord underline">'
'<span class="vlist-t vlist-t2">'
'<span class="vlist-r">'
'<span class="vlist" style="height:0.6833em;">'
'<span style="top:-2.84em;">'
'<span class="pstrut" style="height:3em;"></span>'
'<span class="underline-line" style="border-bottom-width:0.04em;"></span></span>'
'<span style="top:-3em;">'
'<span class="pstrut" style="height:3em;"></span>'
'<span class="mord">'
'<span class="mord mathnormal">A</span>'
'<span class="mord mathnormal" style="margin-right:0.05017em;">B</span></span></span></span>'
'<span class="vlist-s">​</span></span>'
'<span class="vlist-r">'
'<span class="vlist" style="height:0.2em;"><span></span></span></span></span></span></span></span></span></span></p>',[
KatexSpanNode(nodes: [
KatexStrutNode(heightEm: 0.8833, verticalAlignEm: -0.2),
KatexSpanNode(nodes: [
KatexVlistNode(rows: [
KatexVlistRowNode(
verticalOffsetEm: -2.84 + 3,
node: KatexSpanNode(nodes: [
KatexSpanNode(
styles: KatexSpanStyles(borderStyle: KatexBorderStyle(position: KatexBorderPosition.bottom, widthEm: 0.04, color: null)),
nodes: []),
])),
KatexVlistRowNode(
verticalOffsetEm: -3 + 3,
node: KatexSpanNode(nodes: [
KatexSpanNode(nodes: [
KatexSpanNode(
styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic),
text: 'A'),
KatexSpanNode(
styles: KatexSpanStyles(marginRightEm: 0.05017, fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic),
text: 'B'),
]),
])),
]),
]),
]),
]);
}

void main() async {
Expand All @@ -754,6 +857,8 @@ void main() async {
testParseExample(KatexExample.bigOperators);
testParseExample(KatexExample.colonEquals);
testParseExample(KatexExample.nulldelimiter);
testParseExample(KatexExample.overline);
testParseExample(KatexExample.underline);

group('parseCssHexColor', () {
const testCases = [
Expand Down
6 changes: 6 additions & 0 deletions test/widgets/katex_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ void main() {
('a', Offset(2.47, 3.36), Size(10.88, 25.00)),
('b', Offset(15.81, 3.36), Size(8.82, 25.00)),
]),
(KatexExample.overline, skip: false, [
('A', Offset(0.0, 5.6), Size(15.4, 25.0)),
]),
(KatexExample.underline, skip: false, [
('B', Offset(15.4, 5.6), Size(15.6, 25.0)),
]),
];

for (final testCase in testCases) {
Expand Down