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} - + {/.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;
{.}{#.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}