From eaadadecbb274b2cdcd61c790cb3a2d858cccb32 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Sat, 30 Aug 2025 01:11:01 -0500 Subject: [PATCH] rendering md with layouts --- Cargo.lock | 68 +++++++++++++++++++++++- Cargo.toml | 3 ++ src/main.rs | 1 + src/renderer.rs | 104 +++++++++++++++++++++++++++++++++++++ testdata/index.md | 18 +++++++ testdata/layouts/post.html | 9 ++++ 6 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/renderer.rs create mode 100644 testdata/index.md create mode 100644 testdata/layouts/post.html diff --git a/Cargo.lock b/Cargo.lock index c6e9086..1467572 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1056,8 +1056,11 @@ version = "0.1.0" dependencies = [ "eframe", "egui_ltreeview", + "markdown", "rfd", + "serde", "thiserror 2.0.16", + "toml", ] [[package]] @@ -1652,6 +1655,15 @@ dependencies = [ "libc", ] +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "unicode-id", +] + [[package]] name = "memchr" version = "2.7.5" @@ -2549,6 +2561,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2815,12 +2836,36 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +[[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_edit" version = "0.22.27" @@ -2828,10 +2873,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.6.11", "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tracing" version = "0.1.41" @@ -2889,6 +2949,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicode-id" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index efc99e0..e7aa6af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,6 @@ eframe = "0.32.1" egui_ltreeview = "0.5.3" rfd = "0.15.4" thiserror = "2.0.16" +toml = "0.9.5" +markdown = "1.0.0" +serde = "1.0.219" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 83c1aea..db12e61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod fleen_app; +mod renderer; use std::path::{Path, PathBuf}; use eframe::egui::{Color32, Context, Id, RichText}; diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..1eabe1b --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,104 @@ +use std::{default, fs}; +use std::io::Error; +use std::path::PathBuf; +use eframe::egui::TextBuffer; +use markdown::message::Message; +use markdown::{CompileOptions, Constructs, ParseOptions}; +use markdown::mdast::Node; +use serde::Deserialize; +use thiserror::Error; + +pub struct RenderOutput { + pub filename: PathBuf, + pub contents: String +} + +#[derive(Deserialize)] +pub struct Frontmatter { + layout: Option, + title: Option +} + +impl Frontmatter { + fn apply_layout(self, content: String, root: PathBuf) -> Result { + let title = self.title.unwrap_or(String::new()); + if let Some(layout) = self.layout { + let absolute_layout = root.join(layout); + let layout = fs::read_to_string(absolute_layout.clone()).map_err(|e| RenderError::FileReadError(e, absolute_layout))?; + Ok(layout.replace("$title", title.as_str()).replace("$content", content.as_str())) + } else { + Ok(content) + } + } +} + +#[derive(Error, Debug)] +pub enum RenderError { + #[error("Error reading {1}: {0}")] + FileReadError(Error, PathBuf), + #[error("Error parsing Markdown {1}: {0}")] + MarkdownParseError(Message, PathBuf), + #[error("Error parsing frontmatter in {1}: {0}")] + FrontmatterParseError(toml::de::Error, PathBuf) +} + +pub fn render(source: PathBuf, root: PathBuf) -> Result { + let contents = match source.extension().map(|o| o.to_str().unwrap()) { + Some("md") => render_as_markdown(source.clone(), root), + _ => todo!() + }?; + let filename = source.with_extension("html"); + Ok(RenderOutput { filename, contents }) +} + +fn render_as_markdown(source: PathBuf, root: PathBuf) -> Result { + let absolute_source = root.join(source.clone()); + let contents = fs::read_to_string(absolute_source.clone()).map_err(|e| RenderError::FileReadError(e, source.clone()))?; + let options = markdown::Options { + parse: ParseOptions { + constructs: Constructs { + frontmatter: true, + gfm_table: true, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + let html = markdown::to_html_with_options(contents.as_str(), &options).map_err(|e| RenderError::MarkdownParseError(e, source.clone()))?; + let ast = markdown::to_mdast(contents.as_str(), &options.parse).map_err(|e| RenderError::MarkdownParseError(e, source.clone()))?; + + if let Some(frontmatter) = find_frontmatter(ast, source.clone())? { + frontmatter.apply_layout(html, root) + } else { + Ok(html) + } +} + +fn find_frontmatter(node: Node, source: PathBuf) -> Result, RenderError> { + if let Some(children) = node.children() { + for child in children.into_iter() { + if let Node::Toml(toml_str) = child { + let fmatter: Frontmatter = toml::from_str(toml_str.value.as_str()).map_err(|e| RenderError::FrontmatterParseError(e, source))?; + return Ok(Some(fmatter)) + } + } + Ok(None) + } else { Ok(None) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rendering_markdown() { + match render("index.md".into(), "./testdata".into()) { + Err(e) => println!("{}", e), + Ok(ro) => { + println!("File: {}", ro.filename.to_str().unwrap()); + println!("Contents:\n\n{}", ro.contents); + } + } + } +} \ No newline at end of file diff --git a/testdata/index.md b/testdata/index.md new file mode 100644 index 0000000..49b8643 --- /dev/null +++ b/testdata/index.md @@ -0,0 +1,18 @@ ++++ +layout = "layouts/post.html" +title = "Pest Toast" ++++ +Test +=== + +This is a test. This is only a test. +~~~rust +fn main() { + println!("Here is some Rust") +} +~~~ + +|This|is| +|----|--| +|a|table| + diff --git a/testdata/layouts/post.html b/testdata/layouts/post.html new file mode 100644 index 0000000..2d59751 --- /dev/null +++ b/testdata/layouts/post.html @@ -0,0 +1,9 @@ + + + + blog - $title + + +$content + +