Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
102 changes: 101 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 @@ -626,6 +627,32 @@ class _KatexParser {
case 'nobreak':
case 'allowbreak':
case 'mathdefault':
case 'tag':
case 'eqn-num':
case 'mtable':
case 'col-align-l':
case 'col-align-c':
case 'col-align-r':
case 'delimcenter':
case 'accent':
case 'accent-body':
case 'vlist':
case 'vlist-r':
case 'vlist-s':
case 'svg-align':
case 'hide-tail':
case 'halfarrow-left':
case 'halfarrow-right':
case 'brace-left':
case 'brace-center':
case 'brace-right':
case 'root':
case 'sqrt':
case 'pstrut':
case 'arraycolsep':
case 'vertical-separator':
case 'frac-line':
case 'mfrac':
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, can you explain the reasoning for ignoring all these classes? I see many here that we already handle in some way.

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.

sorry for lack of explanation.
with forceRenderKatex off, the katex code kept falling back to source code, so I ignored all possible warnings, but actually by just ignoring overline and underline case the fallback problem was averted. So these cases can be removed.
The reason of this:

case 'overline':
case 'underline':
          break;

is that \overline or \underline and \overline-line or \underline-line are parent-child constructs and by assigning borderStyle to both, the parser ends up with a redundant border on the outer wrapper, and that gives this error ---

Exception caught by widgets library ═══════════════════════════════════ 
'package:flutter/src/widgets/framework.dart': Failed assertion: line 6420 pos 14: '() { 
// check that it really is our descendant 
Element? ancestor = dependent._parent; 
while (ancestor != this && ancestor != null) { 
ancestor = ancestor._parent; 
} 
return ancestor == this; 
}()': is not true.

ignoring those fixes the issue and also matches KaTeX’s expected DOM structure for \overline{AB}.

// Ignore these classes because they don't have a CSS definition
// in katex.scss, but we encounter them in the generated HTML.
// (Why are they there if they're not used? The story seems to be:
Expand All @@ -636,6 +663,24 @@ class _KatexParser {
// )
break;

case 'overline':
case 'underline':
break;
Comment on lines 640 to 643
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason these classes are ignored?

Copy link
Author

Choose a reason for hiding this comment

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

comment #1919 (comment)


case 'overline-line':
borderStyle = KatexBorderStyle(
position: KatexBorderPosition.bottom,
widthEm: 0.04,
);
break;

case 'underline-line':
borderStyle = KatexBorderStyle(
position: KatexBorderPosition.bottom,
widthEm: 0.04,
);
Comment on lines 645 to 660
Copy link
Member

Choose a reason for hiding this comment

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

Please add the CSS source inline from katex.scss for these classes like other cases here.

Copy link
Author

Choose a reason for hiding this comment

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

In katex.scss, I found this --

.overline .overline-line,
.underline .underline-line,
.hline {
    display: inline-block;
    width: 100%;
    border-bottom-style: solid;
}

so the I should add this, right?

// .overline-line { display: inline-block; width: 100%; border-bottom-style: solid; }
 // Border applied via inline style: border-top-width: 0.04em;

break;

default:
assert(debugLog('KaTeX: Unsupported CSS class: $spanClass'));
unsupportedCssClasses.add(spanClass);
Expand All @@ -644,6 +689,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 +714,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 +900,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 +986,7 @@ class KatexSpanStyles {

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

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

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

@override
Expand All @@ -940,7 +1036,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 +1055,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 +1073,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 +1088,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
22 changes: 18 additions & 4 deletions lib/widgets/katex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ class KatexWidget extends StatelessWidget {
Widget build(BuildContext context) {
Widget widget = _KatexNodeList(nodes: nodes);

return Directionality(
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));
child: widget)));
}
}

Expand Down Expand Up @@ -122,6 +123,19 @@ 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;

return Container(
constraints: const BoxConstraints(minWidth: double.infinity),
height: borderWidth,
color: borderColor,
);
}
Comment on lines 127 to 140
Copy link
Member

Choose a reason for hiding this comment

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

Continued from #1919 (comment).

The return here ends up discarding the base widget, again is there a reason to not use BoxDecoration widget with a border.

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.

sorry about discarding base widget, this change should be fine I think

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

about BoxDecoration, I used it at first but the line was not appearing on screen, so I worked around to get a working solution. BoxConstraint method worked so I kept that. Now I figured that using BoxDecoration with BoxConstraints works too.
also BoxConstraints is already in use inside RenderNegativePadding class like in this computeDryLayout, computeDryBaseline etc places.


TextStyle? textStyle;
if (fontFamily != null ||
Expand Down Expand Up @@ -232,11 +246,11 @@ 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 IntrinsicWidth(child: Stack(children: List.unmodifiable(node.rows.map((row) {
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

Choose a reason for hiding this comment

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

In KatexWidget, it ensures the entire expression only takes as much horizontal space as needed and in vlist, it’s used to properly size vertically stacked elements like fractions, so widest child's width becomes the lines's width.

return Transform.translate(
offset: Offset(0, row.verticalOffsetEm * em),
child: _KatexSpan(row.node));
})));
}))));
}
}

Expand Down
107 changes: 106 additions & 1 deletion 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 Expand Up @@ -821,4 +926,4 @@ void main() async {
}, skip: Platform.isWindows, // [intended] purely analyzes source, so
// any one platform is enough; avoid dealing with Windows file paths
);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

nit: the removal of the newline here is probably unintentional.

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