Skip to content

Commit 3ee6263

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

File tree

3 files changed

+178
-1
lines changed

3 files changed

+178
-1
lines changed

lib/model/content.dart

+35
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,41 @@ class KatexSpanNode extends KatexNode {
406406
}
407407
}
408408

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+
409444
class MathBlockNode extends MathNode implements BlockContentNode {
410445
const MathBlockNode({
411446
super.debugHtmlNode,

lib/model/katex.dart

+116-1
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;
@@ -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

+27
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,7 @@ class _KatexNodeList extends StatelessWidget {
880880
baseline: TextBaseline.alphabetic,
881881
child: switch (e) {
882882
KatexSpanNode() => _KatexSpan(e),
883+
KatexVlistNode() => _KatexVlist(e),
883884
});
884885
}))));
885886
}
@@ -968,6 +969,32 @@ class _KatexSpan extends StatelessWidget {
968969
}
969970
}
970971

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

0 commit comments

Comments
 (0)