diff --git a/lib/model/katex.dart b/lib/model/katex.dart index e4bd080e95..77fffaea8d 100644 --- a/lib/model/katex.dart +++ b/lib/model/katex.dart @@ -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++]; @@ -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); @@ -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, @@ -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; @@ -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); @@ -893,6 +965,7 @@ class KatexSpanStyles { final KatexSpanColor? color; final KatexSpanPosition? position; + final KatexBorderStyle? borderStyle; const KatexSpanStyles({ this.widthEm, @@ -907,6 +980,7 @@ class KatexSpanStyles { this.textAlign, this.color, this.position, + this.borderStyle, }); @override @@ -924,6 +998,7 @@ class KatexSpanStyles { textAlign, color, position, + borderStyle, ); @override @@ -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 @@ -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(', ')})'; } @@ -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, @@ -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, ); } } diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart index 4b4f39aa3f..997638df8b 100644 --- a/lib/widgets/katex.dart +++ b/lib/widgets/katex.dart @@ -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( + child: Directionality( + textDirection: TextDirection.ltr, + child: DefaultTextStyle( + style: mkBaseKatexTextStyle(textStyle).copyWith( + color: ContentTheme.of(context).textStylePlainParagraph.color), + child: widget)), + ); } } @@ -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 || @@ -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)); + }))), + ); } } diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 6ebc832e02..bd79e5939e 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( + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Saif.20KaTeX/near/2285099 + r'overline: \overline{AB}', + 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(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}', + '' + '' + '' + '
',[ + 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 { @@ -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..b2f1d420e5 100644 --- a/test/widgets/katex_test.dart +++ b/test/widgets/katex_test.dart @@ -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) {