diff --git a/lib/model/content.dart b/lib/model/content.dart
index e4273f1b3a..e49cb93246 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -411,6 +411,77 @@ class KatexSpanNode extends KatexNode {
}
}
+class KatexStrutNode extends KatexNode {
+ const KatexStrutNode({
+ required this.heightEm,
+ required this.verticalAlignEm,
+ super.debugHtmlNode,
+ });
+
+ final double heightEm;
+ final double? verticalAlignEm;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DoubleProperty('heightEm', heightEm));
+ properties.add(DoubleProperty('verticalAlignEm', verticalAlignEm));
+ }
+}
+
+class KatexVlistNode extends KatexNode {
+ const KatexVlistNode({
+ required this.rows,
+ super.debugHtmlNode,
+ });
+
+ final List rows;
+
+ @override
+ List 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));
+ }
+}
+
+class KatexNegativeMarginNode extends KatexNode {
+ const KatexNegativeMarginNode({
+ required this.leftOffsetEm,
+ required this.nodes,
+ super.debugHtmlNode,
+ }) : assert(leftOffsetEm < 0);
+
+ final double leftOffsetEm;
+ final List nodes;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DoubleProperty('leftOffsetEm', leftOffsetEm));
+ }
+
+ @override
+ List debugDescribeChildren() {
+ return nodes.map((node) => node.toDiagnosticsNode()).toList();
+ }
+}
+
class MathBlockNode extends MathNode implements BlockContentNode {
const MathBlockNode({
super.debugHtmlNode,
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 922546c676..ed41b440f9 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -1,3 +1,4 @@
+import 'package:collection/collection.dart';
import 'package:csslib/parser.dart' as css_parser;
import 'package:csslib/visitor.dart' as css_visitor;
import 'package:flutter/foundation.dart';
@@ -160,6 +161,8 @@ class _KatexParser {
final unsupportedCssClasses = [];
final unsupportedInlineCssProperties = [];
+ final List _ancestorClasses = [];
+
List parseKatexHtml(dom.Element element) {
assert(element.localName == 'span');
assert(element.className == 'katex-html');
@@ -167,16 +170,44 @@ class _KatexParser {
}
List _parseChildSpans(List nodes) {
- return List.unmodifiable(nodes.map((node) {
- if (node case dom.Element(localName: 'span')) {
- return _parseSpan(node);
- } else {
+ var resultSpans = QueueList();
+ for (final node in nodes.reversed) {
+ if (node is! dom.Element || node.localName != 'span') {
throw _KatexHtmlParseError(
node is dom.Element
? 'unsupported html node: ${node.localName}'
: 'unsupported html node');
}
- }));
+
+ _ancestorClasses.add(node.className);
+ final span = _parseSpan(node);
+ assert(_ancestorClasses.removeLast() == node.className);
+
+ if (span is KatexSpanNode) {
+ final marginRightEm = span.styles.marginRightEm;
+ if (marginRightEm != null && marginRightEm.isNegative) {
+ final previousSpans = resultSpans;
+ resultSpans = QueueList();
+ resultSpans.addFirst(KatexNegativeMarginNode(
+ leftOffsetEm: marginRightEm,
+ nodes: previousSpans));
+ }
+ }
+
+ resultSpans.addFirst(span);
+
+ if (span is KatexSpanNode) {
+ final marginLeftEm = span.styles.marginLeftEm;
+ if (marginLeftEm != null && marginLeftEm.isNegative) {
+ final previousSpans = resultSpans;
+ resultSpans = QueueList();
+ resultSpans.addFirst(KatexNegativeMarginNode(
+ leftOffsetEm: marginLeftEm,
+ nodes: previousSpans));
+ }
+ }
+ }
+ return resultSpans;
}
static final _resetSizeClassRegExp = RegExp(r'^reset-size(\d\d?)$');
@@ -185,9 +216,151 @@ 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()) {
+ throw _KatexHtmlParseError();
+ }
+
+ return KatexStrutNode(
+ heightEm: heightEm,
+ verticalAlignEm: verticalAlignEm);
+ } 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 = [];
+
+ 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)!;
+ final pstrutHeight = pstrutStyles.heightEm ?? 0;
+
+ KatexSpanNode innerSpanNode = KatexSpanNode(
+ styles: styles,
+ text: null,
+ nodes: _parseChildSpans(otherSpans));
+
+ final marginRightEm = styles.marginRightEm;
+ final marginLeftEm = styles.marginLeftEm;
+ if (marginRightEm != null && marginRightEm.isNegative) {
+ throw _KatexHtmlParseError();
+ }
+ if (marginLeftEm != null && marginLeftEm.isNegative) {
+ innerSpanNode = KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexNegativeMarginNode(
+ leftOffsetEm: marginLeftEm,
+ nodes: [innerSpanNode]),
+ ]);
+ }
+
+ rows.add(KatexVlistRowNode(
+ verticalOffsetEm: topEm + pstrutHeight,
+ debugHtmlNode: kDebugMode ? innerSpan : null,
+ node: innerSpanNode));
+ } else {
+ throw _KatexHtmlParseError();
+ }
+ }
+
+ final result = KatexVlistNode(
+ rows: rows,
+ debugHtmlNode: kDebugMode ? vlistT : null,
+ );
+
+ if (_ancestorClasses.any(
+ (classes) => classes.split(' ').contains('op-limits'),
+ )) {
+ return KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.center),
+ text: null,
+ nodes: [result]);
+ }
+
+ return result;
+ } 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 +370,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.unmodifiable(element.className.split(' '));
+ final spanClasses = element.className != ''
+ ? List.unmodifiable(element.className.split(' '))
+ : const [];
String? fontFamily;
double? fontSizeEm;
KatexSpanFontWeight? fontWeight;
@@ -214,8 +389,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; }
@@ -398,6 +574,10 @@ class _KatexParser {
_ => throw _KatexHtmlParseError(),
};
+ case 'op-limits':
+ // .op-limits > .vlist-t { text-align: center; }
+ // We handle this above while parsing the vlist.
+
// TODO handle more classes from katex.scss
case 'mord':
@@ -463,6 +643,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 +658,22 @@ 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) continue;
+
+ case 'margin-left':
+ marginLeftEm = _getEm(expression);
+ if (marginLeftEm != null) continue;
}
// TODO handle more CSS properties
@@ -488,6 +688,10 @@ class _KatexParser {
return KatexSpanStyles(
heightEm: heightEm,
+ topEm: topEm,
+ verticalAlignEm: verticalAlignEm,
+ marginRightEm: marginRightEm,
+ marginLeftEm: marginLeftEm,
);
} else {
throw _KatexHtmlParseError();
@@ -524,6 +728,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 +743,10 @@ class KatexSpanStyles {
const KatexSpanStyles({
this.heightEm,
+ this.verticalAlignEm,
+ this.topEm,
+ this.marginRightEm,
+ this.marginLeftEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
@@ -544,6 +758,10 @@ class KatexSpanStyles {
int get hashCode => Object.hash(
'KatexSpanStyles',
heightEm,
+ verticalAlignEm,
+ topEm,
+ marginRightEm,
+ marginLeftEm,
fontFamily,
fontSizeEm,
fontWeight,
@@ -555,6 +773,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 +788,10 @@ class KatexSpanStyles {
String toString() {
final args = [];
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,6 +810,10 @@ 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,
@@ -591,6 +821,32 @@ class KatexSpanStyles {
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 {
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index b49fdb4d9c..752fe9e071 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -22,6 +22,7 @@ import 'code_block.dart';
import 'dialog.dart';
import 'icons.dart';
import 'inset_shadow.dart';
+import 'katex.dart';
import 'lightbox.dart';
import 'message_list.dart';
import 'poll.dart';
@@ -820,42 +821,52 @@ class MathBlock extends StatelessWidget {
children: [TextSpan(text: node.texSource)])));
}
- return _Katex(inline: false, nodes: nodes);
+ return Center(
+ child: SingleChildScrollViewWithScrollbar(
+ scrollDirection: Axis.horizontal,
+ child: Katex(
+ textStyle: ContentTheme.of(context).textStylePlainParagraph,
+ textAlign: TextAlign.center,
+ nodes: nodes)));
}
}
-// Base text style from .katex class in katex.scss :
-// https://github.com/KaTeX/KaTeX/blob/613c3da8/src/styles/katex.scss#L13-L15
-const kBaseKatexTextStyle = TextStyle(
- fontSize: kBaseFontSize * 1.21,
- fontFamily: 'KaTeX_Main',
- height: 1.2);
+/// Creates a base text style for rendering KaTeX content.
+///
+/// This applies the CSS styles defined in .katex class in katex.scss :
+/// https://github.com/KaTeX/KaTeX/blob/613c3da8/src/styles/katex.scss#L13-L15
+///
+/// Requires the [style.fontSize] to be non-null.
+TextStyle mkBaseKatexTextStyle(TextStyle style) {
+ return style.copyWith(
+ fontSize: style.fontSize! * 1.21,
+ fontFamily: 'KaTeX_Main',
+ height: 1.2,
+ fontWeight: FontWeight.normal,
+ fontStyle: FontStyle.normal);
+}
-class _Katex extends StatelessWidget {
- const _Katex({
- required this.inline,
+class Katex extends StatelessWidget {
+ const Katex({
+ super.key,
+ required this.textStyle,
+ this.textAlign,
required this.nodes,
});
- final bool inline;
+ final TextStyle textStyle;
+ final TextAlign? textAlign;
final List 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(
- style: kBaseKatexTextStyle.copyWith(
- color: ContentTheme.of(context).textStylePlainParagraph.color),
+ child: DefaultTextStyle.merge(
+ style: mkBaseKatexTextStyle(textStyle),
+ textAlign: textAlign,
child: widget));
}
}
@@ -872,9 +883,14 @@ class _KatexNodeList extends StatelessWidget {
return WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: switch (e) {
- KatexSpanNode() => _KatexSpan(e),
- });
+ child: MediaQuery(
+ data: MediaQueryData(textScaler: TextScaler.noScaling),
+ child: switch (e) {
+ KatexSpanNode() => _KatexSpan(e),
+ KatexStrutNode() => _KatexStrut(e),
+ KatexVlistNode() => _KatexVlist(e),
+ KatexNegativeMarginNode() => _KatexNegativeMargin(e),
+ }));
}))));
}
}
@@ -946,14 +962,103 @@ class _KatexSpan extends StatelessWidget {
child: widget);
}
- return SizedBox(
+ final marginRight = switch (styles.marginRightEm) {
+ double marginRightEm when marginRightEm >= 0 => marginRightEm * em,
+ _ => null,
+ };
+ final marginLeft = switch (styles.marginLeftEm) {
+ double marginLeftEm when marginLeftEm >= 0 => marginLeftEm * em,
+ _ => null,
+ };
+
+ EdgeInsets? margin;
+ if (marginRight != null || marginLeft != null) {
+ margin = EdgeInsets.zero;
+ if (marginRight != null) {
+ margin += EdgeInsets.only(right: marginRight);
+ }
+ if (marginLeft != null) {
+ margin += EdgeInsets.only(left: marginLeft);
+ }
+ }
+
+ return Container(
+ margin: margin,
height: styles.heightEm != null
? styles.heightEm! * (fontSize ?? em)
: null,
+ transform: styles.topEm != null
+ ? Matrix4.translationValues(0, styles.topEm! * em, 0)
+ : null,
child: widget);
}
}
+class _KatexStrut extends StatelessWidget {
+ const _KatexStrut(this.node);
+
+ final KatexStrutNode node;
+
+ @override
+ Widget build(BuildContext context) {
+ final em = DefaultTextStyle.of(context).style.fontSize!;
+
+ final verticalAlignEm = node.verticalAlignEm;
+ if (verticalAlignEm == null) {
+ return SizedBox(height: node.heightEm * em);
+ }
+
+ return SizedBox(
+ height: node.heightEm * em,
+ child: Baseline(
+ baseline: (verticalAlignEm + node.heightEm) * em,
+ baselineType: TextBaseline.alphabetic,
+ child: const Text('')),
+ );
+ }
+}
+
+class _KatexVlist extends StatelessWidget {
+ const _KatexVlist(this.node);
+
+ final KatexVlistNode node;
+
+ @override
+ Widget build(BuildContext context) {
+ final em = DefaultTextStyle.of(context).style.fontSize!;
+
+ return IntrinsicWidth(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ textBaseline: TextBaseline.alphabetic,
+ children: List.unmodifiable(node.rows.map((row) {
+ return SizedBox(
+ height: 0,
+ child: OverflowBox(
+ maxHeight: double.infinity,
+ child: Transform.translate(
+ offset: Offset(0, row.verticalOffsetEm * em),
+ child: _KatexSpan(row.node),
+ )));
+ }))));
+ }
+}
+
+class _KatexNegativeMargin extends StatelessWidget {
+ const _KatexNegativeMargin(this.node);
+
+ final KatexNegativeMarginNode node;
+
+ @override
+ Widget build(BuildContext context) {
+ final em = DefaultTextStyle.of(context).style.fontSize!;
+
+ return NegativeLeftOffset(
+ leftOffset: node.leftOffsetEm * em,
+ child: _KatexNodeList(nodes: node.nodes));
+ }
+}
+
class WebsitePreview extends StatelessWidget {
const WebsitePreview({super.key, required this.node});
@@ -1272,7 +1377,7 @@ class _InlineContentBuilder {
: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: _Katex(inline: true, nodes: nodes));
+ child: Katex(textStyle: widget.style, nodes: nodes));
case GlobalTimeNode():
return WidgetSpan(alignment: PlaceholderAlignment.middle,
diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart
new file mode 100644
index 0000000000..9b89270c8b
--- /dev/null
+++ b/lib/widgets/katex.dart
@@ -0,0 +1,199 @@
+import 'dart:math' as math;
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter/rendering.dart';
+
+class NegativeLeftOffset extends SingleChildRenderObjectWidget {
+ NegativeLeftOffset({super.key, required this.leftOffset, super.child})
+ : assert(leftOffset.isNegative),
+ _padding = EdgeInsets.only(left: leftOffset);
+
+ final double leftOffset;
+ final EdgeInsetsGeometry _padding;
+
+ @override
+ RenderNegativePadding createRenderObject(BuildContext context) {
+ return RenderNegativePadding(
+ padding: _padding,
+ textDirection: Directionality.maybeOf(context));
+ }
+
+ @override
+ void updateRenderObject(
+ BuildContext context,
+ RenderNegativePadding renderObject,
+ ) {
+ renderObject
+ ..padding = _padding
+ ..textDirection = Directionality.maybeOf(context);
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty('padding', _padding));
+ }
+}
+
+// Like [RenderPadding] but only supports negative values.
+// TODO(upstream): give Padding an option to accept negative padding (at cost of hit-testing not working)
+class RenderNegativePadding extends RenderShiftedBox {
+ RenderNegativePadding({
+ required EdgeInsetsGeometry padding,
+ TextDirection? textDirection,
+ RenderBox? child,
+ }) : assert(!padding.isNonNegative),
+ _textDirection = textDirection,
+ _padding = padding,
+ super(child);
+
+ EdgeInsets? _resolvedPaddingCache;
+ EdgeInsets get _resolvedPadding {
+ final EdgeInsets returnValue = _resolvedPaddingCache ??= padding.resolve(textDirection);
+ return returnValue;
+ }
+
+ void _markNeedResolution() {
+ _resolvedPaddingCache = null;
+ markNeedsLayout();
+ }
+
+ /// The amount to pad the child in each dimension.
+ ///
+ /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
+ /// must not be null.
+ EdgeInsetsGeometry get padding => _padding;
+ EdgeInsetsGeometry _padding;
+ set padding(EdgeInsetsGeometry value) {
+ assert(!value.isNonNegative);
+ if (_padding == value) {
+ return;
+ }
+ _padding = value;
+ _markNeedResolution();
+ }
+
+ /// The text direction with which to resolve [padding].
+ ///
+ /// This may be changed to null, but only after the [padding] has been changed
+ /// to a value that does not depend on the direction.
+ TextDirection? get textDirection => _textDirection;
+ TextDirection? _textDirection;
+ set textDirection(TextDirection? value) {
+ if (_textDirection == value) {
+ return;
+ }
+ _textDirection = value;
+ _markNeedResolution();
+ }
+
+ @override
+ double computeMinIntrinsicWidth(double height) {
+ final EdgeInsets padding = _resolvedPadding;
+ if (child != null) {
+ // Relies on double.infinity absorption.
+ return child!.getMinIntrinsicWidth(math.max(0.0, height - padding.vertical)) +
+ padding.horizontal;
+ }
+ return padding.horizontal;
+ }
+
+ @override
+ double computeMaxIntrinsicWidth(double height) {
+ final EdgeInsets padding = _resolvedPadding;
+ if (child != null) {
+ // Relies on double.infinity absorption.
+ return child!.getMaxIntrinsicWidth(math.max(0.0, height - padding.vertical)) +
+ padding.horizontal;
+ }
+ return padding.horizontal;
+ }
+
+ @override
+ double computeMinIntrinsicHeight(double width) {
+ final EdgeInsets padding = _resolvedPadding;
+ if (child != null) {
+ // Relies on double.infinity absorption.
+ return child!.getMinIntrinsicHeight(math.max(0.0, width - padding.horizontal)) +
+ padding.vertical;
+ }
+ return padding.vertical;
+ }
+
+ @override
+ double computeMaxIntrinsicHeight(double width) {
+ final EdgeInsets padding = _resolvedPadding;
+ if (child != null) {
+ // Relies on double.infinity absorption.
+ return child!.getMaxIntrinsicHeight(math.max(0.0, width - padding.horizontal)) +
+ padding.vertical;
+ }
+ return padding.vertical;
+ }
+
+ @override
+ @protected
+ Size computeDryLayout(covariant BoxConstraints constraints) {
+ final EdgeInsets padding = _resolvedPadding;
+ if (child == null) {
+ return constraints.constrain(Size(padding.horizontal, padding.vertical));
+ }
+ final BoxConstraints innerConstraints = constraints.deflate(padding);
+ final Size childSize = child!.getDryLayout(innerConstraints);
+ return constraints.constrain(
+ Size(padding.horizontal + childSize.width, padding.vertical + childSize.height),
+ );
+ }
+
+ @override
+ double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
+ final RenderBox? child = this.child;
+ if (child == null) {
+ return null;
+ }
+ final EdgeInsets padding = _resolvedPadding;
+ final BoxConstraints innerConstraints = constraints.deflate(padding);
+ final BaselineOffset result =
+ BaselineOffset(child.getDryBaseline(innerConstraints, baseline)) + padding.top;
+ return result.offset;
+ }
+
+ @override
+ void performLayout() {
+ final BoxConstraints constraints = this.constraints;
+ final EdgeInsets padding = _resolvedPadding;
+ if (child == null) {
+ size = constraints.constrain(Size(padding.horizontal, padding.vertical));
+ return;
+ }
+ final BoxConstraints innerConstraints = constraints.deflate(padding);
+ child!.layout(innerConstraints, parentUsesSize: true);
+ final BoxParentData childParentData = child!.parentData! as BoxParentData;
+ childParentData.offset = Offset(padding.left, padding.top);
+ size = constraints.constrain(
+ Size(padding.horizontal + child!.size.width, padding.vertical + child!.size.height),
+ );
+ }
+
+ @override
+ void debugPaintSize(PaintingContext context, Offset offset) {
+ super.debugPaintSize(context, offset);
+ assert(() {
+ final Rect outerRect = offset & size;
+ debugPaintPadding(
+ context.canvas,
+ outerRect,
+ child != null ? _resolvedPaddingCache!.deflateRect(outerRect) : null,
+ );
+ return true;
+ }());
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty('padding', padding));
+ properties.add(EnumProperty('textDirection', textDirection, defaultValue: null));
+ }
+}
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index c90ac54b33..308023f46f 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -519,7 +519,7 @@ class ContentExample {
'λ
',
MathInlineNode(texSource: r'\lambda', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -539,7 +539,7 @@ class ContentExample {
'λ',
[MathBlockNode(texSource: r'\lambda', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -564,7 +564,7 @@ class ContentExample {
'b', [
MathBlockNode(texSource: 'a', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -575,7 +575,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'b', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -603,7 +603,7 @@ class ContentExample {
[QuotationNode([
MathBlockNode(texSource: r'\lambda', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -632,7 +632,7 @@ class ContentExample {
[QuotationNode([
MathBlockNode(texSource: 'a', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -643,7 +643,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'b', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -681,7 +681,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'a', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306),text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -731,10 +731,7 @@ class ContentExample {
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexSpanNode(
- styles: KatexSpanStyles(heightEm: 1.6034),
- text: null,
- nodes: []),
+ KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11
text: '1',
@@ -800,10 +797,7 @@ class ContentExample {
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexSpanNode(
- styles: KatexSpanStyles(heightEm: 1.6034),
- text: null,
- nodes: []),
+ KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1
text: null,
@@ -845,10 +839,7 @@ class ContentExample {
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexSpanNode(
- styles: KatexSpanStyles(heightEm: 3.0),
- text: null,
- nodes: []),
+ KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25),
KatexSpanNode(
styles: KatexSpanStyles(),
text: '⟨',
@@ -893,6 +884,522 @@ class ContentExample {
]),
]);
+ static const mathBlockKatexVertical1 = ContentExample(
+ 'math block katex vertical 1',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176734
+ '```math\na\'\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'a'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ '′
',
+ [
+ MathBlockNode(
+ texSource: 'a\'',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'a',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left),
+ text: null,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -3.113 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: '′',
+ nodes: null),
+ ]),
+ ]),
+ ])),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexVertical2 = ContentExample(
+ 'math block katex vertical 2',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176735
+ '```math\nx_n\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'x'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'n'
+ ''
+ ''
+ '
',
+ [
+ MathBlockNode(
+ texSource: 'x_n',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'x',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left),
+ text: null,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -2.55 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(
+ marginLeftEm: 0,
+ marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'n',
+ nodes: null),
+ ]),
+ ])),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexVertical3 = ContentExample(
+ 'math block katex vertical 3',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176737
+ '```math\ne^x\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'e'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'x
',
+ [
+ MathBlockNode(
+ texSource: 'e^x',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.7144, verticalAlignEm: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'e',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left),
+ text: null,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -3.113 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'x',
+ nodes: null),
+ ]),
+ ])),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexVertical4 = ContentExample(
+ 'math block katex vertical 4',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176738
+ '```math\n_u^o\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'u'
+ ''
+ ''
+ ''
+ 'o'
+ ''
+ ''
+ '
',
+ [
+ MathBlockNode(
+ texSource: "_u^o",
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: []),
+ KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left),
+ text: null,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -2.453 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'u',
+ nodes: null),
+ ]),
+ ])),
+ KatexVlistRowNode(
+ verticalOffsetEm: -3.113 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'o',
+ nodes: null),
+ ]),
+ ])),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexVertical5 = ContentExample(
+ 'math block katex vertical 5',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176739
+ '```math\na\\raisebox{0.25em}{\$b\$}c\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'a'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'b'
+ 'c
',
+ [
+ MathBlockNode(
+ texSource: 'a\\raisebox{0.25em}{\$b\$}c',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'a',
+ nodes: null),
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -3.25 + 3,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'b',
+ nodes: null),
+ ]),
+ ])),
+ ]),
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'c',
+ nodes: null),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexNegativeMargins = ContentExample(
+ 'math block katex negative margins',
+ '```math\n\\KaTeX\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'K'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'A'
+ ''
+ ''
+ 'T'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'E'
+ ''
+ ''
+ ''
+ ''
+ 'X
',
+ [
+ MathBlockNode(
+ texSource: '\\KaTeX',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'),
+ text: 'K',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: -0.17),
+ text: null,
+ nodes: []),
+ KatexNegativeMarginNode(
+ leftOffsetEm: -0.17,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -2.905 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Main',
+ fontSizeEm: 0.7),
+ text: 'A',
+ nodes: null),
+ ])),
+ ]),
+ KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: -0.15),
+ text: null,
+ nodes: []),
+ KatexNegativeMarginNode(
+ leftOffsetEm: -0.15,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Main'),
+ text: 'T',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: -0.1667),
+ text: null,
+ nodes: []),
+ KatexNegativeMarginNode(
+ leftOffsetEm: -0.1667,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -2.7845 + 3,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Main'),
+ text: 'E',
+ nodes: null),
+ ])),
+ ]),
+ KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: -0.125),
+ text: null,
+ nodes: []),
+ KatexNegativeMarginNode(
+ leftOffsetEm: -0.125,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Main'),
+ text: 'X',
+ nodes: null),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
static const imageSingle = ContentExample(
'single image',
// https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103
@@ -1963,10 +2470,13 @@ void main() async {
testParseExample(ContentExample.mathBlockBetweenImages);
testParseExample(ContentExample.mathBlockKatexSizing);
testParseExample(ContentExample.mathBlockKatexNestedSizing);
- // TODO: Re-enable this test after adding support for parsing
- // `vertical-align` in inline styles. Currently it fails
- // because `strut` span has `vertical-align`.
- testParseExample(ContentExample.mathBlockKatexDelimSizing, skip: true);
+ testParseExample(ContentExample.mathBlockKatexDelimSizing);
+ testParseExample(ContentExample.mathBlockKatexVertical1);
+ testParseExample(ContentExample.mathBlockKatexVertical2);
+ testParseExample(ContentExample.mathBlockKatexVertical3);
+ testParseExample(ContentExample.mathBlockKatexVertical4);
+ testParseExample(ContentExample.mathBlockKatexVertical5);
+ testParseExample(ContentExample.mathBlockKatexNegativeMargins);
testParseExample(ContentExample.imageSingle);
testParseExample(ContentExample.imageSingleNoDimensions);
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index 754410fddc..f963e478c9 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_checks/flutter_checks.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -595,16 +596,20 @@ void main() {
final content = ContentExample.mathBlockKatexSizing;
await prepareContent(tester, plainContent(content.html));
+ final context = tester.element(find.byType(MathBlock));
+ final baseTextStyle =
+ mkBaseKatexTextStyle(ContentTheme.of(context).textStylePlainParagraph);
+
final mathBlockNode = content.expectedNodes.single as MathBlockNode;
final baseNode = mathBlockNode.nodes!.single as KatexSpanNode;
final nodes = baseNode.nodes!.skip(1); // Skip .strut node.
for (var katexNode in nodes) {
katexNode = katexNode as KatexSpanNode;
- final fontSize = katexNode.styles.fontSizeEm! * kBaseKatexTextStyle.fontSize!;
+ final fontSize = katexNode.styles.fontSizeEm! * baseTextStyle.fontSize!;
checkKatexText(tester, katexNode.text!,
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontHeight: baseTextStyle.height!);
}
});
@@ -617,17 +622,21 @@ void main() {
final content = ContentExample.mathBlockKatexNestedSizing;
await prepareContent(tester, plainContent(content.html));
- var fontSize = 0.5 * kBaseKatexTextStyle.fontSize!;
+ final context = tester.element(find.byType(MathBlock));
+ final baseTextStyle =
+ mkBaseKatexTextStyle(ContentTheme.of(context).textStylePlainParagraph);
+
+ var fontSize = 0.5 * baseTextStyle.fontSize!;
checkKatexText(tester, '1',
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontHeight: baseTextStyle.height!);
fontSize = 4.976 * fontSize;
checkKatexText(tester, '2',
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontHeight: baseTextStyle.height!);
});
testWidgets('displays KaTeX content with different delimiter sizing', (tester) async {
@@ -643,13 +652,15 @@ void main() {
final baseNode = mathBlockNode.nodes!.single as KatexSpanNode;
var nodes = baseNode.nodes!.skip(1); // Skip .strut node.
- final fontSize = kBaseKatexTextStyle.fontSize!;
+ final context = tester.element(find.byType(MathBlock));
+ final baseTextStyle =
+ mkBaseKatexTextStyle(ContentTheme.of(context).textStylePlainParagraph);
final firstNode = nodes.first as KatexSpanNode;
checkKatexText(tester, firstNode.text!,
fontFamily: 'KaTeX_Main',
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontSize: baseTextStyle.fontSize!,
+ fontHeight: baseTextStyle.height!);
nodes = nodes.skip(1);
for (var katexNode in nodes) {
@@ -658,12 +669,70 @@ void main() {
final fontFamily = katexNode.styles.fontFamily!;
checkKatexText(tester, katexNode.text!,
fontFamily: fontFamily,
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontSize: baseTextStyle.fontSize!,
+ fontHeight: baseTextStyle.height!);
+ }
+ });
+
+ group('characters render at specific offsets with specific size: ', () {
+ final testCases = [
+ (ContentExample.mathBlockKatexVertical1, [
+ ('a', Offset(0.0, 4.36), Size(10.88, 25.0)),
+ ('′', Offset(10.87, 0.26), Size(3.96, 17.0))
+ ]),
+ (ContentExample.mathBlockKatexVertical2, [
+ ('x', Offset(0.0, 4.36), Size(11.76, 25.0)),
+ ('n', Offset(11.76, 12.79), Size(8.63, 17.0))
+ ]),
+ (ContentExample.mathBlockKatexVertical3, [
+ ('e', Offset(0.0, 4.36), Size(9.58, 25.0)),
+ ('x', Offset(9.58, 1.20), Size(8.23, 17.0))
+ ]),
+ (ContentExample.mathBlockKatexVertical4, [
+ ('u', Offset(0.0, 14.82), Size(8.23, 17.0)),
+ ('o', Offset(0.0, 1.24), Size(6.98, 17.0))
+ ]),
+ (ContentExample.mathBlockKatexVertical5, [
+ ('a', Offset(0.0, 4.24), Size(10.88, 25.0)),
+ ('b', Offset(10.87, -0.57), Size(8.82, 25.0)),
+ ('c', Offset(19.69, 4.24), Size(8.9, 25.0))
+ ]),
+ (ContentExample.mathBlockKatexNegativeMargins, [
+ ('K', Offset(0.0, 7.75), Size(16.0, 25.0)),
+ ('A', Offset(12.50, 9.97), Size(10.79, 17.0)),
+ ('T', Offset(20.20, 8.48), Size(14.85, 25.0)),
+ ('E', Offset(31.62, 13.64), Size(14.0, 25.0)),
+ ('X', Offset(43.05, 8.96), Size(15.42, 25.0)),
+ ]),
+ ];
+
+ for (final testCase in testCases) {
+ testWidgets(testCase.$1.description, (tester) async {
+ await _loadKatexFonts();
+
+ addTearDown(testBinding.reset);
+ final globalSettings = testBinding.globalStore.settings;
+ await globalSettings.setBool(BoolGlobalSetting.renderKatex, true);
+ check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isTrue();
+
+ await prepareContent(tester, plainContent(testCase.$1.html));
+
+ final baseRect = tester.getRect(find.byType(Katex));
+
+ for (final characterData in testCase.$2) {
+ final character = characterData.$1;
+ final topLeftOffset = characterData.$2;
+ final size = characterData.$3;
+
+ final rect = tester.getRect(find.text(character));
+ check(rect.topLeft - baseRect.topLeft)
+ .within(distance: 0.05, from: topLeftOffset);
+ check(rect.size)
+ .within(distance: 0.05, from: size);
+ }
+ });
}
- }, skip: true); // TODO: Re-enable this test after adding support for parsing
- // `vertical-align` in inline styles. Currently it fails
- // because `strut` span has `vertical-align`.
+ });
});
/// Make a [TargetFontSizeFinder] to pass to [checkFontSizeRatio],
@@ -1412,3 +1481,48 @@ void main() {
});
});
}
+
+Future _loadKatexFonts() async {
+ const fonts = {
+ 'KaTeX_AMS': ['KaTeX_AMS-Regular.ttf'],
+ 'KaTeX_Caligraphic': [
+ 'KaTeX_Caligraphic-Regular.ttf',
+ 'KaTeX_Caligraphic-Bold.ttf',
+ ],
+ 'KaTeX_Fraktur': [
+ 'KaTeX_Fraktur-Regular.ttf',
+ 'KaTeX_Fraktur-Bold.ttf',
+ ],
+ 'KaTeX_Main': [
+ 'KaTeX_Main-Regular.ttf',
+ 'KaTeX_Main-Bold.ttf',
+ 'KaTeX_Main-Italic.ttf',
+ 'KaTeX_Main-BoldItalic.ttf',
+ ],
+ 'KaTeX_Math': [
+ 'KaTeX_Math-Italic.ttf',
+ 'KaTeX_Math-BoldItalic.ttf',
+ ],
+ 'KaTeX_SansSerif': [
+ 'KaTeX_SansSerif-Regular.ttf',
+ 'KaTeX_SansSerif-Bold.ttf',
+ 'KaTeX_SansSerif-Italic.ttf',
+ ],
+ 'KaTeX_Script': ['KaTeX_Script-Regular.ttf'],
+ 'KaTeX_Size1': ['KaTeX_Size1-Regular.ttf'],
+ 'KaTeX_Size2': ['KaTeX_Size2-Regular.ttf'],
+ 'KaTeX_Size3': ['KaTeX_Size3-Regular.ttf'],
+ 'KaTeX_Size4': ['KaTeX_Size4-Regular.ttf'],
+ 'KaTeX_Typewriter': ['KaTeX_Typewriter-Regular.ttf'],
+ };
+ for (final entry in fonts.entries) {
+ final fontFamily = entry.key;
+ final fontFiles = entry.value;
+
+ final fontLoader = FontLoader(fontFamily);
+ for (final fontFile in fontFiles) {
+ fontLoader.addFont(rootBundle.load('assets/KaTeX/$fontFile'));
+ }
+ await fontLoader.load();
+ }
+}