Tests and more renderer logic
This commit is contained in:
+97
-23
@@ -1,33 +1,48 @@
|
|||||||
use std::{default, fs};
|
use std::fs;
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use eframe::egui::TextBuffer;
|
|
||||||
use markdown::message::Message;
|
use markdown::message::Message;
|
||||||
use markdown::{CompileOptions, Constructs, Options, ParseOptions};
|
use markdown::{Constructs, Options, ParseOptions};
|
||||||
use markdown::mdast::Node;
|
use markdown::mdast::Node;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub struct RenderOutput {
|
/// The things we might return from trying to render a file
|
||||||
pub filename: PathBuf,
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub contents: String
|
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)]
|
#[derive(Deserialize)]
|
||||||
pub struct Frontmatter {
|
pub struct Frontmatter {
|
||||||
layout: Option<String>,
|
layout: Option<String>,
|
||||||
title: Option<String>
|
title: Option<String>,
|
||||||
|
published: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Frontmatter {
|
impl Frontmatter {
|
||||||
fn apply_layout(self, content: String, root: PathBuf) -> Result<String, RenderError> {
|
fn apply_layout(self, content: String, filename: PathBuf, root: PathBuf) -> Result<RenderOutput, RenderError> {
|
||||||
let title = self.title.unwrap_or(String::new());
|
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 absolute_layout = root.join(layout);
|
||||||
let layout = fs::read_to_string(absolute_layout.clone()).map_err(|e| RenderError::FileReadError(e, absolute_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 {
|
} 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)
|
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<RenderOutput, RenderError> {
|
pub fn render(source: PathBuf, root: PathBuf) -> Result<RenderOutput, RenderError> {
|
||||||
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),
|
Some("md") => render_as_markdown(source.clone(), root),
|
||||||
_ => todo!()
|
_ => Ok(RenderOutput::RawFile(source))
|
||||||
}?;
|
}
|
||||||
let filename = source.with_extension("html");
|
|
||||||
Ok(RenderOutput { filename, contents })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_as_markdown(source: PathBuf, root: PathBuf) -> Result<String, RenderError> {
|
// 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<RenderOutput, RenderError> {
|
||||||
let absolute_source = root.join(source.clone());
|
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 contents = fs::read_to_string(absolute_source.clone()).map_err(|e| RenderError::FileReadError(e, source.clone()))?;
|
||||||
let options = markdown_options();
|
let options = markdown_options();
|
||||||
@@ -59,13 +92,13 @@ fn render_as_markdown(source: PathBuf, root: PathBuf) -> Result<String, RenderEr
|
|||||||
let ast = markdown::to_mdast(contents.as_str(), &options.parse).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())? {
|
if let Some(frontmatter) = find_frontmatter(ast, source.clone())? {
|
||||||
frontmatter.apply_layout(html, root)
|
frontmatter.apply_layout(html, source, root)
|
||||||
} else {
|
} else {
|
||||||
Ok(html)
|
Ok(RenderOutput::Rendered(source.with_extension("html"), html))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the
|
// Construct the Markdown options we'll render with
|
||||||
fn markdown_options() -> Options {
|
fn markdown_options() -> Options {
|
||||||
markdown::Options {
|
markdown::Options {
|
||||||
parse: ParseOptions {
|
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<Option<Frontmatter>, RenderError> {
|
fn find_frontmatter(node: Node, source: PathBuf) -> Result<Option<Frontmatter>, RenderError> {
|
||||||
if let Some(children) = node.children() {
|
if let Some(children) = node.children() {
|
||||||
for child in children.into_iter() {
|
for child in children.into_iter() {
|
||||||
@@ -96,8 +130,8 @@ fn find_frontmatter(node: Node, source: PathBuf) -> Result<Option<Frontmatter>,
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn render_index() -> RenderOutput {
|
fn render_file(path: impl Into<PathBuf>) -> RenderOutput {
|
||||||
match render("index.md".into(), "./testdata".into()) {
|
match render(path.into(), "./testdata".into()) {
|
||||||
Ok(ro) => ro,
|
Ok(ro) => ro,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("{}", e);
|
println!("{}", e);
|
||||||
@@ -108,7 +142,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rendering_markdown() {
|
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_eq!(filename.to_str(), Some("index.html")); // Goes to the right filename
|
||||||
assert!(contents.starts_with("<!DOCTYPE html>")); // Uses the layout
|
assert!(contents.starts_with("<!DOCTYPE html>")); // Uses the layout
|
||||||
assert!(contents.matches("Pest Toast").next().is_some()); // Replaces in the title
|
assert!(contents.matches("Pest Toast").next().is_some()); // Replaces in the title
|
||||||
@@ -118,5 +154,43 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_layout() {
|
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("<p>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("<!DOCTYPE html>")); // 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(_)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>$title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
$content
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
+++
|
||||||
|
layout = "_layouts/default.html"
|
||||||
|
title = "Page Title"
|
||||||
|
published = false
|
||||||
|
+++
|
||||||
|
Page Title
|
||||||
|
===
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>blog - $title</title>
|
<title>$title</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
$content
|
$content
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
This file should be skipped entirely because it begins with an underscore
|
||||||
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
+++
|
||||||
|
layout = "_layouts/post.html"
|
||||||
|
published = false
|
||||||
|
+++
|
||||||
|
|
||||||
|
This file should render to hidden because it's unpublished
|
||||||
Vendored
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
+++
|
+++
|
||||||
layout = "layouts/post.html"
|
layout = "_layouts/post.html"
|
||||||
title = "Pest Toast"
|
title = "Pest Toast"
|
||||||
|
published = true
|
||||||
+++
|
+++
|
||||||
Test
|
Test
|
||||||
===
|
===
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
published = true
|
||||||
|
+++
|
||||||
|
|
||||||
|
This file has no layout, so it'll be rendered but nothing around it.
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
This should be echoed back raw because of the extension
|
||||||
Reference in New Issue
Block a user