From 4ffd64737738846d99e778ffc2f93def3e01ad37 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Sun, 31 Aug 2025 00:55:31 -0500 Subject: [PATCH] Tests and more renderer logic --- src/renderer.rs | 120 ++++++++++++++++++----- templates/default_layout.html | 9 ++ templates/markdown_template.md | 8 ++ testdata/{layouts => _layouts}/post.html | 2 +- testdata/_skipped.md | 1 + testdata/dir/.keep | 0 testdata/hidden.md | 6 ++ testdata/index.md | 3 +- testdata/nolayout.md | 5 + testdata/raw.txt | 1 + 10 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 templates/default_layout.html create mode 100644 templates/markdown_template.md rename testdata/{layouts => _layouts}/post.html (69%) create mode 100644 testdata/_skipped.md create mode 100644 testdata/dir/.keep create mode 100644 testdata/hidden.md create mode 100644 testdata/nolayout.md create mode 100644 testdata/raw.txt diff --git a/src/renderer.rs b/src/renderer.rs index 320f52d..71b88db 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,33 +1,48 @@ -use std::{default, fs}; +use std::fs; use std::io::Error; use std::path::PathBuf; -use eframe::egui::TextBuffer; use markdown::message::Message; -use markdown::{CompileOptions, Constructs, Options, ParseOptions}; +use markdown::{Constructs, Options, ParseOptions}; use markdown::mdast::Node; use serde::Deserialize; use thiserror::Error; -pub struct RenderOutput { - pub filename: PathBuf, - pub contents: String +/// The things we might return from trying to render a file +#[derive(Clone, PartialEq, Debug)] +pub enum RenderOutput { + /// A string which had been rendered from some source file or script + Rendered(PathBuf, String), + /// Hidden output should be returned by the test server but not rendered to a file + Hidden(PathBuf, String), + /// The raw contents of the path + RawFile(PathBuf), + /// No contents; this file should not be output / test server should return 404 + NoOutput, + /// This is a directory; the test server won't return anything but we need to mkdir it + Dir(PathBuf) } #[derive(Deserialize)] pub struct Frontmatter { layout: Option, - title: Option + title: Option, + published: bool } impl Frontmatter { - fn apply_layout(self, content: String, root: PathBuf) -> Result { + fn apply_layout(self, content: String, filename: PathBuf, root: PathBuf) -> Result { let title = self.title.unwrap_or(String::new()); - if let Some(layout) = self.layout { + let wrapped = 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())) + layout.replace("$title", title.as_str()).replace("$content", content.as_str()) } else { - Ok(content) + content + }; + if self.published { + Ok(RenderOutput::Rendered(filename.with_extension("html"), wrapped)) + } else { + Ok(RenderOutput::Hidden(filename.with_extension("html"), wrapped)) } } } @@ -42,16 +57,34 @@ pub enum RenderError { FrontmatterParseError(toml::de::Error, PathBuf) } +/// Take a source file path (relative to the root) and the root path, and return a RenderOutput for it pub fn render(source: PathBuf, root: PathBuf) -> Result { - let contents = match source.extension().map(|o| o.to_str().unwrap()) { + if skipped_path(source.clone()) { + return Ok(RenderOutput::NoOutput) + } + if root.join(source.clone()).is_dir() { + return Ok(RenderOutput::Dir(source)) + } + 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 }) + _ => Ok(RenderOutput::RawFile(source)) + } } -fn render_as_markdown(source: PathBuf, root: PathBuf) -> Result { +// If any element of the path starts with an underscore, we want to skip rendering it. +// In addition, if a cheeky person has put .. in the path, just skip it (which will trigger a 404 from the dev server) +fn skipped_path(source: PathBuf) -> bool { + source.into_iter().any(|el| { + match el.to_str() { + Some("..") => true, + Some(s) if s.starts_with("_") => true, + _ => false + } + }) +} + +// This gets called by `render` if the source path extension is md +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(); @@ -59,13 +92,13 @@ fn render_as_markdown(source: PathBuf, root: PathBuf) -> Result Options { markdown::Options { parse: ParseOptions { @@ -80,6 +113,7 @@ fn markdown_options() -> Options { } } +// Look for and try to parse toml frontmatter fn find_frontmatter(node: Node, source: PathBuf) -> Result, RenderError> { if let Some(children) = node.children() { for child in children.into_iter() { @@ -96,8 +130,8 @@ fn find_frontmatter(node: Node, source: PathBuf) -> Result, mod tests { use super::*; - fn render_index() -> RenderOutput { - match render("index.md".into(), "./testdata".into()) { + fn render_file(path: impl Into) -> RenderOutput { + match render(path.into(), "./testdata".into()) { Ok(ro) => ro, Err(e) => { println!("{}", e); @@ -108,7 +142,9 @@ mod tests { #[test] fn test_rendering_markdown() { - let RenderOutput { contents, filename } = render_index(); + let contents = render_file("index.md"); + assert!(matches!(contents, RenderOutput::Rendered(_, _))); + let RenderOutput::Rendered(filename, contents) = contents else { unreachable!() }; assert_eq!(filename.to_str(), Some("index.html")); // Goes to the right filename assert!(contents.starts_with("")); // Uses the layout assert!(contents.matches("Pest Toast").next().is_some()); // Replaces in the title @@ -118,5 +154,43 @@ mod tests { #[test] fn test_no_layout() { + let contents = render_file("nolayout.md"); + assert!(matches!(contents, RenderOutput::Rendered(_, _))); // Normal output + let RenderOutput::Rendered(_, contents) = contents else { unreachable!() }; + assert!(contents.starts_with("

This file has no layout")) // Doesn't use any layout, just the contents + } + + #[test] + fn test_no_output() { + let contents = render_file("_skipped.md"); + assert!(matches!(contents, RenderOutput::NoOutput)); // This begins with an underscore so it's skipped + + let contents = render_file("_layouts/post.html"); + assert!(matches!(contents, RenderOutput::NoOutput)); // This is in a dir that begins with an underscore + } + + #[test] + fn test_hidden() { + let contents = render_file("hidden.md"); + assert!(matches!(contents, RenderOutput::Hidden(_, _))); // The dev server should return it but it shouldn't produce a file + let RenderOutput::Hidden(_, contents) = contents else { unreachable!() }; + assert!(contents.starts_with("")); // Uses the layout + assert!(contents.matches("This file should render to hidden").next().is_some()); + } + + #[test] + fn test_raw() { + let contents = render_file("raw.txt"); + assert_eq!(contents, RenderOutput::RawFile(PathBuf::from("raw.txt"))); + } + + #[test] + fn test_dotdot() { + assert!(matches!(render_file("../src/main.rs"), RenderOutput::NoOutput)); + } + + #[test] + fn test_dir() { + assert!(matches!(render_file("dir"), RenderOutput::Dir(_))); } } \ No newline at end of file diff --git a/templates/default_layout.html b/templates/default_layout.html new file mode 100644 index 0000000..550b2a7 --- /dev/null +++ b/templates/default_layout.html @@ -0,0 +1,9 @@ + + + + $title + + +$content + + diff --git a/templates/markdown_template.md b/templates/markdown_template.md new file mode 100644 index 0000000..1e68770 --- /dev/null +++ b/templates/markdown_template.md @@ -0,0 +1,8 @@ ++++ +layout = "_layouts/default.html" +title = "Page Title" +published = false ++++ +Page Title +=== + diff --git a/testdata/layouts/post.html b/testdata/_layouts/post.html similarity index 69% rename from testdata/layouts/post.html rename to testdata/_layouts/post.html index 2d59751..5831989 100644 --- a/testdata/layouts/post.html +++ b/testdata/_layouts/post.html @@ -1,7 +1,7 @@ - blog - $title + $title $content diff --git a/testdata/_skipped.md b/testdata/_skipped.md new file mode 100644 index 0000000..45151a2 --- /dev/null +++ b/testdata/_skipped.md @@ -0,0 +1 @@ +This file should be skipped entirely because it begins with an underscore \ No newline at end of file diff --git a/testdata/dir/.keep b/testdata/dir/.keep new file mode 100644 index 0000000..e69de29 diff --git a/testdata/hidden.md b/testdata/hidden.md new file mode 100644 index 0000000..7bbe48b --- /dev/null +++ b/testdata/hidden.md @@ -0,0 +1,6 @@ ++++ +layout = "_layouts/post.html" +published = false ++++ + +This file should render to hidden because it's unpublished \ No newline at end of file diff --git a/testdata/index.md b/testdata/index.md index 49b8643..63489ee 100644 --- a/testdata/index.md +++ b/testdata/index.md @@ -1,6 +1,7 @@ +++ -layout = "layouts/post.html" +layout = "_layouts/post.html" title = "Pest Toast" +published = true +++ Test === diff --git a/testdata/nolayout.md b/testdata/nolayout.md new file mode 100644 index 0000000..120323a --- /dev/null +++ b/testdata/nolayout.md @@ -0,0 +1,5 @@ ++++ +published = true ++++ + +This file has no layout, so it'll be rendered but nothing around it. \ No newline at end of file diff --git a/testdata/raw.txt b/testdata/raw.txt new file mode 100644 index 0000000..de9f0e5 --- /dev/null +++ b/testdata/raw.txt @@ -0,0 +1 @@ +This should be echoed back raw because of the extension \ No newline at end of file