Merge pull request #7 from trypsynth/markdown

Added support for <markdown>. Closes #4.
This commit is contained in:
Grace Yoder 2025-07-05 22:09:35 -06:00 committed by GitHub
commit 3897b8dd30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 105 additions and 17 deletions

41
Cargo.lock generated
View file

@ -169,6 +169,15 @@ dependencies = [
"byteorder", "byteorder",
] ]
[[package]]
name = "getopts"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.1.16" version = "0.1.16"
@ -226,6 +235,7 @@ dependencies = [
"anyhow", "anyhow",
"kuchikiki", "kuchikiki",
"mlua", "mlua",
"pulldown-cmark",
"tendril", "tendril",
] ]
@ -572,6 +582,25 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@ -877,12 +906,24 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-width"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"

View file

@ -7,6 +7,7 @@ edition = "2024"
anyhow = "1.0.98" anyhow = "1.0.98"
kuchikiki = "0.8.2" kuchikiki = "0.8.2"
mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] }
pulldown-cmark = "0.13.0"
tendril = "0.4.3" tendril = "0.4.3"

View file

@ -1,13 +1,9 @@
use std::cell::RefCell; use std::{cell::RefCell, fmt::Write, fs::read_to_string, path::PathBuf, rc::Rc};
use std::fmt::Write;
use std::fs::read_to_string;
use std::path::PathBuf;
use std::rc::Rc;
use anyhow::Result; use anyhow::{Result, anyhow};
use anyhow::anyhow;
use kuchikiki::{NodeRef, traits::TendrilSink}; use kuchikiki::{NodeRef, traits::TendrilSink};
use mlua::{Lua, Table}; use mlua::{Lua, Table};
use pulldown_cmark::{Options, Parser, html};
fn create_htmlua_stdlib(l: &Lua, stdout: &Rc<RefCell<String>>) -> mlua::Result<Table> { fn create_htmlua_stdlib(l: &Lua, stdout: &Rc<RefCell<String>>) -> mlua::Result<Table> {
let t = l.create_table()?; let t = l.create_table()?;
@ -69,6 +65,30 @@ pub fn execute_lua(document: NodeRef) -> Result<NodeRef> {
Ok(document) Ok(document)
} }
pub fn process_markdown(document: NodeRef) -> Result<NodeRef> {
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<NodeRef> { pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result<NodeRef> {
if let Some(from_node) = include_from { if let Some(from_node) = include_from {
for i in document for i in document
@ -269,4 +289,33 @@ mod tests {
assert!(d.select_first("exportelement").is_err()); assert!(d.select_first("exportelement").is_err());
assert!(d.select_first("includeelement").is_err()); assert!(d.select_first("includeelement").is_err());
} }
#[test]
fn basic_markdown() {
let page = r#"
<!DOCTYPE html>
<html>
<head>
<title>Markdown Test</title>
</head>
<body>
<h1>Hello World</h1>
<div>
<markdown>
# This is a heading
This is a paragraph with **bold text** and *italic text*.
- Item 1
- Item 2
- Item 3
</markdown>
</div>
</body>
</html>"#;
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());
}
} }

View file

@ -1,24 +1,21 @@
use super::render::execute_lua; use super::render::{execute_lua, expand_template, process_markdown};
use super::render::expand_template;
use anyhow::Result; use anyhow::Result;
use kuchikiki::traits::TendrilSink; use kuchikiki::traits::TendrilSink;
use std::fs::read_to_string; use std::{
use std::path::{Path, PathBuf}; fs::read_to_string,
path::{Path, PathBuf},
};
pub fn serve_content(request_uri: &str) -> Result<String> { pub fn serve_content(request_uri: &str) -> Result<String> {
// TODO: read from config // TODO: read from config
let pages = PathBuf::from("/var/www/htmlua/pages"); let pages = PathBuf::from("/var/www/htmlua/pages");
let components = PathBuf::from("/var/www/htmlua/components"); let components = PathBuf::from("/var/www/htmlua/components");
let safe_path = Path::new(request_uri).strip_prefix("/")?; let safe_path = Path::new(request_uri).strip_prefix("/")?;
let page_path = pages.join(safe_path); let page_path = pages.join(safe_path);
let page_string = read_to_string(&page_path)?; let page_string = read_to_string(&page_path)?;
let doc = kuchikiki::parse_html().one(page_string); let doc = kuchikiki::parse_html().one(page_string);
let full_doc = expand_template(doc, &components, None)?; 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()) Ok(executed_doc.to_string())
} }