Skip to content

Commit e268041

Browse files
content: Handle vertical offset spans in KaTeX content
Implement handling most common types of `vlist` spans.
1 parent ffcdfb0 commit e268041

File tree

6 files changed

+250
-64
lines changed

6 files changed

+250
-64
lines changed

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ see the [milestones][] and the [project board][].
1212
[milestones]: https://github.com/zulip/zulip-flutter/milestones?direction=asc&sort=title
1313
[project board]: https://github.com/orgs/zulip/projects/5/views/4
1414

15-
1615
## Using Zulip
1716

1817
To use Zulip on iOS or Android, install the [official mobile Zulip client][].

lib/model/content.dart

+41-2
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,12 @@ class MathBlockNode extends BlockContentNode {
369369
}
370370
}
371371

372-
class KatexNode extends ContentNode {
373-
const KatexNode({
372+
sealed class KatexNode extends ContentNode {
373+
const KatexNode({super.debugHtmlNode});
374+
}
375+
376+
class KatexSpanNode extends KatexNode {
377+
const KatexSpanNode({
374378
required this.styles,
375379
required this.text,
376380
required this.nodes,
@@ -402,6 +406,41 @@ class KatexNode extends ContentNode {
402406
}
403407
}
404408

409+
class KatexVlistNode extends KatexNode {
410+
const KatexVlistNode({
411+
required this.rows,
412+
super.debugHtmlNode,
413+
});
414+
415+
final List<KatexVlistRowNode> rows;
416+
417+
@override
418+
List<DiagnosticsNode> debugDescribeChildren() {
419+
return rows.map((row) => row.toDiagnosticsNode()).toList();
420+
}
421+
}
422+
423+
class KatexVlistRowNode extends ContentNode {
424+
const KatexVlistRowNode({
425+
required this.verticalOffsetEm,
426+
this.nodes = const [],
427+
});
428+
429+
final double verticalOffsetEm;
430+
final List<KatexNode> nodes;
431+
432+
@override
433+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
434+
super.debugFillProperties(properties);
435+
properties.add(StringProperty('verticalOffsetEm', '$verticalOffsetEm'));
436+
}
437+
438+
@override
439+
List<DiagnosticsNode> debugDescribeChildren() {
440+
return nodes.map((node) => node.toDiagnosticsNode()).toList();
441+
}
442+
}
443+
405444
class ImageNodeList extends BlockContentNode {
406445
const ImageNodeList(this.images, {super.debugHtmlNode});
407446

lib/model/katex.dart

+115-1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,108 @@ class _KatexParser {
140140

141141
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
142142

143+
if (element case dom.Element(localName: 'span', :final className)
144+
when className.startsWith('vlist')) {
145+
switch (element) {
146+
case dom.Element(
147+
localName: 'span',
148+
className: 'vlist-t',
149+
attributes: final attributesVlistT,
150+
nodes: [
151+
dom.Element(
152+
localName: 'span',
153+
className: 'vlist-r',
154+
attributes: final attributesVlistR,
155+
nodes: [
156+
dom.Element(
157+
localName: 'span',
158+
className: 'vlist',
159+
nodes: [
160+
dom.Element(
161+
localName: 'span',
162+
className: '',
163+
nodes: [
164+
dom.Element(localName: 'span', className: 'pstrut')
165+
&& final pstrutSpan,
166+
...,
167+
]) && final innerSpan,
168+
]),
169+
]),
170+
])
171+
when !attributesVlistT.containsKey('style') &&
172+
!attributesVlistR.containsKey('style'):
173+
// TODO vlist element should only have `height` style, which we ignore.
174+
175+
var styles = _parseSpanInlineStyles(innerSpan)!;
176+
final topEm = styles.topEm ?? 0;
177+
178+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
179+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
180+
181+
// TODO handle negative right-margin inline style on row nodes.
182+
return KatexVlistNode(rows: [
183+
KatexVlistRowNode(
184+
verticalOffsetEm: topEm + pstrutHeight,
185+
nodes: _parseChildSpans(innerSpan)),
186+
]);
187+
188+
case dom.Element(
189+
localName: 'span',
190+
className: 'vlist-t vlist-t2',
191+
attributes: final attributesVlistT,
192+
nodes: [
193+
dom.Element(
194+
localName: 'span',
195+
className: 'vlist-r',
196+
attributes: final attributesVlistR,
197+
nodes: [
198+
dom.Element(
199+
localName: 'span',
200+
className: 'vlist',
201+
nodes: [...]) && final vlist1,
202+
dom.Element(localName: 'span', className: 'vlist-s'),
203+
]),
204+
dom.Element(localName: 'span', className: 'vlist-r', nodes: [
205+
dom.Element(localName: 'span', className: 'vlist', nodes: [
206+
dom.Element(localName: 'span', className: '', nodes: []),
207+
])
208+
]),
209+
])
210+
when !attributesVlistT.containsKey('style') &&
211+
!attributesVlistR.containsKey('style'):
212+
// TODO Ensure both should only have a `height` style.
213+
214+
final rows = <KatexVlistRowNode>[];
215+
216+
for (final innerSpan in vlist1.nodes) {
217+
if (innerSpan case dom.Element(
218+
localName: 'span',
219+
className: '',
220+
nodes: [
221+
dom.Element(localName: 'span', className: 'pstrut') &&
222+
final pstrutSpan,
223+
...,
224+
])) {
225+
final styles = _parseSpanInlineStyles(innerSpan)!;
226+
final topEm = styles.topEm ?? 0;
227+
228+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
229+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
230+
231+
// TODO handle negative right-margin inline style on row nodes.
232+
rows.add(KatexVlistRowNode(
233+
verticalOffsetEm: topEm + pstrutHeight,
234+
nodes: _parseChildSpans(innerSpan)));
235+
}
236+
}
237+
238+
return KatexVlistNode(rows: rows);
239+
240+
default:
241+
throw KatexHtmlParseError();
242+
}
243+
}
244+
143245
// Aggregate the CSS styles that apply, in the same order as the CSS
144246
// classes specified for this span, mimicking the behaviour on web.
145247
//
@@ -406,7 +508,7 @@ class _KatexParser {
406508

407509
final inlineStyles = _parseSpanInlineStyles(element);
408510

409-
return KatexNode(
511+
return KatexSpanNode(
410512
styles: inlineStyles != null
411513
? styles.merge(inlineStyles)
412514
: styles,
@@ -422,6 +524,7 @@ class _KatexParser {
422524
final stylesheet = css_parser.parse('*{$styleStr}');
423525
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
424526
double? heightEm;
527+
double? topEm;
425528
double? verticalAlignEm;
426529

427530
for (final declaration in rule.declarationGroup.declarations) {
@@ -435,6 +538,10 @@ class _KatexParser {
435538
heightEm = _getEm(expression);
436539
if (heightEm != null) continue;
437540

541+
case 'top':
542+
topEm = _getEm(expression);
543+
if (topEm != null) continue;
544+
438545
case 'vertical-align':
439546
verticalAlignEm = _getEm(expression);
440547
if (verticalAlignEm != null) continue;
@@ -450,6 +557,7 @@ class _KatexParser {
450557

451558
return KatexSpanStyles(
452559
heightEm: heightEm,
560+
topEm: topEm,
453561
verticalAlignEm: verticalAlignEm,
454562
);
455563
} else {
@@ -484,6 +592,7 @@ enum KatexSpanTextAlign {
484592

485593
class KatexSpanStyles {
486594
double? heightEm;
595+
double? topEm;
487596
double? verticalAlignEm;
488597

489598
String? fontFamily;
@@ -494,6 +603,7 @@ class KatexSpanStyles {
494603

495604
KatexSpanStyles({
496605
this.heightEm,
606+
this.topEm,
497607
this.verticalAlignEm,
498608
this.fontFamily,
499609
this.fontSizeEm,
@@ -506,6 +616,7 @@ class KatexSpanStyles {
506616
int get hashCode => Object.hash(
507617
'KatexSpanStyles',
508618
heightEm,
619+
topEm,
509620
verticalAlignEm,
510621
fontFamily,
511622
fontSizeEm,
@@ -518,6 +629,7 @@ class KatexSpanStyles {
518629
bool operator ==(Object other) {
519630
return other is KatexSpanStyles &&
520631
other.heightEm == heightEm &&
632+
other.topEm == topEm &&
521633
other.verticalAlignEm == verticalAlignEm &&
522634
other.fontFamily == fontFamily &&
523635
other.fontSizeEm == fontSizeEm &&
@@ -530,6 +642,7 @@ class KatexSpanStyles {
530642
String toString() {
531643
final args = <String>[];
532644
if (heightEm != null) args.add('heightEm: $heightEm');
645+
if (topEm != null) args.add('topEm: $topEm');
533646
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
534647
if (fontFamily != null) args.add('fontFamily: $fontFamily');
535648
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
@@ -542,6 +655,7 @@ class KatexSpanStyles {
542655
KatexSpanStyles merge(KatexSpanStyles other) {
543656
return KatexSpanStyles(
544657
heightEm: other.heightEm ?? heightEm,
658+
topEm: other.topEm ?? topEm,
545659
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
546660
fontFamily: other.fontFamily ?? fontFamily,
547661
fontSizeEm: other.fontSizeEm ?? fontSizeEm,

lib/widgets/content.dart

+35-3
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,10 @@ class _Katex extends StatelessWidget {
853853
return WidgetSpan(
854854
alignment: PlaceholderAlignment.baseline,
855855
baseline: TextBaseline.alphabetic,
856-
child: _KatexSpan(e));
856+
child: switch (e) {
857+
KatexSpanNode() => _KatexSpan(e),
858+
KatexVlistNode() => _KatexVlist(e),
859+
});
857860
}))));
858861

859862
if (!inline) {
@@ -874,7 +877,7 @@ class _Katex extends StatelessWidget {
874877
class _KatexSpan extends StatelessWidget {
875878
const _KatexSpan(this.span);
876879

877-
final KatexNode span;
880+
final KatexSpanNode span;
878881

879882
@override
880883
Widget build(BuildContext context) {
@@ -889,7 +892,10 @@ class _KatexSpan extends StatelessWidget {
889892
return WidgetSpan(
890893
alignment: PlaceholderAlignment.baseline,
891894
baseline: TextBaseline.alphabetic,
892-
child: _KatexSpan(e));
895+
child: switch (e) {
896+
KatexSpanNode() => _KatexSpan(e),
897+
KatexVlistNode() => _KatexVlist(e),
898+
});
893899
}))));
894900
}
895901

@@ -960,6 +966,32 @@ class _KatexSpan extends StatelessWidget {
960966
}
961967
}
962968

969+
class _KatexVlist extends StatelessWidget {
970+
const _KatexVlist(this.node);
971+
972+
final KatexVlistNode node;
973+
974+
@override
975+
Widget build(BuildContext context) {
976+
final em = DefaultTextStyle.of(context).style.fontSize!;
977+
978+
return Stack(children: List.unmodifiable(node.rows.map((row) {
979+
return Transform.translate(
980+
offset: Offset(0, row.verticalOffsetEm * em),
981+
child: RichText(text: TextSpan(
982+
children: List.unmodifiable(row.nodes.map((e) {
983+
return WidgetSpan(
984+
alignment: PlaceholderAlignment.baseline,
985+
baseline: TextBaseline.alphabetic,
986+
child: switch (e) {
987+
KatexSpanNode() => _KatexSpan(e),
988+
KatexVlistNode() => _KatexVlist(e),
989+
});
990+
})))));
991+
})));
992+
}
993+
}
994+
963995
class WebsitePreview extends StatelessWidget {
964996
const WebsitePreview({super.key, required this.node});
965997

0 commit comments

Comments
 (0)