From 344b5f6239fc8aa3792bf1ed3056b1e09d89015b Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Tue, 1 Apr 2025 20:06:41 +0530 Subject: [PATCH 1/3] content: Support negative margins on KaTeX spans Negative margin spans on web render to the offset being applied to the specific span and all the adjacent spans, so mimic the same behaviour here. --- lib/model/content.dart | 22 ++++ lib/model/katex.dart | 87 +++++++++++--- lib/widgets/content.dart | 17 +++ lib/widgets/katex.dart | 199 +++++++++++++++++++++++++++++++++ test/model/content_test.dart | 117 +++++++++++++++++++ test/widgets/content_test.dart | 11 ++ 6 files changed, 436 insertions(+), 17 deletions(-) create mode 100644 lib/widgets/katex.dart diff --git a/lib/model/content.dart b/lib/model/content.dart index 9f906d1c4c..28486f634d 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -465,6 +465,28 @@ class KatexVlistRowNode extends ContentNode { } } +class KatexNegativeMarginNode extends KatexNode { + const KatexNegativeMarginNode({ + required this.leftOffsetEm, + required this.nodes, + super.debugHtmlNode, + }) : assert(leftOffsetEm < 0); + + final double leftOffsetEm; + final List nodes; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('leftOffsetEm', leftOffsetEm)); + } + + @override + List debugDescribeChildren() { + return nodes.map((node) => node.toDiagnosticsNode()).toList(); + } +} + class MathBlockNode extends MathNode implements BlockContentNode { const MathBlockNode({ super.debugHtmlNode, diff --git a/lib/model/katex.dart b/lib/model/katex.dart index 8fe55dde77..f5cc85d95a 100644 --- a/lib/model/katex.dart +++ b/lib/model/katex.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:csslib/parser.dart' as css_parser; import 'package:csslib/visitor.dart' as css_visitor; import 'package:flutter/foundation.dart'; @@ -167,16 +168,56 @@ class _KatexParser { } List _parseChildSpans(List nodes) { - return List.unmodifiable(nodes.map((node) { - if (node case dom.Element(localName: 'span')) { - return _parseSpan(node); - } else { + var resultSpans = QueueList(); + for (final node in nodes.reversed) { + if (node is! dom.Element || node.localName != 'span') { throw _KatexHtmlParseError( node is dom.Element ? 'unsupported html node: ${node.localName}' : 'unsupported html node'); } - })); + + var span = _parseSpan(node); + final negativeRightMarginEm = switch (span) { + KatexSpanNode(styles: KatexSpanStyles(:final marginRightEm?)) + when marginRightEm.isNegative => marginRightEm, + _ => null, + }; + final negativeLeftMarginEm = switch (span) { + KatexSpanNode(styles: KatexSpanStyles(:final marginLeftEm?)) + when marginLeftEm.isNegative => marginLeftEm, + _ => null, + }; + if (span is KatexSpanNode) { + if (negativeRightMarginEm != null || negativeLeftMarginEm != null) { + span = KatexSpanNode( + styles: span.styles.filter( + marginRightEm: negativeRightMarginEm == null, + marginLeftEm: negativeLeftMarginEm == null), + text: span.text, + nodes: span.nodes); + } + } + + if (negativeRightMarginEm != null) { + final previousSpans = resultSpans; + resultSpans = QueueList(); + resultSpans.addFirst(KatexNegativeMarginNode( + leftOffsetEm: negativeRightMarginEm, + nodes: previousSpans)); + } + + resultSpans.addFirst(span); + + if (negativeLeftMarginEm != null) { + final previousSpans = resultSpans; + resultSpans = QueueList(); + resultSpans.addFirst(KatexNegativeMarginNode( + leftOffsetEm: negativeLeftMarginEm, + nodes: previousSpans)); + } + } + return resultSpans; } static final _resetSizeClassRegExp = RegExp(r'^reset-size(\d\d?)$'); @@ -291,13 +332,31 @@ class _KatexParser { } final pstrutHeight = pstrutStyles.heightEm ?? 0; + KatexSpanNode innerSpanNode = KatexSpanNode( + styles: styles, + text: null, + nodes: _parseChildSpans(otherSpans)); + + final marginRightEm = styles.marginRightEm; + final marginLeftEm = styles.marginLeftEm; + if (marginRightEm != null && marginRightEm.isNegative) { + throw _KatexHtmlParseError(); + } + if (marginLeftEm != null && marginLeftEm.isNegative) { + innerSpanNode = KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexNegativeMarginNode( + leftOffsetEm: marginLeftEm, + nodes: [innerSpanNode]), + ]); + } + rows.add(KatexVlistRowNode( verticalOffsetEm: topEm + pstrutHeight, debugHtmlNode: kDebugMode ? innerSpan : null, - node: KatexSpanNode( - styles: styles, - text: null, - nodes: _parseChildSpans(otherSpans)))); + node: innerSpanNode)); } else { throw _KatexHtmlParseError(); } @@ -630,17 +689,11 @@ class _KatexParser { case 'margin-right': marginRightEm = _getEm(expression); - if (marginRightEm != null) { - if (marginRightEm < 0) throw _KatexHtmlParseError(); - continue; - } + if (marginRightEm != null) continue; case 'margin-left': marginLeftEm = _getEm(expression); - if (marginLeftEm != null) { - if (marginLeftEm < 0) throw _KatexHtmlParseError(); - continue; - } + if (marginLeftEm != null) continue; } // TODO handle more CSS properties diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index ba05f5205e..b4ef707355 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -24,6 +24,7 @@ import 'dialog.dart'; import 'emoji.dart'; import 'icons.dart'; import 'inset_shadow.dart'; +import 'katex.dart'; import 'lightbox.dart'; import 'message_list.dart'; import 'poll.dart'; @@ -898,6 +899,7 @@ class _KatexNodeList extends StatelessWidget { KatexSpanNode() => _KatexSpan(e), KatexStrutNode() => _KatexStrut(e), KatexVlistNode() => _KatexVlist(e), + KatexNegativeMarginNode() => _KatexNegativeMargin(e), })); })))); } @@ -1046,6 +1048,21 @@ class _KatexVlist extends StatelessWidget { } } +class _KatexNegativeMargin extends StatelessWidget { + const _KatexNegativeMargin(this.node); + + final KatexNegativeMarginNode node; + + @override + Widget build(BuildContext context) { + final em = DefaultTextStyle.of(context).style.fontSize!; + + return NegativeLeftOffset( + leftOffset: node.leftOffsetEm * em, + child: _KatexNodeList(nodes: node.nodes)); + } +} + class WebsitePreview extends StatelessWidget { const WebsitePreview({super.key, required this.node}); diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart new file mode 100644 index 0000000000..9b89270c8b --- /dev/null +++ b/lib/widgets/katex.dart @@ -0,0 +1,199 @@ +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; + +class NegativeLeftOffset extends SingleChildRenderObjectWidget { + NegativeLeftOffset({super.key, required this.leftOffset, super.child}) + : assert(leftOffset.isNegative), + _padding = EdgeInsets.only(left: leftOffset); + + final double leftOffset; + final EdgeInsetsGeometry _padding; + + @override + RenderNegativePadding createRenderObject(BuildContext context) { + return RenderNegativePadding( + padding: _padding, + textDirection: Directionality.maybeOf(context)); + } + + @override + void updateRenderObject( + BuildContext context, + RenderNegativePadding renderObject, + ) { + renderObject + ..padding = _padding + ..textDirection = Directionality.maybeOf(context); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('padding', _padding)); + } +} + +// Like [RenderPadding] but only supports negative values. +// TODO(upstream): give Padding an option to accept negative padding (at cost of hit-testing not working) +class RenderNegativePadding extends RenderShiftedBox { + RenderNegativePadding({ + required EdgeInsetsGeometry padding, + TextDirection? textDirection, + RenderBox? child, + }) : assert(!padding.isNonNegative), + _textDirection = textDirection, + _padding = padding, + super(child); + + EdgeInsets? _resolvedPaddingCache; + EdgeInsets get _resolvedPadding { + final EdgeInsets returnValue = _resolvedPaddingCache ??= padding.resolve(textDirection); + return returnValue; + } + + void _markNeedResolution() { + _resolvedPaddingCache = null; + markNeedsLayout(); + } + + /// The amount to pad the child in each dimension. + /// + /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection] + /// must not be null. + EdgeInsetsGeometry get padding => _padding; + EdgeInsetsGeometry _padding; + set padding(EdgeInsetsGeometry value) { + assert(!value.isNonNegative); + if (_padding == value) { + return; + } + _padding = value; + _markNeedResolution(); + } + + /// The text direction with which to resolve [padding]. + /// + /// This may be changed to null, but only after the [padding] has been changed + /// to a value that does not depend on the direction. + TextDirection? get textDirection => _textDirection; + TextDirection? _textDirection; + set textDirection(TextDirection? value) { + if (_textDirection == value) { + return; + } + _textDirection = value; + _markNeedResolution(); + } + + @override + double computeMinIntrinsicWidth(double height) { + final EdgeInsets padding = _resolvedPadding; + if (child != null) { + // Relies on double.infinity absorption. + return child!.getMinIntrinsicWidth(math.max(0.0, height - padding.vertical)) + + padding.horizontal; + } + return padding.horizontal; + } + + @override + double computeMaxIntrinsicWidth(double height) { + final EdgeInsets padding = _resolvedPadding; + if (child != null) { + // Relies on double.infinity absorption. + return child!.getMaxIntrinsicWidth(math.max(0.0, height - padding.vertical)) + + padding.horizontal; + } + return padding.horizontal; + } + + @override + double computeMinIntrinsicHeight(double width) { + final EdgeInsets padding = _resolvedPadding; + if (child != null) { + // Relies on double.infinity absorption. + return child!.getMinIntrinsicHeight(math.max(0.0, width - padding.horizontal)) + + padding.vertical; + } + return padding.vertical; + } + + @override + double computeMaxIntrinsicHeight(double width) { + final EdgeInsets padding = _resolvedPadding; + if (child != null) { + // Relies on double.infinity absorption. + return child!.getMaxIntrinsicHeight(math.max(0.0, width - padding.horizontal)) + + padding.vertical; + } + return padding.vertical; + } + + @override + @protected + Size computeDryLayout(covariant BoxConstraints constraints) { + final EdgeInsets padding = _resolvedPadding; + if (child == null) { + return constraints.constrain(Size(padding.horizontal, padding.vertical)); + } + final BoxConstraints innerConstraints = constraints.deflate(padding); + final Size childSize = child!.getDryLayout(innerConstraints); + return constraints.constrain( + Size(padding.horizontal + childSize.width, padding.vertical + childSize.height), + ); + } + + @override + double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final EdgeInsets padding = _resolvedPadding; + final BoxConstraints innerConstraints = constraints.deflate(padding); + final BaselineOffset result = + BaselineOffset(child.getDryBaseline(innerConstraints, baseline)) + padding.top; + return result.offset; + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + final EdgeInsets padding = _resolvedPadding; + if (child == null) { + size = constraints.constrain(Size(padding.horizontal, padding.vertical)); + return; + } + final BoxConstraints innerConstraints = constraints.deflate(padding); + child!.layout(innerConstraints, parentUsesSize: true); + final BoxParentData childParentData = child!.parentData! as BoxParentData; + childParentData.offset = Offset(padding.left, padding.top); + size = constraints.constrain( + Size(padding.horizontal + child!.size.width, padding.vertical + child!.size.height), + ); + } + + @override + void debugPaintSize(PaintingContext context, Offset offset) { + super.debugPaintSize(context, offset); + assert(() { + final Rect outerRect = offset & size; + debugPaintPadding( + context.canvas, + outerRect, + child != null ? _resolvedPaddingCache!.deflateRect(outerRect) : null, + ); + return true; + }()); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('padding', padding)); + properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); + } +} diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 93e94b5f85..f1f9038795 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -1165,6 +1165,121 @@ class ContentExample { ]), ]); + static const mathBlockKatexNegativeMargin = ContentExample( + 'math block, KaTeX negative margin', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2223563 + '```math\n1 \\! 2\n```', + '

' + '' + '1 ⁣21 \\! 2' + '

', [ + MathBlockNode(texSource: '1 \\! 2', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(styles: KatexSpanStyles(), text: '1', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: '2', nodes: null), + ]), + ]), + ]), + ]); + + static const mathBlockKatexLogo = ContentExample( + 'math block, KaTeX logo', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2141902 + '```math\n\\KaTeX\n```', + '

' + '' + 'KaTeX' + '\\KaTeX' + '

', [ + MathBlockNode(texSource: '\\KaTeX', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'K', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.905 + 2.7, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 + text: 'A', nodes: null), + ]), + ])), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'T', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.7845 + 3, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'E', nodes: null), + ]), + ])), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'X', nodes: null), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + static const imageSingle = ContentExample( 'single image', // https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103 @@ -2259,6 +2374,8 @@ void main() async { testParseExample(ContentExample.mathBlockKatexSubscript); testParseExample(ContentExample.mathBlockKatexSubSuperScript); testParseExample(ContentExample.mathBlockKatexRaisebox); + testParseExample(ContentExample.mathBlockKatexNegativeMargin); + testParseExample(ContentExample.mathBlockKatexLogo); testParseExample(ContentExample.imageSingle); testParseExample(ContentExample.imageSingleNoDimensions); diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index 7cc249d79b..bcc210e13c 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -619,6 +619,17 @@ void main() { ('b', Offset(10.88, -0.66), Size(8.82, 25.00)), ('c', Offset(19.70, 4.16), Size(8.90, 25.00)), ]), + (ContentExample.mathBlockKatexNegativeMargin, skip: false, [ + ('1', Offset(0.00, 3.12), Size(10.28, 25.00)), + ('2', Offset(6.85, 3.36), Size(10.28, 25.00)), + ]), + (ContentExample.mathBlockKatexLogo, skip: false, [ + ('K', Offset(0.0, 8.64), Size(16.0, 25.0)), + ('A', Offset(12.50, 10.85), Size(10.79, 17.0)), + ('T', Offset(20.21, 9.36), Size(14.85, 25.0)), + ('E', Offset(31.63, 14.52), Size(14.0, 25.0)), + ('X', Offset(43.06, 9.85), Size(15.42, 25.0)), + ]), ]; for (final testCase in testCases) { From 01bff8ea5347a40085bb00cd5b43cfd32956a9ef Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Thu, 17 Jul 2025 05:47:33 +0530 Subject: [PATCH 2/3] content test: Add test for negative margins on a vlist row in KaTeX content --- test/model/content_test.dart | 65 ++++++++++++++++++++++++++++++++++ test/widgets/content_test.dart | 9 ++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/test/model/content_test.dart b/test/model/content_test.dart index f1f9038795..5fb3ddc5d2 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -1280,6 +1280,70 @@ class ContentExample { ]), ]); + static const mathBlockKatexNegativeMarginsOnVlistRow = ContentExample( + 'math block, KaTeX negative margins on a vlist row', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2224918 + '```math\nX_n\n```', + '

' + '' + 'XnX_n' + '

', [ + MathBlockNode(texSource: 'X_n', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.8333, verticalAlignEm: -0.15), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles( + marginRightEm: 0.07847, + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'X', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.55 + 2.7, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles( + marginRightEm: 0.05, + // TODO parser should not emit this `marginLeftEm` here because + // it already generates `KatexNegativeMarginNode` for handling it. + marginLeftEm: -0.0785), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'n', nodes: null), + ]), + ]), + ]), + ])), + ]), + ]), + ]), + ]), + ]), + ]); + static const imageSingle = ContentExample( 'single image', // https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103 @@ -2376,6 +2440,7 @@ void main() async { testParseExample(ContentExample.mathBlockKatexRaisebox); testParseExample(ContentExample.mathBlockKatexNegativeMargin); testParseExample(ContentExample.mathBlockKatexLogo); + testParseExample(ContentExample.mathBlockKatexNegativeMarginsOnVlistRow); testParseExample(ContentExample.imageSingle); testParseExample(ContentExample.imageSingleNoDimensions); diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index bcc210e13c..f24c018af7 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -630,6 +630,13 @@ void main() { ('E', Offset(31.63, 14.52), Size(14.0, 25.0)), ('X', Offset(43.06, 9.85), Size(15.42, 25.0)), ]), + // TODO re-enable this test when parser fixes a bug where + // it emits negative margin in styles, allowing widget + // code to hit an assert. + (ContentExample.mathBlockKatexNegativeMarginsOnVlistRow, skip: true, [ + ('X', Offset(0.00, 7.04), Size(17.03, 25.00)), + ('n', Offset(17.03, 15.90), Size(8.63, 17.00)), + ]), ]; for (final testCase in testCases) { @@ -659,7 +666,7 @@ void main() { check(size) .within(distance: 0.05, from: expectedSize); } - }); + }, skip: testCase.skip); } }); }); From 64956b8f0df83e8ccd7aff28c0ce03beb09bbc46 Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Thu, 17 Jul 2025 05:56:01 +0530 Subject: [PATCH 3/3] content: Filter negative margin styles if present on a vlist row This fixes a bug where if negative margin on a vlist row is present the widget side code would hit an assert. Displaying the error (red screen) in debug mode, but in release mode the negative padding would be applied twice, once by `_KatexNegativeMargin` and another by `Padding` widget. --- lib/model/katex.dart | 21 ++++++++++++--------- test/model/content_test.dart | 6 +----- test/widgets/content_test.dart | 5 +---- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/model/katex.dart b/lib/model/katex.dart index f5cc85d95a..42b3277cee 100644 --- a/lib/model/katex.dart +++ b/lib/model/katex.dart @@ -332,10 +332,7 @@ class _KatexParser { } final pstrutHeight = pstrutStyles.heightEm ?? 0; - KatexSpanNode innerSpanNode = KatexSpanNode( - styles: styles, - text: null, - nodes: _parseChildSpans(otherSpans)); + final KatexSpanNode innerSpanNode; final marginRightEm = styles.marginRightEm; final marginLeftEm = styles.marginLeftEm; @@ -346,11 +343,17 @@ class _KatexParser { innerSpanNode = KatexSpanNode( styles: KatexSpanStyles(), text: null, - nodes: [ - KatexNegativeMarginNode( - leftOffsetEm: marginLeftEm, - nodes: [innerSpanNode]), - ]); + nodes: [KatexNegativeMarginNode( + leftOffsetEm: marginLeftEm, + nodes: [KatexSpanNode( + styles: styles.filter(marginLeftEm: false), + text: null, + nodes: _parseChildSpans(otherSpans))])]); + } else { + innerSpanNode = KatexSpanNode( + styles: styles, + text: null, + nodes: _parseChildSpans(otherSpans)); } rows.add(KatexVlistRowNode( diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 5fb3ddc5d2..e4dd622b19 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -1321,11 +1321,7 @@ class ContentExample { node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ KatexSpanNode( - styles: KatexSpanStyles( - marginRightEm: 0.05, - // TODO parser should not emit this `marginLeftEm` here because - // it already generates `KatexNegativeMarginNode` for handling it. - marginLeftEm: -0.0785), + styles: KatexSpanStyles(marginRightEm: 0.05), text: null, nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index f24c018af7..964d7e1208 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -630,10 +630,7 @@ void main() { ('E', Offset(31.63, 14.52), Size(14.0, 25.0)), ('X', Offset(43.06, 9.85), Size(15.42, 25.0)), ]), - // TODO re-enable this test when parser fixes a bug where - // it emits negative margin in styles, allowing widget - // code to hit an assert. - (ContentExample.mathBlockKatexNegativeMarginsOnVlistRow, skip: true, [ + (ContentExample.mathBlockKatexNegativeMarginsOnVlistRow, skip: false, [ ('X', Offset(0.00, 7.04), Size(17.03, 25.00)), ('n', Offset(17.03, 15.90), Size(8.63, 17.00)), ]),