diff --git a/lib/model/katex.dart b/lib/model/katex.dart index e4bd080e95..bed95d2063 100644 --- a/lib/model/katex.dart +++ b/lib/model/katex.dart @@ -626,6 +626,13 @@ class _KatexParser { case 'nobreak': case 'allowbreak': case 'mathdefault': + case 'overline': + case 'underline': + 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; // 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: @@ -657,6 +664,7 @@ class _KatexParser { marginRightEm: _takeStyleEm(inlineStyles, 'margin-right'), color: _takeStyleColor(inlineStyles, 'color'), position: _takeStylePosition(inlineStyles, 'position'), + borderBottomWidthEm: _takeStyleEm(inlineStyles, 'border-bottom-width') // TODO handle more CSS properties ); if (inlineStyles != null && inlineStyles.isNotEmpty) { @@ -893,6 +901,7 @@ class KatexSpanStyles { final KatexSpanColor? color; final KatexSpanPosition? position; + final double? borderBottomWidthEm; const KatexSpanStyles({ this.widthEm, @@ -907,6 +916,7 @@ class KatexSpanStyles { this.textAlign, this.color, this.position, + this.borderBottomWidthEm, }); @override @@ -924,6 +934,7 @@ class KatexSpanStyles { textAlign, color, position, + borderBottomWidthEm ); @override @@ -940,7 +951,8 @@ class KatexSpanStyles { other.fontStyle == fontStyle && other.textAlign == textAlign && other.color == color && - other.position == position; + other.position == position && + other.borderBottomWidthEm == borderBottomWidthEm; } @override @@ -958,6 +970,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 (borderBottomWidthEm != null) args.add('borderBottomWidthEm: $borderBottomWidthEm'); return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})'; } @@ -975,6 +988,7 @@ class KatexSpanStyles { bool textAlign = true, bool color = true, bool position = true, + bool borderBottomWidthEm = true, }) { return KatexSpanStyles( widthEm: widthEm ? this.widthEm : null, @@ -989,6 +1003,7 @@ class KatexSpanStyles { textAlign: textAlign ? this.textAlign : null, color: color ? this.color : null, position: position ? this.position : null, + borderBottomWidthEm: borderBottomWidthEm ? this.borderBottomWidthEm : null, ); } } diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart index 4b4f39aa3f..490a0a3a78 100644 --- a/lib/widgets/katex.dart +++ b/lib/widgets/katex.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import '../model/content.dart'; @@ -231,12 +231,75 @@ class _KatexVlist extends StatelessWidget { @override Widget build(BuildContext context) { final em = DefaultTextStyle.of(context).style.fontSize!; + final defaultColor = DefaultTextStyle.of(context).style.color ; + + final lines = <_VlistLine>[]; + for (final row in node.rows) { + final lineInfo = _findLineInfo(row.node); + if (lineInfo != null) { + final y = (row.verticalOffsetEm - (lineInfo.heightEm ?? 0) + 0.9) * em; + final thickness = lineInfo.borderWidthEm * em; + lines.add(_VlistLine(y, thickness, lineInfo.color ?? defaultColor)); + }} + + return CustomPaint( + foregroundPainter: _KatexBorderPainter(lines), + child: Stack( + children: List.unmodifiable(node.rows.map((row) { + return Transform.translate( + offset: Offset(0, row.verticalOffsetEm * em), + child: _KatexSpan(row.node)); + })))); + } + ({double? heightEm, double borderWidthEm, Color? color})? _findLineInfo(KatexSpanNode span) { + final borderWidth = span.styles.borderBottomWidthEm; + final color = span.styles.color != null + ? Color.fromARGB(span.styles.color!.a, span.styles.color!.r, span.styles.color!.g, span.styles.color!.b) + : null; + if (borderWidth != null) {return (heightEm: span.styles.heightEm, borderWidthEm: borderWidth, color: color);} + + if (span.nodes != null) { + for (final child in span.nodes!) { + if (child is KatexSpanNode) { + final info = _findLineInfo(child); + if (info != null) return info; + }}} + return null; + } +} + +class _VlistLine { + const _VlistLine(this.y, this.thickness, this.color); + final double y; + final double thickness; + final Color? color; +} + +class _KatexBorderPainter extends CustomPainter { + const _KatexBorderPainter(this.lines); - return Stack(children: List.unmodifiable(node.rows.map((row) { - return Transform.translate( - offset: Offset(0, row.verticalOffsetEm * em), - child: _KatexSpan(row.node)); - }))); + final List<_VlistLine> lines; + + @override + void paint(Canvas canvas, Size size) { + if (lines.isEmpty) return; + final paint = Paint()..style = PaintingStyle.fill; + for (final line in lines) { + paint.color = line.color ?? Colors.black; + canvas.drawRect(Rect.fromLTWH(0, line.y, size.width, line.thickness), paint); + } + } + + @override + bool shouldRepaint(covariant _KatexBorderPainter oldDelegate) { + if (oldDelegate.lines.length != lines.length) return true; + for (var i = 0; i < lines.length; i++) { + if (oldDelegate.lines[i].y != lines[i].y || + oldDelegate.lines[i].thickness != lines[i].thickness || + oldDelegate.lines[i].color != lines[i].color) { + return true; + }} + return false; } } diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 6ebc832e02..008236997d 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(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(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) {