From a340ce3f3bdc613eca42b66104ca370e171463c2 Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Fri, 31 Oct 2025 02:58:10 +0530 Subject: [PATCH 1/5] katex: Wrap KatexVlistNode in IntrinsicWidth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensures overline and underline widths match text’s width instead of expanding to 100% of the container. --- lib/widgets/katex.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart index 4b4f39aa3f..795c342cc9 100644 --- a/lib/widgets/katex.dart +++ b/lib/widgets/katex.dart @@ -232,11 +232,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)); + }))), + ); } } From 696342e7d66e9e49408f65ba16e51205f6a77c95 Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Fri, 31 Oct 2025 03:23:12 +0530 Subject: [PATCH 2/5] katex: Add CSS support for overline and underline borders. Adds parsing and styling support for the overline-line and underline-line CSS classes used in KaTeX rendering. These classes apply a solid border-bottom-style to create the visual line effect. The implementation adds: - KatexSpanBorderBottomStyle enum for border style values - borderBottomStyle and borderBottomWidthEm fields to KatexSpanStyles - Parsing of overline-line/underline-line classes from KaTeX HTML - Support for border-bottom-width inline style property The overline and underline base classes are now explicitly ignored in parsing, as they don't have CSS definitions in katex.scss but appear in the generated HTML for semantic purposes. --- lib/model/katex.dart | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/model/katex.dart b/lib/model/katex.dart index e4bd080e95..4116680498 100644 --- a/lib/model/katex.dart +++ b/lib/model/katex.dart @@ -411,6 +411,7 @@ class _KatexParser { KatexSpanFontWeight? fontWeight; KatexSpanFontStyle? fontStyle; KatexSpanTextAlign? textAlign; + KatexSpanBorderBottomStyle? borderBottomStyle; var index = 0; while (index < spanClasses.length) { final spanClass = spanClasses[index++]; @@ -626,6 +627,8 @@ class _KatexParser { case 'nobreak': case 'allowbreak': case 'mathdefault': + case 'overline': + case 'underline': // 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: @@ -636,6 +639,15 @@ class _KatexParser { // ) break; + case 'overline-line': + case 'underline-line': + // .overline-line, + // .underline-line { width: 100%; border-bottom-style: solid; } + // Border applied via inline style: border-bottom-width: 0.04em; + widthEm = double.infinity; + borderBottomStyle = KatexSpanBorderBottomStyle.solid; + break; + default: assert(debugLog('KaTeX: Unsupported CSS class: $spanClass')); unsupportedCssClasses.add(spanClass); @@ -657,6 +669,8 @@ class _KatexParser { marginRightEm: _takeStyleEm(inlineStyles, 'margin-right'), color: _takeStyleColor(inlineStyles, 'color'), position: _takeStylePosition(inlineStyles, 'position'), + borderBottomStyle: borderBottomStyle, + borderBottomWidthEm: _takeStyleEm(inlineStyles, 'border-bottom-width') // TODO handle more CSS properties ); if (inlineStyles != null && inlineStyles.isNotEmpty) { @@ -840,6 +854,10 @@ enum KatexSpanPosition { relative, } +enum KatexSpanBorderBottomStyle { + solid, +} + class KatexSpanColor { const KatexSpanColor(this.r, this.g, this.b, this.a); @@ -893,6 +911,8 @@ class KatexSpanStyles { final KatexSpanColor? color; final KatexSpanPosition? position; + final KatexSpanBorderBottomStyle? borderBottomStyle; + final double? borderBottomWidthEm; const KatexSpanStyles({ this.widthEm, @@ -907,6 +927,8 @@ class KatexSpanStyles { this.textAlign, this.color, this.position, + this.borderBottomStyle, + this.borderBottomWidthEm, }); @override @@ -924,6 +946,8 @@ class KatexSpanStyles { textAlign, color, position, + borderBottomStyle, + borderBottomWidthEm ); @override @@ -940,7 +964,9 @@ class KatexSpanStyles { other.fontStyle == fontStyle && other.textAlign == textAlign && other.color == color && - other.position == position; + other.position == position && + other.borderBottomStyle == borderBottomStyle && + other.borderBottomWidthEm == borderBottomWidthEm; } @override @@ -958,6 +984,8 @@ class KatexSpanStyles { if (textAlign != null) args.add('textAlign: $textAlign'); if (color != null) args.add('color: $color'); if (position != null) args.add('position: $position'); + if (borderBottomStyle != null) args.add('borderBottomStyle: $borderBottomStyle'); + if (borderBottomWidthEm != null) args.add('borderBottomWidthEm: $borderBottomWidthEm'); return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})'; } @@ -975,6 +1003,8 @@ class KatexSpanStyles { bool textAlign = true, bool color = true, bool position = true, + bool borderBottomStyle = true, + bool borderBottomWidthEm = true, }) { return KatexSpanStyles( widthEm: widthEm ? this.widthEm : null, @@ -989,6 +1019,8 @@ class KatexSpanStyles { textAlign: textAlign ? this.textAlign : null, color: color ? this.color : null, position: position ? this.position : null, + borderBottomStyle: borderBottomStyle ? this.borderBottomStyle : null, + borderBottomWidthEm: borderBottomWidthEm ? this.borderBottomWidthEm : null, ); } } From cab066d150f0fd42d6ba303c412269343ff29f12 Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Fri, 31 Oct 2025 03:34:29 +0530 Subject: [PATCH 3/5] katex: Render overline and underline with using DecoratedBox. Implements the visual rendering of overline and underline elements using DecoratedBox with a bottom border. When a span has a solid border-bottom-style and border width, we wrap the widget in a DecoratedBox with a BorderSide. The border color matches the katexSpan color (or defaults to the current text style color), and the border width is calculated from the em value specified in the inline styles. --- lib/widgets/katex.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart index 795c342cc9..456285cbc4 100644 --- a/lib/widgets/katex.dart +++ b/lib/widgets/katex.dart @@ -123,6 +123,19 @@ class _KatexSpan extends StatelessWidget { null => null, }; + if (styles.borderBottomStyle == KatexSpanBorderBottomStyle.solid && + styles.borderBottomWidthEm != null) { + final borderColor = color ?? DefaultTextStyle.of(context).style.color!; + final borderWidth = styles.borderBottomWidthEm! * em; + + widget = DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: borderColor, width: borderWidth, style: BorderStyle.solid))), + child: widget, + ); + } + TextStyle? textStyle; if (fontFamily != null || fontSize != null || From 958b5b7bd52b633b800206b031a3be1d90f53adc Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Fri, 31 Oct 2025 03:40:10 +0530 Subject: [PATCH 4/5] katex-test: Add unit tests for \overline and \underline --- test/model/katex_test.dart | 105 +++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 6ebc832e02..48bf7d542f 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -731,6 +731,109 @@ class KatexExample extends ContentExample { ]), ]), ]); + + static final overline = KatexExample.block( + r'overline: \overline{AB}', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Saif.20KaTeX/near/2285099 + r'\overline{AB}', + '
' + '' + '' + '
',[ + 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(widthEm: double.infinity, borderBottomStyle: KatexSpanBorderBottomStyle.solid, borderBottomWidthEm: 0.04), + nodes: []), + ])), + ]), + ]), + ]), + ]); + + static final underline = KatexExample.block( + r'underline: \underline{AB}', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Saif.20KaTeX/near/2285099 + r'\underline{AB}', + '' + '' + '' + '
',[ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.8833, verticalAlignEm: -0.2), + KatexSpanNode(nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.84 + 3, + node: KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(widthEm: double.infinity, borderBottomStyle: KatexSpanBorderBottomStyle.solid, borderBottomWidthEm: 0.04), + 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 { @@ -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 = [ From b42a40fb0591de3ddaa1fc8caa03d0a1eec095be Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Fri, 31 Oct 2025 03:41:37 +0530 Subject: [PATCH 5/5] katex-test: Add widget tests for \overline and \underline --- test/widgets/katex_test.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/widgets/katex_test.dart b/test/widgets/katex_test.dart index 0d6a93ca52..fec99f535c 100644 --- a/test/widgets/katex_test.dart +++ b/test/widgets/katex_test.dart @@ -81,6 +81,14 @@ 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.61), Size(15.43, 25.0)), + ('B', Offset(15.43, 5.61), Size(15.61, 25.0)), + ]), + (KatexExample.underline, skip: false, [ + ('A', Offset(0.0, 5.61), Size(15.43, 25.0)), + ('B', Offset(15.43, 5.61), Size(15.61, 25.0)), + ]), ]; for (final testCase in testCases) {