Skip to content

Conversation

@cestef
Copy link
Contributor

@cestef cestef commented Feb 7, 2025

IMPORTANT: Please do not create a Pull Request adding a new feature without discussing it first.

The place to discuss new features is the forum: https://zola.discourse.group/
If you want to add a new feature, please open a thread there first in the feature requests section.

Sanity check:

Code changes

(Delete or ignore this section for documentation changes)

  • Are you doing the PR on the next branch?

If the change is a new feature or adding to/changing an existing one:

  • Have you created/updated the relevant documentation page(s)?

@cestef cestef force-pushed the feature/math-rendering branch 2 times, most recently from bad4e13 to 5d8d725 Compare February 7, 2025 13:09
@cestef
Copy link
Contributor Author

cestef commented Feb 7, 2025

I don't really know how to get rid of these Cargo.lock updates, but anyways

@cestef
Copy link
Contributor Author

cestef commented Feb 7, 2025

This PR adds math rendering on the "server-side" with typst, while keeping the katex option open for latex.

A GenericCache struct has also been added for integrations like these to store the compilation results to save time on subsequent builds. Ideally, each integration would have its cache in the the global <Site>.caches attribute. Cache is stored as binary-encoded data at .cache/<CACHE_NAME>. Typst remote packages are also downloaded to this path.

The output SVGs generated by typst can also be minified via the svgo CLI. This is kind of a hacky implementation because it relies on the CLI but it does the job.

Newly added configuration parameters:

math = "typst"                       # Defaults to "none", one of "typst", "katex"
math_svgo = true                     # Optimize the SVGs generated by the math renderer
math_svgo_config = "svgo.config.mjs" # Optional, Path to the svgo config
math_css = "styles/typst-embed.css"  # path to the CSS file to inject inside the SVGs generated 

CI is failing because [email protected] (used by typst and image) and [email protected] (used for caching keys) require rustc >= 1.81, should we upgrade rustc or downgrade these dependencies ?

The size of the binary also drastically increases because of typst's ecosystem (36.4 MiB -> 62.0 MiB)

@claytonwramsey
Copy link

This is great work! One possible place to improve: the current implementation outputs SVG for equations. It might be better to use MathML to make sites smaller and more accessible.

@cestef
Copy link
Contributor Author

cestef commented Feb 11, 2025

Thanks a lot :D Typst doesn't yet support HTML or MathML output (see typst/typst#5512)

The basic katex implementation however already outputs HTML I think. I don't use LaTeX that much so if anyone is up to help with this, I'd be glad.

@claytonwramsey
Copy link

I think it might be possible to write a backend visitor to output MathML (cf https://github.com/wcshds/typst-math-to-mathml-converter). This would be a lot easier than what the Typst team is building, since we don't have to handle everything else, but the downside is that we're then implementing a possibly buggy different spec from what Typst does.

As a stopgap for accessibility, we might also just include the math source as alt text for the generated SVGs.

@cestef
Copy link
Contributor Author

cestef commented Feb 11, 2025

This indeed sounds a bit risky, including the original source in the alt should be enough for now. I will tinker a bit with the library you linked to try out mathml rendering, I also stumbled accross this one:

https://github.com/xushengfeng/xmmath

@cestef cestef changed the title feat(markdown): add support for math rendering with typst feat(markdown): add support for math rendering with typst and katex Feb 11, 2025
@cestef cestef force-pushed the feature/math-rendering branch 2 times, most recently from d276f92 to 4d37093 Compare February 18, 2025 07:57
@cestef
Copy link
Contributor Author

cestef commented Feb 18, 2025

I have tried out a bit the library you linked, here's the result I get:

image

I am using katex's font for the rendered mathml. There are still a lot of inconsitencies (the spaces between $a$ and $x$, the spacing feeling weird), but overall it seems like a viable solution if we were to tweak things a bit.

For reference, here's the result with SVG rendering:

image

@cestef cestef force-pushed the feature/math-rendering branch from 4d37093 to df62c23 Compare February 18, 2025 10:01
@cestef cestef marked this pull request as ready for review February 18, 2025 10:04
Copy link

@soqb soqb left a comment

Choose a reason for hiding this comment

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

this looks great! i've left a few comments but i've been toying locally for a few days and i'm very happy.

+1 for svg+mathml too. it's always what i gravitate to and it always works well. one day soon mathml will be widespread and complete enough that it will become a viable primary target, but for now, svg rendering is the only consistent option.

on that note, did you ever consider mathjax? the latex support is a little better than katex these days.

V: Serialize + for<'de> Deserialize<'de>,
{
cache_file: PathBuf,
cache: DashMap<K, V>,
Copy link

Choose a reason for hiding this comment

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

i'm not totally sure whether it matters at the kind of scale/concurrency zola operates on, but you might find papaya a better fit for a concurrent hash map. this (biased) blogpost by the author totally convinced me.

_ => FenceSettings::new(""),
};
let (block, begin) = match CodeBlock::new(fence, context.config, path) {
let (block, begin) = match CodeBlock::new(&fence, context.config, path) {
Copy link

Choose a reason for hiding this comment

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

performance wise, though it's somewhat (but not totally) moot, you might want to clone fence.language beforehand so you don't have to pay the larger cost of cloning inside CodeBlock::new.

accumulated_block.clear();
let inner = &accumulated_block;
match code_block_language.as_deref() {
Some("typ") => {
Copy link

Choose a reason for hiding this comment

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

is there a reason katex isn't supported here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't use latex/katex that much, but I think there's no concept of "raw" rendering in katex. From what I understood, it's only made for math rendering, whereas typst can render whole documents. the "raw" mode was initially intended for rendering graphs/tables. Let me know if katex supports such things!

@cestef
Copy link
Contributor Author

cestef commented Feb 19, 2025

Update:

  • As per @soqb suggested, math-related configuration is now grouped into the math attribute:
[markdown.math]
engine = "typst" # typst, katex, or none
svgo = true # true, false, or "path"           
css = "styles/typst-embed.css" # optional
addon = "helpers.typ" # optional

We can also simplfy specify:

[markdown]
math = "typst"

Which is equivalent to

[markdown]
math = {
	engine = "typst"
}
  • A new cache configuration parameter has been added to control caching from typst or katex (and possibly more integrations in the future, thus, I put it in markdown instead of markdown.math):
[markdown]
cache = true # true, false or "path"

By default the caches are now stored in ~/.cache/zola/<CACHE_NAME>.

  • CodeBlocks are now wrapped inside a CodeBlockType enum. This is due to the introduction of "renderable" code blocks:
## Wonderful test

This is markdown content.

```typ
= Hello, World!
This is a raw typst document: $y^2 = x^3 + a x + b$
```

image

  • To allow for future additions (e.g. mathjax), a MathCompiler trait has been added.
    If raw rendering, is supported, code fence languages that it renders should be returned at raw_extensions.
    Only the specified rendering engine is now created and boxed, this means that rendering both typst and katex in the same document is not possible

  • Added addons for math which allow users to add code snippets to all compiled expressions. This is useful when you need to provide helper functions, import default packages or override defaults.

PS: Thank you so much for your feedback soqdb 😄

@Keats
Copy link
Collaborator

Keats commented Feb 19, 2025

I wouldn't bother with the math = "typst" form in the config

@cestef
Copy link
Contributor Author

cestef commented Feb 19, 2025

@soqb after benchmarking papaya and dashmap, it looks like the difference for our scale is very very small...
Benchmarks were ran on a site with around 500 elements in cache:

Hot run (read heavy):
zola-hot
Cold run (write heavy):
zola-cold

@lilymonad
Copy link

I tried this PR, and used katex, but I had to include the katex.css file myself in my base html template. We need this CSS file in order to hide the mathml output. Wasn't it the job of the css attribute ?

@cestef
Copy link
Contributor Author

cestef commented Mar 16, 2025

css attribute was for css injected inside the svg (typst). For KaTeX, there is nothing injected. Maybe I should update the documentation for this.

@lukasjuhrich
Copy link

Hey folks, great work! I'm quite interested in this PR.
Has there been preceding feature discussion I'm unaware of, or is this intended to be a POC that remains open?

I'll gladly check out this branch and use it for now, but I was wondering if there are any plans from the maintainer's side to upstream this, and if yes what the blockers to this PR are (inb4 maintainer capacity).

Cheers!

@cestef
Copy link
Contributor Author

cestef commented May 11, 2025

This was intially just a PoC to fit my own needs for a blog, and I posted a PR here just in case anyone was interested. I'd gladly help you set up a clean PR to integrate it into zola!

@p-r-o-g
Copy link

p-r-o-g commented Jun 23, 2025

I can try to recreate the changes without merge conflict if that would help? Is someone actively working on getting this done?

@cestef
Copy link
Contributor Author

cestef commented Jun 26, 2025

Merge conflicts were resolved

@cestef
Copy link
Contributor Author

cestef commented Jun 26, 2025

Hey @Keats! Any thoughts on this math rendering PR? Would love to get your feedback to move this forward 🙂

@cappuccinocosmico
Copy link

cappuccinocosmico commented Jun 28, 2025

Hey, just a random new user passing through. This PR looks great! For me getting the ability to render typst would be really helpful for some of my use cases. Mainly because typst has so much more functionality for rendering complex mathematics compared to regular katex:
image
Most of the diagram generation seems to work within math mode, and based on my reading of the PR you should be able to make diagrams with something like this

# Markdown

Here *is* some **example** markdown code:
$#diagram(
	spacing: 8pt,
	cell-size: (8mm, 10mm),
	edge-stroke: 1pt,
	edge-corner-radius: 5pt,
	mark-scale: 70%,

	blob((0,1), [Add & Norm], tint: yellow, shape: hexagon),
	edge(),
	blob((0,2), [Multi-Head\ Attention], tint: orange),
	blob((0,4), [Input], shape: house.with(angle: 30deg),
		width: auto, tint: red),

	for x in (-.3, -.1, +.1, +.3) {
		edge((0,2.8), (x,2.8), (x,2), "-|>")
	},
	edge((0,2.8), (0,4)),

	edge((0,3), "l,uu,r", "--|>"),
	edge((0,1), (0, 0.35), "r", (1,3), "r,u", "-|>"),
	edge((1,2), "d,rr,uu,l", "--|>"),

	blob((2,0), [Softmax], tint: green),
	edge("<|-"),
	blob((2,1), [Add & Norm], tint: yellow, shape: hexagon),
	edge(),
	blob((2,2), [Feed\ Forward], tint: blue),
)$

Rendering these would require some modifications since you would have to include some packages from typst universe, but that is something I can totally volunteer to work on as soon as this gets merged.

@cestef
Copy link
Contributor Author

cestef commented Jun 28, 2025

Hey, thank you so much for taking interest in this PR ! It is already possible to include packages by default via the addon field in the config:

[markdown.math]
engine = "typst"
svgo = "svgo.config.mjs"
css = "styles/typst-embed.css"
addon = "helpers.typ"

This will include the helpers.typ file for all math blocks. There, you can import your packages, change the default layout, define helper functions, etc.

What you're trying to achieve can also be done via a typ code block, which will be rendered as "raw" typst:

```typ

= Hello, World!

This is a normal typst file 

$ a + b = 42 $
```

@Keats
Copy link
Collaborator

Keats commented Jul 2, 2025

I think that looks amazing. It's pretty big, I'll set up some time to properly review it

@cestef cestef force-pushed the feature/math-rendering branch from 601f1e7 to fe7d683 Compare July 15, 2025 07:14
@cestef
Copy link
Contributor Author

cestef commented Jul 15, 2025

This should be it, had to fight a bit with git rebase, sorry for the force-pushes

#[default]
None,
Typst,
Katex,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need both typst and katex?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea behind the generic MathCompiler trait was to add support for multiple backends, many people still use LaTeX in my opinion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But for the Zola crowd, do we need multiple renderer?

Choose a reason for hiding this comment

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

Chiming in as a Zola user who's been using cestef's fork: I personally am using the Typst math rendering right now, but I had to manually port all the math on my blog from LaTeX to do so. I think a lot of people using this feature might be moving over from a previous math implementation, so they will likely use LaTeX, but my experience is that writing new posts with Typst is way easier. In the ideal world, it would be possible to specify math on a per-page or per-block basis, but that might be a little too complex.

Copy link
Collaborator

Choose a reason for hiding this comment

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

my experience is that writing new posts with Typst is way easier

That's what I've seen online as well. I don't use any math/LaTeX whatsoever myself so I don't have an opinion myself on that other than keeping things simple

Choose a reason for hiding this comment

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

Imo, its about supporting legacy use cases, 95% of all math/physics papers are still written in latex, and its also the standard format for math in markdown: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/writing-mathematical-expressions. I think the main reason everyone on this thread is using typst is just the attraction to rust. But if no one on this thread actually wants support for Katex, it might be worth it to remove it for now and only add it if someone requests over typst.

Copy link

Choose a reason for hiding this comment

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

I think the main reason everyone on this thread is using typst is just the attraction to rust

This is not the case for everyone probably, example is me, I am rather a Typst user & enthusiast (rust as well but not as much experience using it) and actually would like to use it for a documentation website involving lots of mathematics code. Typst is gaining functionality and convenience very quickly and left LaTeX already behind in most aspects (my opinion).

Copy link

@lilymonad lilymonad Aug 10, 2025

Choose a reason for hiding this comment

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

I personaly still use LaTeX because it is what I used untill now, and also for tikz figures and graphs (not with zola, just in my other documents). If Typst has the same math rendering quality as Katex and maintaining multiple engines is a pain, it would bother me to switch to Typst but I would do it anyways. I won't complain on this decision, I'm already grateful people made math rendering possible.

I can only see one downside of having only Typst: if people write things both in their blog and in research papers, they would need to write things in two languages each time. But I guess it's rare.

Choose a reason for hiding this comment

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

I think the main reason everyone on this thread is using typst is just the attraction to rust

Every rust rewrite has a portion of supporters purely based on the fact that it is new, fancy, and written in rust. Existence of such supporters is a terrible indicator to judge a project's actual quality, soundness of architectural decisions and overall adoption trajectory.

When typst first got out, I was very critical because it was quite young and it had a few rough edges, but I quickly flipped because it made me way faster. I wrote my 140p thesis in typst and will never go back. After only one year of learning I was able to get my hands dirty with custom commands and document introspection in a way that after 7 years of latex I was never able to come close to.
Typst does numerous things a ton better in a way that latex will never be able to: To name one, TeX is a macro processing language, and this will never change. That makes it impossible to give good syntactical error messages or editor completion (heuristics are always possible, but as soon as you have custom commands you are out the window), and you will not be able to change that. To name another, the packaging ecosystem is easy to use and growing rapidly. And we don't have to talk about compilation speed (although to be fair the static web-rendering use case is different from getting a live preview while typing, where one sees the most benefit).

All this is to say two things.
First, I guarantee you there will be a growing portion of users who will want to use typst and feel very strongly about its adoption in up-and-coming projects such as zola.
Second, LaTeX is still a requirement for most people, and will continue to be. A portion of such people are likely to cast out zola if they want to typeset math but don't want the burden of learning a „fancy new typesetting language that nobody uses and will probably die in a few years, anyway“. Especially in academia things are slowly moving (rightfully so), and the change in adoption will probably come with a generational change; some communities will never reach it. Something similar is happening with automated theorem proving, and something similar happened in math with the introduction of category theoretical methods. We're talking on the scale of decades here.

Sure, much of this is anecdotal, and I might be wrong about my projections, but I like to think spending 6+ months doing nothing but writing typst and having had extensive LaTeX experience beforehand counts for something other than „irrelevant because opinion-based and not measurable“.

I am however not sure what you should do with this information, because I can't seriously tell someone to “just support both” without being the one who has to carry the maintenance burden.

}

#[derive(Clone, Debug, PartialEq, Eq, Default, Hash)]
pub enum BoolWithPath {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's that? Can you add a comment?

#[derive(Clone, Debug, Serialize, Default)]
pub struct MathRenderer {
pub engine: MathRenderingEngine,
pub svgo: BoolWithPath,
Copy link
Collaborator

Choose a reason for hiding this comment

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

is that a path to the binary?

pub engine: MathRenderingEngine,
pub svgo: BoolWithPath,
pub css: Option<String>,
pub addon: Option<String>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Only one addon supported? What are addons?

enum MathRendererConfig {
// String case: math = "engine"
Engine(MathRenderingEngine),
// Table case with full configuration
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's just only have the full table so we don't have to support that kind of things


// Generate cache key
let key = {
let mut hasher = XxHash64::with_seed(42);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the speed difference worth adding that crate? Image proc still uses the std DefaultHasher because in practice it's not really noticeable

ImageFormat::Webp => {
// let _pixmap = typst_render::render(page, 300.0);
// TODO: svg2webp
unimplemented!("WebP is not supported yet")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it not supported in typst?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

typst supports png output, but not webp yet. https://typst.app/docs/reference/png/

Choose a reason for hiding this comment

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

Would it be easier to go the PNG=>WebP route?

{code}
"#,
)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want people to be able to use their own templates?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't see much use, people can already configure templates further with the addon option, which includes additional code in the templates. inline and block templates only consist of base page settings and a math block. For further customization people can use the raw rendering via code blocks.

let output_path = path.join(config.output_dir.clone());

let caches = match config.markdown.cache {
config::BoolWithPath::True(ref maybe_path) => Some(Arc::new(match maybe_path {
Copy link
Collaborator

Choose a reason for hiding this comment

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

When would the user want to specify their own cache directory?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CI/CD pipelines or network shared cache, I can remove this if you feel it's overkill

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah i think it's better to keep things simple


## SVG Optimization

Enable `svgo = true` to optimize the generated SVG files using [SVGO](https://svgo.dev). This can significantly reduce the file size of complex mathematical expressions.
Copy link
Collaborator

Choose a reason for hiding this comment

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

so the reason we do that is because it's inlined as an image right? So there's no SVG to optimize in a build pipeline. I'm surprised there's not a library like svgo in Rust but I would leave that part out for now

/// Whether to cache the rendered math
pub cache: BoolWithPath,
/// Whether to enable GitHub-style alerts
pub github_alerts: bool,

Choose a reason for hiding this comment

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

Github alerts seems to be duplicated in this enum, if you try to compile the program you get an error around this
image

github_alerts: false,
math: MathRenderer::default(),
cache: BoolWithPath::True(None),
github_alerts: false,

Choose a reason for hiding this comment

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

Also errors out on this declaration as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.