Skip to content

KaTeX(5/n): Handle 'position' & 'top' property in KaTeX span inline style #1627

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions lib/model/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,42 @@ class KatexStrutNode extends KatexNode {
}
}

class KatexVlistNode extends KatexNode {
const KatexVlistNode({
required this.rows,
super.debugHtmlNode,
});

final List<KatexVlistRowNode> rows;

@override
List<DiagnosticsNode> debugDescribeChildren() {
return rows.map((row) => row.toDiagnosticsNode()).toList();
}
}

class KatexVlistRowNode extends ContentNode {
const KatexVlistRowNode({
required this.verticalOffsetEm,
required this.node,
super.debugHtmlNode,
});

final double verticalOffsetEm;
final KatexSpanNode node;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('verticalOffsetEm', verticalOffsetEm));
}

@override
List<DiagnosticsNode> debugDescribeChildren() {
return [node.toDiagnosticsNode()];
}
}

class MathBlockNode extends MathNode implements BlockContentNode {
const MathBlockNode({
super.debugHtmlNode,
Expand Down
177 changes: 172 additions & 5 deletions lib/model/katex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,112 @@ class _KatexParser {
debugHtmlNode: debugHtmlNode);
}

if (element.className == 'vlist-t'
|| element.className == 'vlist-t vlist-t2') {
final vlistT = element;
if (vlistT.nodes.isEmpty) throw _KatexHtmlParseError();
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: []),
]) && final vlist,
]),
]) {
// In the generated HTML the .vlist in second .vlist-r span will have
// a "height" inline style which we ignore, because it doesn't seem
// to have any effect in rendering on the web.
// But also make sure there aren't any other inline styles present.
final vlistStyles = _parseSpanInlineStyles(vlist);
if (vlistStyles != null
&& vlistStyles.filter(heightEm: false) != const KatexSpanStyles()) {
throw _KatexHtmlParseError();
}
} 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) {
// Same as above for the second .vlist-r span, .vlist span in first
// .vlist-r span will have "height" inline style which we ignore,
// because it doesn't seem to have any effect in rendering on
// the web.
// But also make sure there aren't any other inline styles present.
final vlistStyles = _parseSpanInlineStyles(vlist);
if (vlistStyles != null
&& vlistStyles.filter(heightEm: false) != const KatexSpanStyles()) {
throw _KatexHtmlParseError();
}

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);
if (styles == null) throw _KatexHtmlParseError();
if (styles.verticalAlignEm != null) throw _KatexHtmlParseError();
final topEm = styles.topEm ?? 0;

styles = styles.filter(topEm: false);

final pstrutStyles = _parseSpanInlineStyles(pstrutSpan);
if (pstrutStyles == null) throw _KatexHtmlParseError();
if (pstrutStyles.filter(heightEm: false)
!= const KatexSpanStyles()) {
throw _KatexHtmlParseError();
}
final pstrutHeight = pstrutStyles.heightEm ?? 0;

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: debugHtmlNode,
);
} else {
throw _KatexHtmlParseError();
}
} else {
throw _KatexHtmlParseError();
}
}

final inlineStyles = _parseSpanInlineStyles(element);
if (inlineStyles != null) {
// We expect `vertical-align` inline style to be only present on a
Expand All @@ -224,7 +330,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;
Expand Down Expand Up @@ -474,10 +582,21 @@ class _KatexParser {
}
if (text == null && spans == null) throw _KatexHtmlParseError();

final mergedStyles = inlineStyles != null
? styles.merge(inlineStyles)
: styles;

// We expect `top` style to be only present if `position: relative`
// is also present. As both are non-inherited CSS attributes and
// should only ever be present together.
// TODO account for other sides (left, right, bottom).
if (mergedStyles.topEm != null
&& mergedStyles.position != KatexSpanPosition.relative) {
throw _KatexHtmlParseError();
}

return KatexSpanNode(
styles: inlineStyles != null
? styles.merge(inlineStyles)
: styles,
styles: mergedStyles,
text: text,
nodes: spans,
debugHtmlNode: debugHtmlNode);
Expand All @@ -492,8 +611,10 @@ class _KatexParser {
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
double? heightEm;
double? verticalAlignEm;
double? topEm;
double? marginRightEm;
double? marginLeftEm;
KatexSpanPosition? position;

for (final declaration in rule.declarationGroup.declarations) {
if (declaration case css_visitor.Declaration(
Expand All @@ -510,6 +631,10 @@ class _KatexParser {
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) {
Expand All @@ -523,6 +648,13 @@ class _KatexParser {
if (marginLeftEm < 0) throw _KatexHtmlParseError();
continue;
}

case 'position':
position = switch (_getLiteral(expression)) {
'relative' => KatexSpanPosition.relative,
_ => null,
};
if (position != null) continue;
}

// TODO handle more CSS properties
Expand All @@ -537,9 +669,11 @@ class _KatexParser {

return KatexSpanStyles(
heightEm: heightEm,
topEm: topEm,
verticalAlignEm: verticalAlignEm,
marginRightEm: marginRightEm,
marginLeftEm: marginLeftEm,
position: position,
);
} else {
throw _KatexHtmlParseError();
Expand All @@ -556,6 +690,17 @@ class _KatexParser {
}
return null;
}

/// Returns the CSS literal string value if the given [expression] is
/// actually a literal expression, else returns null.
String? _getLiteral(css_visitor.Expression expression) {
if (expression case css_visitor.LiteralTerm(:final value)) {
if (value case css_visitor.Identifier(:final name)) {
return name;
}
}
return null;
}
}

enum KatexSpanFontWeight {
Expand All @@ -573,11 +718,17 @@ enum KatexSpanTextAlign {
right,
}

enum KatexSpanPosition {
relative,
}

@immutable
class KatexSpanStyles {
final double? heightEm;
final double? verticalAlignEm;

final double? topEm;

final double? marginRightEm;
final double? marginLeftEm;

Expand All @@ -587,58 +738,68 @@ class KatexSpanStyles {
final KatexSpanFontStyle? fontStyle;
final KatexSpanTextAlign? textAlign;

final KatexSpanPosition? position;

const KatexSpanStyles({
this.heightEm,
this.verticalAlignEm,
this.topEm,
this.marginRightEm,
this.marginLeftEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
this.fontStyle,
this.textAlign,
this.position,
});

@override
int get hashCode => Object.hash(
'KatexSpanStyles',
heightEm,
verticalAlignEm,
topEm,
marginRightEm,
marginLeftEm,
fontFamily,
fontSizeEm,
fontWeight,
fontStyle,
textAlign,
position,
);

@override
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 &&
other.fontStyle == fontStyle &&
other.textAlign == textAlign;
other.textAlign == textAlign &&
other.position == position;
}

@override
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');
if (fontStyle != null) args.add('fontStyle: $fontStyle');
if (textAlign != null) args.add('textAlign: $textAlign');
if (position != null) args.add('position: $position');
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
}

Expand All @@ -653,37 +814,43 @@ class KatexSpanStyles {
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,
position: other.position ?? position,
);
}

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,
bool position = 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,
position: position ? this.position : null,
);
}
}
Expand Down
Loading
Loading