-
Notifications
You must be signed in to change notification settings - Fork 316
KaTeX (2/n): Support horizontal and vertical offsets for spans #1452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f17ab35
c4628c7
832d1b2
c8558fb
113e3dc
9827271
22f09ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -185,9 +185,122 @@ class _KatexParser { | |||||||||
KatexNode _parseSpan(dom.Element element) { | ||||||||||
// TODO maybe check if the sequence of ancestors matter for spans. | ||||||||||
|
||||||||||
if (element.className.startsWith('strut')) { | ||||||||||
if (element.className == 'strut' && element.nodes.isEmpty) { | ||||||||||
final styles = _parseSpanInlineStyles(element); | ||||||||||
if (styles == null) throw _KatexHtmlParseError(); | ||||||||||
|
||||||||||
final heightEm = styles.heightEm; | ||||||||||
if (heightEm == null) throw _KatexHtmlParseError(); | ||||||||||
final verticalAlignEm = styles.verticalAlignEm; | ||||||||||
|
||||||||||
// Ensure only `height` and `vertical-align` inline styles are present. | ||||||||||
if (styles.filter(heightEm: false, verticalAlignEm: false) != | ||||||||||
KatexSpanStyles()) { | ||||||||||
Comment on lines
+198
to
+199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: binary operator goes after newline, not before (so it doesn't get visually lost off at the ragged right edge of the code):
Suggested change
Comment on lines
+198
to
+199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will allocate two fresh KatexSpanStyles objects, with all their fields, in order to check that the other fields of We can eliminate one of those allocations easily by saying The other one is from (Definitely it's good to be doing this check, excluding unanticipated shapes of the tree.) |
||||||||||
throw _KatexHtmlParseError(); | ||||||||||
} | ||||||||||
|
||||||||||
return KatexStrutNode( | ||||||||||
heightEm: heightEm, | ||||||||||
verticalAlignEm: verticalAlignEm); | ||||||||||
Comment on lines
+203
to
+205
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: should get debugHtmlNode |
||||||||||
} else { | ||||||||||
throw _KatexHtmlParseError(); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
if (element.className.startsWith('vlist')) { | ||||||||||
if (element case dom.Element( | ||||||||||
localName: 'span', | ||||||||||
className: 'vlist-t' || 'vlist-t vlist-t2', | ||||||||||
nodes: [...], | ||||||||||
) && final vlistT) { | ||||||||||
if (vlistT.attributes.containsKey('style')) throw _KatexHtmlParseError(); | ||||||||||
|
||||||||||
final hasTwoVlistR = vlistT.className == 'vlist-t vlist-t2'; | ||||||||||
if (!hasTwoVlistR && vlistT.nodes.length != 1) throw _KatexHtmlParseError(); | ||||||||||
|
||||||||||
if (hasTwoVlistR) { | ||||||||||
if (vlistT.nodes case [ | ||||||||||
_, | ||||||||||
dom.Element(localName: 'span', className: 'vlist-r', nodes: [ | ||||||||||
dom.Element(localName: 'span', className: 'vlist', nodes: [ | ||||||||||
dom.Element(localName: 'span', className: '', nodes: []), | ||||||||||
]), | ||||||||||
]), | ||||||||||
]) { | ||||||||||
// Do nothing. | ||||||||||
} else { | ||||||||||
throw _KatexHtmlParseError(); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
if (vlistT.nodes.first | ||||||||||
case dom.Element(localName: 'span', className: 'vlist-r') && | ||||||||||
final vlistR) { | ||||||||||
if (vlistR.attributes.containsKey('style')) throw _KatexHtmlParseError(); | ||||||||||
|
||||||||||
if (vlistR.nodes.first | ||||||||||
case dom.Element(localName: 'span', className: 'vlist') && | ||||||||||
final vlist) { | ||||||||||
final rows = <KatexVlistRowNode>[]; | ||||||||||
|
||||||||||
for (final innerSpan in vlist.nodes) { | ||||||||||
if (innerSpan case dom.Element( | ||||||||||
localName: 'span', | ||||||||||
nodes: [ | ||||||||||
dom.Element(localName: 'span', className: 'pstrut') && | ||||||||||
final pstrutSpan, | ||||||||||
...final otherSpans, | ||||||||||
], | ||||||||||
)) { | ||||||||||
if (innerSpan.className != '') { | ||||||||||
throw _KatexHtmlParseError('unexpected CSS class for ' | ||||||||||
'vlist inner span: ${innerSpan.className}'); | ||||||||||
} | ||||||||||
|
||||||||||
var styles = _parseSpanInlineStyles(innerSpan)!; | ||||||||||
final topEm = styles.topEm ?? 0; | ||||||||||
|
||||||||||
styles = styles.filter(topEm: false); | ||||||||||
|
||||||||||
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!; | ||||||||||
Comment on lines
+261
to
+266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like these need the same check that happens at the other _parseSpanInlineStyles call site below, to rule out vertical-align. |
||||||||||
final pstrutHeight = pstrutStyles.heightEm ?? 0; | ||||||||||
Comment on lines
+266
to
+267
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like we wouldn't notice if this span had additional inline style properties we weren't acting on. So it should get the same check we have in the |
||||||||||
|
||||||||||
rows.add(KatexVlistRowNode( | ||||||||||
verticalOffsetEm: topEm + pstrutHeight, | ||||||||||
debugHtmlNode: kDebugMode ? innerSpan : null, | ||||||||||
node: KatexSpanNode( | ||||||||||
styles: styles, | ||||||||||
text: null, | ||||||||||
nodes: _parseChildSpans(otherSpans)))); | ||||||||||
} else { | ||||||||||
throw _KatexHtmlParseError(); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
return KatexVlistNode( | ||||||||||
rows: rows, | ||||||||||
debugHtmlNode: kDebugMode ? vlistT : null, | ||||||||||
); | ||||||||||
} else { | ||||||||||
throw _KatexHtmlParseError(); | ||||||||||
} | ||||||||||
} else { | ||||||||||
throw _KatexHtmlParseError(); | ||||||||||
} | ||||||||||
} else { | ||||||||||
throw _KatexHtmlParseError(); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
final debugHtmlNode = kDebugMode ? element : null; | ||||||||||
|
||||||||||
final inlineStyles = _parseSpanInlineStyles(element); | ||||||||||
if (inlineStyles != null) { | ||||||||||
// We expect `vertical-align` inline style to be only present on a | ||||||||||
// `strut` span, for which we emit `KatexStrutNode` separately. | ||||||||||
if (inlineStyles.verticalAlignEm != null) throw _KatexHtmlParseError(); | ||||||||||
} | ||||||||||
|
||||||||||
// Aggregate the CSS styles that apply, in the same order as the CSS | ||||||||||
// classes specified for this span, mimicking the behaviour on web. | ||||||||||
|
@@ -197,7 +310,9 @@ class _KatexParser { | |||||||||
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss | ||||||||||
// A copy of class definition (where possible) is accompanied in a comment | ||||||||||
// with each case statement to keep track of updates. | ||||||||||
final spanClasses = List<String>.unmodifiable(element.className.split(' ')); | ||||||||||
final spanClasses = element.className != '' | ||||||||||
? List<String>.unmodifiable(element.className.split(' ')) | ||||||||||
: const <String>[]; | ||||||||||
String? fontFamily; | ||||||||||
double? fontSizeEm; | ||||||||||
KatexSpanFontWeight? fontWeight; | ||||||||||
|
@@ -214,8 +329,9 @@ class _KatexParser { | |||||||||
|
||||||||||
case 'strut': | ||||||||||
// .strut { ... } | ||||||||||
// Do nothing, it has properties that don't need special handling. | ||||||||||
break; | ||||||||||
// We expect the 'strut' class to be the only class in a span, | ||||||||||
// in which case we handle it separately and emit `KatexStrutNode`. | ||||||||||
throw _KatexHtmlParseError(); | ||||||||||
|
||||||||||
case 'textbf': | ||||||||||
// .textbf { font-weight: bold; } | ||||||||||
|
@@ -463,6 +579,10 @@ class _KatexParser { | |||||||||
final stylesheet = css_parser.parse('*{$styleStr}'); | ||||||||||
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) { | ||||||||||
double? heightEm; | ||||||||||
double? verticalAlignEm; | ||||||||||
double? topEm; | ||||||||||
double? marginRightEm; | ||||||||||
double? marginLeftEm; | ||||||||||
|
||||||||||
for (final declaration in rule.declarationGroup.declarations) { | ||||||||||
if (declaration case css_visitor.Declaration( | ||||||||||
|
@@ -474,6 +594,28 @@ class _KatexParser { | |||||||||
case 'height': | ||||||||||
heightEm = _getEm(expression); | ||||||||||
if (heightEm != null) continue; | ||||||||||
|
||||||||||
case 'vertical-align': | ||||||||||
verticalAlignEm = _getEm(expression); | ||||||||||
if (verticalAlignEm != null) continue; | ||||||||||
|
||||||||||
case 'top': | ||||||||||
topEm = _getEm(expression); | ||||||||||
if (topEm != null) continue; | ||||||||||
|
||||||||||
case 'margin-right': | ||||||||||
marginRightEm = _getEm(expression); | ||||||||||
if (marginRightEm != null) { | ||||||||||
if (marginRightEm < 0) throw _KatexHtmlParseError(); | ||||||||||
continue; | ||||||||||
} | ||||||||||
|
||||||||||
case 'margin-left': | ||||||||||
marginLeftEm = _getEm(expression); | ||||||||||
if (marginLeftEm != null) { | ||||||||||
if (marginLeftEm < 0) throw _KatexHtmlParseError(); | ||||||||||
continue; | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
// TODO handle more CSS properties | ||||||||||
|
@@ -488,6 +630,10 @@ class _KatexParser { | |||||||||
|
||||||||||
return KatexSpanStyles( | ||||||||||
heightEm: heightEm, | ||||||||||
topEm: topEm, | ||||||||||
verticalAlignEm: verticalAlignEm, | ||||||||||
marginRightEm: marginRightEm, | ||||||||||
marginLeftEm: marginLeftEm, | ||||||||||
); | ||||||||||
} else { | ||||||||||
throw _KatexHtmlParseError(); | ||||||||||
|
@@ -524,6 +670,12 @@ enum KatexSpanTextAlign { | |||||||||
@immutable | ||||||||||
class KatexSpanStyles { | ||||||||||
final double? heightEm; | ||||||||||
final double? verticalAlignEm; | ||||||||||
|
||||||||||
final double? topEm; | ||||||||||
|
||||||||||
final double? marginRightEm; | ||||||||||
final double? marginLeftEm; | ||||||||||
|
||||||||||
final String? fontFamily; | ||||||||||
final double? fontSizeEm; | ||||||||||
|
@@ -533,6 +685,10 @@ class KatexSpanStyles { | |||||||||
|
||||||||||
const KatexSpanStyles({ | ||||||||||
this.heightEm, | ||||||||||
this.verticalAlignEm, | ||||||||||
this.topEm, | ||||||||||
this.marginRightEm, | ||||||||||
this.marginLeftEm, | ||||||||||
this.fontFamily, | ||||||||||
this.fontSizeEm, | ||||||||||
this.fontWeight, | ||||||||||
|
@@ -544,6 +700,10 @@ class KatexSpanStyles { | |||||||||
int get hashCode => Object.hash( | ||||||||||
'KatexSpanStyles', | ||||||||||
heightEm, | ||||||||||
verticalAlignEm, | ||||||||||
topEm, | ||||||||||
marginRightEm, | ||||||||||
marginLeftEm, | ||||||||||
fontFamily, | ||||||||||
fontSizeEm, | ||||||||||
fontWeight, | ||||||||||
|
@@ -555,6 +715,10 @@ class KatexSpanStyles { | |||||||||
bool operator ==(Object other) { | ||||||||||
return other is KatexSpanStyles && | ||||||||||
other.heightEm == heightEm && | ||||||||||
other.verticalAlignEm == verticalAlignEm && | ||||||||||
other.topEm == topEm && | ||||||||||
other.marginRightEm == marginRightEm && | ||||||||||
other.marginLeftEm == marginLeftEm && | ||||||||||
other.fontFamily == fontFamily && | ||||||||||
other.fontSizeEm == fontSizeEm && | ||||||||||
other.fontWeight == fontWeight && | ||||||||||
|
@@ -566,6 +730,10 @@ class KatexSpanStyles { | |||||||||
String toString() { | ||||||||||
final args = <String>[]; | ||||||||||
if (heightEm != null) args.add('heightEm: $heightEm'); | ||||||||||
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm'); | ||||||||||
if (topEm != null) args.add('topEm: $topEm'); | ||||||||||
if (marginRightEm != null) args.add('marginRightEm: $marginRightEm'); | ||||||||||
if (marginLeftEm != null) args.add('marginLeftEm: $marginLeftEm'); | ||||||||||
if (fontFamily != null) args.add('fontFamily: $fontFamily'); | ||||||||||
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm'); | ||||||||||
if (fontWeight != null) args.add('fontWeight: $fontWeight'); | ||||||||||
|
@@ -584,13 +752,43 @@ class KatexSpanStyles { | |||||||||
KatexSpanStyles merge(KatexSpanStyles other) { | ||||||||||
return KatexSpanStyles( | ||||||||||
heightEm: other.heightEm ?? heightEm, | ||||||||||
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm, | ||||||||||
topEm: other.topEm ?? topEm, | ||||||||||
marginRightEm: other.marginRightEm ?? marginRightEm, | ||||||||||
marginLeftEm: other.marginLeftEm ?? marginLeftEm, | ||||||||||
fontFamily: other.fontFamily ?? fontFamily, | ||||||||||
fontSizeEm: other.fontSizeEm ?? fontSizeEm, | ||||||||||
fontStyle: other.fontStyle ?? fontStyle, | ||||||||||
fontWeight: other.fontWeight ?? fontWeight, | ||||||||||
textAlign: other.textAlign ?? textAlign, | ||||||||||
); | ||||||||||
} | ||||||||||
|
||||||||||
KatexSpanStyles filter({ | ||||||||||
bool heightEm = true, | ||||||||||
bool verticalAlignEm = true, | ||||||||||
bool topEm = true, | ||||||||||
bool marginRightEm = true, | ||||||||||
bool marginLeftEm = true, | ||||||||||
bool fontFamily = true, | ||||||||||
bool fontSizeEm = true, | ||||||||||
bool fontWeight = true, | ||||||||||
bool fontStyle = true, | ||||||||||
bool textAlign = true, | ||||||||||
}) { | ||||||||||
return KatexSpanStyles( | ||||||||||
heightEm: heightEm ? this.heightEm : null, | ||||||||||
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null, | ||||||||||
topEm: topEm ? this.topEm : null, | ||||||||||
marginRightEm: marginRightEm ? this.marginRightEm : null, | ||||||||||
marginLeftEm: marginLeftEm ? this.marginLeftEm : null, | ||||||||||
fontFamily: fontFamily ? this.fontFamily : null, | ||||||||||
fontSizeEm: fontSizeEm ? this.fontSizeEm : null, | ||||||||||
fontWeight: fontWeight ? this.fontWeight : null, | ||||||||||
fontStyle: fontStyle ? this.fontStyle : null, | ||||||||||
textAlign: textAlign ? this.textAlign : null, | ||||||||||
); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
class _KatexHtmlParseError extends Error { | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the
startsWith
logic necessary? Seems like we could just skip to the equality check.