diff --git a/Cargo.lock b/Cargo.lock
index c1bd77d..9808599 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "anstream"
version = "0.6.15"
@@ -123,6 +132,15 @@ version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+[[package]]
+name = "cc"
+version = "1.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
+dependencies = [
+ "shlex",
+]
+
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -445,6 +463,10 @@ dependencies = [
"serde_json",
"tokio",
"toml",
+ "tree-sitter-bash",
+ "tree-sitter-highlight",
+ "tree-sitter-nix",
+ "tree-sitter-python",
"url",
]
@@ -517,9 +539,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
-version = "1.0.88"
+version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
+checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
@@ -539,6 +561,35 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@@ -582,10 +633,11 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.130"
+version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "610f75ff4a8e3cb29b85da56eabdd1bff5b06739059a4b8e2967fef32e5d9944"
+checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [
+ "indexmap",
"itoa",
"memchr",
"ryu",
@@ -601,6 +653,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
[[package]]
name = "slab"
version = "0.4.9"
@@ -617,10 +675,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
-name = "syn"
-version = "2.0.79"
+name = "streaming-iterator"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
+
+[[package]]
+name = "syn"
+version = "2.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
@@ -633,6 +697,26 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+[[package]]
+name = "thiserror"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tinyvec"
version = "1.8.0"
@@ -693,6 +777,68 @@ dependencies = [
"winnow",
]
+[[package]]
+name = "tree-sitter"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5168a515fe492af54c5cc8800ff8c840be09fa5168de45838afaecd3e008bce4"
+dependencies = [
+ "cc",
+ "regex",
+ "regex-syntax",
+ "serde_json",
+ "streaming-iterator",
+ "tree-sitter-language",
+]
+
+[[package]]
+name = "tree-sitter-bash"
+version = "0.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
+[[package]]
+name = "tree-sitter-highlight"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "457164f56e8dbbd0dc620c239bd7e2eb6025b76e4d1593a690bd4d9ed37bf168"
+dependencies = [
+ "regex",
+ "streaming-iterator",
+ "thiserror",
+ "tree-sitter",
+]
+
+[[package]]
+name = "tree-sitter-language"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8"
+
+[[package]]
+name = "tree-sitter-nix"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a8d4f8705d377d63242a075331d2d8c1dcc9828fd74aa13d7145185b3d9c004"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
+[[package]]
+name = "tree-sitter-python"
+version = "0.23.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
[[package]]
name = "unicode-bidi"
version = "0.3.17"
diff --git a/Cargo.toml b/Cargo.toml
index 47d1263..b9a5a57 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,10 @@ serde = { version = "1.0.189", default-features = false, features = ["std", "der
serde_json = "1.0.107"
tokio = { version = "1.30.0", default-features = false, features = ["rt", "rt-multi-thread", "fs", "io-util"] }
toml = "0.8.2"
+tree-sitter-bash = "0.23.3"
+tree-sitter-highlight = "0.25.2"
+tree-sitter-nix = "0.0.2"
+tree-sitter-python = "0.23.6"
url = "2.5.0"
# Optimized build for any sort of release.
diff --git a/TODO.org b/TODO.org
new file mode 100644
index 0000000..3f4c277
--- /dev/null
+++ b/TODO.org
@@ -0,0 +1,18 @@
+* Things to do [4/14]
+** DONE If the paragraph only contains an image, text-align center
+** DONE Syntax highlighting for code blocks
+** TODO Render gnuplot
+** TODO Pretty-print the timestamps
+** TODO Support Table of Contents
+** TODO Support line numbers in code blocks
+** TODO Support references to code block lines
+** TODO Only include text up to first heading on homepage and include a "read more" link
+** DONE Make loading language-specific CSS files conditional on the presence of src blocks using those languages
+** TODO Set up tracing so I can use warning and such
+** TODO Make copying of language-specific CSS files conditional on the presence of src blocks using those languages
+** TODO Switch to an entirely lazily-evaluated output tree
+** TODO Add highlighting for languages [1/2]
+*** DONE bash
+*** TODO gnuplot
+https://github.com/dpezto/tree-sitter-gnuplot is not on crates.io so I'd have to add a git dependency to use it. This would prevent publishing this crate to crates.io.
+** DONE Bug: carry over highlight starts when breaking lines
diff --git a/default_environment/stylesheet/language_bash.css b/default_environment/stylesheet/language_bash.css
new file mode 100644
index 0000000..43bbac6
--- /dev/null
+++ b/default_environment/stylesheet/language_bash.css
@@ -0,0 +1,39 @@
+:root {
+ --srclg-bash-srchl-comment-color: #048a81;
+ --srclg-bash-srchl-function-color: #e95a62;
+ --srclg-bash-srchl-keyword-color: #1a936f;
+ --srclg-bash-srchl-property-color: inherit;
+ --srclg-bash-srchl-string-color: #ecc30b;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --srclg-bash-srchl-comment-color: #fb757e;
+ --srclg-bash-srchl-function-color: #16a59d;
+ --srclg-bash-srchl-keyword-color: #e56c90;
+ --srclg-bash-srchl-property-color: inherit;
+ --srclg-bash-srchl-string-color: #133cf4;
+ }
+}
+
+.main_content {
+ .src_block {
+ &.srclg_bash {
+ .srchl_comment {
+ color: var(--srclg-bash-srchl-comment-color);
+ }
+ .srchl_function {
+ color: var(--srclg-bash-srchl-function-color);
+ }
+ .srchl_keyword {
+ color: var(--srclg-bash-srchl-keyword-color);
+ }
+ .srchl_property {
+ color: var(--srclg-bash-srchl-property-color);
+ }
+ .srchl_string {
+ color: var(--srclg-bash-srchl-string-color);
+ }
+ }
+ }
+}
diff --git a/default_environment/stylesheet/language_nix.css b/default_environment/stylesheet/language_nix.css
new file mode 100644
index 0000000..d0f1ea0
--- /dev/null
+++ b/default_environment/stylesheet/language_nix.css
@@ -0,0 +1,39 @@
+:root {
+ --srclg-nix-srchl-keyword-color: #1a936f;
+ --srclg-nix-srchl-comment-color: #048a81;
+ --srclg-nix-srchl-property-color: #bfbccb;
+ --srclg-nix-srchl-string-color: #ecc30b;
+ --srclg-nix-srchl-string-special-path-color: #067bc2;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --srclg-nix-srchl-keyword-color: #e56c90;
+ --srclg-nix-srchl-comment-color: #fb757e;
+ --srclg-nix-srchl-property-color: #404334;
+ --srclg-nix-srchl-string-color: #133cf4;
+ --srclg-nix-srchl-string-special-path-color: #f9843d;
+ }
+}
+
+.main_content {
+ .src_block {
+ &.srclg_nix {
+ .srchl_keyword {
+ color: var(--srclg-nix-srchl-keyword-color);
+ }
+ .srchl_comment {
+ color: var(--srclg-nix-srchl-comment-color);
+ }
+ .srchl_property {
+ color: var(--srclg-nix-srchl-property-color);
+ }
+ .srchl_string {
+ color: var(--srclg-nix-srchl-string-color);
+ }
+ .srchl_string_special_path {
+ color: var(--srclg-nix-srchl-string-special-path-color);
+ }
+ }
+ }
+}
diff --git a/default_environment/stylesheet/language_python.css b/default_environment/stylesheet/language_python.css
new file mode 100644
index 0000000..266d5e1
--- /dev/null
+++ b/default_environment/stylesheet/language_python.css
@@ -0,0 +1,51 @@
+/* ea912c */
+/* e95a62 */
+:root {
+ --srclg-python-srchl-comment-color: #048a81;
+ --srclg-python-srchl-function-builtin-color: #e95a62;
+ --srclg-python-srchl-keyword-color: #1a936f;
+ --srclg-python-srchl-property-color: inherit;
+ --srclg-python-srchl-string-color: #ecc30b;
+ --srclg-python-srchl-type-color: #067bc2;
+ --srclg-python-srchl-variable-color: #ea912c;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --srclg-python-srchl-comment-color: #fb757e;
+ --srclg-python-srchl-function-builtin-color: #16a59d;
+ --srclg-python-srchl-keyword-color: #e56c90;
+ --srclg-python-srchl-property-color: inherit;
+ --srclg-python-srchl-string-color: #133cf4;
+ --srclg-python-srchl-type-color: #f9843d;
+ --srclg-python-srchl-variable-color: #156ed3;
+ }
+}
+
+.main_content {
+ .src_block {
+ &.srclg_python {
+ .srchl_comment {
+ color: var(--srclg-python-srchl-comment-color);
+ }
+ .srchl_function_builtin {
+ color: var(--srclg-python-srchl-function-builtin-color);
+ }
+ .srchl_keyword {
+ color: var(--srclg-python-srchl-keyword-color);
+ }
+ .srchl_property {
+ color: var(--srclg-python-srchl-property-color);
+ }
+ .srchl_string {
+ color: var(--srclg-python-srchl-string-color);
+ }
+ .srchl_type {
+ color: var(--srclg-python-srchl-type-color);
+ }
+ .srchl_variable {
+ color: var(--srclg-python-srchl-variable-color);
+ }
+ }
+ }
+}
diff --git a/default_environment/stylesheet/main.css b/default_environment/stylesheet/main.css
index 5c5ef00..77e69ec 100644
--- a/default_environment/stylesheet/main.css
+++ b/default_environment/stylesheet/main.css
@@ -9,9 +9,8 @@
--blog-post-background-color: #0a0a0a;
- --src-font-family:
- ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
- "DejaVu Sans Mono", monospace;
+ --src-font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo,
+ Consolas, "DejaVu Sans Mono", monospace;
--src-block-background-color: #141414;
--src-block-border-color: #84828f;
@@ -56,9 +55,8 @@
body {
color: var(--site-text-color);
background-color: var(--site-background-color);
- font-family:
- source-sans-pro, Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans",
- sans-serif;
+ font-family: source-sans-pro, Seravek, "Gill Sans Nova", Ubuntu, Calibri,
+ "DejaVu Sans", sans-serif;
a:link,
a:visited {
@@ -161,6 +159,7 @@ body {
font-size: 1rem;
font-family: var(--src-font-family);
margin: 1rem 0;
+ font-variant-ligatures: none;
.src_language {
display: inline-block;
@@ -185,11 +184,13 @@ body {
.inline_source_block {
font-family: var(--src-font-family);
font-size: 1.2rem;
+ font-variant-ligatures: none;
}
.code,
.verbatim {
font-family: var(--src-font-family);
+ font-variant-ligatures: none;
}
.quote_block {
diff --git a/default_environment/templates/html/src_block.dust b/default_environment/templates/html/src_block.dust
index f0a6357..b7fac67 100644
--- a/default_environment/templates/html/src_block.dust
+++ b/default_environment/templates/html/src_block.dust
@@ -1,10 +1,15 @@
-
+
{?.language}
{.language}
{/.language}
{#.lines}
- {.} |
+ {#.children}{@select key=.type}
+ {@eq value="raw_text"}{.content} {/eq}
+ {@eq value="highlight_start"}{/eq}
+ {@eq value="highlight_end"}{/eq}
+ {@none}{!TODO: make this panic!}ERROR: Unrecognized type {.type}.{/none}
+ {/select}{/.children} |
{/.lines}
diff --git a/src/context/blog_post_page.rs b/src/context/blog_post_page.rs
index 9dce558..eb81bd9 100644
--- a/src/context/blog_post_page.rs
+++ b/src/context/blog_post_page.rs
@@ -1,3 +1,5 @@
+use std::collections::HashSet;
+
use serde::Serialize;
use super::render_context::RenderContext;
@@ -51,28 +53,6 @@ render!(
render_context,
{
push_file!(render_context, &original.page.src, {
- let css_files = vec![
- get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "stylesheet/reset.css",
- )?,
- get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "stylesheet/main.css",
- )?,
- ];
- let js_files = vec![get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "blog_post.js",
- )?];
- let global_settings =
- GlobalSettings::new(original.page.title.clone(), css_files, js_files);
let page_header = PageHeader::new(
render_context.config.get_site_title().map(str::to_string),
Some(get_web_path(
@@ -114,6 +94,46 @@ render!(
ret
};
+ let mut css_files = vec![
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "stylesheet/reset.css",
+ )?,
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "stylesheet/main.css",
+ )?,
+ ];
+ let additional_css_files = render_context
+ .dependency_manager
+ .lock()
+ .unwrap()
+ .list_css()?
+ .map(|css_name| {
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ format!("stylesheet/{}", css_name),
+ )
+ })
+ .collect::, _>>()?;
+ css_files.extend(additional_css_files.into_iter());
+
+ let js_files = vec![get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "blog_post.js",
+ )?];
+
+ let global_settings =
+ GlobalSettings::new(original.page.title.clone(), css_files, js_files);
+
let ret = RenderBlogPostPage {
global_settings,
page_header: Some(page_header),
diff --git a/src/context/blog_stream.rs b/src/context/blog_stream.rs
index fac3606..635575e 100644
--- a/src/context/blog_stream.rs
+++ b/src/context/blog_stream.rs
@@ -1,3 +1,5 @@
+use std::collections::HashSet;
+
use serde::Serialize;
use super::macros::render;
@@ -49,31 +51,6 @@ render!(
original,
render_context,
{
- let css_files = vec![
- get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "stylesheet/reset.css",
- )?,
- get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "stylesheet/main.css",
- )?,
- ];
- let js_files = vec![get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "blog_post.js",
- )?];
- let global_settings = GlobalSettings::new(
- render_context.config.get_site_title().map(str::to_string),
- css_files,
- js_files,
- );
let page_header = PageHeader::new(
render_context.config.get_site_title().map(str::to_string),
Some(get_web_path(
@@ -105,6 +82,49 @@ render!(
None
};
+ let mut css_files = vec![
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "stylesheet/reset.css",
+ )?,
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "stylesheet/main.css",
+ )?,
+ ];
+ let additional_css_files = render_context
+ .dependency_manager
+ .lock()
+ .unwrap()
+ .list_css()?
+ .map(|css_name| {
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ format!("stylesheet/{}", css_name),
+ )
+ })
+ .collect::, _>>()?;
+ css_files.extend(additional_css_files.into_iter());
+
+ let js_files = vec![get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "blog_post.js",
+ )?];
+
+ let global_settings = GlobalSettings::new(
+ render_context.config.get_site_title().map(str::to_string),
+ css_files,
+ js_files,
+ );
+
Ok(RenderBlogStream {
global_settings,
page_header: Some(page_header),
diff --git a/src/context/dependency.rs b/src/context/dependency.rs
index e79063f..9b42aaf 100644
--- a/src/context/dependency.rs
+++ b/src/context/dependency.rs
@@ -7,6 +7,7 @@ use super::RenderContext;
#[derive(Debug)]
pub(crate) enum Dependency {
StaticFile { absolute_path: PathBuf },
+ CssFile { name: String },
}
impl Dependency {
@@ -40,6 +41,11 @@ impl Dependency {
}
Ok(())
}
+ Dependency::CssFile { name: _ } => {
+ // We don't do anything because the CSS files are already copied into the output from natter's default environment.
+ // TODO: When we add support for CSS outside the default environment, we should add support for dynamically picking which ones to copy here.
+ Ok(())
+ }
}
}
}
diff --git a/src/context/dependency_manager.rs b/src/context/dependency_manager.rs
index a5355b6..7667c47 100644
--- a/src/context/dependency_manager.rs
+++ b/src/context/dependency_manager.rs
@@ -65,4 +65,20 @@ impl DependencyManager {
std::mem::swap(&mut self.dependencies, &mut dependencies);
dependencies
}
+
+ pub(crate) fn include_css(&mut self, name: N) -> Result<(), CustomError>
+ where
+ std::string::String: From,
+ {
+ self.dependencies
+ .push(Dependency::CssFile { name: name.into() });
+ Ok(())
+ }
+
+ pub(crate) fn list_css(&self) -> Result, CustomError> {
+ Ok(self.dependencies.iter().filter_map(|dep| match dep {
+ Dependency::CssFile { name } => Some(name),
+ _ => None,
+ }))
+ }
}
diff --git a/src/context/page.rs b/src/context/page.rs
index cd8e5f0..be4108d 100644
--- a/src/context/page.rs
+++ b/src/context/page.rs
@@ -1,3 +1,5 @@
+use std::collections::HashSet;
+
use super::footnote_definition::RenderRealFootnoteDefinition;
use super::macros::render;
use super::render_context::RenderContext;
@@ -30,27 +32,6 @@ pub(crate) struct RenderPage {
render!(RenderPage, IPage, original, render_context, {
push_file!(render_context, &original.src, {
- let css_files = vec![
- get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "stylesheet/reset.css",
- )?,
- get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "stylesheet/main.css",
- )?,
- ];
- let js_files = vec![get_web_path(
- render_context.config,
- render_context.output_root_directory,
- render_context.output_file,
- "blog_post.js",
- )?];
- let global_settings = GlobalSettings::new(original.title.clone(), css_files, js_files);
let page_header = PageHeader::new(
render_context.config.get_site_title().map(str::to_string),
Some(get_web_path(
@@ -92,6 +73,45 @@ render!(RenderPage, IPage, original, render_context, {
ret
};
+ let mut css_files = vec![
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "stylesheet/reset.css",
+ )?,
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "stylesheet/main.css",
+ )?,
+ ];
+ let additional_css_files = render_context
+ .dependency_manager
+ .lock()
+ .unwrap()
+ .list_css()?
+ .map(|css_name| {
+ get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ format!("stylesheet/{}", css_name),
+ )
+ })
+ .collect::, _>>()?;
+ css_files.extend(additional_css_files.into_iter());
+
+ let js_files = vec![get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ "blog_post.js",
+ )?];
+
+ let global_settings = GlobalSettings::new(original.title.clone(), css_files, js_files);
+
let ret = RenderPage {
global_settings,
page_header: Some(page_header),
diff --git a/src/context/src_block.rs b/src/context/src_block.rs
index 2f6298d..cfc98dc 100644
--- a/src/context/src_block.rs
+++ b/src/context/src_block.rs
@@ -3,6 +3,7 @@ use serde::Serialize;
use super::render_context::RenderContext;
use crate::error::CustomError;
use crate::intermediate::ISrcBlock;
+use crate::intermediate::ISrcSegment;
use super::macros::render;
@@ -10,15 +11,85 @@ use super::macros::render;
#[serde(tag = "type")]
#[serde(rename = "src_block")]
pub(crate) struct RenderSrcBlock {
- lines: Vec,
+ lines: Vec,
language: Option,
post_blank: organic::types::PostBlank,
}
-render!(RenderSrcBlock, ISrcBlock, original, _render_context, {
+#[derive(Debug, Serialize)]
+pub(crate) struct RenderSrcLine {
+ children: Vec,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(tag = "type")]
+pub(crate) enum RenderSrcSegment {
+ #[serde(rename = "raw_text")]
+ RawText { content: String },
+
+ #[serde(rename = "highlight_start")]
+ HighlightStart { name: String },
+
+ #[serde(rename = "highlight_end")]
+ HighlightEnd,
+}
+
+render!(RenderSrcBlock, ISrcBlock, original, render_context, {
+ let lines = original
+ .lines
+ .iter()
+ .map(|original_line| {
+ let children = original_line
+ .children
+ .iter()
+ .map(|original_segment| match original_segment {
+ ISrcSegment::RawText(body) => RenderSrcSegment::RawText {
+ content: body.to_owned(),
+ },
+ ISrcSegment::HighlightStart { name } => RenderSrcSegment::HighlightStart {
+ name: css_safe_name(name),
+ },
+ ISrcSegment::HighlightEnd => RenderSrcSegment::HighlightEnd,
+ })
+ .collect();
+ RenderSrcLine { children }
+ })
+ .collect();
+ match original.language.as_deref() {
+ Some("bash") => {
+ render_context
+ .dependency_manager
+ .lock()
+ .unwrap()
+ .include_css("language_bash.css")?;
+ }
+ Some("nix") => {
+ render_context
+ .dependency_manager
+ .lock()
+ .unwrap()
+ .include_css("language_nix.css")?;
+ }
+ Some("python") => {
+ render_context
+ .dependency_manager
+ .lock()
+ .unwrap()
+ .include_css("language_python.css")?;
+ }
+ _ => {}
+ };
Ok(RenderSrcBlock {
- lines: original.lines.clone(),
+ lines,
language: original.language.clone(),
post_blank: original.post_blank,
})
});
+
+fn css_safe_name(inp: S) -> String
+where
+ std::string::String: From,
+{
+ let inp: String = inp.into();
+ inp.replace(".", "_")
+}
diff --git a/src/intermediate/footnote_definition.rs b/src/intermediate/footnote_definition.rs
index 38085bf..f82753e 100644
--- a/src/intermediate/footnote_definition.rs
+++ b/src/intermediate/footnote_definition.rs
@@ -32,6 +32,7 @@ pub(crate) struct IRealFootnoteDefinition {
}
impl IRealFootnoteDefinition {
+ #[allow(clippy::needless_lifetimes)]
pub(crate) async fn new<'orig, 'parse>(
_intermediate_context: IntermediateContext<'orig, 'parse>,
footnote_id: usize,
diff --git a/src/intermediate/macros.rs b/src/intermediate/macros.rs
index 54f877e..203355d 100644
--- a/src/intermediate/macros.rs
+++ b/src/intermediate/macros.rs
@@ -9,6 +9,7 @@ macro_rules! inoop {
}
impl $istruct {
+ #[allow(clippy::extra_unused_lifetimes)]
pub(crate) async fn new<'reg, 'orig, 'parse>(
_intermediate_context: crate::intermediate::IntermediateContext<'orig, 'parse>,
original: &'orig organic::types::$pstruct<'parse>,
diff --git a/src/intermediate/mod.rs b/src/intermediate/mod.rs
index 44c257f..41950f1 100644
--- a/src/intermediate/mod.rs
+++ b/src/intermediate/mod.rs
@@ -127,6 +127,7 @@ pub(crate) use regular_link::LinkTarget;
pub(crate) use section::ISection;
pub(crate) use special_block::ISpecialBlock;
pub(crate) use src_block::ISrcBlock;
+pub(crate) use src_block::ISrcSegment;
pub(crate) use statistics_cookie::IStatisticsCookie;
pub(crate) use strike_through::IStrikeThrough;
pub(crate) use subscript::ISubscript;
diff --git a/src/intermediate/paragraph.rs b/src/intermediate/paragraph.rs
index b9e134c..6b022b6 100644
--- a/src/intermediate/paragraph.rs
+++ b/src/intermediate/paragraph.rs
@@ -1,5 +1,3 @@
-use std::any::Any;
-
use super::macros::intermediate;
use super::IObject;
use crate::error::CustomError;
@@ -33,6 +31,7 @@ intermediate!(
);
impl IParagraph {
+ #[allow(clippy::needless_lifetimes)]
pub(crate) async fn artificial<'orig, 'parse>(
_intermediate_context: crate::intermediate::IntermediateContext<'orig, 'parse>,
children: Vec,
@@ -52,19 +51,19 @@ impl IParagraph {
.children
.iter()
.filter(|c| match c {
- IObject::RegularLink(iregular_link) => match &iregular_link.target {
- super::LinkTarget::Image { src, alt } => true,
- _ => false,
- },
+ IObject::RegularLink(iregular_link) => matches!(
+ &iregular_link.target,
+ super::LinkTarget::Image { src: _, alt: _ }
+ ),
_ => false,
})
.count();
num_images == 1
&& self.children.iter().all(|c| match c {
- IObject::RegularLink(iregular_link) => match &iregular_link.target {
- super::LinkTarget::Image { src, alt } => true,
- _ => false,
- },
+ IObject::RegularLink(iregular_link) => matches!(
+ &iregular_link.target,
+ super::LinkTarget::Image { src: _, alt: _ }
+ ),
IObject::PlainText(iplain_text) => {
iplain_text.source.chars().all(|c| c.is_ascii_whitespace())
}
diff --git a/src/intermediate/registry.rs b/src/intermediate/registry.rs
index f9335b7..30d5830 100644
--- a/src/intermediate/registry.rs
+++ b/src/intermediate/registry.rs
@@ -180,6 +180,7 @@ async fn convert_definition_contents<'orig, 'parse>(
}
/// Take a footnote definition that has not yet received a reference and move it into the active footnotes.
+#[allow(clippy::needless_lifetimes)]
pub(crate) async fn promote_footnote_definition<'orig, 'parse>(
intermediate_context: IntermediateContext<'orig, 'parse>,
label: &'parse str,
diff --git a/src/intermediate/regular_link.rs b/src/intermediate/regular_link.rs
index 63544e0..8f384b6 100644
--- a/src/intermediate/regular_link.rs
+++ b/src/intermediate/regular_link.rs
@@ -186,7 +186,7 @@ impl LinkTarget {
if path.is_absolute() {
return Ok(format!("file://{}", input));
}
- return Ok(input.into_owned());
+ Ok(input.into_owned())
}
/// Get file name from the last segment of an image path.
@@ -199,7 +199,7 @@ impl LinkTarget {
let path = Path::new(input.as_ref());
match path
.components()
- .last()
+ .next_back()
.ok_or("Images should have at least one component in their path.")?
{
std::path::Component::Prefix(_) => {
@@ -209,9 +209,7 @@ impl LinkTarget {
std::path::Component::RootDir
| std::path::Component::CurDir
| std::path::Component::ParentDir => {
- return Err(
- "Final component of an image path should be a normal component.".into(),
- );
+ Err("Final component of an image path should be a normal component.".into())
}
std::path::Component::Normal(file_name) => Ok(file_name
.to_str()
@@ -236,6 +234,7 @@ mod tests {
let registry = Registry::new();
let registry = Arc::new(Mutex::new(registry));
let intermediate_context = IntermediateContext::new(registry)?;
+ #[allow(clippy::single_element_loop)]
for (inp, typ) in [(
"https://test.example/foo",
LinkType::Protocol(Cow::from("https")),
diff --git a/src/intermediate/src_block.rs b/src/intermediate/src_block.rs
index b0d948b..e13c518 100644
--- a/src/intermediate/src_block.rs
+++ b/src/intermediate/src_block.rs
@@ -1,14 +1,31 @@
+use std::borrow::Borrow;
+
use super::macros::intermediate;
use crate::error::CustomError;
use organic::types::StandardProperties;
+use tree_sitter_highlight::HighlightConfiguration;
+use tree_sitter_highlight::HighlightEvent;
+use tree_sitter_highlight::Highlighter;
#[derive(Debug, Clone)]
pub(crate) struct ISrcBlock {
- pub(crate) lines: Vec,
+ pub(crate) lines: Vec,
pub(crate) language: Option,
pub(crate) post_blank: organic::types::PostBlank,
}
+#[derive(Debug, Clone)]
+pub(crate) struct ISrcLine {
+ pub(crate) children: Vec,
+}
+
+#[derive(Debug, Clone)]
+pub(crate) enum ISrcSegment {
+ RawText(String),
+ HighlightStart { name: String },
+ HighlightEnd,
+}
+
intermediate!(
ISrcBlock,
&'orig organic::types::SrcBlock<'parse>,
@@ -59,14 +76,66 @@ intermediate!(
})
.collect();
let language = original.language.map(str::to_owned);
+
+ match language.as_deref() {
+ Some(lang @ "bash") => {
+ let highlighted = highlight_bash(&lines);
+ if let Ok(highlighted) = highlighted {
+ return Ok(ISrcBlock {
+ lines: highlighted,
+ language,
+ post_blank: original.get_post_blank(),
+ });
+ } else {
+ println!("Warning: Failed to highlight {} source.", lang);
+ }
+ }
+ Some(lang @ "nix") => {
+ let highlighted = highlight_nix(&lines);
+ if let Ok(highlighted) = highlighted {
+ return Ok(ISrcBlock {
+ lines: highlighted,
+ language,
+ post_blank: original.get_post_blank(),
+ });
+ } else {
+ println!("Warning: Failed to highlight {} source.", lang);
+ }
+ }
+ Some(lang @ "python") => {
+ let highlighted = highlight_python(&lines);
+ if let Ok(highlighted) = highlighted {
+ return Ok(ISrcBlock {
+ lines: highlighted,
+ language,
+ post_blank: original.get_post_blank(),
+ });
+ } else {
+ println!("Warning: Failed to highlight {} source.", lang);
+ }
+ }
+ Some(lang) => {
+ println!("Warning: No highlighting for language: {}", lang);
+ }
+ _ => {}
+ };
+ let highlighted = highlight_plain(&lines)?;
Ok(ISrcBlock {
- lines,
+ lines: highlighted,
language,
post_blank: original.get_post_blank(),
})
}
);
+impl ISrcLine {
+ pub(crate) fn new() -> ISrcLine {
+ ISrcLine {
+ children: Vec::new(),
+ }
+ }
+}
+
fn ascii_whitespace_value(c: char) -> usize {
match c {
' ' => 1,
@@ -76,3 +145,161 @@ fn ascii_whitespace_value(c: char) -> usize {
_ => unreachable!("Only ascii whitespace can reach this code."),
}
}
+
+fn highlight_plain(lines: &[L]) -> Result, CustomError>
+where
+ std::string::String: for<'a> From<&'a L>,
+{
+ Ok(lines
+ .iter()
+ .map(|l| {
+ let mut line = ISrcLine::new();
+ line.children.push(ISrcSegment::RawText(l.into()));
+ line
+ })
+ .collect())
+}
+
+fn highlight_tree_sitter(
+ config: HighlightConfiguration,
+ highlight_names: &[&str],
+ lines: &[L],
+) -> Result, CustomError>
+where
+ L: Borrow,
+{
+ let combined_text = lines.join("");
+
+ // Need 1 highlighter per thread
+ let mut highlighter = Highlighter::new();
+
+ let highlights = highlighter
+ .highlight(&config, combined_text.as_bytes(), None, |_| None)
+ .unwrap();
+
+ let mut highlighted_text: Vec = Vec::with_capacity(lines.len());
+ let mut current_line = ISrcLine::new();
+ let mut highlight_stack: Vec<&str> = Vec::new();
+ for event in highlights {
+ match event.unwrap() {
+ HighlightEvent::Source { start, end } => {
+ let mut span = &combined_text[start..end];
+ while let Some(line_break_index) = span.find('\n') {
+ let first_line = &span[..(line_break_index + 1)];
+ current_line
+ .children
+ .push(ISrcSegment::RawText(first_line.to_owned()));
+ current_line.children.extend(
+ highlight_stack
+ .iter()
+ .map(|_name| ISrcSegment::HighlightEnd),
+ );
+ highlighted_text.push(current_line);
+ current_line = ISrcLine::new();
+ current_line
+ .children
+ .extend(
+ highlight_stack
+ .iter()
+ .map(|name| ISrcSegment::HighlightStart {
+ name: (*name).into(),
+ }),
+ );
+ span = &span[(line_break_index + 1)..];
+ }
+ if !span.is_empty() {
+ current_line
+ .children
+ .push(ISrcSegment::RawText(span.to_owned()));
+ }
+ }
+ HighlightEvent::HighlightStart(s) => {
+ highlight_stack.push(highlight_names[s.0]);
+ current_line.children.push(ISrcSegment::HighlightStart {
+ name: highlight_names[s.0].into(),
+ });
+ }
+ HighlightEvent::HighlightEnd => {
+ highlight_stack.pop();
+ current_line.children.push(ISrcSegment::HighlightEnd);
+ }
+ }
+ }
+
+ debug_assert!(highlight_stack.is_empty());
+
+ Ok(highlighted_text)
+}
+
+fn highlight_bash(lines: &[L]) -> Result, CustomError>
+where
+ L: Borrow,
+{
+ let highlight_names = ["comment", "function", "keyword", "property", "string"];
+ let language = tree_sitter_bash::LANGUAGE.into();
+ let mut config =
+ HighlightConfiguration::new(language, "bash", tree_sitter_bash::HIGHLIGHT_QUERY, "", "")
+ .unwrap();
+ config.configure(&highlight_names);
+ highlight_tree_sitter(config, &highlight_names, lines)
+}
+
+fn highlight_nix(lines: &[L]) -> Result, CustomError>
+where
+ L: Borrow,
+{
+ let highlight_names = [
+ "comment",
+ "keyword",
+ "property",
+ "string",
+ "string.special.path",
+ // "string.special.uri",
+ ];
+ let language = tree_sitter_nix::LANGUAGE.into();
+ let mut config =
+ HighlightConfiguration::new(language, "nix", tree_sitter_nix::HIGHLIGHTS_QUERY, "", "")
+ .unwrap();
+ config.configure(&highlight_names);
+ highlight_tree_sitter(config, &highlight_names, lines)
+}
+
+fn highlight_python(lines: &[L]) -> Result, CustomError>
+where
+ L: Borrow,
+{
+ let highlight_names = [
+ "comment",
+ "function.builtin",
+ "keyword",
+ "property",
+ "string",
+ "type",
+ "variable",
+ ];
+ let language = tree_sitter_python::LANGUAGE.into();
+ let mut config = HighlightConfiguration::new(
+ language,
+ "python",
+ tree_sitter_python::HIGHLIGHTS_QUERY,
+ "",
+ "",
+ )
+ .unwrap();
+ config.configure(&highlight_names);
+ highlight_tree_sitter(config, &highlight_names, lines)
+}
+
+// use tree_sitter::Parser;
+// fn dump_nix(body: B) -> Result<(), CustomError>
+// where
+// B: AsRef,
+// {
+// let mut parser = Parser::new();
+// parser
+// .set_language(&tree_sitter_nix::LANGUAGE.into())
+// .expect("Error loading Nix grammar");
+// let mut tree = parser.parse(body.as_ref(), None).unwrap();
+// println!("{}", tree.root_node());
+// Ok(())
+// }
diff --git a/src/intermediate/timestamp.rs b/src/intermediate/timestamp.rs
index 8df50f4..8e2ca2a 100644
--- a/src/intermediate/timestamp.rs
+++ b/src/intermediate/timestamp.rs
@@ -1,4 +1,3 @@
-use super::macros::inoop;
use super::macros::intermediate;
use super::util::coalesce_whitespace;
use crate::error::CustomError;