Skip to content

KaTeX (4/n): Ignore classes that don't have CSS definition #1601

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 11 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
8 changes: 6 additions & 2 deletions lib/model/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,12 @@ abstract class MathNode extends ContentNode {
}
}

class KatexNode extends ContentNode {
const KatexNode({
sealed class KatexNode extends ContentNode {
const KatexNode({super.debugHtmlNode});
}

class KatexSpanNode extends KatexNode {
const KatexSpanNode({
required this.styles,
required this.text,
required this.nodes,
Expand Down
123 changes: 110 additions & 13 deletions lib/model/katex.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:csslib/parser.dart' as css_parser;
import 'package:csslib/visitor.dart' as css_visitor;
import 'package:flutter/foundation.dart';
import 'package:html/dom.dart' as dom;

Expand Down Expand Up @@ -109,19 +111,14 @@ class _KatexParser {
bool get hasError => _hasError;
bool _hasError = false;

void _logError(String message) {
assert(debugLog(message));
_hasError = true;
}

List<KatexNode> parseKatexHtml(dom.Element element) {
assert(element.localName == 'span');
assert(element.className == 'katex-html');
return _parseChildSpans(element);
return _parseChildSpans(element.nodes);
}

List<KatexNode> _parseChildSpans(dom.Element element) {
return List.unmodifiable(element.nodes.map((node) {
List<KatexNode> _parseChildSpans(List<dom.Node> nodes) {
return List.unmodifiable(nodes.map((node) {
if (node case dom.Element(localName: 'span')) {
return _parseSpan(node);
} else {
Expand All @@ -136,6 +133,10 @@ class _KatexParser {
KatexNode _parseSpan(dom.Element element) {
// TODO maybe check if the sequence of ancestors matter for spans.

final debugHtmlNode = kDebugMode ? element : null;

final inlineStyles = _parseSpanInlineStyles(element);

// Aggregate the CSS styles that apply, in the same order as the CSS
// classes specified for this span, mimicking the behaviour on web.
//
Expand Down Expand Up @@ -275,6 +276,21 @@ class _KatexParser {
fontStyle = KatexSpanFontStyle.normal;

// TODO handle skipped class declarations between .mainrm and
// .mspace .

case 'mspace':
// .mspace { ... }
// Do nothing, it has properties that don't need special handling.
break;

// TODO handle skipped class declarations between .mspace and
// .msupsub .

case 'msupsub':
// .msupsub { text-align: left; }
textAlign = KatexSpanTextAlign.left;

// TODO handle skipped class declarations between .msupsub and
// .sizing .

case 'sizing':
Expand Down Expand Up @@ -329,12 +345,24 @@ class _KatexParser {

case 'mord':
case 'mopen':
case 'mtight':
case 'text':
case 'mrel':
case 'mop':
case 'mclose':
case 'minner':
case 'mbin':
case 'mpunct':
case 'nobreak':
case 'allowbreak':
case 'mathdefault':
// Ignore these classes because they don't have a CSS definition
// in katex.scss, but we encounter them in the generated HTML.
break;

default:
_logError('KaTeX: Unsupported CSS class: $spanClass');
assert(debugLog('KaTeX: Unsupported CSS class: $spanClass'));
_hasError = true;
}
}
final styles = KatexSpanStyles(
Expand All @@ -350,14 +378,66 @@ class _KatexParser {
if (element.nodes case [dom.Text(:final data)]) {
text = data;
} else {
spans = _parseChildSpans(element);
spans = _parseChildSpans(element.nodes);
}
if (text == null && spans == null) throw KatexHtmlParseError();

return KatexNode(
styles: styles,
return KatexSpanNode(
styles: inlineStyles != null
? styles.merge(inlineStyles)
: styles,
text: text,
nodes: spans);
nodes: spans,
debugHtmlNode: debugHtmlNode);
}

KatexSpanStyles? _parseSpanInlineStyles(dom.Element element) {
if (element.attributes case {'style': final styleStr}) {
// `package:csslib` doesn't seem to have a way to parse inline styles:
// https://github.com/dart-lang/tools/issues/1173
// So, workaround that by wrapping it in a universal declaration.
final stylesheet = css_parser.parse('*{$styleStr}');
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
double? heightEm;

for (final declaration in rule.declarationGroup.declarations) {
if (declaration case css_visitor.Declaration(
:final property,
expression: css_visitor.Expressions(
expressions: [css_visitor.Expression() && final expression]),
)) {
switch (property) {
case 'height':
heightEm = _getEm(expression);
if (heightEm != null) continue;
}

// TODO handle more CSS properties
assert(debugLog('KaTeX: Unsupported CSS expression:'
' ${expression.toDebugString()}'));
_hasError = true;
} else {
throw KatexHtmlParseError();
}
}

return KatexSpanStyles(
heightEm: heightEm,
);
} else {
throw KatexHtmlParseError();
}
}
return null;
}

/// Returns the CSS `em` unit value if the given [expression] is actually an
/// `em` unit expression, else returns null.
double? _getEm(css_visitor.Expression expression) {
if (expression is css_visitor.EmTerm && expression.value is num) {
return (expression.value as num).toDouble();
}
return null;
}
}

Expand All @@ -378,13 +458,16 @@ enum KatexSpanTextAlign {

@immutable
class KatexSpanStyles {
final double? heightEm;

final String? fontFamily;
final double? fontSizeEm;
final KatexSpanFontWeight? fontWeight;
final KatexSpanFontStyle? fontStyle;
final KatexSpanTextAlign? textAlign;

const KatexSpanStyles({
this.heightEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
Expand All @@ -395,6 +478,7 @@ class KatexSpanStyles {
@override
int get hashCode => Object.hash(
'KatexSpanStyles',
heightEm,
fontFamily,
fontSizeEm,
fontWeight,
Expand All @@ -405,6 +489,7 @@ class KatexSpanStyles {
@override
bool operator ==(Object other) {
return other is KatexSpanStyles &&
other.heightEm == heightEm &&
other.fontFamily == fontFamily &&
other.fontSizeEm == fontSizeEm &&
other.fontWeight == fontWeight &&
Expand All @@ -415,13 +500,25 @@ class KatexSpanStyles {
@override
String toString() {
final args = <String>[];
if (heightEm != null) args.add('heightEm: $heightEm');
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');
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
}

KatexSpanStyles merge(KatexSpanStyles other) {
return KatexSpanStyles(
heightEm: other.heightEm ?? heightEm,
fontFamily: other.fontFamily ?? fontFamily,
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
fontStyle: other.fontStyle ?? fontStyle,
fontWeight: other.fontWeight ?? fontWeight,
textAlign: other.textAlign ?? textAlign,
);
}
}

class KatexHtmlParseError extends Error {
Expand Down
31 changes: 17 additions & 14 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,11 @@ class MathBlock extends StatelessWidget {
children: [TextSpan(text: node.texSource)])));
}

return _Katex(inline: false, nodes: nodes);
return Center(
child: SingleChildScrollViewWithScrollbar(
scrollDirection: Axis.horizontal,
child: _Katex(
nodes: nodes)));
}
}

Expand All @@ -831,24 +835,15 @@ const kBaseKatexTextStyle = TextStyle(

class _Katex extends StatelessWidget {
const _Katex({
required this.inline,
required this.nodes,
});

final bool inline;
final List<KatexNode> nodes;

@override
Widget build(BuildContext context) {
Widget widget = _KatexNodeList(nodes: nodes);

if (!inline) {
widget = Center(
child: SingleChildScrollViewWithScrollbar(
scrollDirection: Axis.horizontal,
child: widget));
}

return Directionality(
textDirection: TextDirection.ltr,
child: DefaultTextStyle(
Expand All @@ -870,15 +865,17 @@ class _KatexNodeList extends StatelessWidget {
return WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: _KatexSpan(e));
child: switch (e) {
KatexSpanNode() => _KatexSpan(e),
});
}))));
}
}

class _KatexSpan extends StatelessWidget {
const _KatexSpan(this.node);

final KatexNode node;
final KatexSpanNode node;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -941,7 +938,13 @@ class _KatexSpan extends StatelessWidget {
textAlign: textAlign,
child: widget);
}
return widget;

return SizedBox(
height: styles.heightEm != null
? styles.heightEm! * em
: null,
child: widget,
);
}
}

Expand Down Expand Up @@ -1263,7 +1266,7 @@ class _InlineContentBuilder {
: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: _Katex(inline: true, nodes: nodes));
child: _Katex(nodes: nodes));

case GlobalTimeNode():
return WidgetSpan(alignment: PlaceholderAlignment.middle,
Expand Down
Loading