Skip to content

Commit e8cd58a

Browse files
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.
1 parent 22f09ed commit e8cd58a

File tree

6 files changed

+441
-23
lines changed

6 files changed

+441
-23
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: 56 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,42 @@ 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+
final span = _parseSpan(node);
181+
182+
if (span is KatexSpanNode) {
183+
final marginRightEm = span.styles.marginRightEm;
184+
if (marginRightEm != null && marginRightEm.isNegative) {
185+
final previousSpans = resultSpans;
186+
resultSpans = QueueList<KatexNode>();
187+
resultSpans.addFirst(KatexNegativeMarginNode(
188+
leftOffsetEm: marginRightEm,
189+
nodes: previousSpans));
190+
}
191+
}
192+
193+
resultSpans.addFirst(span);
194+
195+
if (span is KatexSpanNode) {
196+
final marginLeftEm = span.styles.marginLeftEm;
197+
if (marginLeftEm != null && marginLeftEm.isNegative) {
198+
final previousSpans = resultSpans;
199+
resultSpans = QueueList<KatexNode>();
200+
resultSpans.addFirst(KatexNegativeMarginNode(
201+
leftOffsetEm: marginLeftEm,
202+
nodes: previousSpans));
203+
}
204+
}
205+
}
206+
return resultSpans;
180207
}
181208

182209
static final _resetSizeClassRegExp = RegExp(r'^reset-size(\d\d?)$');
@@ -266,13 +293,31 @@ class _KatexParser {
266293
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
267294
final pstrutHeight = pstrutStyles.heightEm ?? 0;
268295

296+
KatexSpanNode innerSpanNode = KatexSpanNode(
297+
styles: styles,
298+
text: null,
299+
nodes: _parseChildSpans(otherSpans));
300+
301+
final marginRightEm = styles.marginRightEm;
302+
final marginLeftEm = styles.marginLeftEm;
303+
if (marginRightEm != null && marginRightEm.isNegative) {
304+
throw _KatexHtmlParseError();
305+
}
306+
if (marginLeftEm != null && marginLeftEm.isNegative) {
307+
innerSpanNode = KatexSpanNode(
308+
styles: KatexSpanStyles(),
309+
text: null,
310+
nodes: [
311+
KatexNegativeMarginNode(
312+
leftOffsetEm: marginLeftEm,
313+
nodes: [innerSpanNode]),
314+
]);
315+
}
316+
269317
rows.add(KatexVlistRowNode(
270318
verticalOffsetEm: topEm + pstrutHeight,
271319
debugHtmlNode: kDebugMode ? innerSpan : null,
272-
node: KatexSpanNode(
273-
styles: styles,
274-
text: null,
275-
nodes: _parseChildSpans(otherSpans))));
320+
node: innerSpanNode));
276321
} else {
277322
throw _KatexHtmlParseError();
278323
}
@@ -605,17 +650,11 @@ class _KatexParser {
605650

606651
case 'margin-right':
607652
marginRightEm = _getEm(expression);
608-
if (marginRightEm != null) {
609-
if (marginRightEm < 0) throw _KatexHtmlParseError();
610-
continue;
611-
}
653+
if (marginRightEm != null) continue;
612654

613655
case 'margin-left':
614656
marginLeftEm = _getEm(expression);
615-
if (marginLeftEm != null) {
616-
if (marginLeftEm < 0) throw _KatexHtmlParseError();
617-
continue;
618-
}
657+
if (marginLeftEm != null) continue;
619658
}
620659

621660
// TODO handle more CSS properties

lib/widgets/content.dart

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'code_block.dart';
2222
import 'dialog.dart';
2323
import 'icons.dart';
2424
import 'inset_shadow.dart';
25+
import 'katex.dart';
2526
import 'lightbox.dart';
2627
import 'message_list.dart';
2728
import 'poll.dart';
@@ -884,6 +885,7 @@ class _KatexNodeList extends StatelessWidget {
884885
KatexSpanNode() => _KatexSpan(e),
885886
KatexStrutNode() => _KatexStrut(e),
886887
KatexVlistNode() => _KatexVlist(e),
888+
KatexNegativeMarginNode() => _KatexNegativeMargin(e),
887889
}));
888890
}))));
889891
}
@@ -957,23 +959,21 @@ class _KatexSpan extends StatelessWidget {
957959
}
958960

959961
final marginRight = switch (styles.marginRightEm) {
960-
double marginRightEm => marginRightEm * em,
961-
null => null,
962+
double marginRightEm when marginRightEm >= 0 => marginRightEm * em,
963+
_ => null,
962964
};
963965
final marginLeft = switch (styles.marginLeftEm) {
964-
double marginLeftEm => marginLeftEm * em,
965-
null => null,
966+
double marginLeftEm when marginLeftEm >= 0 => marginLeftEm * em,
967+
_ => null,
966968
};
967969

968970
EdgeInsets? margin;
969971
if (marginRight != null || marginLeft != null) {
970972
margin = EdgeInsets.zero;
971973
if (marginRight != null) {
972-
assert(marginRight >= 0);
973974
margin += EdgeInsets.only(right: marginRight);
974975
}
975976
if (marginLeft != null) {
976-
assert(marginLeft >= 0);
977977
margin += EdgeInsets.only(left: marginLeft);
978978
}
979979
}
@@ -1031,6 +1031,21 @@ class _KatexVlist extends StatelessWidget {
10311031
}
10321032
}
10331033

1034+
class _KatexNegativeMargin extends StatelessWidget {
1035+
const _KatexNegativeMargin(this.node);
1036+
1037+
final KatexNegativeMarginNode node;
1038+
1039+
@override
1040+
Widget build(BuildContext context) {
1041+
final em = DefaultTextStyle.of(context).style.fontSize!;
1042+
1043+
return NegativeLeftOffset(
1044+
leftOffset: node.leftOffsetEm * em,
1045+
child: _KatexNodeList(nodes: node.nodes));
1046+
}
1047+
}
1048+
10341049
class WebsitePreview extends StatelessWidget {
10351050
const WebsitePreview({super.key, required this.node});
10361051

0 commit comments

Comments
 (0)