Skip to content

Commit d1f5d5c

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 0a66a8d commit d1f5d5c

File tree

6 files changed

+444
-23
lines changed

6 files changed

+444
-23
lines changed

lib/model/content.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,28 @@ class KatexVlistRowNode extends ContentNode {
455455
}
456456
}
457457

458+
class KatexNegativeMarginNode extends KatexNode {
459+
const KatexNegativeMarginNode({
460+
required this.leftOffsetEm,
461+
required this.nodes,
462+
super.debugHtmlNode,
463+
}) : assert(leftOffsetEm < 0);
464+
465+
final double leftOffsetEm;
466+
final List<KatexNode> nodes;
467+
468+
@override
469+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
470+
super.debugFillProperties(properties);
471+
properties.add(DoubleProperty('leftOffsetEm', leftOffsetEm));
472+
}
473+
474+
@override
475+
List<DiagnosticsNode> debugDescribeChildren() {
476+
return nodes.map((node) => node.toDiagnosticsNode()).toList();
477+
}
478+
}
479+
458480
class MathBlockNode extends MathNode implements BlockContentNode {
459481
const MathBlockNode({
460482
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';
@@ -118,13 +119,39 @@ class _KatexParser {
118119
}
119120

120121
List<KatexNode> _parseChildSpans(List<dom.Node> nodes) {
121-
return List.unmodifiable(nodes.map((node) {
122-
if (node case dom.Element(localName: 'span')) {
123-
return _parseSpan(node);
124-
} else {
122+
var resultSpans = QueueList<KatexNode>();
123+
for (final node in nodes.reversed) {
124+
if (node is! dom.Element || node.localName != 'span') {
125125
throw KatexHtmlParseError();
126126
}
127-
}));
127+
128+
final span = _parseSpan(node);
129+
130+
if (span is KatexSpanNode) {
131+
final marginRightEm = span.styles.marginRightEm;
132+
if (marginRightEm != null && marginRightEm.isNegative) {
133+
final previousSpans = resultSpans;
134+
resultSpans = QueueList<KatexNode>();
135+
resultSpans.addFirst(KatexNegativeMarginNode(
136+
leftOffsetEm: marginRightEm,
137+
nodes: previousSpans));
138+
}
139+
}
140+
141+
resultSpans.addFirst(span);
142+
143+
if (span is KatexSpanNode) {
144+
final marginLeftEm = span.styles.marginLeftEm;
145+
if (marginLeftEm != null && marginLeftEm.isNegative) {
146+
final previousSpans = resultSpans;
147+
resultSpans = QueueList<KatexNode>();
148+
resultSpans.addFirst(KatexNegativeMarginNode(
149+
leftOffsetEm: marginLeftEm,
150+
nodes: previousSpans));
151+
}
152+
}
153+
}
154+
return resultSpans;
128155
}
129156

130157
static final _resetSizeClassRegExp = RegExp(r'^reset-size(\d\d?)$');
@@ -210,13 +237,31 @@ class _KatexParser {
210237
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
211238
final pstrutHeight = pstrutStyles.heightEm ?? 0;
212239

240+
KatexSpanNode innerSpanNode = KatexSpanNode(
241+
styles: styles,
242+
text: null,
243+
nodes: _parseChildSpans(otherSpans));
244+
245+
final marginRightEm = styles.marginRightEm;
246+
final marginLeftEm = styles.marginLeftEm;
247+
if (marginRightEm != null && marginRightEm.isNegative) {
248+
throw KatexHtmlParseError();
249+
}
250+
if (marginLeftEm != null && marginLeftEm.isNegative) {
251+
innerSpanNode = KatexSpanNode(
252+
styles: KatexSpanStyles(),
253+
text: null,
254+
nodes: [
255+
KatexNegativeMarginNode(
256+
leftOffsetEm: marginLeftEm,
257+
nodes: [innerSpanNode]),
258+
]);
259+
}
260+
213261
rows.add(KatexVlistRowNode(
214262
verticalOffsetEm: topEm + pstrutHeight,
215263
debugHtmlNode: kDebugMode ? innerSpan : null,
216-
node: KatexSpanNode(
217-
styles: styles,
218-
text: null,
219-
nodes: _parseChildSpans(otherSpans))));
264+
node: innerSpanNode));
220265
} else {
221266
throw KatexHtmlParseError();
222267
}
@@ -532,17 +577,11 @@ class _KatexParser {
532577

533578
case 'margin-right':
534579
marginRightEm = _getEm(expression);
535-
if (marginRightEm != null) {
536-
if (marginRightEm < 0) throw KatexHtmlParseError();
537-
continue;
538-
}
580+
if (marginRightEm != null) continue;
539581

540582
case 'margin-left':
541583
marginLeftEm = _getEm(expression);
542-
if (marginLeftEm != null) {
543-
if (marginLeftEm < 0) throw KatexHtmlParseError();
544-
continue;
545-
}
584+
if (marginLeftEm != null) continue;
546585
}
547586

548587
// TODO handle more CSS properties

lib/widgets/content.dart

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'code_block.dart';
2020
import 'dialog.dart';
2121
import 'icons.dart';
2222
import 'inset_shadow.dart';
23+
import 'katex.dart';
2324
import 'lightbox.dart';
2425
import 'message_list.dart';
2526
import 'poll.dart';
@@ -882,6 +883,7 @@ class _KatexNodeList extends StatelessWidget {
882883
KatexSpanNode() => _KatexSpan(e),
883884
KatexStrutNode() => _KatexStrut(e),
884885
KatexVlistNode() => _KatexVlist(e),
886+
KatexNegativeMarginNode() => _KatexNegativeMargin(e),
885887
}));
886888
}))));
887889
}
@@ -955,23 +957,21 @@ class _KatexSpan extends StatelessWidget {
955957
}
956958

957959
final marginRight = switch (styles.marginRightEm) {
958-
double marginRightEm => marginRightEm * em,
959-
null => null,
960+
double marginRightEm when marginRightEm >= 0 => marginRightEm * em,
961+
_ => null,
960962
};
961963
final marginLeft = switch (styles.marginLeftEm) {
962-
double marginLeftEm => marginLeftEm * em,
963-
null => null,
964+
double marginLeftEm when marginLeftEm >= 0 => marginLeftEm * em,
965+
_ => null,
964966
};
965967

966968
EdgeInsets? margin;
967969
if (marginRight != null || marginLeft != null) {
968970
margin = EdgeInsets.zero;
969971
if (marginRight != null) {
970-
assert(marginRight >= 0);
971972
margin += EdgeInsets.only(right: marginRight);
972973
}
973974
if (marginLeft != null) {
974-
assert(marginLeft >= 0);
975975
margin += EdgeInsets.only(left: marginLeft);
976976
}
977977
}
@@ -1030,6 +1030,21 @@ class _KatexVlist extends StatelessWidget {
10301030
}
10311031
}
10321032

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

0 commit comments

Comments
 (0)