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

Conversation

rajveermalviya
Copy link
Member

@rajveermalviya rajveermalviya commented Jun 24, 2025

Flutter Web
image image

Survey results

-  …, 31380 of them were KaTeX containing messages and 3817 of those failed.
-  There were 1075 math block nodes out of which 614 failed.
-  There were 164014 math inline nodes out of which 6470 failed.
-  Because of hard fail: unsupported inline CSS property: top:
-    1546 messages failed.
-  Because of unsupported inline css property: position:
-    1356 messages failed.
+  …, 31380 of them were KaTeX containing messages and 2801 of those failed.
+  There were 1075 math block nodes out of which 538 failed.
+  There were 164014 math inline nodes out of which 4710 failed.
   Because of hard fail: unexpected CSS class for vlist inner span: svg-align:
-    644 messages failed.
-  Because of unsupported css class: accent:
-    609 messages failed.
+    648 messages failed.
   Because of unsupported inline css property: border-bottom-width:
-    605 messages failed.
+    631 messages failed.
+  Because of unsupported css class: accent:
+    614 messages failed.
   Because of unsupported css class: op-limits:
-    517 messages failed.
+    521 messages failed.
   Because of unsupported css class: fix:
-    473 messages failed.
+    488 messages failed.
   Because of unsupported css class: inner:
-    473 messages failed.
-  Because of unsupported css class: accent-body:
-    455 messages failed.
-  Because of unsupported inline css property: left:
-    455 messages failed.
+    488 messages failed.
   Because of unsupported css class: rlap:
-    450 messages failed.
+    465 messages failed.
   Because of unsupported css class: thinbox:
-    448 messages failed.
+    463 messages failed.
   Because of unsupported css class: vbox:
-    448 messages failed.
+    463 messages failed.
   Because of unsupported css class: nulldelimiter:
-    437 messages failed.
+    461 messages failed.
+  Because of unsupported css class: accent-body:
+    460 messages failed.
+  Because of unsupported inline css property: left:
+    460 messages failed.
   Because of unsupported css class: mfrac:
-    410 messages failed.
+    433 messages failed.
   Because of unsupported css class: frac-line:
-    389 messages failed.
+    416 messages failed.
   Because of unsupported css class: x-arrow:
-    389 messages failed.
+    392 messages failed.
   Because of unsupported css class: x-arrow-pad:
-    380 messages failed.
+    383 messages failed.
+  Because of hard fail: unsupported inline CSS property "top", when "position: null":
+    235 messages failed.
   Because of unsupported css class: delimcenter:
-    226 messages failed.
+    235 messages failed.
   Because of unsupported css class: mtable:
     209 messages failed.
@@ -53,9 +51,9 @@
     86 messages failed.
   Because of unsupported css class: col-align-r:
-    80 messages failed.
+    83 messages failed.
+  Because of hard fail: unsupported html node:
+    78 messages failed.
   Because of unsupported css class: arraycolsep:
     77 messages failed.
-  Because of hard fail: unsupported html node:
-    75 messages failed.
   Because of unsupported css class: sqrt:
     75 messages failed.
@@ -74,10 +72,10 @@
   Because of hard fail: unsupported html node: svg:
     28 messages failed.
+  Because of unsupported css class: newline:
+    28 messages failed.
   Because of unsupported css class: overlay:
     28 messages failed.
   Because of unsupported css class: llap:
     26 messages failed.
-  Because of unsupported css class: newline:
-    25 messages failed.
   Because of unsupported css class: cjk_fallback:
     20 messages failed.
@@ -127,3 +125,3 @@
     1 messages failed.                                                                                                                                                       

Fixes: #1671

@rajveermalviya rajveermalviya added the maintainer review PR ready for review by Zulip maintainers label Jun 24, 2025
@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
@rajveermalviya rajveermalviya force-pushed the pr-tex-content-5 branch 2 times, most recently from 1e20472 to e545476 Compare July 17, 2025 20:26
@rajveermalviya rajveermalviya changed the title KaTeX(5/n): Handle 'position' and ensure required inline styles for big operators KaTeX(5/n): Handle 'position' & 'top' property in KaTeX span inline style Jul 17, 2025
@gnprice
Copy link
Member

gnprice commented Jul 22, 2025

Thanks for building this! See my comments on #1670 for a few things I'd like to see on all the open KaTeX PRs: #1670 (review), #1670 (comment) .

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! Generally this all looks good — various small comments below.

Would you also post screenshots of an example where there's a bit more going on? With these screenshots, if we just ignored top then it'd be hard to tell from the screenshots that anything was wrong, because there's nothing next to the \int that it's getting aligned with. So even \int dx would help a lot.


It also looks like the value of top in this example is quite small:

top:-0.0011em;

And in fact, that's small enough that it defeats the widget test in this PR. If I delete the logic in _KatexSpan.build that consumes styles.topEm, then the tests all still pass, because 1.1 milli-ems is a very small fraction of a pixel and is below the epsilon the test (appropriately) uses.

Can you find an example where the effect is bigger? Try other big operators, like \sum; or limits above or below the operators, like \int_0^1.

@@ -765,6 +766,34 @@ class _KatexParser {
_hasError = true;
return null;
}

/// Remove the given property from the given style map,
/// and parse as a literal value of CSS position attribute.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// and parse as a literal value of CSS position attribute.
/// and parse as a CSS position value.

Here's the relevant spec and MDN doc:
https://drafts.csswg.org/css-position/#position-property
https://developer.mozilla.org/en-US/docs/Web/CSS/position

Scanning those:

  • I don't think "literal" adds anything to the meaning here.
  • The CSS terminology is that position and things like it are "properties", not "attributes".

Comment on lines 644 to 647
// Currently, we expect `top` to be inside a vlist (which we handle it
// separately above), and if it's along with a `position: relative`.
if (styles.topEm != null && styles.position != KatexSpanPosition.relative) {
throw _KatexHtmlParseError('unsupported inline CSS property "top", when "position: ${styles.position}"');
Copy link
Member

Choose a reason for hiding this comment

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

This comment doesn't parse for me.

I think the vlist part is beside the point now: we do expect a top property, but only when accompanied by position: relative.

And just saying that isn't useful as a comment, because it repeats what the code in the condition already says. The thing that is useful to say in a comment, though, is why. So:

Suggested change
// Currently, we expect `top` to be inside a vlist (which we handle it
// separately above), and if it's along with a `position: relative`.
if (styles.topEm != null && styles.position != KatexSpanPosition.relative) {
throw _KatexHtmlParseError('unsupported inline CSS property "top", when "position: ${styles.position}"');
if (styles.topEm != null && styles.position != KatexSpanPosition.relative) {
// The meaning of `top` would be different without `position: relative`.
throw _KatexHtmlParseError(
'unsupported inline CSS property "top" given "position: ${styles.position}"');

I.e.,

The meaning of top would be different without position: relative.

As MDN says:
https://developer.mozilla.org/en-US/docs/Web/CSS/top

The effect of top depends on how the element is positioned (i.e., the value of the position property):
[… and then a list of 4 quite different meanings for the 5 possible values of position.]

@@ -97,10 +97,6 @@ class _KatexSpan extends StatelessWidget {

final styles = node.styles;

// Currently, we expect `top` to be only present with the
// vlist inner row span, and parser handles that explicitly.
assert(styles.topEm == null);
Copy link
Member

Choose a reason for hiding this comment

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

This assert still seems quite relevant — but only in the case where position is null.

Comment on lines 184 to 186
};

if (offset != null) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: these are closely related; keep joined as one stanza

Suggested change
};
if (offset != null) {
};
if (offset != null) {

Comment on lines 181 to 188
final offset = switch (styles.topEm) {
final topEm? => Offset(0, topEm * em),
null => null,
};

if (offset != null) {
widget = Transform.translate(
offset: offset,
Copy link
Member

Choose a reason for hiding this comment

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

Though actually this seems like it can be tightened further than that: check if styles.topEm is non-null, and if so then do the Transform.translate. It's fine to have a ! null-assertion inside the body of that conditional — it's easy for the reader to see why the assertion will always hold.

@gnprice
Copy link
Member

gnprice commented Aug 14, 2025

-  …, 31380 of them were KaTeX containing messages and 3817 of those failed.
-  There were 1075 math block nodes out of which 614 failed.
-  There were 164014 math inline nodes out of which 6470 failed.
-  Because of hard fail: unsupported inline CSS property: top:
-    1546 messages failed.
-  Because of unsupported inline css property: position:
-    1356 messages failed.
+  …, 31380 of them were KaTeX containing messages and 2801 of those failed.

A very nice improvement! Will be glad to have this in.

@rajveermalviya rajveermalviya force-pushed the pr-tex-content-5 branch 3 times, most recently from f2775c9 to b5b7f80 Compare August 15, 2025 11:59
@rajveermalviya
Copy link
Member Author

rajveermalviya commented Aug 15, 2025

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

I wasn't able to find an example with big operators having a larger top offset, but I did find another case where position: relative; top: … inline style is present i.e for /colonequals and it also has a larger top offset. So, added a test for that.

@rajveermalviya rajveermalviya requested a review from gnprice August 15, 2025 12:00
@rajveermalviya rajveermalviya force-pushed the pr-tex-content-5 branch 2 times, most recently from 55b0136 to 24fb254 Compare August 15, 2025 12: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! Small comments below.

In the new screenshots, there's one thing that puzzles me, though it's probably independent of this PR: in the \colonequals message the (horizontal) spacing between the colon and the equals-sign seems to be noticeably wider in Flutter than on web:
image
image

As a follow-up, can you try to diagnose why that is? (I took a quick look at the various CSS class names in katex.scss and didn't find any obvious candidates.)

static final bigOperators = KatexExample.block(
r'big operators: \int',
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2240766
r'\int dx',
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I wasn't clear enough — for making this example more complex, I really did mean just the screenshots 🙂. In the test code, it's nice to keep it as simple as possible. (And this test doesn't really gain anything from having the additional elements.)

Comment on lines 76 to 80
(KatexExample.bigOperators, skip: false, [
('∫', Offset(0.00, 12.02), Size(11.43, 25.00)),
('d', Offset(24.00, 12.03), Size(10.69, 25.00)),
('x', Offset(34.70, 12.03), Size(11.76, 25.00)),
]),
Copy link
Member

Choose a reason for hiding this comment

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

Maybe even leave this one out of this widget test, since the test isn't sensitive enough to pick up whether the code is working.

Allowing support for handling KaTeX HTML for big operators.

Fixes: zulip#1671
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.

Handle big operators in KaTeX
2 participants