Skip to content

Commit 1f3e881

Browse files
committed
Merge remote-tracking branch 'pr/1452'
2 parents 0ada484 + 22f09ed commit 1f3e881

File tree

5 files changed

+871
-66
lines changed

5 files changed

+871
-66
lines changed

lib/model/content.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,55 @@ class KatexSpanNode extends KatexNode {
411411
}
412412
}
413413

414+
class KatexStrutNode extends KatexNode {
415+
const KatexStrutNode({
416+
required this.heightEm,
417+
required this.verticalAlignEm,
418+
super.debugHtmlNode,
419+
});
420+
421+
final double heightEm;
422+
final double? verticalAlignEm;
423+
424+
@override
425+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
426+
super.debugFillProperties(properties);
427+
properties.add(DoubleProperty('heightEm', heightEm));
428+
properties.add(DoubleProperty('verticalAlignEm', verticalAlignEm));
429+
}
430+
}
431+
432+
class KatexVlistNode extends KatexNode {
433+
const KatexVlistNode({
434+
required this.rows,
435+
super.debugHtmlNode,
436+
});
437+
438+
final List<KatexVlistRowNode> rows;
439+
440+
@override
441+
List<DiagnosticsNode> debugDescribeChildren() {
442+
return rows.map((row) => row.toDiagnosticsNode()).toList();
443+
}
444+
}
445+
446+
class KatexVlistRowNode extends ContentNode {
447+
const KatexVlistRowNode({
448+
required this.verticalOffsetEm,
449+
required this.node,
450+
super.debugHtmlNode,
451+
});
452+
453+
final double verticalOffsetEm;
454+
final KatexSpanNode node;
455+
456+
@override
457+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
458+
super.debugFillProperties(properties);
459+
properties.add(DoubleProperty('verticalOffsetEm', verticalOffsetEm));
460+
}
461+
}
462+
414463
class MathBlockNode extends MathNode implements BlockContentNode {
415464
const MathBlockNode({
416465
super.debugHtmlNode,

lib/model/katex.dart

Lines changed: 201 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,122 @@ class _KatexParser {
185185
KatexNode _parseSpan(dom.Element element) {
186186
// TODO maybe check if the sequence of ancestors matter for spans.
187187

188+
if (element.className.startsWith('strut')) {
189+
if (element.className == 'strut' && element.nodes.isEmpty) {
190+
final styles = _parseSpanInlineStyles(element);
191+
if (styles == null) throw _KatexHtmlParseError();
192+
193+
final heightEm = styles.heightEm;
194+
if (heightEm == null) throw _KatexHtmlParseError();
195+
final verticalAlignEm = styles.verticalAlignEm;
196+
197+
// Ensure only `height` and `vertical-align` inline styles are present.
198+
if (styles.filter(heightEm: false, verticalAlignEm: false) !=
199+
KatexSpanStyles()) {
200+
throw _KatexHtmlParseError();
201+
}
202+
203+
return KatexStrutNode(
204+
heightEm: heightEm,
205+
verticalAlignEm: verticalAlignEm);
206+
} else {
207+
throw _KatexHtmlParseError();
208+
}
209+
}
210+
211+
if (element.className.startsWith('vlist')) {
212+
if (element case dom.Element(
213+
localName: 'span',
214+
className: 'vlist-t' || 'vlist-t vlist-t2',
215+
nodes: [...],
216+
) && final vlistT) {
217+
if (vlistT.attributes.containsKey('style')) throw _KatexHtmlParseError();
218+
219+
final hasTwoVlistR = vlistT.className == 'vlist-t vlist-t2';
220+
if (!hasTwoVlistR && vlistT.nodes.length != 1) throw _KatexHtmlParseError();
221+
222+
if (hasTwoVlistR) {
223+
if (vlistT.nodes case [
224+
_,
225+
dom.Element(localName: 'span', className: 'vlist-r', nodes: [
226+
dom.Element(localName: 'span', className: 'vlist', nodes: [
227+
dom.Element(localName: 'span', className: '', nodes: []),
228+
]),
229+
]),
230+
]) {
231+
// Do nothing.
232+
} else {
233+
throw _KatexHtmlParseError();
234+
}
235+
}
236+
237+
if (vlistT.nodes.first
238+
case dom.Element(localName: 'span', className: 'vlist-r') &&
239+
final vlistR) {
240+
if (vlistR.attributes.containsKey('style')) throw _KatexHtmlParseError();
241+
242+
if (vlistR.nodes.first
243+
case dom.Element(localName: 'span', className: 'vlist') &&
244+
final vlist) {
245+
final rows = <KatexVlistRowNode>[];
246+
247+
for (final innerSpan in vlist.nodes) {
248+
if (innerSpan case dom.Element(
249+
localName: 'span',
250+
nodes: [
251+
dom.Element(localName: 'span', className: 'pstrut') &&
252+
final pstrutSpan,
253+
...final otherSpans,
254+
],
255+
)) {
256+
if (innerSpan.className != '') {
257+
throw _KatexHtmlParseError('unexpected CSS class for '
258+
'vlist inner span: ${innerSpan.className}');
259+
}
260+
261+
var styles = _parseSpanInlineStyles(innerSpan)!;
262+
final topEm = styles.topEm ?? 0;
263+
264+
styles = styles.filter(topEm: false);
265+
266+
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
267+
final pstrutHeight = pstrutStyles.heightEm ?? 0;
268+
269+
rows.add(KatexVlistRowNode(
270+
verticalOffsetEm: topEm + pstrutHeight,
271+
debugHtmlNode: kDebugMode ? innerSpan : null,
272+
node: KatexSpanNode(
273+
styles: styles,
274+
text: null,
275+
nodes: _parseChildSpans(otherSpans))));
276+
} else {
277+
throw _KatexHtmlParseError();
278+
}
279+
}
280+
281+
return KatexVlistNode(
282+
rows: rows,
283+
debugHtmlNode: kDebugMode ? vlistT : null,
284+
);
285+
} else {
286+
throw _KatexHtmlParseError();
287+
}
288+
} else {
289+
throw _KatexHtmlParseError();
290+
}
291+
} else {
292+
throw _KatexHtmlParseError();
293+
}
294+
}
295+
188296
final debugHtmlNode = kDebugMode ? element : null;
189297

190298
final inlineStyles = _parseSpanInlineStyles(element);
299+
if (inlineStyles != null) {
300+
// We expect `vertical-align` inline style to be only present on a
301+
// `strut` span, for which we emit `KatexStrutNode` separately.
302+
if (inlineStyles.verticalAlignEm != null) throw _KatexHtmlParseError();
303+
}
191304

192305
// Aggregate the CSS styles that apply, in the same order as the CSS
193306
// classes specified for this span, mimicking the behaviour on web.
@@ -197,7 +310,9 @@ class _KatexParser {
197310
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
198311
// A copy of class definition (where possible) is accompanied in a comment
199312
// with each case statement to keep track of updates.
200-
final spanClasses = List<String>.unmodifiable(element.className.split(' '));
313+
final spanClasses = element.className != ''
314+
? List<String>.unmodifiable(element.className.split(' '))
315+
: const <String>[];
201316
String? fontFamily;
202317
double? fontSizeEm;
203318
KatexSpanFontWeight? fontWeight;
@@ -214,8 +329,9 @@ class _KatexParser {
214329

215330
case 'strut':
216331
// .strut { ... }
217-
// Do nothing, it has properties that don't need special handling.
218-
break;
332+
// We expect the 'strut' class to be the only class in a span,
333+
// in which case we handle it separately and emit `KatexStrutNode`.
334+
throw _KatexHtmlParseError();
219335

220336
case 'textbf':
221337
// .textbf { font-weight: bold; }
@@ -463,6 +579,10 @@ class _KatexParser {
463579
final stylesheet = css_parser.parse('*{$styleStr}');
464580
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
465581
double? heightEm;
582+
double? verticalAlignEm;
583+
double? topEm;
584+
double? marginRightEm;
585+
double? marginLeftEm;
466586

467587
for (final declaration in rule.declarationGroup.declarations) {
468588
if (declaration case css_visitor.Declaration(
@@ -474,6 +594,28 @@ class _KatexParser {
474594
case 'height':
475595
heightEm = _getEm(expression);
476596
if (heightEm != null) continue;
597+
598+
case 'vertical-align':
599+
verticalAlignEm = _getEm(expression);
600+
if (verticalAlignEm != null) continue;
601+
602+
case 'top':
603+
topEm = _getEm(expression);
604+
if (topEm != null) continue;
605+
606+
case 'margin-right':
607+
marginRightEm = _getEm(expression);
608+
if (marginRightEm != null) {
609+
if (marginRightEm < 0) throw _KatexHtmlParseError();
610+
continue;
611+
}
612+
613+
case 'margin-left':
614+
marginLeftEm = _getEm(expression);
615+
if (marginLeftEm != null) {
616+
if (marginLeftEm < 0) throw _KatexHtmlParseError();
617+
continue;
618+
}
477619
}
478620

479621
// TODO handle more CSS properties
@@ -488,6 +630,10 @@ class _KatexParser {
488630

489631
return KatexSpanStyles(
490632
heightEm: heightEm,
633+
topEm: topEm,
634+
verticalAlignEm: verticalAlignEm,
635+
marginRightEm: marginRightEm,
636+
marginLeftEm: marginLeftEm,
491637
);
492638
} else {
493639
throw _KatexHtmlParseError();
@@ -524,6 +670,12 @@ enum KatexSpanTextAlign {
524670
@immutable
525671
class KatexSpanStyles {
526672
final double? heightEm;
673+
final double? verticalAlignEm;
674+
675+
final double? topEm;
676+
677+
final double? marginRightEm;
678+
final double? marginLeftEm;
527679

528680
final String? fontFamily;
529681
final double? fontSizeEm;
@@ -533,6 +685,10 @@ class KatexSpanStyles {
533685

534686
const KatexSpanStyles({
535687
this.heightEm,
688+
this.verticalAlignEm,
689+
this.topEm,
690+
this.marginRightEm,
691+
this.marginLeftEm,
536692
this.fontFamily,
537693
this.fontSizeEm,
538694
this.fontWeight,
@@ -544,6 +700,10 @@ class KatexSpanStyles {
544700
int get hashCode => Object.hash(
545701
'KatexSpanStyles',
546702
heightEm,
703+
verticalAlignEm,
704+
topEm,
705+
marginRightEm,
706+
marginLeftEm,
547707
fontFamily,
548708
fontSizeEm,
549709
fontWeight,
@@ -555,6 +715,10 @@ class KatexSpanStyles {
555715
bool operator ==(Object other) {
556716
return other is KatexSpanStyles &&
557717
other.heightEm == heightEm &&
718+
other.verticalAlignEm == verticalAlignEm &&
719+
other.topEm == topEm &&
720+
other.marginRightEm == marginRightEm &&
721+
other.marginLeftEm == marginLeftEm &&
558722
other.fontFamily == fontFamily &&
559723
other.fontSizeEm == fontSizeEm &&
560724
other.fontWeight == fontWeight &&
@@ -566,6 +730,10 @@ class KatexSpanStyles {
566730
String toString() {
567731
final args = <String>[];
568732
if (heightEm != null) args.add('heightEm: $heightEm');
733+
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
734+
if (topEm != null) args.add('topEm: $topEm');
735+
if (marginRightEm != null) args.add('marginRightEm: $marginRightEm');
736+
if (marginLeftEm != null) args.add('marginLeftEm: $marginLeftEm');
569737
if (fontFamily != null) args.add('fontFamily: $fontFamily');
570738
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
571739
if (fontWeight != null) args.add('fontWeight: $fontWeight');
@@ -584,13 +752,43 @@ class KatexSpanStyles {
584752
KatexSpanStyles merge(KatexSpanStyles other) {
585753
return KatexSpanStyles(
586754
heightEm: other.heightEm ?? heightEm,
755+
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
756+
topEm: other.topEm ?? topEm,
757+
marginRightEm: other.marginRightEm ?? marginRightEm,
758+
marginLeftEm: other.marginLeftEm ?? marginLeftEm,
587759
fontFamily: other.fontFamily ?? fontFamily,
588760
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
589761
fontStyle: other.fontStyle ?? fontStyle,
590762
fontWeight: other.fontWeight ?? fontWeight,
591763
textAlign: other.textAlign ?? textAlign,
592764
);
593765
}
766+
767+
KatexSpanStyles filter({
768+
bool heightEm = true,
769+
bool verticalAlignEm = true,
770+
bool topEm = true,
771+
bool marginRightEm = true,
772+
bool marginLeftEm = true,
773+
bool fontFamily = true,
774+
bool fontSizeEm = true,
775+
bool fontWeight = true,
776+
bool fontStyle = true,
777+
bool textAlign = true,
778+
}) {
779+
return KatexSpanStyles(
780+
heightEm: heightEm ? this.heightEm : null,
781+
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
782+
topEm: topEm ? this.topEm : null,
783+
marginRightEm: marginRightEm ? this.marginRightEm : null,
784+
marginLeftEm: marginLeftEm ? this.marginLeftEm : null,
785+
fontFamily: fontFamily ? this.fontFamily : null,
786+
fontSizeEm: fontSizeEm ? this.fontSizeEm : null,
787+
fontWeight: fontWeight ? this.fontWeight : null,
788+
fontStyle: fontStyle ? this.fontStyle : null,
789+
textAlign: textAlign ? this.textAlign : null,
790+
);
791+
}
594792
}
595793

596794
class _KatexHtmlParseError extends Error {

0 commit comments

Comments
 (0)