Skip to content

KaTeX (1/n): Initial support for displaying basic KaTeX content #1408

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

Merged
merged 7 commits into from
Apr 22, 2025

Conversation

rajveermalviya
Copy link
Member

@rajveermalviya rajveermalviya commented Mar 13, 2025

This initial implementation include:

  • Displaying the characters in their corresponding
    custom text styles (fonts, font weight, font style).

  • Character and symbol sizing.

  • And some subset of inline styles.

This results in support for displaying some simple KaTeX functions.

Related: #46

@rajveermalviya rajveermalviya force-pushed the pr-tex-content-1 branch 2 times, most recently from 476f6aa to 5164619 Compare March 13, 2025 20:43
@rajveermalviya rajveermalviya force-pushed the pr-tex-content-1 branch 4 times, most recently from c2a9c4a to babe5c8 Compare April 1, 2025 16:19
@rajveermalviya rajveermalviya changed the title content: Add initial support for displaying some KaTeX content KaTeX (1/n): Initial support for displaying basic KaTeX content Apr 1, 2025
@PIG208 PIG208 requested review from gnprice and PIG208 April 1, 2025 22:17
@PIG208 PIG208 self-assigned this Apr 1, 2025
@PIG208 PIG208 added the maintainer review PR ready for review by Zulip maintainers label Apr 1, 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.

Exciting!

Generally this structure looks good. Here's a high-level review — so just a handful of comments mostly about things that I think will help make the changes clearer to understand.

Then once these aspects look good (should be quick, I think), we'll do maintainer review as usual.

Comment on lines 82 to 85
check(globalSettings).getBool(BoolGlobalSetting.renderKatex)
.isFalse();
assert(!BoolGlobalSetting.placeholderIgnore.default_);
assert(!BoolGlobalSetting.renderKatex.default_);
Copy link
Member

Choose a reason for hiding this comment

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

No need to add these feature flags to the tests (or to remove them from the tests when later taking them out) — adding them can be purely a matter of the one line adding the enum value, plus its dartdoc (and blank line above that).

That also means that adding the feature flag can be squashed into the first commit that uses it.

final spanClass = spanClasses[index];
switch (spanClass) {
case 'textbf':
// .textbf { font-weight: bold; }
Copy link
Member

Choose a reason for hiding this comment

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

These are quoting from KaTeX's CSS (or rather the source file for its CSS), and that's an important reference for understanding this code. So let's include a link to that in a comment.

Comment on lines 376 to 377
final String? text;
final List<KatexSpanNode> nodes;
Copy link
Member

Choose a reason for hiding this comment

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

It looks like these two are mutually exclusive: there's always at most one present, and in fact exactly one.

Let's make that invariant explicit in this class, by at least an assertion at the constructor.

It'd be good to also add some dartdoc on these two fields to say how they relate to each other and to the parent node.


final String texSource;
final List<KatexSpanNode>? nodes;
Copy link
Member

Choose a reason for hiding this comment

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

It looks like the next PR #1452 changes this to be a list of KatexNode, a new common base class for KatexSpanNode and some others.

It's probably cleanest if this says KatexNode from the start, then. That makes the conceptual relationship to this parent node (MathBlockNode) somewhat clearer — it's not that a math block necessarily contains only "KaTeX span nodes" as direct children, it can contain "KaTeX nodes" in general, and it's just that at this stage the spans are the only kind of KaTeX node that are yet implemented.

Comment on lines 276 to 449
default:
// TODO handle more CSS properties
assert(debugLog('Unsupported CSS property: $property of type ${expression.runtimeType}'));
}
Copy link
Member

Choose a reason for hiding this comment

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

It seems like this code has three levels of how well it supports a given KaTeX blob:

  • The HTML doesn't follow any structure we've yet anticipated. The parser in this file throws KatexHtmlParseError, producing nodes of null.
  • The HTML contains something we know we don't yet support, but there's no exception; nodes ends up with some kind of parse result.
  • The HTML consists only of constructs we believe are fully supported — meaning we believe the current version of the code will produce widgets that make the intended math show up exactly the same as with KaTeX on the web.

This default case is an example of that middle state.

The middle state is very useful for development: you want to let the parser do its thing as best it currently can, and see how the resulting widgets look, while you work on supporting that case.

But I'd also really like to be able to run the parser in a mode where it will only accept constructs we believe are fully supported. That way we can run it on a corpus of public messages collected by the scripts in tools/content/, and get a survey of what remains to be implemented. That mode could also be useful for turning KaTeX support on by default, but only for expressions we believe will show up exactly right, and for other expressions falling back to the raw TeX like we do in main today.

I think the most effective way to draw that boundary will be to do so starting in this first PR, so that when we include a given construct on the "fully supported" side of it we do so at the same time as we're implementing support for that construct and thinking about it in detail.

Maybe have two experimental flags?

  • One flag enables showing KaTeX at all. Things that are fully supported get rendered; things that are incompletely supported, just like those not supported, get the fallback.
  • A second flag enables showing KaTeX even where it's incomplete — so the behavior this revision has when renderKatex is true. (When the first flag is false, this one would just do nothing.)

Then when support is mostly complete, we might turn the first flag into always-true, while keeping the second flag as experimental (and default-false).

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, added another flag (forceRenderKatex) to control if the KaTeX content should be rendered even if there were some errors encountered while parsing the HTML. Currently, the "ignorable errors" happen either when the parser encounters an unknown CSS class or an unknown CSS property in the inline styles.

@rajveermalviya
Copy link
Member Author

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

@rajveermalviya rajveermalviya requested a review from gnprice April 2, 2025 21:43
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!

This resolves the high-level feedback I had — @PIG208, over to you for maintainer review.

Comment on lines 165 to 270
}

// Work around the duplicated case statement with a new switch block,
// to preserve the same order and to keep the cases mirroring the CSS
// definitions in katex.scss .
switch (spanClass) {
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 had wondered about these two separate switch statements 🙂 — this comment is helpful.

Copy link
Member

@PIG208 PIG208 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 working on this! I just went through the entire PR and played with this a bit on my device. It looks pretty good!

With regards to testing, I'm not sure what the current plan is. It doesn't seem quite useful to have tests structured basically a duplicate of all the KaTeX classes we support. Maybe it would be better to test with examples crawled online?

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copy link
Member

Choose a reason for hiding this comment

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

fonts: Add KaTeX custom fonts
    
These fonts will be used in later commits to show KaTeX
content.

For future reference, perhaps we can also mention where we found these fonts in the commit message with a link.

testWidgets('displays TeX source; experimental flag default', (tester) async {
final globalSettings = testBinding.globalStore.settings;
await globalSettings.setBool(BoolGlobalSetting.renderKatex, null);
check(globalSettings.getBool(BoolGlobalSetting.renderKatex)).isFalse();
Copy link
Member

Choose a reason for hiding this comment

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

nit: we can use check(globalSettings).getBool here

@@ -864,7 +906,10 @@ class GlobalTimeNode extends InlineContentNode {

////////////////////////////////////////////////////////////////

String? _parseMath(dom.Element element, {required bool block}) {
({List<KatexNode>? spans, bool debugHasError, String texSource})? _parseMath(
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps it would be cleaner to make a class for the return value of this, and make factories for classes are constructed from the result?

result.add(MathBlockNode(
texSource: texSource,
texSource: parsed.texSource,
nodes: parsed.spans,
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 intentionally that we leave out debugHasError for this?

//
// Each case in the switch blocks below is a separate CSS class definition
// in the same order as in katex.scss :
// https://github.com/KaTeX/KaTeX/blob/2fe1941b7e6c0603680ef6edd799bd8a8b46871a/src/styles/katex.scss
Copy link
Member

Choose a reason for hiding this comment

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

nit: use just the first 8 characters of the commit hash to shorten the line

Copy link
Member

Choose a reason for hiding this comment

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

(FTR the length I normally use is 9 hex digits: https://github.com/zulip/zulip-mobile/blob/main/docs/style.md#mentioning-commits

Use 9 (or 10) hex digits to identify a commit. To help make this convenient, you can tell Git to routinely print 9-digit abbreviations for you by setting git config core.abbrev 9.

Rationale: The full 40 hex digits is a lot, and generally doesn't flow well in prose. On the other hand 7 hex digits, which is what GitHub shows in its UI whenever it doesn't show 40, is short enough that there's a material risk of collisions: in zulip.git there are already a handful of commits that are ambiguous when identified by just 7 digits, and in a very large project like the Linux kernel such collisions can become routine.

A 9-digit abbreviation is still short enough to fit well in running text; and it's long enough that it's extremely unlikely any given such abbreviation will ever be ambiguous.

For KaTeX 8 is fine and probably 7 would be fine, because it's a smaller repo; but I just use 9 everywhere to simplify.)


KatexSpanStyles? _parseSpanInlineStyles(dom.Element element) {
if (element.attributes case {'style': final styleStr}) {
final stylesheet = css_parser.parse('*{$styleStr}');
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if we need to sanitize styleStr. I guess not: the html package might have done that, and in the worst case this will just fail to parse.

Regardless, it should be helpful to explain why we want to wrap styleStr here.

if (expression is css_visitor.EmTerm && expression.value is num) {
return (expression.value as num).toDouble();
}
return null;
Copy link
Member

Choose a reason for hiding this comment

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

Is this case, i.e. non-em values, something that we eventually want to add support for?

Copy link
Member Author

Choose a reason for hiding this comment

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

We only support em values currently because, they are the only ones observed in the Katex HTML currently, but if we encounter any other than this will return null and we handle that later in switch block. Error-ing if we get an unexpected type (non-em).

switch (property) {
case 'margin-left':
marginLeftEm = _getEm(expression);
if (marginLeftEm != null) continue;
Copy link
Member

Choose a reason for hiding this comment

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

nit: this continue and the following ones look like no-ops


default:
// TODO handle more CSS properties
assert(debugLog('Unsupported CSS property: $property of type ${expression.runtimeType}'));
Copy link
Member

Choose a reason for hiding this comment

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

Should we also use _logError here?

}

class KatexSpanStyles {
double? heightEm;
Copy link
Member

Choose a reason for hiding this comment

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

Looks like heightEm was introduced here but not used/handled. Is this intended for the next PR?

@gnprice
Copy link
Member

gnprice commented Apr 3, 2025

With regards with testing

Yeah, good question. It looks like the current version of the PR doesn't add tests for most of the functionality. Let's at a minimum have tests that cover the interesting logic like for sizing and delimsizing.

It doesn't seem quite useful to have tests structured basically a duplicate of all the KaTeX classes we support. Maybe it would be better to test with examples crawled online?

I think these are two complementary types of tests which are both useful:

  • In the test suite, we have examples that are simplified as much as possible — to make them clear to understand, and easy to update for refactorings when needed — while still being complex enough to exercise the desired logic.
  • Outside the test suite, but at times like when we're doing major development on this area, we systematically test a corpus of public real-world examples. When that finds things we'd previously missed, we take those examples and reduce them into new test cases to add to the test suite.

We'll definitely want to be doing the second kind once we're a couple of PRs in to this current effort and feel that we've covered the majority of usage — it'll help us prioritize which remaining areas to do next.

@rajveermalviya rajveermalviya force-pushed the pr-tex-content-1 branch 3 times, most recently from 5d39bef to 3764df5 Compare April 8, 2025 18:23
@rajveermalviya
Copy link
Member Author

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

@rajveermalviya rajveermalviya requested a review from PIG208 April 8, 2025 18:30
Copy link
Member

@PIG208 PIG208 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! I did another pass and left some small comments.

final String texSource;
}

MathParserResult? parseMath(dom.Element element, { required bool block }) {
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 have a dartdoc for this function, for things like the meaning of null as the return value and what is expected of element.

switch (property) {
case 'height':
heightEm = _getEm(expression);
if (heightEm != null) continue;
Copy link
Member

Choose a reason for hiding this comment

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

Similar to #1408 (comment), I'm not sure if we need continue's here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I should've replied to the above comment how I addressed it in the revision.

I had addressed that bug by moving the _logError call outside of the switch block. If _getEm returns null then the condition here will be false, in which case it would fall down to _logError where the error will be logged – reporting we got something other than em here.

Copy link
Member

Choose a reason for hiding this comment

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

Ah that makes sense. Thanks.

@@ -33,6 +34,8 @@ import 'model.dart';
/// * lib/model/content.dart, which implements of the content parser.
/// * tools/content/fetch_messages.dart, which produces the corpora.
void main() async {
TestZulipBinding.ensureInitialized();
Copy link
Member

Choose a reason for hiding this comment

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

Since this is a bit different from regular tests, it might be worth it to explain why we need this here.

Copy link
Member

Choose a reason for hiding this comment

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

nit: remove "to" from "retrieves the GlobalSettings to for the experimental flag".


/// The text or a single character this KaTeX span contains, generally
/// observed to be the leaf node in the KaTeX HTML tree.
/// It will be null if this span has child nodes.
Copy link
Member

Choose a reason for hiding this comment

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

This seems to be talking about a "span", do we expect all KatexNode's to each correspond to a _KatexSpan?

Comment on lines 852 to 854
fontSize: kBaseFontSize * 1.21,
fontFamily: 'KaTeX_Main',
height: 1.2),
Copy link
Member

Choose a reason for hiding this comment

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

1.21 and 1.2 seem quite mysterious. Let's perhaps create some const's for them or leave some comments explaining them.

@@ -805,7 +805,6 @@ class MathBlock extends StatelessWidget {
@override
Widget build(BuildContext context) {
final contentTheme = ContentTheme.of(context);

Copy link
Member

Choose a reason for hiding this comment

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

Looks like this belongs to another commit: 32aea8e

@rajveermalviya rajveermalviya force-pushed the pr-tex-content-1 branch 2 times, most recently from 52ff92c to 0a8c846 Compare April 10, 2025 14:04
@rajveermalviya
Copy link
Member Author

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

@PIG208
Copy link
Member

PIG208 commented Apr 10, 2025

Thanks for the revision! I just tried this out locally again. Marking this for Greg's review. Left a question on the font style.

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 @rajveermalviya for building this, and @PIG208 for the previous reviews!

Exciting to have this feature underway. Here's a review of the first 7/8 commits:
f7a4721 deps: Add csslib as a direct dependency
b4b38d7 fonts: Add KaTeX custom fonts
0106b95 content [nfc]: Alias package:intl import as intl
671fa12 content [nfc]: Use Dart patterns for parsing math content
1fca686 binding: Add getGlobalStoreSync method
7609852 content: Add KaTeX spans parser, initial rendering; w/o styles
c273b0a content: Support basic text styles for KaTeX content

so leaving one more for a later round:
0a8c846 content: Support parsing and handling inline styles for KaTeX content

(I see Zixuan also has one open comment just above.)

Comment on lines 86 to 91
/// Get the app's singleton [GlobalStore], null if not already loaded.
///
/// Where possible, use [GlobalStoreWidget.of] to get access to a [GlobalStore].
/// Use this method only in contexts like notifications where
/// a widget tree may not exist.
GlobalStore? getGlobalStoreSync();
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
/// Get the app's singleton [GlobalStore], null if not already loaded.
///
/// Where possible, use [GlobalStoreWidget.of] to get access to a [GlobalStore].
/// Use this method only in contexts like notifications where
/// a widget tree may not exist.
GlobalStore? getGlobalStoreSync();
/// Get the app's singleton [GlobalStore] if already loaded, else null.
///
/// Where possible, use [GlobalStoreWidget.of] to get access to a [GlobalStore].
/// Use this method only in contexts where getting access to a [BuildContext]
/// is inconvenient.
GlobalStore? getGlobalStoreSync();

Because this new method doesn't cause the global store to get loaded if it wasn't already, it's really most useful in contexts where a widget tree does presumably exist (because that will have caused the global store to get loaded).

Comment on lines 569 to 567
testWidgets('displays TeX source; experimental flag enabled', (tester) async {
final globalSettings = testBinding.globalStore.settings;
Copy link
Member

Choose a reason for hiding this comment

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

A test should always reset the binding state if it manipulates it:

Suggested change
testWidgets('displays TeX source; experimental flag enabled', (tester) async {
final globalSettings = testBinding.globalStore.settings;
testWidgets('displays TeX source; experimental flag enabled', (tester) async {
addTearDown(testBinding.reset);
final globalSettings = testBinding.globalStore.settings;

Comment on lines 560 to 561
testWidgets('displays TeX source; experimental flag default', (tester) async {
final globalSettings = testBinding.globalStore.settings;
await globalSettings.setBool(BoolGlobalSetting.renderKatex, null);
check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isFalse();

await prepareContent(tester, plainContent(ContentExample.mathBlock.html));
Copy link
Member

Choose a reason for hiding this comment

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

Then when the test cases that set the setting properly reset the state, I think these can be simplified:

Suggested change
testWidgets('displays TeX source; experimental flag default', (tester) async {
final globalSettings = testBinding.globalStore.settings;
await globalSettings.setBool(BoolGlobalSetting.renderKatex, null);
check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isFalse();
await prepareContent(tester, plainContent(ContentExample.mathBlock.html));
testWidgets('displays TeX source; experimental flag default', (tester) async {
await prepareContent(tester, plainContent(ContentExample.mathBlock.html));

because the default is by definition the state the setting starts in when it hasn't been set.


final String texSource;
final List<KatexNode>? nodes;
Copy link
Member

Choose a reason for hiding this comment

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

This should get dartdoc in particular to clarify what null means.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, in fact I guess it can just copy what MathParserResult.nodes has 🙂

Comment on lines 376 to 385
/// The text or a single character this KaTeX node contains, generally
/// observed to be the leaf node in the KaTeX HTML tree.
/// It will be null if [nodes] is non-null.
final String? text;
Copy link
Member

Choose a reason for hiding this comment

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

Is "a single character" a different case from "the text"? It sounds like that's just the text when the text happens to be only one character.

Comment on lines 493 to 430
KatexSpanFontStyle? fontStyle;
KatexSpanFontWeight? fontWeight;
Copy link
Member

Choose a reason for hiding this comment

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

nit: put these in same order as the enums are defined

(And I think weight, then style, is the preferred way around: one says "bold italic", e.g. in the names of the fonts this PR adds, not "italic bold")


@override
String toString() {
if (this == _zero) return '${objectRuntimeType(this, 'KatexSpanStyles')}()';
Copy link
Member

Choose a reason for hiding this comment

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

This is just an optimization, right?

Can leave it out in favor of simplifying the code — toString on these should only come up in errors or debugging.

Comment on lines 889 to 892
if (styles.fontFamily != null) {
textStyle ??= TextStyle();
textStyle = textStyle.copyWith(fontFamily: styles.fontFamily);
}
Copy link
Member

Choose a reason for hiding this comment

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

How about:

Suggested change
if (styles.fontFamily != null) {
textStyle ??= TextStyle();
textStyle = textStyle.copyWith(fontFamily: styles.fontFamily);
}
final fontFamily = styles.fontFamily;

and then construct textStyle and textAlign at the end after defining several locals like that?

I think that would be simpler. It'd also avoid copying a TextStyle repeatedly when there are a couple of different properties being set.


@override
Widget build(BuildContext context) {
final em = DefaultTextStyle.of(context).style.fontSize!;
Copy link
Member

Choose a reason for hiding this comment

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

This lookup is only used in one condition, so can move inside that condition. That way we don't do the lookup when it doesn't end up being needed.

'<span class="mord mathnormal sizing reset-size6 size1">λ</span></span></span></span></span></p>',
[
MathBlockNode(
texSource: "\\tiny {\\lambda \\Huge \\lambda}",
Copy link
Member

Choose a reason for hiding this comment

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

Can you write a widget test for this?

I think that'll actually be more pertinent than the parsing test (though we'll naturally have this parsing test as a consequence given the way the ContentExample system is structured, and that's fine): the important thing isn't that one layer says "0.5em" and then a layer inside it says "4.976em" so much as it is that the text winds up at the size that would be 2.488em if found at the root of the math tree.

For the widget test it may be helpful to have this example split into two examples: one for each of the two blocks in this example. That way the widget test can render only the one that it intends to inspect.

@rajveermalviya rajveermalviya force-pushed the pr-tex-content-1 branch 6 times, most recently from a24eb43 to 8644903 Compare April 21, 2025 13:52
@rajveermalviya
Copy link
Member Author

first parenthesis (span with class "mopen" in HTML) appears differently in Flutter than on web.

@PIG208, that turned out to be a weird bug in upstream Flutter when displaying text with KaTeX_Math font with FontStyle.italic.

For now, I've added a workaround that should fix this behavior (forcing FontStyle.normal for this font on Android), and also reported the issue upstream — flutter/flutter#167474.

@rajveermalviya
Copy link
Member Author

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

@rajveermalviya rajveermalviya requested a review from gnprice April 21, 2025 14:03
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! Just a couple of nits below, on those same first 7/8 commits.

Then let's save the last commit:
8644903 content: Support parsing and handling inline styles for KaTeX content

for the next PR. That way we can go ahead and merge the rest of them very soon.

Comment on lines +738 to +741
KatexNode(
styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11
text: '1',
nodes: null),
Copy link
Member

Choose a reason for hiding this comment

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

Ah neat — the use of numerals instead of letters here makes this expected parse result simpler, too, with no fontFamily or fontStyle.

(following up on #1408 (comment))

fontSize != null ||
fontWeight != null ||
fontStyle != null) {
// TODO remove this workaround when upstream fixes the broken rendering
Copy link
Member

Choose a reason for hiding this comment

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

nit: say TODO(upstream) to make it greppable

(In particular, we might occasionally grep to find upstream issues to check in on, or follow up on.)

}

if (styles.verticalAlignEm != null) {
final em = DefaultTextStyle.of(context).style.fontSize!;
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 see — in #1408 (comment) I wrote that this was used only in one condition, but that stops being true in the commit after the commits I was reading :-)

Because at the end of the branch it gets used in three different places, not just one place under one condition, the way you had this lookup at the top of the function is fine.

Comment on lines 582 to 585
check(mergedStyleOf(tester, text))
.which((it) => it.isNotNull()
..fontFamily.equals(fontFamily)
..fontSize.equals(fontSize));
Copy link
Member

Choose a reason for hiding this comment

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

nit: simplify

Suggested change
check(mergedStyleOf(tester, text))
.which((it) => it.isNotNull()
..fontFamily.equals(fontFamily)
..fontSize.equals(fontSize));
check(mergedStyleOf(tester, text)).isNotNull()
..fontFamily.equals(fontFamily)
..fontSize.equals(fontSize);

This means the same thing, right?

Comment on lines +620 to +629
var fontSize = 0.5 * kBaseKatexTextStyle.fontSize!;
checkKatexText(tester, '1',
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
fontHeight: kBaseKatexTextStyle.height!);

fontSize = 4.976 * fontSize;
checkKatexText(tester, '2',
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
fontHeight: kBaseKatexTextStyle.height!);
Copy link
Member

Choose a reason for hiding this comment

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

This still feels less end-to-end than I would like (continuing from #1408 (comment)).

This might be something that's clearer for me to express by demonstration, though. There are a couple of other refactors I'm thinking I'll try doing upon merging this PR, so I may include this in that effort.

Copy link
Member

Choose a reason for hiding this comment

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

There are a couple of other refactors I'm thinking I'll try doing upon merging this PR

Sent most of those just now as #1478.

Others are still a draft, as I mentioned there:

This doesn't include the changes I've sketched for passing the font size down directly through the recursion in the parser and widgets. I'll want to look a bit more at how those would interact with the changes in #1452 (which introduce a number of additional length-valued properties) before I'm confident just what shape that part of the API should take.

and I haven't yet tried revising these tests. (That would naturally go along with some of the changes still in draft.)

if (index + 2 < spanClass.length) {
final resetSizeClass = spanClasses[index + 1];
final sizeClass = spanClasses[index + 2];
Copy link
Member

Choose a reason for hiding this comment

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

Oh, one bug here:

Suggested change
if (index + 2 < spanClass.length) {
final resetSizeClass = spanClasses[index + 1];
final sizeClass = spanClasses[index + 2];
if (index + 2 < spanClasses.length) {
final resetSizeClass = spanClasses[index + 1];
final sizeClass = spanClasses[index + 2];

🙂

Comment on lines +825 to +826
const kBaseKatexTextStyle = TextStyle(
fontSize: kBaseFontSize * 1.21,
Copy link
Member

Choose a reason for hiding this comment

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

A separate bug: this doesn't correctly handle scaling the font size up when the TeX appears inside a heading. For example: #test here > math @ 💬

To do that, this factor of 1.21 should instead get applied in the same way as the scaling factor for the font size of code spans. See references to kInlineCodeFontSizeFactor.

Let's make that fix as a separate commit, though, which can go in a follow-up PR. That way we can still merge this PR's changes very soon, as a baseline for further progress.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a fix for this in ffcdfb0, in #1452.

It will be used in later commits to parse inline CSS styles
in HTML while parsing KaTeX HTML spans.
Downloaded from:
  https://github.com/KaTeX/KaTeX/tree/2fe1941b7/fonts

These fonts will be used in later commits to show KaTeX content.
This later avoids a collision for the `TextDirection` type,
which is also defined in `dart:ui`.
This will be useful in the later commits to query GlobalSettings
while content HTML parsing phase.
With this, if the new experimental flag is enabled, the result will
be really basic rendering of each text character in KaTeX spans.
This adds another experimental flag called `forceRenderKatex`
which, if enabled, ignores any errors generated by the parser
(like when encountering an unsupported CSS class) tries to do
a "broken" render of the available span and their styles.
Allowing the developer to test the different KaTeX content in
the wild easily, while still in development.
@rajveermalviya rajveermalviya force-pushed the pr-tex-content-1 branch 2 times, most recently from d055d66 to 1ca7f8a Compare April 22, 2025 15:24
@rajveermalviya
Copy link
Member Author

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

@rajveermalviya rajveermalviya requested a review from gnprice April 22, 2025 15:35
@gnprice
Copy link
Member

gnprice commented Apr 22, 2025

Thanks! Looks good; merging.

@gnprice gnprice merged commit 1ca7f8a into zulip:main Apr 22, 2025
1 check passed
@rajveermalviya rajveermalviya deleted the pr-tex-content-1 branch April 23, 2025 13:06
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.

3 participants