@@ -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
525671class 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
596794class _KatexHtmlParseError extends Error {
0 commit comments