add syntax highlighting for code blocks

This commit is contained in:
zyl 2024-11-07 14:56:13 -08:00
parent a8d8096ba6
commit e3630208fa
Signed by: zyl
SSH key fingerprint: SHA256:uxxbSXbdroP/OnKBGnEDk5q7EKB2razvstC/KmzdXXs
8 changed files with 251 additions and 6 deletions

145
Cargo.lock generated
View file

@ -17,6 +17,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.11" version = "0.8.11"
@ -124,7 +130,7 @@ dependencies = [
"derive_builder", "derive_builder",
"diligent-date-parser", "diligent-date-parser",
"never", "never",
"quick-xml", "quick-xml 0.36.2",
] ]
[[package]] [[package]]
@ -143,7 +149,7 @@ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.7.4",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@ -154,6 +160,21 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 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]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -339,6 +360,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.13" version = "0.5.13"
@ -614,6 +644,16 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -879,7 +919,7 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
dependencies = [ dependencies = [
"base64", "base64 0.21.7",
"bytes", "bytes",
"headers-core", "headers-core",
"http 0.2.12", "http 0.2.12",
@ -1322,6 +1362,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.7.3" version = "0.7.3"
@ -1410,6 +1456,15 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "0.8.11"
@ -1526,6 +1581,28 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 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]] [[package]]
name = "owo-colors" name = "owo-colors"
version = "3.5.0" version = "3.5.0"
@ -1753,6 +1830,25 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 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]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -1807,6 +1903,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.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.36.2" version = "0.36.2"
@ -1976,7 +2081,7 @@ dependencies = [
"derive_builder", "derive_builder",
"mime", "mime",
"never", "never",
"quick-xml", "quick-xml 0.36.2",
"url", "url",
] ]
@ -2246,6 +2351,28 @@ dependencies = [
"syn 2.0.87", "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]] [[package]]
name = "tera" name = "tera"
version = "1.20.0" version = "1.20.0"
@ -2727,6 +2854,7 @@ dependencies = [
"rss", "rss",
"serde", "serde",
"serde_yml", "serde_yml",
"syntect",
"tera", "tera",
"time", "time",
"tokio", "tokio",
@ -2913,6 +3041,15 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 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]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.4" version = "0.7.4"

View file

@ -23,6 +23,7 @@ rayon = "1"
rss = {version = "2", features = ["validation"]} rss = {version = "2", features = ["validation"]}
serde = {version = "1", features = ["derive"]} serde = {version = "1", features = ["derive"]}
serde_yml = "0.0.12" serde_yml = "0.0.12"
syntect = "5"
tera = "1" tera = "1"
time = {version = "0.3", features = ["serde-human-readable"]} time = {version = "0.3", features = ["serde-human-readable"]}
tokio = {version = "1.10", features = [ tokio = {version = "1.10", features = [

View file

@ -3,6 +3,7 @@ title: webdog
description: "static site builder for dogs" description: "static site builder for dogs"
sass_styles: [index.scss] sass_styles: [index.scss]
cdn_url: "https://i.zyl.gay" 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: resources:
blog: blog:

View file

@ -10,7 +10,7 @@ extra:
welcome to webdog, the static site generator fit for a dog :3 welcome to webdog, the static site generator fit for a dog :3
``` ```sh
git clone https://zyllian/webdog git clone https://zyllian/webdog
cd webdog cd webdog
cargo install . cargo install .

View file

@ -166,3 +166,17 @@ abbr {
.flex-spacer { .flex-spacer {
flex-grow: 1; flex-grow: 1;
} }
.wd-codeblock {
position: relative;
.copy {
position: absolute;
top: 0;
right: 0;
}
& > pre {
padding: 8px;
}
}

View file

@ -6,11 +6,15 @@ use eyre::{eyre, Context, OptionExt};
use lol_html::{element, html_content::ContentType, HtmlRewriter, Settings}; use lol_html::{element, html_content::ContentType, HtmlRewriter, Settings};
use pulldown_cmark::{Options, Parser}; use pulldown_cmark::{Options, Parser};
use serde::Serialize; use serde::Serialize;
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
use tera::Tera; use tera::Tera;
use url::Url; use url::Url;
use crate::{resource::ResourceBuilder, util, PageMetadata, Site, ROOT_PATH, SASS_PATH}; 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. /// Struct containing data to be sent to templates when rendering them.
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct TemplateData<'a, T> { struct TemplateData<'a, T> {
@ -33,6 +37,10 @@ struct TemplateData<'a, T> {
pub struct SiteBuilder { pub struct SiteBuilder {
/// The Handlebars registry used to render templates. /// The Handlebars registry used to render templates.
pub(crate) tera: Tera, 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. /// The site info used to build the site.
pub site: Site, pub site: Site,
/// The path to the build directory. /// The path to the build directory.
@ -66,6 +74,8 @@ impl SiteBuilder {
Self { Self {
tera, tera,
syntax_set: SyntaxSet::load_defaults_newlines(),
theme_set: ThemeSet::load_defaults(),
resource_builders: HashMap::new(), resource_builders: HashMap::new(),
site, site,
build_path, build_path,
@ -92,6 +102,13 @@ impl SiteBuilder {
std::fs::create_dir(&self.build_path).wrap_err("Failed to create build directory")?; 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); let root_path = self.site.site_path.join(ROOT_PATH);
if root_path.exists() { if root_path.exists() {
for entry in walkdir::WalkDir::new(&root_path) { 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. /// Performs actions that need to be done when the config changes while serving.
pub fn reload(&mut self) -> eyre::Result<()> { pub fn reload(&mut self) -> eyre::Result<()> {
self.site
.config
.check(self)
.wrap_err("site config failed check:")?;
self.resource_builders.clear(); self.resource_builders.clear();
for (prefix, config) in &self.site.config.resources { for (prefix, config) in &self.site.config.resources {
self.resource_builders self.resource_builders
@ -152,6 +173,10 @@ impl SiteBuilder {
}), }),
element!("head", |el| { element!("head", |el| {
el.prepend(r#"<meta charset="utf-8">"#, ContentType::Html); el.prepend(r#"<meta charset="utf-8">"#, ContentType::Html);
el.append(
r#"<script type="text/javascript" src="/webdog/webdog.js" defer></script>"#,
ContentType::Html,
);
if self.serving { if self.serving {
el.append(r#"<script src="/_dev.js"></script>"#, ContentType::Html); el.append(r#"<script src="/_dev.js"></script>"#, ContentType::Html);
} }
@ -306,7 +331,47 @@ impl SiteBuilder {
.with_context(|| format!("Failed to read page at {}", page_path.display()))?; .with_context(|| format!("Failed to read page at {}", page_path.display()))?;
let page = crate::frontmatter::FrontMatter::parse(input)?; 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#"<div class="wd-codeblock">
<button class="copy">Copy</button>
{}
</div>"#,
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(); let mut page_html = String::new();
pulldown_cmark::html::push_html(&mut page_html, parser); pulldown_cmark::html::push_html(&mut page_html, parser);

10
src/js/webdog.js Normal file
View file

@ -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);
});
}
})();

View file

@ -43,6 +43,12 @@ pub struct SiteConfig {
pub sass_styles: Vec<PathBuf>, pub sass_styles: Vec<PathBuf>,
/// URL to the CDN used for the site's images. /// URL to the CDN used for the site's images.
pub cdn_url: Url, 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. /// List of resources the site should build.
pub resources: HashMap<String, ResourceBuilderConfig>, pub resources: HashMap<String, ResourceBuilderConfig>,
} }
@ -52,6 +58,17 @@ impl SiteConfig {
pub fn cdn_url(&self, file: &str) -> eyre::Result<Url> { pub fn cdn_url(&self, file: &str) -> eyre::Result<Url> {
Ok(self.cdn_url.join(file)?) 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) /// Struct for the front matter in templates. (nothing here yet)