From 38be7b73656d70e21118e1a8b7b6b21c8c20a849 Mon Sep 17 00:00:00 2001 From: Quin Date: Fri, 11 Jul 2025 16:55:49 -0600 Subject: [PATCH 1/6] Basic toml config support. --- Cargo.lock | 129 ++++++++++++++++++++++++++++++++++- htmlua-parser/Cargo.toml | 4 +- htmlua-parser/src/config.rs | 98 ++++++++++++++++++++++++++ htmlua-parser/src/helpers.rs | 10 +-- htmlua-parser/src/lib.rs | 3 +- htmlua-parser/src/render.rs | 28 +++++--- htmlua-parser/src/serve.rs | 33 +++++---- 7 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 htmlua-parser/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 8096bb3..1449110 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,27 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.60.2", +] + [[package]] name = "dtoa" version = "1.0.10" @@ -311,13 +332,16 @@ name = "htmlua-parser" version = "0.1.0" dependencies = [ "anyhow", + "dirs", "html5ever 0.35.0", "kuchikiki", "markup5ever 0.11.0", "mlua", "pulldown-cmark", + "serde", "syntect", "tendril", + "toml", ] [[package]] @@ -375,6 +399,16 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libredox" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -571,6 +605,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "2.10.1" @@ -915,6 +955,17 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.12", +] + [[package]] name = "regex-syntax" version = "0.8.5" @@ -1044,6 +1095,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "servo_arc" version = "0.1.1" @@ -1148,7 +1208,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", "yaml-rust", ] @@ -1176,7 +1236,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1190,6 +1259,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "time" version = "0.3.41" @@ -1221,6 +1301,45 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +dependencies = [ + "indexmap 2.10.0", + "serde", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "typeid" version = "1.0.3" @@ -1452,6 +1571,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" + [[package]] name = "winsafe" version = "0.0.19" diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index 28416b7..9e5018e 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -5,14 +5,16 @@ edition = "2024" [dependencies] anyhow = "1.0.98" +dirs = "6.0.0" html5ever = "0.35.0" kuchikiki = "0.8.2" markup5ever = "0.11.0" mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } pulldown-cmark = "0.13.0" +serde = { version = "1.0.219", features = ["derive"] } syntect = "5.2.0" tendril = "0.4.3" - +toml = "0.9.2" [lints.clippy] pedantic = { level = "warn", priority = -1 } diff --git a/htmlua-parser/src/config.rs b/htmlua-parser/src/config.rs new file mode 100644 index 0000000..70ea7a8 --- /dev/null +++ b/htmlua-parser/src/config.rs @@ -0,0 +1,98 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Config { + pub paths: PathConfig, + pub server: ServerConfig, + pub syntax_highlighting: SyntaxConfig, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PathConfig { + pub pages: PathBuf, + pub components: PathBuf, + pub themes: PathBuf, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ServerConfig { + pub host: String, + pub port: u16, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SyntaxConfig { + pub default_theme: String, + pub load_custom_themes: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + paths: PathConfig { + pages: PathBuf::from("/var/www/htmlua/pages"), + components: PathBuf::from("/var/www/htmlua/components"), + themes: PathBuf::from("/var/www/htmlua/themes"), + }, + server: ServerConfig { + host: "127.0.0.1".to_string(), + port: 8080, + }, + syntax_highlighting: SyntaxConfig { + default_theme: "base16-ocean.dark".to_string(), + load_custom_themes: true, + }, + } + } +} + +impl Config { + pub fn load() -> Result { + let config_path = Self::get_config_path(); + if config_path.exists() { + let config_content = fs::read_to_string(&config_path) + .with_context(|| format!("Failed to read config file: {}", config_path.display()))?; + let config: Config = toml::from_str(&config_content) + .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?; + Ok(config) + } else { + let default_config = Config::default(); + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create config directory: {}", parent.display()))?; + } + let config_content = + toml::to_string_pretty(&default_config).context("Failed to serialize default config")?; + fs::write(&config_path, config_content) + .with_context(|| format!("Failed to write default config to: {}", config_path.display()))?; + Ok(default_config) + } + } + + fn get_config_path() -> PathBuf { + if cfg!(windows) { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from("C:\\ProgramData")) + .join("htmlua") + .join("config.toml") + } else { + PathBuf::from("/etc/htmlua.toml") + } + } + + pub fn save(&self) -> Result<()> { + let config_path = Self::get_config_path(); + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create config directory: {}", parent.display()))?; + } + let config_content = toml::to_string_pretty(self).context("Failed to serialize config")?; + fs::write(&config_path, config_content) + .with_context(|| format!("Failed to write config to: {}", config_path.display()))?; + Ok(()) + } + + pub fn config_file_path() -> PathBuf { Self::get_config_path() } +} diff --git a/htmlua-parser/src/helpers.rs b/htmlua-parser/src/helpers.rs index 90439f7..bd99f81 100644 --- a/htmlua-parser/src/helpers.rs +++ b/htmlua-parser/src/helpers.rs @@ -1,15 +1,15 @@ -use std::{fs::File, io::{BufRead, BufReader, Read, Seek, SeekFrom}, path::PathBuf}; +use std::{ + fs::File, + io::{BufRead, BufReader, Read, Seek, SeekFrom}, + path::PathBuf, +}; use anyhow::Result; use kuchikiki::NodeRef; use markup5ever::{LocalName, Namespace, QualName}; use tendril::TendrilSink; - - - pub fn read_doc_from_file(path: PathBuf) -> Result { - let mut file = File::open(path)?; let mut reader = BufReader::new(&mut file); diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index cd87be2..525486e 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; +pub mod helpers; pub mod render; pub mod serve; -pub mod helpers; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 6444d60..9360f16 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, fmt::Write, fs::read_to_string, path::PathBuf, rc::Rc}; +use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc}; use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; @@ -12,7 +12,7 @@ use syntect::{ util::LinesWithEndings, }; -use crate::helpers::read_doc_from_file; +use crate::{helpers::read_doc_from_file, serve::get_config}; fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result { let t = l.create_table()?; @@ -108,7 +108,11 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from let exported = from_node .select_first(format!("exportelement.{name}").as_str()) .map_err(|()| anyhow!("Error finding exportelement"))?; - exported.as_node().children().rev().for_each(|c| i.as_node().insert_after(c)); + exported + .as_node() + .children() + .rev() + .for_each(|c| i.as_node().insert_after(c)); } } while let Ok(i) = document.select_first("includeelement") { @@ -130,8 +134,13 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from item_path.push(include_path); let new_node = read_doc_from_file(item_path)?; let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; - replaced_node.select_first("html").map_err(|()| anyhow!("Error finding html"))?.as_node().children().rev().for_each(|c| i.as_node().insert_after(c)); - + replaced_node + .select_first("html") + .map_err(|()| anyhow!("Error finding html"))? + .as_node() + .children() + .rev() + .for_each(|c| i.as_node().insert_after(c)); } } while let Ok(i) = document.select_first("include") { @@ -141,16 +150,15 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from } pub fn process_syntax_highlighting(document: NodeRef) -> Result { + let config = get_config(); let ps = SyntaxSet::load_defaults_newlines(); let mut ts = ThemeSet::load_defaults(); let syntax_elements: Vec<_> = match document.select("syntaxhighlight") { Ok(e) => e.collect(), Err(()) => return Err(anyhow!("Unable to find syntaxhighlight elements")), }; - if !syntax_elements.is_empty() { - // TODO: read from config - let themes = PathBuf::from("/var/www/htmlua/themes"); - let _ = ts.add_from_folder(themes); + if !syntax_elements.is_empty() && config.syntax_highlighting.load_custom_themes { + let _ = ts.add_from_folder(&config.paths.themes); } for node in syntax_elements { let attrs = match node.as_node().as_element() { @@ -158,7 +166,7 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { None => continue, }; let language = attrs.get("lang").unwrap_or("text"); - let theme_name = attrs.get("theme").unwrap_or("base16-ocean.dark"); + let theme_name = attrs.get("theme").unwrap_or(&config.syntax_highlighting.default_theme); if let Some(text_node) = node.as_node().first_child() { if let Some(code_text) = text_node.as_text() { let syntax = ps diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 06de0ea..9821210 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,20 +1,29 @@ -use crate::helpers::read_doc_from_file; - -use super::render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}; +use crate::{ + config::Config, + helpers::read_doc_from_file, + render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}, +}; use anyhow::Result; -use std::path::{Path, PathBuf} -; +use std::{path::Path, sync::OnceLock}; + +static CONFIG: OnceLock = OnceLock::new(); + +pub fn get_config() -> &'static Config { + CONFIG.get_or_init(|| { + Config::load().unwrap_or_else(|e| { + eprintln!("Warning: Failed to load config: {}", e); + eprintln!("Using default configuration"); + Config::default() + }) + }) +} pub fn serve_content(request_uri: &str) -> Result { - // TODO: read from config - let pages = PathBuf::from("/var/www/htmlua/pages"); - let components = PathBuf::from("/var/www/htmlua/components"); + let config = get_config(); let safe_path = Path::new(request_uri).strip_prefix("/")?; - let page_path = pages.join(safe_path); - + let page_path = config.paths.pages.join(safe_path); let doc = read_doc_from_file(page_path)?; - - let full_doc = expand_template(doc, &components, None)?; + let full_doc = expand_template(doc, &config.paths.components, None)?; let markdown_doc = process_markdown(full_doc)?; let highlighted_doc = process_syntax_highlighting(markdown_doc)?; let executed_doc = execute_lua(highlighted_doc)?; From 8dfaf0b386c7b2cceaae929d7c81e6c2c2d59ad9 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 12 Jul 2025 22:54:46 -0600 Subject: [PATCH 2/6] Added footnote support --- htmlua-parser/src/render.rs | 66 ++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 9360f16..d0fd029 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -1,7 +1,8 @@ use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc}; -use anyhow::{Result, anyhow}; +use anyhow::{anyhow, Result}; use kuchikiki::{NodeRef, traits::TendrilSink}; +use markup5ever::{LocalName, Namespace, QualName}; use mlua::{Lua, Table}; use pulldown_cmark::{Options, Parser, html}; use syntect::{ @@ -196,6 +197,29 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { Ok(document) } +pub fn generate_footnotes(document: NodeRef) -> Result { + let Ok(footnote_container) = document.select_first("footnotecontainer") else { return Ok(document) }; + + let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); + for (i, footnote) in document.select("footnote").map_err(|()| anyhow!("Failed to get footnote"))?.enumerate() { + let i = i + 1; + let fn_text = footnote.text_contents(); + let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( + format!("{i}") + ).select_first("a").map_err(|()| anyhow!("parse err"))?; + footnote.as_node().insert_after(sup_tag.as_node().clone()); + let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( + format!("

{i}: {fn_text}

") + ).select_first("p").map_err(|()| anyhow!("parse err"))?; + footnote_container.as_node().insert_before(text_tag.as_node().clone()); + } + while let Ok(i) = document.select_first("footnote") { + i.as_node().detach(); + } + footnote_container.as_node().detach(); + Ok(document) +} + #[cfg(test)] mod tests { use super::*; @@ -386,6 +410,46 @@ mod tests { assert!(d.select_first("includeelement").is_err()); } + #[test] + fn footnotes() { + let page = r" + + + + Basic HTML Page + + +
+

asdf

+

asdfum actually

+

asdf

+

asdfno

+
+
+ +
+ + "; + let document = kuchikiki::parse_html().one(page); + let d = generate_footnotes(document).unwrap(); + let sup1 = d.select_first("#ft-sup-1").unwrap(); + assert_eq!(sup1.attributes.borrow().get("title").unwrap(), "um actually"); + assert_eq!(sup1.text_contents(), "1"); + + let sup2 = d.select_first("#ft-sup-2").unwrap(); + assert_eq!(sup2.attributes.borrow().get("title").unwrap(), "no"); + assert_eq!(sup2.text_contents(), "2"); + + let ft_text_1 = d.select_first("#ft-text-1").unwrap(); + assert!(ft_text_1.text_contents().contains("um actually")); + + let ft_text_2 = d.select_first("#ft-text-2").unwrap(); + assert!(ft_text_2.text_contents().contains("no")); + + assert!(d.select_first("footnote").is_err()); + assert!(d.select_first("footnotecontainer").is_err()); + } + #[test] fn basic_markdown() { let page = r" From 3632dd5e09dcc1f8d09335c1144541ee1ab5ed63 Mon Sep 17 00:00:00 2001 From: Quin Date: Sat, 12 Jul 2025 23:54:21 -0600 Subject: [PATCH 3/6] Add import sorting options to .rustfmt.toml. --- .rustfmt.toml | 2 ++ htmlua-apache-cgi/src/main.rs | 3 ++- htmlua-parser/src/config.rs | 3 ++- htmlua-parser/src/lib.rs | 2 +- htmlua-parser/src/render.rs | 26 +++++++++++++++++--------- htmlua-parser/src/serve.rs | 6 ++++-- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 21ebfc1..25b733d 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -9,3 +9,5 @@ brace_style = "PreferSameLine" blank_lines_upper_bound = 5 combine_control_expr = false wrap_comments = false +group_imports = "StdExternalCrate" +imports_granularity = "Crate" diff --git a/htmlua-apache-cgi/src/main.rs b/htmlua-apache-cgi/src/main.rs index ab91cb1..5998ff6 100644 --- a/htmlua-apache-cgi/src/main.rs +++ b/htmlua-apache-cgi/src/main.rs @@ -1,6 +1,7 @@ -use htmlua_parser::serve::serve_content; use std::env; +use htmlua_parser::serve::serve_content; + fn main() { println!("Content-Type: text/html\n"); diff --git a/htmlua-parser/src/config.rs b/htmlua-parser/src/config.rs index 70ea7a8..ffe1320 100644 --- a/htmlua-parser/src/config.rs +++ b/htmlua-parser/src/config.rs @@ -1,6 +1,7 @@ +use std::{fs, path::PathBuf}; + use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use std::{fs, path::PathBuf}; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Config { diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index 2dc831c..d2d5599 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,5 +1,5 @@ +pub mod config; pub mod helpers; pub mod htmlua_stdlib; -pub mod config; pub mod render; pub mod serve; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index cc56eda..7046db8 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -5,7 +5,7 @@ use std::{ rc::Rc, }; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use markup5ever::{LocalName, Namespace, QualName}; use mlua::{Lua, Table}; @@ -182,19 +182,27 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { } pub fn generate_footnotes(document: NodeRef) -> Result { - let Ok(footnote_container) = document.select_first("footnotecontainer") else { return Ok(document) }; + let Ok(footnote_container) = document.select_first("footnotecontainer") else { + return Ok(document); + }; let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); - for (i, footnote) in document.select("footnote").map_err(|()| anyhow!("Failed to get footnote"))?.enumerate() { + for (i, footnote) in document + .select("footnote") + .map_err(|()| anyhow!("Failed to get footnote"))? + .enumerate() + { let i = i + 1; let fn_text = footnote.text_contents(); - let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( - format!("{i}") - ).select_first("a").map_err(|()| anyhow!("parse err"))?; + let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()) + .one(format!("{i}")) + .select_first("a") + .map_err(|()| anyhow!("parse err"))?; footnote.as_node().insert_after(sup_tag.as_node().clone()); - let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( - format!("

{i}: {fn_text}

") - ).select_first("p").map_err(|()| anyhow!("parse err"))?; + let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()) + .one(format!("

{i}: {fn_text}

")) + .select_first("p") + .map_err(|()| anyhow!("parse err"))?; footnote_container.as_node().insert_before(text_tag.as_node().clone()); } while let Ok(i) = document.select_first("footnote") { diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 9821210..715bfcc 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,10 +1,12 @@ +use std::{path::Path, sync::OnceLock}; + +use anyhow::Result; + use crate::{ config::Config, helpers::read_doc_from_file, render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}, }; -use anyhow::Result; -use std::{path::Path, sync::OnceLock}; static CONFIG: OnceLock = OnceLock::new(); From 17759c3210a88a1babbde650e565442ce169a1c6 Mon Sep 17 00:00:00 2001 From: Quin Date: Sat, 12 Jul 2025 23:56:50 -0600 Subject: [PATCH 4/6] Sort imports with the help of rustfmt. --- .rustfmt.toml | 2 ++ htmlua-apache-cgi/src/main.rs | 3 ++- htmlua-parser/src/config.rs | 3 ++- htmlua-parser/src/lib.rs | 2 +- htmlua-parser/src/render.rs | 26 +++++++++++++++++--------- htmlua-parser/src/serve.rs | 6 ++++-- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 21ebfc1..25b733d 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -9,3 +9,5 @@ brace_style = "PreferSameLine" blank_lines_upper_bound = 5 combine_control_expr = false wrap_comments = false +group_imports = "StdExternalCrate" +imports_granularity = "Crate" diff --git a/htmlua-apache-cgi/src/main.rs b/htmlua-apache-cgi/src/main.rs index ab91cb1..5998ff6 100644 --- a/htmlua-apache-cgi/src/main.rs +++ b/htmlua-apache-cgi/src/main.rs @@ -1,6 +1,7 @@ -use htmlua_parser::serve::serve_content; use std::env; +use htmlua_parser::serve::serve_content; + fn main() { println!("Content-Type: text/html\n"); diff --git a/htmlua-parser/src/config.rs b/htmlua-parser/src/config.rs index 70ea7a8..ffe1320 100644 --- a/htmlua-parser/src/config.rs +++ b/htmlua-parser/src/config.rs @@ -1,6 +1,7 @@ +use std::{fs, path::PathBuf}; + use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use std::{fs, path::PathBuf}; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Config { diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index 2dc831c..d2d5599 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,5 +1,5 @@ +pub mod config; pub mod helpers; pub mod htmlua_stdlib; -pub mod config; pub mod render; pub mod serve; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index cc56eda..7046db8 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -5,7 +5,7 @@ use std::{ rc::Rc, }; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use markup5ever::{LocalName, Namespace, QualName}; use mlua::{Lua, Table}; @@ -182,19 +182,27 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { } pub fn generate_footnotes(document: NodeRef) -> Result { - let Ok(footnote_container) = document.select_first("footnotecontainer") else { return Ok(document) }; + let Ok(footnote_container) = document.select_first("footnotecontainer") else { + return Ok(document); + }; let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); - for (i, footnote) in document.select("footnote").map_err(|()| anyhow!("Failed to get footnote"))?.enumerate() { + for (i, footnote) in document + .select("footnote") + .map_err(|()| anyhow!("Failed to get footnote"))? + .enumerate() + { let i = i + 1; let fn_text = footnote.text_contents(); - let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( - format!("{i}") - ).select_first("a").map_err(|()| anyhow!("parse err"))?; + let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()) + .one(format!("{i}")) + .select_first("a") + .map_err(|()| anyhow!("parse err"))?; footnote.as_node().insert_after(sup_tag.as_node().clone()); - let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( - format!("

{i}: {fn_text}

") - ).select_first("p").map_err(|()| anyhow!("parse err"))?; + let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()) + .one(format!("

{i}: {fn_text}

")) + .select_first("p") + .map_err(|()| anyhow!("parse err"))?; footnote_container.as_node().insert_before(text_tag.as_node().clone()); } while let Ok(i) = document.select_first("footnote") { diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 9821210..715bfcc 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,10 +1,12 @@ +use std::{path::Path, sync::OnceLock}; + +use anyhow::Result; + use crate::{ config::Config, helpers::read_doc_from_file, render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}, }; -use anyhow::Result; -use std::{path::Path, sync::OnceLock}; static CONFIG: OnceLock = OnceLock::new(); From a07d893e6e137cc3641312e95b191a018c8643db Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:26:51 -0600 Subject: [PATCH 5/6] fixed parser bug --- cspell.json | 4 +++- htmlua-parser/src/helpers.rs | 5 ++-- htmlua-parser/src/render.rs | 23 ++++++++++++++++++- .../tests/components/include_base.html | 9 ++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 htmlua-parser/tests/components/include_base.html diff --git a/cspell.json b/cspell.json index e8c260e..44ee262 100644 --- a/cspell.json +++ b/cspell.json @@ -2,7 +2,9 @@ "words": [ "htmlua", "headermap", - "reqwest" + "reqwest", + "includeelement", + "exportelement" ], "userWords": [ "mlua" diff --git a/htmlua-parser/src/helpers.rs b/htmlua-parser/src/helpers.rs index bd99f81..3db0443 100644 --- a/htmlua-parser/src/helpers.rs +++ b/htmlua-parser/src/helpers.rs @@ -6,9 +6,10 @@ use std::{ use anyhow::Result; use kuchikiki::NodeRef; -use markup5ever::{LocalName, Namespace, QualName}; +use markup5ever::{ns, LocalName, Namespace, namespace_url, QualName}; use tendril::TendrilSink; + pub fn read_doc_from_file(path: PathBuf) -> Result { let mut file = File::open(path)?; let mut reader = BufReader::new(&mut file); @@ -26,7 +27,7 @@ pub fn read_doc_from_file(path: PathBuf) -> Result { if is_whole_doc { Ok(kuchikiki::parse_html().one(page_string)) } else { - let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); + let ctx_name = QualName::new(None, ns!(), LocalName::from("div")); Ok(kuchikiki::parse_fragment(ctx_name, Vec::new()).one(page_string)) } } diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 7046db8..e141020 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -215,6 +215,7 @@ pub fn generate_footnotes(document: NodeRef) -> Result { #[cfg(test)] mod tests { use httptest::{Expectation, ServerPool, matchers::*, responders::*}; + use markup5ever::{ns, namespace_url}; use super::*; @@ -346,7 +347,6 @@ mod tests { let document = kuchikiki::parse_html().one(page); let d = execute_lua(expand_template(document, &p, None).unwrap()).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); - println!("{}", d.to_string()); assert_eq!(text, "Test from Lua!\n"); assert!(d.select_first("lua").is_err()); let text = d.select_first("#inc1").unwrap().as_node().text_contents(); @@ -404,6 +404,27 @@ mod tests { assert!(d.select_first("includeelement").is_err()); } + #[test] + fn include_base() { + let page = r#" + + title_test + body_el + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let ctx_name = QualName::new(None, ns!(html), LocalName::from("div")); + let document = kuchikiki::parse_fragment(ctx_name, Vec::new()).one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("head").unwrap().as_node().select_first("title").unwrap().as_node().text_contents(); + assert_eq!(text, "title_test"); + let text = d.select_first("#tb").unwrap().as_node().text_contents(); + assert_eq!(text, "body_el"); + assert!(d.select_first("exportelement").is_err()); + assert!(d.select_first("includeelement").is_err()); + } + #[test] fn footnotes() { let page = r" diff --git a/htmlua-parser/tests/components/include_base.html b/htmlua-parser/tests/components/include_base.html new file mode 100644 index 0000000..fa86fe6 --- /dev/null +++ b/htmlua-parser/tests/components/include_base.html @@ -0,0 +1,9 @@ + + + + + +

Hello World

+ + + From bfa5d30ae4cd56804093fefd0d91f5f18116d0bc Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:35:51 -0600 Subject: [PATCH 6/6] fixed parser bug --- htmlua-parser/src/config.rs | 1 + htmlua-parser/src/helpers.rs | 2 +- htmlua-parser/src/render.rs | 13 ++++++++++--- htmlua-parser/src/serve.rs | 10 ++++++---- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/htmlua-parser/src/config.rs b/htmlua-parser/src/config.rs index ffe1320..de734ed 100644 --- a/htmlua-parser/src/config.rs +++ b/htmlua-parser/src/config.rs @@ -95,5 +95,6 @@ impl Config { Ok(()) } + #[must_use] pub fn config_file_path() -> PathBuf { Self::get_config_path() } } diff --git a/htmlua-parser/src/helpers.rs b/htmlua-parser/src/helpers.rs index 3db0443..2992f70 100644 --- a/htmlua-parser/src/helpers.rs +++ b/htmlua-parser/src/helpers.rs @@ -6,7 +6,7 @@ use std::{ use anyhow::Result; use kuchikiki::NodeRef; -use markup5ever::{ns, LocalName, Namespace, namespace_url, QualName}; +use markup5ever::{LocalName, QualName, namespace_url, ns}; use tendril::TendrilSink; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index e141020..6a70315 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -8,7 +8,7 @@ use std::{ use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use markup5ever::{LocalName, Namespace, QualName}; -use mlua::{Lua, Table}; +use mlua::Lua; use pulldown_cmark::{Options, Parser, html}; use syntect::{ easy::HighlightLines, @@ -215,7 +215,7 @@ pub fn generate_footnotes(document: NodeRef) -> Result { #[cfg(test)] mod tests { use httptest::{Expectation, ServerPool, matchers::*, responders::*}; - use markup5ever::{ns, namespace_url}; + use markup5ever::{namespace_url, ns}; use super::*; @@ -417,7 +417,14 @@ mod tests { let ctx_name = QualName::new(None, ns!(html), LocalName::from("div")); let document = kuchikiki::parse_fragment(ctx_name, Vec::new()).one(page); let d = expand_template(document, &p, None).unwrap(); - let text = d.select_first("head").unwrap().as_node().select_first("title").unwrap().as_node().text_contents(); + let text = d + .select_first("head") + .unwrap() + .as_node() + .select_first("title") + .unwrap() + .as_node() + .text_contents(); assert_eq!(text, "title_test"); let text = d.select_first("#tb").unwrap().as_node().text_contents(); assert_eq!(text, "body_el"); diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 715bfcc..375e885 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,6 +1,6 @@ use std::{path::Path, sync::OnceLock}; -use anyhow::Result; +use anyhow::{Result, anyhow}; use crate::{ config::Config, @@ -13,7 +13,7 @@ static CONFIG: OnceLock = OnceLock::new(); pub fn get_config() -> &'static Config { CONFIG.get_or_init(|| { Config::load().unwrap_or_else(|e| { - eprintln!("Warning: Failed to load config: {}", e); + eprintln!("Warning: Failed to load config: {e}"); eprintln!("Using default configuration"); Config::default() }) @@ -24,8 +24,10 @@ pub fn serve_content(request_uri: &str) -> Result { let config = get_config(); let safe_path = Path::new(request_uri).strip_prefix("/")?; let page_path = config.paths.pages.join(safe_path); - let doc = read_doc_from_file(page_path)?; - let full_doc = expand_template(doc, &config.paths.components, None)?; + let doc = read_doc_from_file(page_path)? + .select_first("html") + .map_err(|()| anyhow!("failed to read doc"))?; + let full_doc = expand_template(doc.as_node().to_owned(), &config.paths.components, None)?; let markdown_doc = process_markdown(full_doc)?; let highlighted_doc = process_syntax_highlighting(markdown_doc)?; let executed_doc = execute_lua(highlighted_doc)?;