Skip to content

Commit 5d8d725

Browse files
committed
feat(markdown): math rendering with typst
1 parent bcbcd1e commit 5d8d725

File tree

21 files changed

+2082
-388
lines changed

21 files changed

+2082
-388
lines changed

Diff for: Cargo.lock

+1,226-365
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: components/config/src/config/markup.rs

+22
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ pub struct ThemeCss {
2323
pub filename: String,
2424
}
2525

26+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
27+
#[serde(rename_all = "lowercase")]
28+
pub enum MathRendering {
29+
#[default]
30+
None,
31+
Typst,
32+
KaTeX,
33+
}
34+
2635
#[derive(Clone, Debug, Serialize, Deserialize)]
2736
#[serde(default)]
2837
pub struct Markdown {
@@ -65,6 +74,15 @@ pub struct Markdown {
6574
/// Whether to insert a link for each header like the ones you can see in this site if you hover one
6675
/// The default template can be overridden by creating a `anchor-link.html` in the `templates` directory
6776
pub insert_anchor_links: InsertAnchor,
77+
/// Whether to enable math rendering in markdown files
78+
pub math: MathRendering,
79+
/// Whether to optimize generated math SVGs with svgo
80+
pub math_svgo: bool,
81+
/// Svgo configuration file path
82+
pub math_svgo_config: Option<String>,
83+
/// Whether to enable automatic dark mode switching based on "prefers-color-scheme" for math
84+
/// Injected CSS path for light mode
85+
pub math_css: Option<String>,
6886
}
6987

7088
impl Markdown {
@@ -240,6 +258,10 @@ impl Default for Markdown {
240258
extra_theme_set: Arc::new(None),
241259
lazy_async_image: false,
242260
insert_anchor_links: InsertAnchor::None,
261+
math: MathRendering::default(),
262+
math_svgo: false,
263+
math_css: None,
264+
math_svgo_config: None,
243265
}
244266
}
245267
}

Diff for: components/config/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub use crate::config::{
88
languages::LanguageOptions,
99
link_checker::LinkChecker,
1010
link_checker::LinkCheckerLevel,
11+
markup::MathRendering,
1112
search::{IndexFormat, Search},
1213
slugify::Slugify,
1314
taxonomies::TaxonomyConfig,

Diff for: components/content/src/page.rs

+13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
/// A page, can be a blog post or a basic page
22
use std::collections::HashMap;
33
use std::path::{Path, PathBuf};
4+
use std::sync::Arc;
45

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

910
use config::Config;
1011
use errors::{Context, Result};
12+
use markdown::context::Caches;
13+
1114
use markdown::{render_content, RenderContext};
1215
use utils::slugs::slugify_paths;
1316
use utils::table_of_contents::Heading;
@@ -212,6 +215,7 @@ impl Page {
212215
config: &Config,
213216
anchor_insert: InsertAnchor,
214217
shortcode_definitions: &HashMap<String, ShortcodeDefinition>,
218+
caches: Arc<Caches>,
215219
) -> Result<()> {
216220
let mut context = RenderContext::new(
217221
tera,
@@ -220,9 +224,12 @@ impl Page {
220224
&self.permalink,
221225
permalinks,
222226
anchor_insert,
227+
caches,
223228
);
224229
context.set_shortcode_definitions(shortcode_definitions);
225230
context.set_current_page_path(&self.file.relative);
231+
context.set_parent_absolute(&self.file.parent);
232+
226233
context.tera_context.insert("page", &SerializingPage::new(self, None, false));
227234

228235
let res = render_content(&self.raw_content, &context)
@@ -300,8 +307,10 @@ mod tests {
300307
use std::fs::{create_dir, File};
301308
use std::io::Write;
302309
use std::path::{Path, PathBuf};
310+
use std::sync::Arc;
303311

304312
use libs::globset::{Glob, GlobSetBuilder};
313+
use markdown::context::Caches;
305314
use tempfile::tempdir;
306315
use templates::ZOLA_TERA;
307316

@@ -329,6 +338,7 @@ Hello world"#;
329338
&config,
330339
InsertAnchor::None,
331340
&HashMap::new(),
341+
Arc::new(Caches::default()),
332342
)
333343
.unwrap();
334344

@@ -357,6 +367,7 @@ Hello world"#;
357367
&config,
358368
InsertAnchor::None,
359369
&HashMap::new(),
370+
Arc::new(Caches::default()),
360371
)
361372
.unwrap();
362373

@@ -527,6 +538,7 @@ Hello world
527538
&config,
528539
InsertAnchor::None,
529540
&HashMap::new(),
541+
Arc::new(Caches::default()),
530542
)
531543
.unwrap();
532544
assert_eq!(page.summary, Some("<p>Hello world</p>".to_string()));
@@ -561,6 +573,7 @@ And here's another. [^3]
561573
&config,
562574
InsertAnchor::None,
563575
&HashMap::new(),
576+
Arc::new(Caches::default()),
564577
)
565578
.unwrap();
566579
assert_eq!(

Diff for: components/content/src/section.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::collections::HashMap;
22
use std::path::{Path, PathBuf};
3+
use std::sync::Arc;
34

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

67
use config::Config;
78
use errors::{Context, Result};
9+
use markdown::context::Caches;
810
use markdown::{render_content, RenderContext};
911
use utils::fs::read_file;
1012
use utils::net::is_external_link;
@@ -150,6 +152,7 @@ impl Section {
150152
tera: &Tera,
151153
config: &Config,
152154
shortcode_definitions: &HashMap<String, ShortcodeDefinition>,
155+
caches: Arc<Caches>,
153156
) -> Result<()> {
154157
let mut context = RenderContext::new(
155158
tera,
@@ -158,9 +161,11 @@ impl Section {
158161
&self.permalink,
159162
permalinks,
160163
self.meta.insert_anchor_links.unwrap_or(config.markdown.insert_anchor_links),
164+
caches,
161165
);
162166
context.set_shortcode_definitions(shortcode_definitions);
163167
context.set_current_page_path(&self.file.relative);
168+
context.set_parent_absolute(&self.file.parent);
164169
context
165170
.tera_context
166171
.insert("section", &SerializingSection::new(self, SectionSerMode::ForMarkdown));

Diff for: components/libs/Cargo.toml

+24-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,22 @@ ammonia = "4"
99
atty = "0.2.11"
1010
base64 = "0.22"
1111
csv = "1"
12-
elasticlunr-rs = { version = "3.0.2", features = ["da", "no", "de", "du", "es", "fi", "fr", "hu", "it", "pt", "ro", "ru", "sv", "tr"] }
12+
elasticlunr-rs = { version = "3.0.2", features = [
13+
"da",
14+
"no",
15+
"de",
16+
"du",
17+
"es",
18+
"fi",
19+
"fr",
20+
"hu",
21+
"it",
22+
"pt",
23+
"ro",
24+
"ru",
25+
"sv",
26+
"tr",
27+
] }
1328
filetime = "0.2"
1429
gh-emoji = "1"
1530
glob = "0.3"
@@ -21,14 +36,19 @@ nom-bibtex = "0.5"
2136
num-format = "0.4"
2237
once_cell = "1"
2338
percent-encoding = "2"
24-
pulldown-cmark = { version = "0.12.2", default-features = false, features = ["html", "simd"] }
39+
pulldown-cmark = { version = "0.12.2", default-features = false, features = [
40+
"html",
41+
"simd",
42+
] }
2543
pulldown-cmark-escape = { version = "0.11", default-features = false }
2644
quickxml_to_serde = "0.6"
2745
rayon = "1"
2846
regex = "1"
2947
relative-path = "1"
30-
reqwest = { version = "0.12", default-features = false, features = ["blocking"] }
31-
grass = {version = "0.13", default-features = false, features = ["random"]}
48+
reqwest = { version = "0.12", default-features = false, features = [
49+
"blocking",
50+
] }
51+
grass = { version = "0.13", default-features = false, features = ["random"] }
3252
serde_json = "1"
3353
serde_yaml = "0.9"
3454
sha2 = "0.10"

Diff for: components/markdown/Cargo.toml

+12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ config = { path = "../config" }
1414
console = { path = "../console" }
1515
libs = { path = "../libs" }
1616

17+
typst = "0.12.0"
18+
typst-assets = { version = "0.12.0", features = ["fonts"] }
19+
typst-svg = "0.12.0"
20+
time = { version = "0.3.37", features = ["local-offset"] }
21+
flate2 = "1.0.35"
22+
tar = "0.4.43"
23+
ttf-parser = "0.25.1"
24+
urlencoding = "2.1.3"
25+
bincode = "1.3.3"
26+
serde = { version = "1.0.130", features = ["derive"] }
27+
dashmap = { version = "6.1.0", features = ["serde"] }
28+
twox-hash = "2.1.0"
1729
[dev-dependencies]
1830
templates = { path = "../templates" }
1931
insta = "1.12.0"

Diff for: components/markdown/benches/all.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
#![feature(test)]
22
extern crate test;
33

4-
use std::collections::HashMap;
4+
use std::{collections::HashMap, sync::Arc};
55

66
use config::Config;
77
use libs::tera::Tera;
8-
use markdown::{render_content, RenderContext};
8+
use markdown::{context::Caches, render_content, RenderContext};
99
use utils::types::InsertAnchor;
1010

1111
const CONTENT: &str = r#"
@@ -95,6 +95,7 @@ fn bench_render_content_with_highlighting(b: &mut test::Bencher) {
9595
current_page_permalink,
9696
&permalinks_ctx,
9797
InsertAnchor::None,
98+
Arc::new(Caches::default()),
9899
);
99100
let shortcode_def = utils::templates::get_shortcodes(&tera);
100101
context.set_shortcode_definitions(&shortcode_def);
@@ -116,6 +117,7 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) {
116117
current_page_permalink,
117118
&permalinks_ctx,
118119
InsertAnchor::None,
120+
Arc::new(Caches::default()),
119121
);
120122
let shortcode_def = utils::templates::get_shortcodes(&tera);
121123
context.set_shortcode_definitions(&shortcode_def);
@@ -137,6 +139,7 @@ fn bench_render_content_no_shortcode(b: &mut test::Bencher) {
137139
current_page_permalink,
138140
&permalinks_ctx,
139141
InsertAnchor::None,
142+
Arc::new(Caches::default()),
140143
);
141144

142145
b.iter(|| render_content(&content2, &context).unwrap());
@@ -159,6 +162,7 @@ fn bench_render_content_with_emoji(b: &mut test::Bencher) {
159162
current_page_permalink,
160163
&permalinks_ctx,
161164
InsertAnchor::None,
165+
Arc::new(Caches::default()),
162166
);
163167

164168
b.iter(|| render_content(&content2, &context).unwrap());

Diff for: components/markdown/src/cache.rs

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use bincode;
2+
use dashmap::DashMap;
3+
use serde::{Deserialize, Serialize};
4+
use std::fs::{self, File, OpenOptions};
5+
use std::hash::Hash;
6+
7+
use std::io::{self, Read, Write};
8+
use std::path::{Path, PathBuf};
9+
10+
/// Generic cache using DashMap, storing data in a binary file
11+
#[derive(Debug)]
12+
pub struct GenericCache<K, V>
13+
where
14+
K: Eq + Hash + Serialize + for<'de> Deserialize<'de>,
15+
V: Serialize + for<'de> Deserialize<'de>,
16+
{
17+
cache_file: PathBuf,
18+
cache: DashMap<K, V>,
19+
}
20+
21+
impl<K, V> GenericCache<K, V>
22+
where
23+
K: Eq + Hash + Serialize + for<'de> Deserialize<'de>,
24+
V: Serialize + for<'de> Deserialize<'de>,
25+
{
26+
/// Create a new cache for a specific type
27+
pub fn new(base_cache_dir: &Path, filename: &str) -> crate::Result<Self> {
28+
// Create the base cache directory
29+
fs::create_dir_all(base_cache_dir)?;
30+
31+
// Full path to the cache file
32+
let cache_file = base_cache_dir.join(filename);
33+
34+
// Attempt to load existing cache
35+
let cache = match Self::read_cache(&cache_file) {
36+
Ok(loaded_cache) => {
37+
println!("Loaded cache from {:?} ({:?})", cache_file, loaded_cache.len());
38+
loaded_cache
39+
}
40+
Err(e) => {
41+
println!("Failed to load cache: {}", e);
42+
DashMap::new()
43+
}
44+
};
45+
46+
Ok(Self { cache_file, cache })
47+
}
48+
49+
/// Read cache from file
50+
fn read_cache(cache_file: &Path) -> io::Result<DashMap<K, V>> {
51+
if !cache_file.exists() {
52+
return Ok(DashMap::new());
53+
}
54+
55+
let mut file = File::open(cache_file)?;
56+
let mut buffer = Vec::new();
57+
file.read_to_end(&mut buffer)?;
58+
59+
bincode::deserialize(&buffer).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
60+
}
61+
62+
/// Write cache to file
63+
pub fn write(&self) -> io::Result<()> {
64+
let serialized = bincode::serialize(&self.cache)
65+
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
66+
67+
let mut file =
68+
OpenOptions::new().write(true).create(true).truncate(true).open(&self.cache_file)?;
69+
70+
file.write_all(&serialized)?;
71+
Ok(())
72+
}
73+
74+
/// Get a reference to the underlying DashMap
75+
pub fn inner(&self) -> &DashMap<K, V> {
76+
&self.cache
77+
}
78+
79+
pub fn get(&self, key: &K) -> Option<V>
80+
where
81+
V: Clone,
82+
{
83+
self.cache.get(key).map(|r| r.value().clone())
84+
}
85+
86+
pub fn insert(&self, key: K, value: V) {
87+
self.cache.insert(key, value);
88+
}
89+
90+
/// Clear the cache and remove the file
91+
pub fn clear(&self) -> crate::Result<()> {
92+
self.cache.clear();
93+
94+
if self.cache_file.exists() {
95+
fs::remove_file(&self.cache_file)?;
96+
}
97+
98+
Ok(())
99+
}
100+
}

Diff for: components/markdown/src/codeblock/mod.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub struct CodeBlock<'config> {
8585

8686
impl<'config> CodeBlock<'config> {
8787
pub fn new<'fence_info>(
88-
fence: FenceSettings<'fence_info>,
88+
fence: &FenceSettings<'fence_info>,
8989
config: &'config Config,
9090
// path to the current file if there is one, to point where the error is
9191
path: Option<&'config str>,
@@ -118,8 +118,8 @@ impl<'config> CodeBlock<'config> {
118118
highlighter,
119119
line_numbers: fence.line_numbers,
120120
line_number_start: fence.line_number_start,
121-
highlight_lines: fence.highlight_lines,
122-
hide_lines: fence.hide_lines,
121+
highlight_lines: fence.highlight_lines.clone(),
122+
hide_lines: fence.hide_lines.clone(),
123123
},
124124
html_start,
125125
))

0 commit comments

Comments
 (0)