-
Notifications
You must be signed in to change notification settings - Fork 310
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?
Conversation
d00e10f
to
7ff4f9e
Compare
7ff4f9e
to
c0dd0a1
Compare
sealed class KatexNode extends ContentNode { | ||
const KatexNode({super.debugHtmlNode}); | ||
} | ||
|
||
class KatexSpanNode extends KatexNode { |
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.
One point that I noticed while developing #1478 (and checking how my draft of that branch interacted with this PR): it'd be good for this commit:
e268041 content: Handle vertical offset spans in KaTeX content
to get split up like so:
- First, an NFC prep commit introduces the distinction between KatexNode and KatexSpanNode. At this stage, the latter is the only subclass of the former.
- Then another commit makes the substantive changes this commit is about, including introducing the sibling subclasses KatexVlistNode and KatexVlistRowNode.
One reason that'd be useful is that the split between KatexNode and KatexSpanNode is somewhat nontrivial in itself: some of the references to KatexNode continue to say KatexNode, while others switch to saying KatexSpanNode, so the commit is expressing meaningful information by its choices of which references go which way.
b17033a
to
16cb28f
Compare
16cb28f
to
376cfe7
Compare
It seems like this had slipped through the cracks :-) but it looks ready for review, so I applied the label. Also rebased now that #1478 is merged, so this now contains only the changes that are specific to this PR. |
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.
Thanks for the PR! I think the last few commits will need some tests for the more complicated parts. I went over the changes and left some comments, but haven't extensively tested the changes manually yet.
lib/model/katex.dart
Outdated
_logError('KaTeX: Unsupported CSS property: $property of ' | ||
'type ${expression.runtimeType}'); |
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.
One other possibility for this to be logged is when _getEm
returns null. I think for debugging purpose, including the value expression
might be helpful.
if (expression is css_visitor.EmTerm && expression.value is num) { | ||
return (expression.value as num).toDouble(); | ||
} | ||
return null; |
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.
Let's perhaps have a short dartdoc on what this does and what null
means, since the if (…Em != null) continue;
's make this return value quite relevant.
lib/model/katex.dart
Outdated
final double? heightEm; | ||
final double? marginRightEm; | ||
final double? topEm; | ||
final double? verticalAlignEm; |
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.
I think it's a good idea to separate them into groups; to add on this, how about having a short comment before each group explaining how we group them together (like we do with design variables)?
lib/widgets/content.dart
Outdated
} | ||
|
||
return Container( | ||
margin: styles.marginRightEm != null && !styles.marginRightEm!.isNegative |
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.
Hmm, !styles.marginRightEm!.isNegative
seems a bit contrived. I think isNegative
negated is not as clear as >= 0
. However, do we need the margin when marginRightEm
is 0?
styles: inlineStyles != null | ||
? styles.merge(inlineStyles) | ||
: styles, |
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.
Having tests for this (the case when there are both inline styles and others) might be useful. Not sure how common that is.
lib/widgets/content.dart
Outdated
child: RichText(text: TextSpan( | ||
children: List.unmodifiable(row.nodes.map((e) { | ||
return WidgetSpan( | ||
alignment: PlaceholderAlignment.baseline, | ||
baseline: TextBaseline.alphabetic, | ||
child: switch (e) { | ||
KatexSpanNode() => _KatexSpan(e), | ||
KatexVlistNode() => _KatexVlist(e), | ||
KatexNegativeMarginNode() => _KatexNegativeMargin(e), | ||
}); | ||
}))))); |
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.
looks like this can be replaced with _KatexNodeList(nodes: row.nodes)
lib/widgets/content.dart
Outdated
child: Text.rich(TextSpan( | ||
children: List.unmodifiable(node.nodes.map((e) { | ||
return WidgetSpan( | ||
alignment: PlaceholderAlignment.baseline, | ||
baseline: TextBaseline.alphabetic, | ||
child: switch (e) { | ||
KatexSpanNode() => _KatexSpan(e), | ||
KatexVlistNode() => _KatexVlist(e), | ||
KatexNegativeMarginNode() => _KatexNegativeMargin(e), | ||
}); | ||
}))))); |
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.
_KatexNodeList
also seems helpful here.
lib/widgets/content.dart
Outdated
} | ||
|
||
class _KatexNegativeMargin extends StatelessWidget { | ||
const _KatexNegativeMargin(this.node); |
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.
Because the name implies that the margin should be negative, maybe we should also add assertions to ensure that that is true.
lib/model/katex.dart
Outdated
List<KatexNode> _parseChildSpans(dom.Element element) { | ||
return List.unmodifiable(element.nodes.map((node) { | ||
if (node case dom.Element(localName: 'span')) { | ||
return _parseSpan(node); | ||
} else { | ||
var resultSpans = <KatexNode>[]; | ||
for (final node in element.nodes.reversed) { | ||
if (node is! dom.Element || node.localName != 'span') { | ||
throw KatexHtmlParseError(); | ||
} | ||
})); | ||
|
||
final span = _parseSpan(node); | ||
resultSpans.add(span); | ||
|
||
if (span is KatexSpanNode) { | ||
final marginRightEm = span.styles.marginRightEm; | ||
if (marginRightEm != null && marginRightEm.isNegative) { | ||
final previousSpansReversed = | ||
resultSpans.reversed.toList(growable: false); | ||
resultSpans = []; | ||
resultSpans.add(KatexNegativeMarginNode( | ||
marginRightEm: marginRightEm, | ||
nodes: previousSpansReversed)); | ||
} | ||
} | ||
} | ||
|
||
return resultSpans.reversed.toList(growable: false); | ||
} |
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.
Neat solution! A bit tricky to understand. I think we can use QueueList
(and with .addFirst
) to avoid the hassle of reversing and unreversing the list.
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.
It will be helpful to have tests for this.
lib/model/katex.dart
Outdated
final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!; | ||
final pstrutHeight = pstrutStyles.heightEm ?? 0; | ||
|
||
// TODO handle negative right-margin inline style on row nodes. |
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.
Do we consider this complete after the final commit ("content: Support negative right-margin on KaTeX spans")? I'm not too sure if something else is left to be done here.
This seems to contradict with the earlier assumption that vlist elements contain only the height
style.
376cfe7
to
d879646
Compare
d4202c7
to
49d724d
Compare
This will prevent string interpolation being evaluated during release build. Especially useful in later commit where it becomes more expensive.
c917d14
to
ec14306
Compare
Thanks for the review @PIG208! Pushed a new revision, PTAL. I removed the commit for supporting negative margins from this PR, and will create a separate PR that introduces it again. |
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.
Thanks! Left some comments.
While testing this, I ran into some issues with text scaling (the relevant commit is "content: Scale inline KaTeX content based on the surrounding text"); posted screenshots in one of the comments below.
Otherwise, this works pretty well!
lib/model/katex.dart
Outdated
? List<String>.unmodifiable(element.className.split(' ')) | ||
: const <String>[]; |
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.
nit: indentation
(ContentExample.mathBlockKatexVertical5, [ | ||
('a', Offset(0.0, 4.16), Size(10.88, 25.0)), | ||
('b', Offset(10.88, -0.65), Size(8.82, 25.0)), | ||
('c', Offset(19.70, 4.16), Size(8.90, 25.0)), |
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.
Interesting setup! This resembles the golden tests that use images instead of offset/size values to verify rendering results.
test/widgets/content_test.dart
Outdated
Future.value(fontFile.readAsBytesSync().buffer.asByteData()); | ||
await (FontLoader(fontName)..addFont(bytes)).load(); | ||
} | ||
} |
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.
nit: newline at the end
@@ -1406,3 +1475,21 @@ void main() { | |||
}); | |||
}); | |||
} | |||
|
|||
Future<void> _loadKatexFonts() async { |
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.
We have a similar thing in emoji_reactoins_test.dart
:
Future<void> prepare() async {
addTearDown(testBinding.reset);
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
await store.addUser(eg.selfUser);
// TODO do this more centrally, or put in reusable helper
final Future<ByteData> font = rootBundle.load('assets/Source_Sans_3/SourceSans3VF-Upright.otf');
final fontLoader = FontLoader('Source Sans 3')..addFont(font);
await fontLoader.load();
}
Sharing the helper is not necessary for this PR, but it looks like something we can extract to reuse in the future.
lib/model/katex.dart
Outdated
break; | ||
|
||
// TODO handle skipped class declarations between .mspace and | ||
// .thinbox . |
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.
nit: should this be .msupsub?
@@ -850,8 +860,7 @@ class _Katex extends StatelessWidget { | |||
return Directionality( | |||
textDirection: TextDirection.ltr, | |||
child: DefaultTextStyle( | |||
style: kBaseKatexTextStyle.copyWith( | |||
color: ContentTheme.of(context).textStylePlainParagraph.color), | |||
style: mkBaseKatexTextStyle(textStyle), |
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.
@@ -850,8 +860,7 @@ class _Katex extends StatelessWidget { | |||
return Directionality( | |||
textDirection: TextDirection.ltr, | |||
child: DefaultTextStyle( | |||
style: kBaseKatexTextStyle.copyWith( | |||
color: ContentTheme.of(context).textStylePlainParagraph.color), | |||
style: mkBaseKatexTextStyle(textStyle), |
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.
lib/model/katex.dart
Outdated
rows.add(KatexVlistRowNode( | ||
verticalOffsetEm: topEm + pstrutHeight, | ||
node: KatexSpanNode( | ||
styles: styles, | ||
text: null, | ||
nodes: _parseChildSpans(otherSpans)))); |
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.
nit: indentation
lib/widgets/content.dart
Outdated
return widget; | ||
|
||
if (styles.verticalAlignEm != null && styles.heightEm != null) { | ||
assert(widget == defaultWidget); |
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.
I think this assertion is based on the broader assumption that some inline styles never co-occur to vertical-align
or height
. This seems like something we can comment on.
This applies the correct font scaling if the KaTeX content is inside a header.
And rename previous type to KatexSpanNode, also while making it a subtype of KatexNode.
In KaTeX HTML it is used to set the baseline of the content in a span, so handle it separately here.
And inline the behaviour for `inline: false` in MathBlock widget.
Implement handling most common types of `vlist` spans.
ec14306
to
0e00af6
Compare
|
||
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 { |
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.
+ // TODO: Re-enable this test after adding support for parsing
+ // `vertical-align` in inline styles. Currently it fails
+ // because `strut` span has `vertical-align`.
+ //
+ // testWidgets('displays KaTeX content with different delimiter sizing', (tester) async {
Instead of commenting out the whole test (which makes the diff big), pass skip: true
. For examples, see git grep -A2 skip: test/
.
The other reason skip:
is better than commenting out is that it makes the test case still exist in the test framework and get counted; the total number of skipped tests is printed after the number of passed and failed tests. That's helpful for occasionally sweeping to confirm we don't have tests accidentally left skipped that might point to live bugs.
styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 | ||
text: '0', | ||
nodes: null), | ||
]), | ||
]), | ||
]); | ||
|
||
static final mathBlockKatexNestedSizing = ContentExample( | ||
static const mathBlockKatexNestedSizing = ContentExample( |
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.
+ // TODO: Re-enable this test after adding support for parsing
+ // `vertical-align` in inline styles. Currently it fails
+ // because `strut` span has `vertical-align`.
+ //
+ // static const mathBlockKatexDelimSizing = ContentExample(
Similarly, use skip: true
instead of commenting.
For this test, that requires a bit of setup to make this file's helpers and the "all content examples are tested" check handle the skip:
argument. I've just pushed a small added commit at the end of this PR branch to do that:
f9a79d8 content test [nfc]: Enable skips in testParseExample and testParse
So please rebase that to earlier in the branch as needed, and then write:
testParseExample(ContentExample.mathBlockKatexNestedSizing, skip: true);
Stacked on top of: #1408
Related: #46