Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
84de5f5
feat(markdown): math rendering with typst
cestef Feb 7, 2025
e380fe1
fix: remove useless ENABLE_GFM option
cestef Feb 7, 2025
f661eae
fix: extra closing tags being added in codeblocks
cestef Feb 7, 2025
9bfb6f8
fix: allow inline html for summaries
cestef Feb 7, 2025
3a08fb9
fix: formatting
cestef Feb 7, 2025
dfc5129
fix: math engine `none` panic
cestef Feb 9, 2025
06d7c0c
feat: support `cache_dir` to control where the cache is saved for math
cestef Feb 9, 2025
d9323fa
docs: start writing docs for math rendering
cestef Feb 9, 2025
d77b56c
feat(markdown): add basic tests for typst
cestef Feb 9, 2025
e804654
feat(markdown): cache typst packages to the correct directory
cestef Feb 9, 2025
ba7ef5f
feat: basic katex rendering
cestef Feb 9, 2025
4349f77
feat: complete katex rendering + a bit of refactoring
cestef Feb 9, 2025
77308e2
fix: katex test
cestef Feb 9, 2025
965b845
fix: switch to `duktape` js engine for katex
cestef Feb 10, 2025
385dbbe
refactor: math configuration
cestef Feb 19, 2025
8d8cf3c
fix: don't use nightly feature flags
cestef Feb 19, 2025
348e5e0
fix: set the correct cache depending on the engine
cestef Feb 19, 2025
7847d86
refactor: create cache only on write
cestef Feb 19, 2025
689d706
fix: don't build codeblock for rendered codeblocks
cestef Feb 19, 2025
18b7333
fix: deserialize HashMap instead of Option<HashMap>
cestef Feb 19, 2025
386f4e9
feat: support `math = "engine"`
cestef Feb 19, 2025
f1ba873
fix: infinite recursion when deserializing config
cestef Feb 19, 2025
b1a526e
refactor: remove `CodeBlockType`
cestef Feb 19, 2025
01bb110
refactor: remove `math = "engine"` syntax support
cestef Feb 19, 2025
0605915
docs: update math documentation
cestef Feb 19, 2025
8f335f1
docs: typo
cestef Feb 19, 2025
821e706
feat: add github alerts (#2893)
banditopazzo Jun 9, 2025
fe7d683
fix: upgrade to typst 0.13 and fix rendered codeblocks
cestef Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,131 changes: 1,126 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

149 changes: 147 additions & 2 deletions components/config/src/config/markup.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::{path::Path, sync::Arc};
use std::{fmt, path::Path, sync::Arc};

use libs::syntect::{
highlighting::{Theme, ThemeSet},
html::css_for_theme_with_class_style,
parsing::{SyntaxSet, SyntaxSetBuilder},
};
use serde::{Deserialize, Serialize};
use serde::{de, Deserialize, Deserializer, Serialize};

use errors::{bail, Result};
use utils::types::InsertAnchor;
Expand All @@ -23,6 +23,142 @@ pub struct ThemeCss {
pub filename: String,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum MathRenderingEngine {
#[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(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ImageFormat {
#[default]
Svg,
Webp,
}

struct BoolWithPathVisitor;

impl<'de> de::Visitor<'de> for BoolWithPathVisitor {
type Value = BoolWithPath;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a boolean or string")
}

fn visit_bool<E>(self, value: bool) -> Result<BoolWithPath, E>
where
E: de::Error,
{
Ok(if value { BoolWithPath::True(None) } else { BoolWithPath::False })
}

fn visit_str<E>(self, value: &str) -> Result<BoolWithPath, E>
where
E: de::Error,
{
Ok(BoolWithPath::True(Some(value.to_string())))
}

fn visit_string<E>(self, value: String) -> Result<BoolWithPath, E>
where
E: de::Error,
{
Ok(BoolWithPath::True(Some(value)))
}
}

#[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?

#[default]
False,
True(Option<String>),
}

impl<'de> Deserialize<'de> for BoolWithPath {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(BoolWithPathVisitor)
}
}

impl Serialize for BoolWithPath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
BoolWithPath::False => serializer.serialize_bool(false),
BoolWithPath::True(None) => serializer.serialize_bool(true),
BoolWithPath::True(Some(s)) => serializer.serialize_str(s),
}
}
}

#[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 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?

pub format: ImageFormat,
}

impl<'de> Deserialize<'de> for MathRenderer {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// Helper struct that mirrors MathRenderer
#[derive(Deserialize)]
struct MathRendererHelper {
#[serde(default)]
engine: MathRenderingEngine,
#[serde(default)]
svgo: BoolWithPath,
css: Option<String>,
addon: Option<String>,
#[serde(default)]
format: ImageFormat,
}

#[derive(Deserialize)]
#[serde(untagged)]
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

Full(MathRendererHelper),
}

// Parse using the helper type
let config = MathRendererConfig::deserialize(deserializer)?;

// Convert to MathRenderer
match config {
MathRendererConfig::Engine(engine) => Ok(MathRenderer {
engine,
svgo: BoolWithPath::default(),
css: None,
addon: None,
format: ImageFormat::Svg,
}),
MathRendererConfig::Full(helper) => Ok(MathRenderer {
engine: helper.engine,
svgo: helper.svgo,
css: helper.css,
addon: helper.addon,
format: helper.format,
}),
}
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Markdown {
Expand Down Expand Up @@ -67,6 +203,12 @@ pub struct Markdown {
pub insert_anchor_links: InsertAnchor,
/// Whether to enable GitHub-style alerts
pub github_alerts: bool,
/// Whether to enable math rendering in markdown files
pub math: MathRenderer,
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about making it an Option?

/// Whether to cache the rendered math
pub cache: BoolWithPath,
Copy link
Collaborator

Choose a reason for hiding this comment

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

should that be in MathRenderer?

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 was to put it here in case we needed cache for other stuff than math rendering in the future, the comment above is erroneous

/// 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

}

impl Markdown {
Expand Down Expand Up @@ -243,6 +385,9 @@ impl Default for Markdown {
lazy_async_image: false,
insert_anchor_links: InsertAnchor::None,
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

}
}
}
11 changes: 11 additions & 0 deletions components/config/src/highlighting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ pub struct SyntaxAndTheme<'config> {
pub source: HighlightSource,
}

impl Default for SyntaxAndTheme<'_> {
fn default() -> Self {
SyntaxAndTheme {
syntax: SYNTAX_SET.find_syntax_plain_text(),
syntax_set: &SYNTAX_SET as &SyntaxSet,
theme: None,
source: HighlightSource::Plain,
}
}
}

pub fn resolve_syntax_and_theme<'config>(
language: Option<&'_ str>,
config: &'config Config,
Expand Down
1 change: 1 addition & 0 deletions components/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub use crate::config::{
languages::LanguageOptions,
link_checker::LinkChecker,
link_checker::LinkCheckerLevel,
markup::{BoolWithPath, ImageFormat, MathRenderingEngine},
search::{IndexFormat, Search},
slugify::Slugify,
taxonomies::TaxonomyConfig,
Expand Down
15 changes: 15 additions & 0 deletions components/content/src/page.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/// A page, can be a blog post or a basic page
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use libs::once_cell::sync::Lazy;
use libs::regex::Regex;
use libs::tera::{Context as TeraContext, Tera};

use config::Config;
use errors::{Context, Result};
use markdown::context::Caches;

use markdown::{render_content, RenderContext};
use utils::slugs::slugify_paths;
use utils::table_of_contents::Heading;
Expand Down Expand Up @@ -212,6 +215,7 @@ impl Page {
config: &Config,
anchor_insert: InsertAnchor,
shortcode_definitions: &HashMap<String, ShortcodeDefinition>,
caches: Option<Arc<Caches>>,
) -> Result<()> {
let mut context = RenderContext::new(
tera,
Expand All @@ -220,9 +224,16 @@ impl Page {
&self.permalink,
permalinks,
anchor_insert,
caches,
);
context.set_shortcode_definitions(shortcode_definitions);
context.set_current_page_path(&self.file.relative);
context.set_parent_absolute(
&self.file.parent,
self.file.colocated_path.as_ref(),
&self.file.components,
);

context.tera_context.insert("page", &SerializingPage::new(self, None, false));

let res = render_content(&self.raw_content, &context)
Expand Down Expand Up @@ -330,6 +341,7 @@ Hello world"#;
&config,
InsertAnchor::None,
&HashMap::new(),
None,
)
.unwrap();

Expand Down Expand Up @@ -358,6 +370,7 @@ Hello world"#;
&config,
InsertAnchor::None,
&HashMap::new(),
None,
)
.unwrap();

Expand Down Expand Up @@ -528,6 +541,7 @@ Hello world
&config,
InsertAnchor::None,
&HashMap::new(),
None,
)
.unwrap();
assert_eq!(page.summary, Some("<p>Hello world</p>".to_string()));
Expand Down Expand Up @@ -562,6 +576,7 @@ And here's another. [^3]
&config,
InsertAnchor::None,
&HashMap::new(),
None,
)
.unwrap();
assert_eq!(
Expand Down
9 changes: 9 additions & 0 deletions components/content/src/section.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use libs::tera::{Context as TeraContext, Tera};

use config::Config;
use errors::{Context, Result};
use markdown::context::Caches;
use markdown::{render_content, RenderContext};
use utils::fs::read_file;
use utils::net::is_external_link;
Expand Down Expand Up @@ -150,6 +152,7 @@ impl Section {
tera: &Tera,
config: &Config,
shortcode_definitions: &HashMap<String, ShortcodeDefinition>,
caches: Option<Arc<Caches>>,
) -> Result<()> {
let mut context = RenderContext::new(
tera,
Expand All @@ -158,9 +161,15 @@ impl Section {
&self.permalink,
permalinks,
self.meta.insert_anchor_links.unwrap_or(config.markdown.insert_anchor_links),
caches,
);
context.set_shortcode_definitions(shortcode_definitions);
context.set_current_page_path(&self.file.relative);
context.set_parent_absolute(
&self.file.parent,
self.file.colocated_path.as_ref(),
&self.components,
);
context
.tera_context
.insert("section", &SerializingSection::new(self, SectionSerMode::ForMarkdown));
Expand Down
15 changes: 15 additions & 0 deletions components/markdown/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ config = { path = "../config" }
console = { path = "../console" }
libs = { path = "../libs" }

typst = "0.13.1"
typst-assets = { version = "0.13.1", features = ["fonts"] }
typst-svg = "0.13.1"
time = { version = "0.3.37", features = ["local-offset"] }
flate2 = "1.0.35"
tar = "0.4.43"
ttf-parser = "0.25.1"
urlencoding = "2.1.3"
bincode = "1.3.3"
serde = { version = "1.0.130", features = ["derive"] }
dashmap = { version = "6.1.0", features = ["serde"] }
twox-hash = "2.1.0"
dirs = "6.0.0"
katex = { version = "0.4.6", default-features = false, features = ["duktape"] }

[dev-dependencies]
templates = { path = "../templates" }
insta = "1.12.0"
4 changes: 4 additions & 0 deletions components/markdown/benches/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ fn bench_render_content_with_highlighting(b: &mut test::Bencher) {
current_page_permalink,
&permalinks_ctx,
InsertAnchor::None,
None,
);
let shortcode_def = utils::templates::get_shortcodes(&tera);
context.set_shortcode_definitions(&shortcode_def);
Expand All @@ -116,6 +117,7 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) {
current_page_permalink,
&permalinks_ctx,
InsertAnchor::None,
None,
);
let shortcode_def = utils::templates::get_shortcodes(&tera);
context.set_shortcode_definitions(&shortcode_def);
Expand All @@ -137,6 +139,7 @@ fn bench_render_content_no_shortcode(b: &mut test::Bencher) {
current_page_permalink,
&permalinks_ctx,
InsertAnchor::None,
None,
);

b.iter(|| render_content(&content2, &context).unwrap());
Expand All @@ -159,6 +162,7 @@ fn bench_render_content_with_emoji(b: &mut test::Bencher) {
current_page_permalink,
&permalinks_ctx,
InsertAnchor::None,
None,
);

b.iter(|| render_content(&content2, &context).unwrap());
Expand Down
Loading
Loading