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, ); } } diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart index 4b4f39aa3f..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 || @@ -232,11 +245,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)); + }))), + ); } } 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}', + '

' + '' + 'AB\\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}', + '

' + '' + 'AB\\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 = [ 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) {