Skip to content

KaTeX (3/n): Support negative margins for spans #1559

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 7 commits into
base: main
Choose a base branch
from

Conversation

rajveermalviya
Copy link
Member

@rajveermalviya rajveermalviya commented Jun 10, 2025

Stacked on top of #1698.

Related: #46

@gnprice
Copy link
Member

gnprice commented Jun 10, 2025

@rajveermalviya Copying here for visibility what you told me on our call today: the reason this is a draft (not ready to merge) is that you're still working on the widget test. Apart from that, you consider it ready to review.

@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Jun 10, 2025
@rajveermalviya rajveermalviya marked this pull request as ready for review June 11, 2025 17:34
@rajveermalviya rajveermalviya force-pushed the pr-tex-content-3 branch 2 times, most recently from 843207d to d1f5d5c Compare June 11, 2025 19:19
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Here's small comments from a partial skim.

Comment on lines 39 to 41
// Like [RenderPadding] but supports negative values.
class RenderNegativePadding extends RenderShiftedBox {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave a TODO tracking our intention to get a version of this upstream:

Suggested change
// Like [RenderPadding] but supports negative values.
class RenderNegativePadding extends RenderShiftedBox {
// Like [RenderPadding] but supports negative values.
// TODO(upstream): give Padding an option to accept negative padding (at cost of hit-testing not working)
class RenderNegativePadding extends RenderShiftedBox {

Comment on lines 200 to 201
extension on EdgeInsetsGeometry {
bool get isNegative => !isNonNegative;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is clearer (and hardly much longer) if just inlined.

In particular there's some ambiguity in that "is negative" sounds like it might require all the sides to be negative. So being more transparent, with one fewer layer of indirection, helps mitigate that.

@rajveermalviya
Copy link
Member Author

Thanks for the initial comments @gnprice. Revision pushed.

@gnprice gnprice added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels Jul 4, 2025
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I haven't yet re-reviewed this, as work on #1452 is still ongoing.

Would you rebase this atop the current #1452? That's helpful for me to be able to continue shipping them both in releases.

One comment below from when I tried a version of that rebase just now.

Comment on lines 961 to 962
final marginRight = switch (styles.marginRightEm) {
double marginRightEm when marginRightEm >= 0 => marginRightEm * em,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intended to be possible to get to this point and have a negative marginRightEm?

I believe not — and it looks like if it did happen, this code would just ignore it, which seems like clearly a bug.

So instead of this "when" condition, let's have an assertion:

    assert((styles.marginLeftEm ?? 0) >= 0);

Similarly for right margin.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated so that parser makes sure not to emit a node with negative margin styles, as it already emits a separate KatexNegativeMarginNode for handling that on the widget side. And widget side retains the assert(margin.isNonNegative);.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks — that definitely sounds cleaner.

@rajveermalviya rajveermalviya force-pushed the pr-tex-content-3 branch 2 times, most recently from bd56117 to dfabd4d Compare July 9, 2025 14:58
@rajveermalviya
Copy link
Member Author

Thanks for the review @gnprice! Pushed an update, PTAL.

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This logic looks good. Small comments below on the tests.

(I also have some ideas on how to refactor and simplify the logic — but for the same reasons as at #1698 (comment), because this has already shipped in recent releases, I'll want to save that kind of change for after this PR has been merged.)

Comment on lines 1363 to 1216
MathBlockNode(
texSource: '\\KaTeX',
nodes: [
KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155),
KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
KatexSpanNode(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Let's make the boring parts of this more compact, and also involve less indentation. Like so:

Suggested change
MathBlockNode(
texSource: '\\KaTeX',
nodes: [
KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155),
KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
KatexSpanNode(
MathBlockNode(texSource: '\\KaTeX', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155),
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
KatexSpanNode(

and similarly below.

Comment on lines 1327 to 1196
static const mathBlockKatexNegativeMargins = ContentExample(
'math block katex negative margins',
'```math\n\\KaTeX\n```',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is fine to have. But let's also have a more minimal, focused example that's just about negative margins. So:

Suggested change
static const mathBlockKatexNegativeMargins = ContentExample(
'math block katex negative margins',
'```math\n\\KaTeX\n```',
static const mathBlockKatexLogo = ContentExample(
'math block KaTeX logo',
'```math\n\\KaTeX\n```',

and then for a minimal example, try $$ 1 \! 2 $$. The minimal example can get a name like "katex negative margins".

For anyone who's investigating this in the future — or who makes a change, the change causes a regression here, and they need to investigate the regression to figure out the right fix — that minimal, focused example should be helpful as the place to look.

Turns out that anything under KatexVlistRowNode wasn't being
tested by content tests, fix that by implementing this method.

Fortunately there were no fixes needed in the tests.
Also add a comment explaining the reason of ignoring the `height`
inline styles value.
Negative margin spans on web render to the offset being applied
to the specific span and all the adjacent spans, so mimic the
same behaviour here.
…ontent

Skipped currently, because the parser incorrectly doesn't filter
the negative margin styles on the vlist row span after generating a
`KatexNegativeMarginNode` to specifically handle negative margins.
This fixes a bug where if negative margin on a vlist row is present
the widget side code would hit an assert. Displaying the error
(red screen) in debug mode, but in release mode the negative padding
would be applied twice, once by `_KatexNegativeMargin` and another by
`Padding` widget.
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision! Comments below, mainly on the two new commits.

Comment on lines +1305 to +1311
MathBlockNode(
texSource: 'X_n',
nodes: [
KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same story as #1559 (comment) and #1698 (comment)

Comment on lines +1168 to +1171
static const mathBlockKatexNegativeMargin = ContentExample(
'math block, KaTeX negative margin',
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2223563
'```math\n1 \\! 2\n```',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be sure to include this new, minimal example (from #1559 (comment)) ­in the widget tests too, so that it can fully do its work.

Comment on lines +349 to +354
styles: styles.filter(topEm: false, marginLeftEm: false),
text: null,
nodes: _parseChildSpans(otherSpans))])]);
} else {
innerSpanNode = KatexSpanNode(
styles: styles.filter(topEm: false),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point styles has already had topEm filtered out, right? So I believe that filtering is redundant.

testParseExample(ContentExample.mathBlockKatexRaisebox);
testParseExample(ContentExample.mathBlockKatexNegativeMargin);
testParseExample(ContentExample.mathBlockKatexLogo);
testParseExample(ContentExample.mathBlockNegativeMarginsOnVlistRow);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the last two commits (the commits newly added in yesterday's revision, mentioned at #1698 (comment)):
f845c6d content test: Add test for negative margins on a vlist row in KaTeX content
0ec2cc5 content: Filter negative margin styles if present on a vlist row

+  testParseExample(ContentExample.mathBlockNegativeMarginsOnVlistRow,
+    // TODO fix parser to filter negative margins styles from
+    //   vlist row span styles.
+    skip: true);
-  testParseExample(ContentExample.mathBlockNegativeMarginsOnVlistRow,
-    // TODO fix parser to filter negative margins styles from
-    //   vlist row span styles.
-    skip: true);
+  testParseExample(ContentExample.mathBlockNegativeMarginsOnVlistRow);

This version doesn't really give any added information compared to just squashing the two commits together. The normal arrangement is to have the tests appear in the same commit as the changes they're testing, because then the tests can be read in the light of those changes and vice versa, which can help for understanding both of them.

There's a variation of this split which would be useful, though:

  • The first commit adds the test, but doesn't skip it; instead, the expectation reflects the old wrong behavior, and a TODO comment points out where it's wrong.
  • Then the second commit makes the fix, and updates the test to expect the right behavior.

If the updates to the test are short and readable — simpler to read than the whole test itself — then this can be a useful way of highlighting exactly which aspect of the test expectation is the thing getting fixed by the change.

If I'm understanding this change correctly, then this would be a good example of that: the bit that's wrong would be a single line with a marginLeftEm.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relatedly:

content: Filter negative margin styles if present on a vlist row

This fixes a bug where if negative margin on a vlist row is present
the widget side code would hit an assert. Displaying the error
(red screen) in debug mode, but in release mode the negative padding
would be applied twice, once by `_KatexNegativeMargin` and another by
`Padding` widget.

Since one of the symptoms is that the widget code hits an assert, let's have a widget test that would exercise that assert. :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants