From e3630208fa4390509a727a3856e5f93505831c11 Mon Sep 17 00:00:00 2001 From: zyl Date: Thu, 7 Nov 2024 14:56:13 -0800 Subject: [PATCH] add syntax highlighting for code blocks --- Cargo.lock | 145 +++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + site/config.yaml | 1 + site/pages/index.md | 2 +- site/sass/index.scss | 14 +++++ src/builder.rs | 67 +++++++++++++++++++- src/js/webdog.js | 10 +++ src/lib.rs | 17 +++++ 8 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 src/js/webdog.js diff --git a/Cargo.lock b/Cargo.lock index 978ece0..6456aa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.11" @@ -124,7 +130,7 @@ dependencies = [ "derive_builder", "diligent-date-parser", "never", - "quick-xml", + "quick-xml 0.36.2", ] [[package]] @@ -143,7 +149,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -154,6 +160,21 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -339,6 +360,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -614,6 +644,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -879,7 +919,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "headers-core", "http 0.2.12", @@ -1322,6 +1362,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "litemap" version = "0.7.3" @@ -1410,6 +1456,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -1526,6 +1581,28 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "owo-colors" version = "3.5.0" @@ -1753,6 +1830,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml 0.32.0", + "serde", + "time", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1807,6 +1903,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.36.2" @@ -1976,7 +2081,7 @@ dependencies = [ "derive_builder", "mime", "never", - "quick-xml", + "quick-xml 0.36.2", "url", ] @@ -2246,6 +2351,28 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "tera" version = "1.20.0" @@ -2727,6 +2854,7 @@ dependencies = [ "rss", "serde", "serde_yml", + "syntect", "tera", "time", "tokio", @@ -2913,6 +3041,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[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]] name = "yoke" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index fe35c32..7d40939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ rayon = "1" rss = {version = "2", features = ["validation"]} serde = {version = "1", features = ["derive"]} serde_yml = "0.0.12" +syntect = "5" tera = "1" time = {version = "0.3", features = ["serde-human-readable"]} tokio = {version = "1.10", features = [ diff --git a/site/config.yaml b/site/config.yaml index 899c165..cd39031 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -3,6 +3,7 @@ title: webdog description: "static site builder for dogs" sass_styles: [index.scss] cdn_url: "https://i.zyl.gay" +code_theme: base16-ocean.dark # options: base16-ocean.dark, base16-eighties.dark, base16-mocha.dark, base16-ocean.light, InspiredGitHub, Solarized (dark), and Solarized (light) resources: blog: diff --git a/site/pages/index.md b/site/pages/index.md index e7dd11d..356856c 100644 --- a/site/pages/index.md +++ b/site/pages/index.md @@ -10,7 +10,7 @@ extra: welcome to webdog, the static site generator fit for a dog :3 -``` +```sh git clone https://zyllian/webdog cd webdog cargo install . diff --git a/site/sass/index.scss b/site/sass/index.scss index bcf071c..e110914 100644 --- a/site/sass/index.scss +++ b/site/sass/index.scss @@ -166,3 +166,17 @@ abbr { .flex-spacer { flex-grow: 1; } + +.wd-codeblock { + position: relative; + + .copy { + position: absolute; + top: 0; + right: 0; + } + + & > pre { + padding: 8px; + } +} diff --git a/src/builder.rs b/src/builder.rs index e3ce38e..901bd80 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -6,11 +6,15 @@ use eyre::{eyre, Context, OptionExt}; use lol_html::{element, html_content::ContentType, HtmlRewriter, Settings}; use pulldown_cmark::{Options, Parser}; use serde::Serialize; +use syntect::{highlighting::ThemeSet, parsing::SyntaxSet}; use tera::Tera; use url::Url; use crate::{resource::ResourceBuilder, util, PageMetadata, Site, ROOT_PATH, SASS_PATH}; +/// Path for static webdog resources included with the site build. +const WEBDOG_PATH: &str = "webdog"; + /// Struct containing data to be sent to templates when rendering them. #[derive(Debug, Serialize)] struct TemplateData<'a, T> { @@ -33,6 +37,10 @@ struct TemplateData<'a, T> { pub struct SiteBuilder { /// The Handlebars registry used to render templates. pub(crate) tera: Tera, + /// The syntax set used to render source code. + pub(crate) syntax_set: SyntaxSet, + /// The theme set used to render source code. + pub(crate) theme_set: ThemeSet, /// The site info used to build the site. pub site: Site, /// The path to the build directory. @@ -66,6 +74,8 @@ impl SiteBuilder { Self { tera, + syntax_set: SyntaxSet::load_defaults_newlines(), + theme_set: ThemeSet::load_defaults(), resource_builders: HashMap::new(), site, build_path, @@ -92,6 +102,13 @@ impl SiteBuilder { std::fs::create_dir(&self.build_path).wrap_err("Failed to create build directory")?; } + let webdog_path = self.build_path.join(WEBDOG_PATH); + std::fs::create_dir(&webdog_path)?; + std::fs::write( + webdog_path.join("webdog.js"), + include_str!("./js/webdog.js"), + )?; + let root_path = self.site.site_path.join(ROOT_PATH); if root_path.exists() { for entry in walkdir::WalkDir::new(&root_path) { @@ -114,6 +131,10 @@ impl SiteBuilder { /// Performs actions that need to be done when the config changes while serving. pub fn reload(&mut self) -> eyre::Result<()> { + self.site + .config + .check(self) + .wrap_err("site config failed check:")?; self.resource_builders.clear(); for (prefix, config) in &self.site.config.resources { self.resource_builders @@ -152,6 +173,10 @@ impl SiteBuilder { }), element!("head", |el| { el.prepend(r#""#, ContentType::Html); + el.append( + r#""#, + ContentType::Html, + ); if self.serving { el.append(r#""#, ContentType::Html); } @@ -306,7 +331,47 @@ impl SiteBuilder { .with_context(|| format!("Failed to read page at {}", page_path.display()))?; let page = crate::frontmatter::FrontMatter::parse(input)?; - let parser = Parser::new_ext(&page.content, Options::all()); + let mut language = None; + let parser = Parser::new_ext(&page.content, Options::all()).filter_map(|event| { + // syntax highlighting for code blocks + match event { + pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock( + pulldown_cmark::CodeBlockKind::Fenced(name), + )) => { + language = Some(name); + None + } + pulldown_cmark::Event::Text(code) => { + if let Some(language) = language.take() { + let syntax_reference = self + .syntax_set + .find_syntax_by_token(&language) + .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()); + let html = format!( + r#"
+ + {} +
"#, + syntect::html::highlighted_html_for_string( + &code, + &self.syntax_set, + syntax_reference, + self.theme_set + .themes + .get(&self.site.config.code_theme) + .as_ref() + .expect("should never fail"), + ) + .expect("failed to highlight syntax") + ); + Some(pulldown_cmark::Event::Html(html.into())) + } else { + Some(pulldown_cmark::Event::Text(code)) + } + } + _ => Some(event), + } + }); let mut page_html = String::new(); pulldown_cmark::html::push_html(&mut page_html, parser); diff --git a/src/js/webdog.js b/src/js/webdog.js new file mode 100644 index 0000000..b6ab271 --- /dev/null +++ b/src/js/webdog.js @@ -0,0 +1,10 @@ +(function () { + "use strict"; + + for (const copyButton of document.querySelectorAll(".wd-codeblock .copy")) { + const source = copyButton.parentElement.querySelector("pre").innerText; + copyButton.addEventListener("click", async () => { + await navigator.clipboard.writeText(source); + }); + } +})(); diff --git a/src/lib.rs b/src/lib.rs index b1e8496..caa00ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,12 @@ pub struct SiteConfig { pub sass_styles: Vec, /// URL to the CDN used for the site's images. pub cdn_url: Url, + /// The theme to use for the site's code blocks. + /// TODO: dark/light themes + /// TODO: export themes as CSS instead of styling HTML directly + /// TODO: allow loading user themes + pub code_theme: String, + /// List of resources the site should build. pub resources: HashMap, } @@ -52,6 +58,17 @@ impl SiteConfig { pub fn cdn_url(&self, file: &str) -> eyre::Result { Ok(self.cdn_url.join(file)?) } + + /// Checks the site config for errors. + pub fn check(&self, builder: &SiteBuilder) -> eyre::Result<()> { + builder + .theme_set + .themes + .contains_key(&self.code_theme) + .then_some(()) + .ok_or_else(|| eyre::eyre!("missing code theme: {}", self.code_theme))?; + Ok(()) + } } /// Struct for the front matter in templates. (nothing here yet)