feat: add syntect code highlighting with catppuccin themes
- Add syntect dependency (server feature, fancy-regex backend) - Create highlight module with LazyLock globals for SyntaxSet + themes - Intercept CodeBlock events in markdown rendering for syntax highlighting - Update ammonia whitelist to allow span/pre/code class/style attributes - Add generate_highlight_css binary for CSS generation - Add highlight-css Makefile target (runs before tailwindcss) - Import generated highlight.css in input.css - Remove hardcoded code block colors, let catppuccin CSS take over
This commit is contained in:
parent
37d95e6a33
commit
11261836c7
135
Cargo.lock
generated
135
Cargo.lock
generated
@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@ -233,6 +239,30 @@ version = "1.8.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-set"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@ -572,6 +602,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
@ -1482,12 +1521,33 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fancy-regex"
|
||||||
|
version = "0.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
|
||||||
|
dependencies = [
|
||||||
|
"bit-set",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -2276,6 +2336,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linked-hash-map"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@ -2484,6 +2550,16 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -2780,6 +2856,19 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plist"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"indexmap",
|
||||||
|
"quick-xml",
|
||||||
|
"serde",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "postgres-protocol"
|
name = "postgres-protocol"
|
||||||
version = "0.6.11"
|
version = "0.6.11"
|
||||||
@ -2915,6 +3004,15 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.39.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@ -3437,6 +3535,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@ -3617,6 +3721,27 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syntect"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
|
||||||
|
dependencies = [
|
||||||
|
"bincode",
|
||||||
|
"fancy-regex",
|
||||||
|
"flate2",
|
||||||
|
"fnv",
|
||||||
|
"once_cell",
|
||||||
|
"plist",
|
||||||
|
"regex-syntax",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"walkdir",
|
||||||
|
"yaml-rust",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-configuration"
|
name = "system-configuration"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -4868,6 +4993,15 @@ version = "0.8.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
|
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaml-rust"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||||
|
dependencies = [
|
||||||
|
"linked-hash-map",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yggdrasil"
|
name = "yggdrasil"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -4885,6 +5019,7 @@ dependencies = [
|
|||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"syntect",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-postgres",
|
"tokio-postgres",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
|||||||
@ -22,6 +22,7 @@ rand = { version = "0.8", features = ["getrandom"] }
|
|||||||
getrandom = { version = "0.2", features = ["js"] }
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
http = "1"
|
http = "1"
|
||||||
ammonia = { version = "4", optional = true }
|
ammonia = { version = "4", optional = true }
|
||||||
|
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "default-fancy", "html", "parsing", "dump-load"], optional = true }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
web-sys = { version = "0.3", features = ["Document", "Window", "HtmlDocument", "Storage", "Element", "DomTokenList", "MediaQueryList", "HtmlScriptElement"] }
|
web-sys = { version = "0.3", features = ["Document", "Window", "HtmlDocument", "Storage", "Element", "DomTokenList", "MediaQueryList", "HtmlScriptElement"] }
|
||||||
@ -50,4 +51,5 @@ server = [
|
|||||||
"dep:tracing-subscriber",
|
"dep:tracing-subscriber",
|
||||||
"dep:tower-http",
|
"dep:tower-http",
|
||||||
"dep:ammonia",
|
"dep:ammonia",
|
||||||
|
"dep:syntect",
|
||||||
]
|
]
|
||||||
|
|||||||
6
Makefile
6
Makefile
@ -1,10 +1,14 @@
|
|||||||
.PHONY: dev build css css-watch clean build-editor
|
.PHONY: dev build css css-watch clean build-editor highlight-css
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@$(MAKE) build-editor
|
@$(MAKE) build-editor
|
||||||
|
@$(MAKE) highlight-css
|
||||||
@tailwindcss -i input.css -o public/style.css --minify
|
@tailwindcss -i input.css -o public/style.css --minify
|
||||||
@dx build --release
|
@dx build --release
|
||||||
|
|
||||||
|
highlight-css:
|
||||||
|
@cargo run --bin generate_highlight_css
|
||||||
|
|
||||||
build-editor:
|
build-editor:
|
||||||
@echo "Building Tiptap editor..."
|
@echo "Building Tiptap editor..."
|
||||||
@cd libs/tiptap-editor && npm install && npx vite build
|
@cd libs/tiptap-editor && npm install && npx vite build
|
||||||
|
|||||||
1895
generated/highlight.css
Normal file
1895
generated/highlight.css
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "./generated/highlight.css";
|
||||||
|
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@ -342,7 +343,6 @@
|
|||||||
.md-content pre {
|
.md-content pre {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: var(--content-gap-paper);
|
margin-bottom: var(--content-gap-paper);
|
||||||
background: var(--color-paper-code-block);
|
|
||||||
border-radius: var(--radius-paper);
|
border-radius: var(--radius-paper);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@ -350,7 +350,6 @@
|
|||||||
.md-content pre code {
|
.md-content pre code {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: rgb(213, 213, 214);
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@ -172,10 +172,13 @@ async fn ensure_unique_slug(
|
|||||||
fn clean_html(input: &str) -> String {
|
fn clean_html(input: &str) -> String {
|
||||||
let mut builder = ammonia::Builder::default();
|
let mut builder = ammonia::Builder::default();
|
||||||
builder
|
builder
|
||||||
.add_generic_attributes(&["class", "aria-hidden", "aria-label", "id", "role", "accesskey", "title"])
|
.add_generic_attributes(&["class", "style", "aria-hidden", "aria-label", "id", "role", "accesskey", "title"])
|
||||||
.add_tags(&["details", "summary"])
|
.add_tags(&["details", "summary"])
|
||||||
.url_relative(ammonia::UrlRelative::PassThrough)
|
.url_relative(ammonia::UrlRelative::PassThrough)
|
||||||
.add_tag_attributes("a", &["class", "aria-hidden", "aria-label"])
|
.add_tag_attributes("a", &["class", "aria-hidden", "aria-label"])
|
||||||
|
.add_tag_attributes("span", &["class", "style"])
|
||||||
|
.add_tag_attributes("pre", &["class", "style"])
|
||||||
|
.add_tag_attributes("code", &["class", "style"])
|
||||||
.add_tag_attributes("h1", &["id", "class"])
|
.add_tag_attributes("h1", &["id", "class"])
|
||||||
.add_tag_attributes("h2", &["id", "class"])
|
.add_tag_attributes("h2", &["id", "class"])
|
||||||
.add_tag_attributes("h3", &["id", "class"])
|
.add_tag_attributes("h3", &["id", "class"])
|
||||||
@ -243,12 +246,14 @@ fn render_markdown_enhanced(md: &str) -> RenderedContent {
|
|||||||
let mut html = String::new();
|
let mut html = String::new();
|
||||||
let mut heading_idx = 0;
|
let mut heading_idx = 0;
|
||||||
let mut in_heading = false;
|
let mut in_heading = false;
|
||||||
|
let mut in_codeblock = false;
|
||||||
|
let mut code_lang: Option<String> = None;
|
||||||
|
let mut code_buffer = String::new();
|
||||||
let mut non_heading_events: Vec<Event> = Vec::new();
|
let mut non_heading_events: Vec<Event> = Vec::new();
|
||||||
|
|
||||||
for event in parser {
|
for event in parser {
|
||||||
match event {
|
match event {
|
||||||
Event::Start(Tag::Heading { level, .. }) => {
|
Event::Start(Tag::Heading { level, .. }) => {
|
||||||
// Flush non-heading events first
|
|
||||||
if !non_heading_events.is_empty() {
|
if !non_heading_events.is_empty() {
|
||||||
pulldown_cmark::html::push_html(&mut html, non_heading_events.into_iter());
|
pulldown_cmark::html::push_html(&mut html, non_heading_events.into_iter());
|
||||||
non_heading_events = Vec::new();
|
non_heading_events = Vec::new();
|
||||||
@ -286,9 +291,39 @@ fn render_markdown_enhanced(md: &str) -> RenderedContent {
|
|||||||
}
|
}
|
||||||
in_heading = false;
|
in_heading = false;
|
||||||
}
|
}
|
||||||
|
Event::Start(Tag::CodeBlock(kind)) => {
|
||||||
|
if !non_heading_events.is_empty() {
|
||||||
|
pulldown_cmark::html::push_html(&mut html, non_heading_events.into_iter());
|
||||||
|
non_heading_events = Vec::new();
|
||||||
|
}
|
||||||
|
in_codeblock = true;
|
||||||
|
code_lang = match kind {
|
||||||
|
pulldown_cmark::CodeBlockKind::Fenced(lang) => {
|
||||||
|
if lang.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(lang.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
code_buffer.clear();
|
||||||
|
}
|
||||||
|
Event::Text(text) if in_codeblock => {
|
||||||
|
code_buffer.push_str(&text);
|
||||||
|
}
|
||||||
|
Event::End(TagEnd::CodeBlock) => {
|
||||||
|
let highlighted =
|
||||||
|
crate::highlight::server::highlight_code(&code_buffer, code_lang.as_deref());
|
||||||
|
html.push_str("<pre><code>");
|
||||||
|
html.push_str(&highlighted);
|
||||||
|
html.push_str("</code></pre>");
|
||||||
|
in_codeblock = false;
|
||||||
|
code_lang = None;
|
||||||
|
code_buffer.clear();
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if in_heading {
|
if in_heading {
|
||||||
// Manually render heading content
|
|
||||||
match event {
|
match event {
|
||||||
Event::Text(text) => html.push_str(&clean_html(&text)),
|
Event::Text(text) => html.push_str(&clean_html(&text)),
|
||||||
Event::Code(code) => {
|
Event::Code(code) => {
|
||||||
@ -298,7 +333,7 @@ fn render_markdown_enhanced(md: &str) -> RenderedContent {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else {
|
} else if !in_codeblock {
|
||||||
non_heading_events.push(event);
|
non_heading_events.push(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/bin/generate_highlight_css.rs
Normal file
104
src/bin/generate_highlight_css.rs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
use syntect::highlighting::ThemeSet;
|
||||||
|
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let latte = ThemeSet::get_theme("themes/Catppuccin Latte.tmTheme")
|
||||||
|
.expect("Failed to load Catppuccin Latte theme");
|
||||||
|
let mocha = ThemeSet::get_theme("themes/Catppuccin Mocha.tmTheme")
|
||||||
|
.expect("Failed to load Catppuccin Mocha theme");
|
||||||
|
|
||||||
|
let latte_css = css_for_theme_with_class_style(&latte, ClassStyle::Spaced)
|
||||||
|
.expect("Failed to generate Latte CSS");
|
||||||
|
let mocha_css = css_for_theme_with_class_style(&mocha, ClassStyle::Spaced)
|
||||||
|
.expect("Failed to generate Mocha CSS");
|
||||||
|
|
||||||
|
let latte_clean = strip_comments(&latte_css);
|
||||||
|
let mocha_clean = strip_comments(&mocha_css);
|
||||||
|
|
||||||
|
let latte_rewritten = rewrite_rules(&latte_clean, ".md-content pre code", "");
|
||||||
|
let mocha_rewritten = rewrite_rules(&mocha_clean, ".md-content pre code", ".dark ");
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
output.push_str("/* Auto-generated by generate_highlight_css — DO NOT EDIT */\n\n");
|
||||||
|
output.push_str("/* Catppuccin Latte (light) */\n");
|
||||||
|
output.push_str(&latte_rewritten);
|
||||||
|
output.push_str("\n/* Catppuccin Mocha (dark) */\n");
|
||||||
|
output.push_str(&mocha_rewritten);
|
||||||
|
|
||||||
|
std::fs::create_dir_all("generated").expect("Failed to create generated/");
|
||||||
|
std::fs::write("generated/highlight.css", output)
|
||||||
|
.expect("Failed to write generated/highlight.css");
|
||||||
|
|
||||||
|
println!("Generated generated/highlight.css");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_comments(css: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(css.len());
|
||||||
|
let chars: Vec<char> = css.chars().collect();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < chars.len() {
|
||||||
|
if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {
|
||||||
|
while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
result.push(chars[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_rules(css: &str, base: &str, prefix: &str) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut pos = 0;
|
||||||
|
let bytes = css.as_bytes();
|
||||||
|
|
||||||
|
while pos < bytes.len() {
|
||||||
|
let rest = &css[pos..];
|
||||||
|
|
||||||
|
let open = match rest.find('{') {
|
||||||
|
Some(i) => i,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
let selector = rest[..open].trim();
|
||||||
|
if selector.is_empty() {
|
||||||
|
pos += open + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let after_open = pos + open + 1;
|
||||||
|
let mut depth = 1usize;
|
||||||
|
let mut close = after_open;
|
||||||
|
while close < bytes.len() && depth > 0 {
|
||||||
|
if bytes[close] == b'{' {
|
||||||
|
depth += 1;
|
||||||
|
} else if bytes[close] == b'}' {
|
||||||
|
depth -= 1;
|
||||||
|
}
|
||||||
|
close += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = css[after_open..close - 1].trim();
|
||||||
|
pos = close;
|
||||||
|
|
||||||
|
if body.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if selector == ".code" {
|
||||||
|
out.push_str(&format!("{}{} {{\n{}\n}}\n\n", prefix, base, body));
|
||||||
|
} else {
|
||||||
|
let rewritten = selector
|
||||||
|
.split(',')
|
||||||
|
.map(|s| format!("{}{} {}", prefix, base, s.trim()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",\n");
|
||||||
|
out.push_str(&format!("{} {{\n{}\n}}\n\n", rewritten, body));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
68
src/highlight.rs
Normal file
68
src/highlight.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub mod server {
|
||||||
|
use std::sync::{LazyLock, OnceLock};
|
||||||
|
|
||||||
|
use syntect::highlighting::{Theme, ThemeSet};
|
||||||
|
use syntect::html::{ClassedHTMLGenerator, ClassStyle};
|
||||||
|
use syntect::parsing::{SyntaxReference, SyntaxSet};
|
||||||
|
use syntect::util::LinesWithEndings;
|
||||||
|
|
||||||
|
static SYNTAX_SET: LazyLock<SyntaxSet> =
|
||||||
|
LazyLock::new(|| SyntaxSet::load_defaults_newlines());
|
||||||
|
|
||||||
|
static LATTE_THEME: LazyLock<Theme> = LazyLock::new(|| {
|
||||||
|
ThemeSet::get_theme("themes/Catppuccin Latte.tmTheme")
|
||||||
|
.expect("Failed to load Catppuccin Latte theme")
|
||||||
|
});
|
||||||
|
|
||||||
|
static MOCHA_THEME: LazyLock<Theme> = LazyLock::new(|| {
|
||||||
|
ThemeSet::get_theme("themes/Catppuccin Mocha.tmTheme")
|
||||||
|
.expect("Failed to load Catppuccin Mocha theme")
|
||||||
|
});
|
||||||
|
|
||||||
|
static FALLBACK_SYNTAX: OnceLock<SyntaxReference> = OnceLock::new();
|
||||||
|
|
||||||
|
fn find_syntax(lang: Option<&str>) -> &'static SyntaxReference {
|
||||||
|
let ss = &*SYNTAX_SET;
|
||||||
|
if let Some(lang) = lang {
|
||||||
|
if !lang.is_empty() {
|
||||||
|
if let Some(s) = ss.find_syntax_by_extension(lang) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
if let Some(s) = ss.find_syntax_by_name(lang) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let plain = ss
|
||||||
|
.find_syntax_by_extension("txt")
|
||||||
|
.or_else(|| ss.find_syntax_by_name("Plain Text"))
|
||||||
|
.expect("no plain text syntax");
|
||||||
|
FALLBACK_SYNTAX.get_or_init(|| plain.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight_code(code: &str, lang: Option<&str>) -> String {
|
||||||
|
let syntax = find_syntax(lang);
|
||||||
|
let ss = &*SYNTAX_SET;
|
||||||
|
let mut generator =
|
||||||
|
ClassedHTMLGenerator::new_with_class_style(syntax, ss, ClassStyle::Spaced);
|
||||||
|
|
||||||
|
for line in LinesWithEndings::from(code) {
|
||||||
|
let _ = generator.parse_html_for_line_which_includes_newline(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
generator.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_latte_theme() -> &'static Theme {
|
||||||
|
&*LATTE_THEME
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mocha_theme() -> &'static Theme {
|
||||||
|
&*MOCHA_THEME
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_syntax_set() -> &'static SyntaxSet {
|
||||||
|
&*SYNTAX_SET
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ mod auth;
|
|||||||
mod components;
|
mod components;
|
||||||
mod context;
|
mod context;
|
||||||
mod db;
|
mod db;
|
||||||
|
mod highlight;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
mod models;
|
mod models;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user