diff --git a/Cargo.lock b/Cargo.lock index 17554c8..b13312c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "getopts" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -226,6 +235,7 @@ dependencies = [ "anyhow", "kuchikiki", "mlua", + "pulldown-cmark", "tendril", ] @@ -572,6 +582,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.1", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quote" version = "1.0.40" @@ -877,12 +906,24 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index b611fde..c3cbe07 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.98" kuchikiki = "0.8.2" mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } +pulldown-cmark = "0.13.0" tendril = "0.4.3" diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 58a8259..21f66ba 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -1,13 +1,9 @@ -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::read_to_string; -use std::path::PathBuf; -use std::rc::Rc; +use std::{cell::RefCell, fmt::Write, fs::read_to_string, path::PathBuf, rc::Rc}; -use anyhow::Result; -use anyhow::anyhow; +use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use mlua::{Lua, Table}; +use pulldown_cmark::{Options, Parser, html}; fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result { let t = l.create_table()?; @@ -69,6 +65,30 @@ pub fn execute_lua(document: NodeRef) -> Result { Ok(document) } +pub fn process_markdown(document: NodeRef) -> Result { + let markdown_elements: Vec<_> = match document.select("markdown") { + Ok(e) => e.collect(), + Err(()) => return Err(anyhow!("Unable to find markdown elements")), + }; + for node in markdown_elements { + if let Some(text_node) = node.as_node().first_child() { + if let Some(markdown_text) = text_node.as_text() { + let borrowed_text = markdown_text.borrow(); + let parser = Parser::new_ext(&borrowed_text, Options::all()); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + let html_fragment = kuchikiki::parse_html().one(html_output); + for child in html_fragment.children() { + node.as_node().insert_before(child); + } + // Remove the original markdown node. + node.as_node().detach(); + } + } + } + Ok(document) +} + pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result { if let Some(from_node) = include_from { for i in document @@ -237,4 +257,33 @@ mod tests { assert!(d.select_first("exportelement").is_err()); assert!(d.select_first("includeelement").is_err()); } + + #[test] + fn basic_markdown() { + let page = r#" + + + + Markdown Test + + +

Hello World

+
+ +# This is a heading +This is a paragraph with **bold text** and *italic text*. + +- Item 1 +- Item 2 +- Item 3 + +
+ + "#; + let document = kuchikiki::parse_html().one(page); + let d = process_markdown(document).unwrap(); + assert!(d.select_first("markdown").is_err()); + assert!(d.select_first("p").is_ok()); + assert!(d.select_first("ul").is_ok()); + } } diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 1dfd306..eb9a043 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,24 +1,21 @@ -use super::render::execute_lua; -use super::render::expand_template; +use super::render::{execute_lua, expand_template, process_markdown}; use anyhow::Result; use kuchikiki::traits::TendrilSink; -use std::fs::read_to_string; -use std::path::{Path, PathBuf}; +use std::{ + fs::read_to_string, + path::{Path, PathBuf}, +}; 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 safe_path = Path::new(request_uri).strip_prefix("/")?; let page_path = pages.join(safe_path); - let page_string = read_to_string(&page_path)?; - let doc = kuchikiki::parse_html().one(page_string); let full_doc = expand_template(doc, &components, None)?; - - let executed_doc = execute_lua(full_doc)?; - + let markdown_doc = process_markdown(full_doc)?; + let executed_doc = execute_lua(markdown_doc)?; Ok(executed_doc.to_string()) }