Skip to content

Commit e6610be

Browse files
committed
Merge remote-tracking branch 'pr/1559'
2 parents 3dac839 + 2e60440 commit e6610be

File tree

6 files changed

+451
-17
lines changed

6 files changed

+451
-17
lines changed

lib/model/content.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,28 @@ class KatexVlistRowNode extends ContentNode {
460460
}
461461
}
462462

463+
class KatexNegativeMarginNode extends KatexNode {
464+
const KatexNegativeMarginNode({
465+
required this.leftOffsetEm,
466+
required this.nodes,
467+
super.debugHtmlNode,
468+
}) : assert(leftOffsetEm < 0);
469+
470+
final double leftOffsetEm;
471+
final List<KatexNode> nodes;
472+
473+
@override
474+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
475+
super.debugFillProperties(properties);
476+
properties.add(DoubleProperty('leftOffsetEm', leftOffsetEm));
477+
}
478+
479+
@override
480+
List<DiagnosticsNode> debugDescribeChildren() {
481+
return nodes.map((node) => node.toDiagnosticsNode()).toList();
482+
}
483+
}
484+
463485
class MathBlockNode extends MathNode implements BlockContentNode {
464486
const MathBlockNode({
465487
super.debugHtmlNode,

lib/model/katex.dart

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:collection/collection.dart';
12
import 'package:csslib/parser.dart' as css_parser;
23
import 'package:csslib/visitor.dart' as css_visitor;
34
import 'package:flutter/foundation.dart';
@@ -167,16 +168,56 @@ class _KatexParser {
167168
}
168169

169170
List<KatexNode> _parseChildSpans(List<dom.Node> nodes) {
170-
return List.unmodifiable(nodes.map((node) {
171-
if (node case dom.Element(localName: 'span')) {
172-
return _parseSpan(node);
173-
} else {
171+
var resultSpans = QueueList<KatexNode>();
172+
for (final node in nodes.reversed) {
173+
if (node is! dom.Element || node.localName != 'span') {
174174
throw _KatexHtmlParseError(
175175
node is dom.Element
176176
? 'unsupported html node: ${node.localName}'
177177
: 'unsupported html node');
178178
}
179-
}));
179+
180+
var span = _parseSpan(node);
181+
final negativeRightMarginEm = switch (span) {
182+
KatexSpanNode(styles: KatexSpanStyles(:final marginRightEm?))
183+
when marginRightEm.isNegative => marginRightEm,
184+
_ => null,
185+
};
186+
final negativeLeftMarginEm = switch (span) {
187+
KatexSpanNode(styles: KatexSpanStyles(:final marginLeftEm?))
188+
when marginLeftEm.isNegative => marginLeftEm,
189+
_ => null,
190+
};
191+
if (span is KatexSpanNode) {
192+
if (negativeRightMarginEm != null || negativeLeftMarginEm != null) {
193+
span = KatexSpanNode(
194+
styles: span.styles.filter(
195+
marginRightEm: negativeRightMarginEm == null,
196+
marginLeftEm: negativeLeftMarginEm == null),
197+
text: span.text,
198+
nodes: span.nodes);
199+
}
200+
}
201+
202+
if (negativeRightMarginEm != null) {
203+
final previousSpans = resultSpans;
204+
resultSpans = QueueList<KatexNode>();
205+
resultSpans.addFirst(KatexNegativeMarginNode(
206+
leftOffsetEm: negativeRightMarginEm,
207+
nodes: previousSpans));
208+
}
209+
210+
resultSpans.addFirst(span);
211+
212+
if (negativeLeftMarginEm != null) {
213+
final previousSpans = resultSpans;
214+
resultSpans = QueueList<KatexNode>();
215+
resultSpans.addFirst(KatexNegativeMarginNode(
216+
leftOffsetEm: negativeLeftMarginEm,
217+
nodes: previousSpans));
218+
}
219+
}
220+
return resultSpans;
180221
}
181222

182223
static final _resetSizeClassRegExp = RegExp(r'^reset-size(\d\d?)$');
@@ -272,13 +313,31 @@ class _KatexParser {
272313
}
273314
final pstrutHeight = pstrutStyles.heightEm ?? 0;
274315

316+
KatexSpanNode innerSpanNode = KatexSpanNode(
317+
styles: styles,
318+
text: null,
319+
nodes: _parseChildSpans(otherSpans));
320+
321+
final marginRightEm = styles.marginRightEm;
322+
final marginLeftEm = styles.marginLeftEm;
323+
if (marginRightEm != null && marginRightEm.isNegative) {
324+
throw _KatexHtmlParseError();
325+
}
326+
if (marginLeftEm != null && marginLeftEm.isNegative) {
327+
innerSpanNode = KatexSpanNode(
328+
styles: KatexSpanStyles(),
329+
text: null,
330+
nodes: [
331+
KatexNegativeMarginNode(
332+
leftOffsetEm: marginLeftEm,
333+
nodes: [innerSpanNode]),
334+
]);
335+
}
336+
275337
rows.add(KatexVlistRowNode(
276338
verticalOffsetEm: topEm + pstrutHeight,
277339
debugHtmlNode: kDebugMode ? innerSpan : null,
278-
node: KatexSpanNode(
279-
styles: styles,
280-
text: null,
281-
nodes: _parseChildSpans(otherSpans))));
340+
node: innerSpanNode));
282341
} else {
283342
throw _KatexHtmlParseError();
284343
}
@@ -610,17 +669,11 @@ class _KatexParser {
610669

611670
case 'margin-right':
612671
marginRightEm = _getEm(expression);
613-
if (marginRightEm != null) {
614-
if (marginRightEm < 0) throw _KatexHtmlParseError();
615-
continue;
616-
}
672+
if (marginRightEm != null) continue;
617673

618674
case 'margin-left':
619675
marginLeftEm = _getEm(expression);
620-
if (marginLeftEm != null) {
621-
if (marginLeftEm < 0) throw _KatexHtmlParseError();
622-
continue;
623-
}
676+
if (marginLeftEm != null) continue;
624677
}
625678

626679
// TODO handle more CSS properties

lib/widgets/content.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'dialog.dart';
2424
import 'emoji.dart';
2525
import 'icons.dart';
2626
import 'inset_shadow.dart';
27+
import 'katex.dart';
2728
import 'lightbox.dart';
2829
import 'message_list.dart';
2930
import 'poll.dart';
@@ -898,6 +899,7 @@ class _KatexNodeList extends StatelessWidget {
898899
KatexSpanNode() => _KatexSpan(e),
899900
KatexStrutNode() => _KatexStrut(e),
900901
KatexVlistNode() => _KatexVlist(e),
902+
KatexNegativeMarginNode() => _KatexNegativeMargin(e),
901903
}));
902904
}))));
903905
}
@@ -1046,6 +1048,21 @@ class _KatexVlist extends StatelessWidget {
10461048
}
10471049
}
10481050

1051+
class _KatexNegativeMargin extends StatelessWidget {
1052+
const _KatexNegativeMargin(this.node);
1053+
1054+
final KatexNegativeMarginNode node;
1055+
1056+
@override
1057+
Widget build(BuildContext context) {
1058+
final em = DefaultTextStyle.of(context).style.fontSize!;
1059+
1060+
return NegativeLeftOffset(
1061+
leftOffset: node.leftOffsetEm * em,
1062+
child: _KatexNodeList(nodes: node.nodes));
1063+
}
1064+
}
1065+
10491066
class WebsitePreview extends StatelessWidget {
10501067
const WebsitePreview({super.key, required this.node});
10511068

lib/widgets/katex.dart

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import 'dart:math' as math;
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter/widgets.dart';
5+
import 'package:flutter/rendering.dart';
6+
7+
class NegativeLeftOffset extends SingleChildRenderObjectWidget {
8+
NegativeLeftOffset({super.key, required this.leftOffset, super.child})
9+
: assert(leftOffset.isNegative),
10+
_padding = EdgeInsets.only(left: leftOffset);
11+
12+
final double leftOffset;
13+
final EdgeInsetsGeometry _padding;
14+
15+
@override
16+
RenderNegativePadding createRenderObject(BuildContext context) {
17+
return RenderNegativePadding(
18+
padding: _padding,
19+
textDirection: Directionality.maybeOf(context));
20+
}
21+
22+
@override
23+
void updateRenderObject(
24+
BuildContext context,
25+
RenderNegativePadding renderObject,
26+
) {
27+
renderObject
28+
..padding = _padding
29+
..textDirection = Directionality.maybeOf(context);
30+
}
31+
32+
@override
33+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
34+
super.debugFillProperties(properties);
35+
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', _padding));
36+
}
37+
}
38+
39+
// Like [RenderPadding] but only supports negative values.
40+
// TODO(upstream): give Padding an option to accept negative padding (at cost of hit-testing not working)
41+
class RenderNegativePadding extends RenderShiftedBox {
42+
RenderNegativePadding({
43+
required EdgeInsetsGeometry padding,
44+
TextDirection? textDirection,
45+
RenderBox? child,
46+
}) : assert(!padding.isNonNegative),
47+
_textDirection = textDirection,
48+
_padding = padding,
49+
super(child);
50+
51+
EdgeInsets? _resolvedPaddingCache;
52+
EdgeInsets get _resolvedPadding {
53+
final EdgeInsets returnValue = _resolvedPaddingCache ??= padding.resolve(textDirection);
54+
return returnValue;
55+
}
56+
57+
void _markNeedResolution() {
58+
_resolvedPaddingCache = null;
59+
markNeedsLayout();
60+
}
61+
62+
/// The amount to pad the child in each dimension.
63+
///
64+
/// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
65+
/// must not be null.
66+
EdgeInsetsGeometry get padding => _padding;
67+
EdgeInsetsGeometry _padding;
68+
set padding(EdgeInsetsGeometry value) {
69+
assert(!value.isNonNegative);
70+
if (_padding == value) {
71+
return;
72+
}
73+
_padding = value;
74+
_markNeedResolution();
75+
}
76+
77+
/// The text direction with which to resolve [padding].
78+
///
79+
/// This may be changed to null, but only after the [padding] has been changed
80+
/// to a value that does not depend on the direction.
81+
TextDirection? get textDirection => _textDirection;
82+
TextDirection? _textDirection;
83+
set textDirection(TextDirection? value) {
84+
if (_textDirection == value) {
85+
return;
86+
}
87+
_textDirection = value;
88+
_markNeedResolution();
89+
}
90+
91+
@override
92+
double computeMinIntrinsicWidth(double height) {
93+
final EdgeInsets padding = _resolvedPadding;
94+
if (child != null) {
95+
// Relies on double.infinity absorption.
96+
return child!.getMinIntrinsicWidth(math.max(0.0, height - padding.vertical)) +
97+
padding.horizontal;
98+
}
99+
return padding.horizontal;
100+
}
101+
102+
@override
103+
double computeMaxIntrinsicWidth(double height) {
104+
final EdgeInsets padding = _resolvedPadding;
105+
if (child != null) {
106+
// Relies on double.infinity absorption.
107+
return child!.getMaxIntrinsicWidth(math.max(0.0, height - padding.vertical)) +
108+
padding.horizontal;
109+
}
110+
return padding.horizontal;
111+
}
112+
113+
@override
114+
double computeMinIntrinsicHeight(double width) {
115+
final EdgeInsets padding = _resolvedPadding;
116+
if (child != null) {
117+
// Relies on double.infinity absorption.
118+
return child!.getMinIntrinsicHeight(math.max(0.0, width - padding.horizontal)) +
119+
padding.vertical;
120+
}
121+
return padding.vertical;
122+
}
123+
124+
@override
125+
double computeMaxIntrinsicHeight(double width) {
126+
final EdgeInsets padding = _resolvedPadding;
127+
if (child != null) {
128+
// Relies on double.infinity absorption.
129+
return child!.getMaxIntrinsicHeight(math.max(0.0, width - padding.horizontal)) +
130+
padding.vertical;
131+
}
132+
return padding.vertical;
133+
}
134+
135+
@override
136+
@protected
137+
Size computeDryLayout(covariant BoxConstraints constraints) {
138+
final EdgeInsets padding = _resolvedPadding;
139+
if (child == null) {
140+
return constraints.constrain(Size(padding.horizontal, padding.vertical));
141+
}
142+
final BoxConstraints innerConstraints = constraints.deflate(padding);
143+
final Size childSize = child!.getDryLayout(innerConstraints);
144+
return constraints.constrain(
145+
Size(padding.horizontal + childSize.width, padding.vertical + childSize.height),
146+
);
147+
}
148+
149+
@override
150+
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
151+
final RenderBox? child = this.child;
152+
if (child == null) {
153+
return null;
154+
}
155+
final EdgeInsets padding = _resolvedPadding;
156+
final BoxConstraints innerConstraints = constraints.deflate(padding);
157+
final BaselineOffset result =
158+
BaselineOffset(child.getDryBaseline(innerConstraints, baseline)) + padding.top;
159+
return result.offset;
160+
}
161+
162+
@override
163+
void performLayout() {
164+
final BoxConstraints constraints = this.constraints;
165+
final EdgeInsets padding = _resolvedPadding;
166+
if (child == null) {
167+
size = constraints.constrain(Size(padding.horizontal, padding.vertical));
168+
return;
169+
}
170+
final BoxConstraints innerConstraints = constraints.deflate(padding);
171+
child!.layout(innerConstraints, parentUsesSize: true);
172+
final BoxParentData childParentData = child!.parentData! as BoxParentData;
173+
childParentData.offset = Offset(padding.left, padding.top);
174+
size = constraints.constrain(
175+
Size(padding.horizontal + child!.size.width, padding.vertical + child!.size.height),
176+
);
177+
}
178+
179+
@override
180+
void debugPaintSize(PaintingContext context, Offset offset) {
181+
super.debugPaintSize(context, offset);
182+
assert(() {
183+
final Rect outerRect = offset & size;
184+
debugPaintPadding(
185+
context.canvas,
186+
outerRect,
187+
child != null ? _resolvedPaddingCache!.deflateRect(outerRect) : null,
188+
);
189+
return true;
190+
}());
191+
}
192+
193+
@override
194+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
195+
super.debugFillProperties(properties);
196+
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
197+
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
198+
}
199+
}

0 commit comments

Comments
 (0)