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)?;