working builder for my blog

This commit is contained in:
Daniel Bulant 2025-07-08 17:51:35 +02:00
parent 7334b49d03
commit 3a782fbfa1
No known key found for this signature in database
15 changed files with 1328 additions and 74 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
target target
node_modules node_modules
assets/syntax_cache.packdump

19
Cargo.lock generated
View file

@ -204,6 +204,16 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "file-exists-macro"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d89c0a483302ef815dfa9f8615b9fc98946eb7345b11c59d9fe7381e0ae2a95"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.0" version = "1.1.0"
@ -290,6 +300,7 @@ name = "mdsvexrs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"file-exists-macro",
"itertools", "itertools",
"markdown", "markdown",
"regex", "regex",
@ -393,9 +404,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.39" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -503,9 +514,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.99" version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -6,7 +6,7 @@ edition = "2021"
[features] [features]
fancy = ["syntect/default-fancy"] fancy = ["syntect/default-fancy"]
onig = ["syntect/default-onig"] onig = ["syntect/default-onig"]
default = ["onig"] default = ["onig", "fancy"]
[dependencies] [dependencies]
markdown = { version = "1.0.0-alpha.21", features = ["serde"]} markdown = { version = "1.0.0-alpha.21", features = ["serde"]}
@ -17,3 +17,7 @@ serde = { version = "1.0.215", features = ["derive"] }
regex = "1.11.1" regex = "1.11.1"
clap = { version = "4.5.21", features = ["derive"] } clap = { version = "4.5.21", features = ["derive"] }
syntect = { version = "5.0", default-features = false } syntect = { version = "5.0", default-features = false }
file-exists-macro = "0.1"
[build-dependencies]
syntect = { version = "5.0", default-features = false, features = ["dump-create"] }

View file

@ -5,3 +5,7 @@ A faster markdown preprocessor for svelte. Compiles `.md` files into `.svelte` w
Note that this, like the original MDSvex, trusts it's input and doesn't escape HTML or script files. Note that this, like the original MDSvex, trusts it's input and doesn't escape HTML or script files.
This version is not yet tested and published. This version is not yet tested and published.
Note that not all svelte syntax is supported yet. Notably, only HTML-like content is handled. If you get invalid syntax, try moving it into a component and just referencing that component. Templates, Ifs etc are not supported.
Not all languages may be highlighted as syntect doesn't include support for all languages. Sublime syntax is supported and can be added on `Context.syntax_set`, but WASM don't have a way to set it - make an issue to embed the language instead.

1181
assets/svelte.sublime-syntax Normal file

File diff suppressed because it is too large Load diff

10
build.rs Normal file
View file

@ -0,0 +1,10 @@
use syntect::{dumps::dump_to_uncompressed_file, parsing::SyntaxSet};
fn main() {
println!("cargo:rerun-if-changed=assets");
let mut ss = SyntaxSet::load_defaults_newlines().into_builder();
ss.add_from_folder("assets", true).unwrap();
let ss = ss.build();
dump_to_uncompressed_file(&ss, "target/syntax_cache.packdump").expect("Failed to dump syntax cache");
}

View file

@ -1,5 +1,5 @@
interface Options { export interface Options {
layout: string; layout: string;
} }
@ -8,4 +8,4 @@ interface Plugin {
markup: (opts: { content: string, filename: string }) => { code: string } | undefined; markup: (opts: { content: string, filename: string }) => { code: string } | undefined;
} }
export function mdsvexrs(options): Plugin; export function mdsvexrs(options: Options): Plugin;

View file

@ -1,4 +1,4 @@
import { render } from "mdsvexrs-wasm" import * as wasm from "mdsvexrs-wasm"
export function mdsvexrs(options) { export function mdsvexrs(options) {
return { return {
@ -7,7 +7,7 @@ export function mdsvexrs(options) {
if(!filename || !filename.endsWith('.md')) return if(!filename || !filename.endsWith('.md')) return
return { return {
code: render(content, options) code: wasm.render(content, options.layout)
} }
} }
} }

View file

@ -3,11 +3,12 @@
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"types": "index.d.ts",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies": { "dependencies": {
"mdsvexrs-wasm": "0.1.0" "mdsvexrs-wasm": "file:../wasm/pkg"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",

View file

@ -201,6 +201,16 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "file-exists-macro"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d89c0a483302ef815dfa9f8615b9fc98946eb7345b11c59d9fe7381e0ae2a95"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.0" version = "1.1.0"
@ -287,6 +297,7 @@ name = "mdsvexrs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"file-exists-macro",
"itertools", "itertools",
"markdown", "markdown",
"regex", "regex",
@ -370,9 +381,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.39" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -480,9 +491,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.99" version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -0,0 +1,6 @@
{
"scripts": {
"build": "wasm-pack build --target nodejs"
},
"packageManager": "pnpm@9.5.0+sha1.8c155dc114e1689d18937974f6571e0ceee66f1d"
}

View file

@ -5,30 +5,36 @@ use mdsvexrs::Context;
#[global_allocator] #[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen] // #[wasm_bindgen]
pub struct Options { // pub struct Options {
layout: String, // layout: String,
// path: String, // // path: String,
} // }
// #[wasm_bindgen]
// impl Options {
// #[wasm_bindgen(getter)]
// pub fn layout(&self) -> String {
// self.layout.clone()
// }
// #[wasm_bindgen(setter)]
// pub fn set_layout(&mut self, layout: String) {
// self.layout = layout;
// }
// }
// #[wasm_bindgen]
// pub fn get_default_options() -> Options {
// Options {
// layout: String::new(),
// }
// }
#[wasm_bindgen] #[wasm_bindgen]
impl Options { pub fn render(contents: &str, layout: &str) -> String {
#[wasm_bindgen(getter)]
pub fn layout(&self) -> String {
self.layout.clone()
}
#[wasm_bindgen(setter)]
pub fn set_layout(&mut self, layout: String) {
self.layout = layout;
}
}
#[wasm_bindgen]
pub fn render(contents: &str, options: Options) -> String {
Context::new(mdsvexrs::MdsvexrsOptions { Context::new(mdsvexrs::MdsvexrsOptions {
layout: options.layout.to_string(), layout: layout.to_string(),
// path: options.path, // path: options.path,
}) })
.convert(contents) .convert(contents)

3
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,3 @@
packages:
- packages/vite
- packages/wasm/pkg

View file

@ -1,6 +1,6 @@
use std::{ use std::{
sync::LazyLock, sync::LazyLock,
time::{Duration, Instant}, time::{Duration/*, Instant*/},
}; };
use itertools::Itertools; use itertools::Itertools;
@ -18,11 +18,7 @@ use markdown::{
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use syntect::{ use syntect::{
easy::HighlightLines, dumps::from_uncompressed_data, easy::HighlightLines, highlighting::ThemeSet, html::{append_highlighted_html_for_styled_line, IncludeBackground}, parsing::{SyntaxDefinition, SyntaxSet}, util::LinesWithEndings
highlighting::ThemeSet,
html::{append_highlighted_html_for_styled_line, IncludeBackground},
parsing::SyntaxSet,
util::LinesWithEndings,
}; };
#[derive(Debug)] #[derive(Debug)]
@ -174,7 +170,7 @@ impl ToHtml for InlineCode {
impl ToHtml for InlineMath { impl ToHtml for InlineMath {
fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult {
todo!() ToHtmlResult::new(self.value.clone(), false)
} }
} }
@ -542,13 +538,13 @@ pub struct Context {
pub options: MdsvexrsOptions, pub options: MdsvexrsOptions,
pub titles: Vec<Title>, pub titles: Vec<Title>,
syntax_set: SyntaxSet, pub syntax_set: SyntaxSet,
theme_set: ThemeSet, pub theme_set: ThemeSet,
pub(crate) highlight_times: Duration, // pub(crate) highlight_times: Duration,
pub(crate) parse_time: Duration, // pub(crate) parse_time: Duration,
pub(crate) visit_time: Duration, // pub(crate) visit_time: Duration,
pub(crate) convert_time: Duration, // pub(crate) convert_time: Duration,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -567,7 +563,7 @@ pub struct MdsvexrsOptions {
impl Context { impl Context {
pub fn new(options: MdsvexrsOptions) -> Self { pub fn new(options: MdsvexrsOptions) -> Self {
let syntax_set = SyntaxSet::load_defaults_newlines(); let syntax_set: SyntaxSet = from_uncompressed_data(include_bytes!("../target/syntax_cache.packdump")).unwrap();
let theme_set = ThemeSet::load_defaults(); let theme_set = ThemeSet::load_defaults();
Context { Context {
@ -580,16 +576,17 @@ impl Context {
default_lang: None, default_lang: None,
script: None, script: None,
options, options,
highlight_times: Duration::ZERO, // highlight_times: Duration::ZERO,
parse_time: Duration::ZERO, // parse_time: Duration::ZERO,
visit_time: Duration::ZERO, // visit_time: Duration::ZERO,
convert_time: Duration::ZERO, // convert_time: Duration::ZERO,
} }
} }
fn highlight(&mut self, code: HighlightRequest) -> String { fn highlight(&mut self, code: HighlightRequest) -> String {
let theme = &self.theme_set.themes["InspiredGitHub"]; let theme = &self.theme_set.themes["base16-ocean.dark"];
let start = Instant::now(); // #[cfg(not(target_arch = "wasm32"))]
// let start = Instant::now();
let mut lang = &code.lang; let mut lang = &code.lang;
if lang.is_empty() { if lang.is_empty() {
@ -603,7 +600,7 @@ impl Context {
let syntax = match syntax { let syntax = match syntax {
Some(t) => t, Some(t) => t,
None => { None => {
return format!("<pre><code>{}</code></pre>", html_encode(&code.code)); return format!("<pre><code lang=\"{}\">{}</code></pre>", html_encode(lang), html_encode(&code.code));
} }
}; };
let mut highlighter = HighlightLines::new(syntax, theme); let mut highlighter = HighlightLines::new(syntax, theme);
@ -615,7 +612,7 @@ impl Context {
let regions = highlighter let regions = highlighter
.highlight_line(&code.code, &self.syntax_set) .highlight_line(&code.code, &self.syntax_set)
.unwrap(); .unwrap();
string += "<code>"; string += &format!("<code lang=\"{}\">", html_encode(lang));
append_highlighted_html_for_styled_line( append_highlighted_html_for_styled_line(
&regions[..], &regions[..],
IncludeBackground::No, IncludeBackground::No,
@ -625,7 +622,7 @@ impl Context {
string += "</code>"; string += "</code>";
} }
false => { false => {
string += "<pre>\n<code>"; string += &format!("<pre><code lang=\"{}\">", html_encode(lang));
for line in LinesWithEndings::from(&code.code) { for line in LinesWithEndings::from(&code.code) {
let regions = highlighter.highlight_line(line, &self.syntax_set).unwrap(); let regions = highlighter.highlight_line(line, &self.syntax_set).unwrap();
append_highlighted_html_for_styled_line( append_highlighted_html_for_styled_line(
@ -639,7 +636,9 @@ impl Context {
} }
}; };
self.highlight_times += start.elapsed(); // #[cfg(not(target_arch = "wasm32"))] {
// self.highlight_times += start.elapsed();
// }
string string
} }
@ -648,12 +647,18 @@ impl Context {
} }
pub fn convert(&mut self, input: &str) -> String { pub fn convert(&mut self, input: &str) -> String {
let start = Instant::now(); // #[cfg(not(target_arch = "wasm32"))]
// let start = Instant::now();
let ast = markdown::to_mdast(input, &DEFAULT_MD_OPTIONS).unwrap(); let ast = markdown::to_mdast(input, &DEFAULT_MD_OPTIONS).unwrap();
self.parse_time = start.elapsed(); // #[cfg(not(target_arch = "wasm32"))] {
let start = Instant::now(); // self.parse_time = start.elapsed();
// }
// #[cfg(not(target_arch = "wasm32"))]
// let start = Instant::now();
ast.visit(self); ast.visit(self);
self.visit_time = start.elapsed(); // #[cfg(not(target_arch = "wasm32"))] {
// self.visit_time = start.elapsed();
// }
if let Some(yaml) = &self.yaml { if let Some(yaml) = &self.yaml {
if let Some(val) = yaml.get("defaultLang") { if let Some(val) = yaml.get("defaultLang") {
@ -661,10 +666,13 @@ impl Context {
} }
} }
let start = Instant::now(); // #[cfg(not(target_arch = "wasm32"))]
// let start = Instant::now();
let res = ast.to_html(self); let res = ast.to_html(self);
let html = finish(res); let html = finish(res);
self.convert_time = start.elapsed(); // #[cfg(not(target_arch = "wasm32"))] {
// self.convert_time = start.elapsed();
// }
if let Some(yaml) = &mut self.yaml { if let Some(yaml) = &mut self.yaml {
yaml.insert( yaml.insert(
@ -695,17 +703,22 @@ impl Context {
format!( format!(
"<script context=\"module\">export const metadata = {frontmatter}</script> "<script context=\"module\">export const metadata = {frontmatter}</script>
{script} {script}
<MDXLayout {{...metadata}}> <MDXLayout {{...metadata}} {{...$$restProps}}>
{html} {html}
</MDXLayout>" </MDXLayout>"
) )
} }
// #[cfg(not(target_arch = "wasm32"))]
// pub fn print_timings(&self) {
// println!("Parse: {:?}", self.parse_time);
// println!("Visit: {:?}", self.visit_time);
// println!("Convert: {:?}", self.convert_time);
// println!("Highlight: {:?}", self.highlight_times);
// }
// #[cfg(target_arch = "wasm32")]
pub fn print_timings(&self) { pub fn print_timings(&self) {
println!("Parse: {:?}", self.parse_time); println!("wasm timings are not available");
println!("Visit: {:?}", self.visit_time);
println!("Convert: {:?}", self.convert_time);
println!("Highlight: {:?}", self.highlight_times);
} }
} }

View file

@ -1,9 +1,10 @@
use std::io::{stdin, Read}; use std::io::{stdin, Read};
use clap::Parser; use clap::{Parser, Subcommand};
use mdsvexrs::Context; use mdsvexrs::Context;
#[derive(Parser)] #[derive(Parser)]
#[command(version, about, long_about = None)]
struct Args { struct Args {
#[arg(short, long)] #[arg(short, long)]
layout: String, layout: String,
@ -13,8 +14,10 @@ struct Args {
timings: bool, timings: bool,
} }
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
let mut ctx = Context::new(mdsvexrs::MdsvexrsOptions { let mut ctx = Context::new(mdsvexrs::MdsvexrsOptions {
layout: args.layout, layout: args.layout,
// path: args.path, // path: args.path,