Skip to content

Commit 4c667d0

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

File tree

6 files changed

+248
-64
lines changed

6 files changed

+248
-64
lines changed

README.md

Lines changed: 0 additions & 1 deletion
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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,12 @@ abstract class MathNode extends ContentNode {
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 MathBlockNode extends MathNode implements BlockContentNode {
406445
const MathBlockNode({
407446
super.debugHtmlNode,

lib/model/katex.dart

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,110 @@ class _KatexParser {
138138
KatexNode _parseSpan(dom.Element element) {
139139
// TODO maybe check if the sequence of ancestors matter for spans.
140140

141+
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
142+
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+
141245
// Aggregate the CSS styles that apply, in the same order as the CSS
142246
// classes specified for this span, mimicking the behaviour on web.
143247
//
@@ -146,7 +250,6 @@ class _KatexParser {
146250
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
147251
// A copy of class definition (where possible) is accompanied in a comment
148252
// with each case statement to keep track of updates.
149-
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
150253
String? fontFamily;
151254
double? fontSizeEm;
152255
KatexSpanFontWeight? fontWeight;
@@ -358,7 +461,7 @@ class _KatexParser {
358461

359462
final inlineStyles = _parseSpanInlineStyles(element);
360463

361-
return KatexNode(
464+
return KatexSpanNode(
362465
styles: inlineStyles != null
363466
? styles.merge(inlineStyles)
364467
: styles,
@@ -374,6 +477,7 @@ class _KatexParser {
374477
final stylesheet = css_parser.parse('*{$styleStr}');
375478
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
376479
double? heightEm;
480+
double? topEm;
377481
double? verticalAlignEm;
378482

379483
for (final declaration in rule.declarationGroup.declarations) {
@@ -387,6 +491,10 @@ class _KatexParser {
387491
heightEm = _getEm(expression);
388492
if (heightEm != null) continue;
389493

494+
case 'top':
495+
topEm = _getEm(expression);
496+
if (topEm != null) continue;
497+
390498
case 'vertical-align':
391499
verticalAlignEm = _getEm(expression);
392500
if (verticalAlignEm != null) continue;
@@ -402,6 +510,7 @@ class _KatexParser {
402510

403511
return KatexSpanStyles(
404512
heightEm: heightEm,
513+
topEm: topEm,
405514
verticalAlignEm: verticalAlignEm,
406515
);
407516
} else {
@@ -437,6 +546,7 @@ enum KatexSpanTextAlign {
437546
@immutable
438547
class KatexSpanStyles {
439548
final double? heightEm;
549+
final double? topEm;
440550
final double? verticalAlignEm;
441551

442552
final String? fontFamily;
@@ -447,6 +557,7 @@ class KatexSpanStyles {
447557

448558
const KatexSpanStyles({
449559
this.heightEm,
560+
this.topEm,
450561
this.verticalAlignEm,
451562
this.fontFamily,
452563
this.fontSizeEm,
@@ -459,6 +570,7 @@ class KatexSpanStyles {
459570
int get hashCode => Object.hash(
460571
'KatexSpanStyles',
461572
heightEm,
573+
topEm,
462574
verticalAlignEm,
463575
fontFamily,
464576
fontSizeEm,
@@ -471,6 +583,7 @@ class KatexSpanStyles {
471583
bool operator ==(Object other) {
472584
return other is KatexSpanStyles &&
473585
other.heightEm == heightEm &&
586+
other.topEm == topEm &&
474587
other.verticalAlignEm == verticalAlignEm &&
475588
other.fontFamily == fontFamily &&
476589
other.fontSizeEm == fontSizeEm &&
@@ -483,6 +596,7 @@ class KatexSpanStyles {
483596
String toString() {
484597
final args = <String>[];
485598
if (heightEm != null) args.add('heightEm: $heightEm');
599+
if (topEm != null) args.add('topEm: $topEm');
486600
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
487601
if (fontFamily != null) args.add('fontFamily: $fontFamily');
488602
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
@@ -495,6 +609,7 @@ class KatexSpanStyles {
495609
KatexSpanStyles merge(KatexSpanStyles other) {
496610
return KatexSpanStyles(
497611
heightEm: other.heightEm ?? heightEm,
612+
topEm: other.topEm ?? topEm,
498613
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
499614
fontFamily: other.fontFamily ?? fontFamily,
500615
fontSizeEm: other.fontSizeEm ?? fontSizeEm,

lib/widgets/content.dart

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -877,15 +877,18 @@ class _KatexNodeList extends StatelessWidget {
877877
return WidgetSpan(
878878
alignment: PlaceholderAlignment.baseline,
879879
baseline: TextBaseline.alphabetic,
880-
child: _KatexSpan(e));
880+
child: switch (e) {
881+
KatexSpanNode() => _KatexSpan(e),
882+
KatexVlistNode() => _KatexVlist(e),
883+
});
881884
}))));
882885
}
883886
}
884887

885888
class _KatexSpan extends StatelessWidget {
886889
const _KatexSpan(this.node);
887890

888-
final KatexNode node;
891+
final KatexSpanNode node;
889892

890893
@override
891894
Widget build(BuildContext context) {
@@ -965,6 +968,32 @@ class _KatexSpan extends StatelessWidget {
965968
}
966969
}
967970

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

0 commit comments

Comments
 (0)