From 367c64e813b0ebe10e5d0232e6ce1482edce506a Mon Sep 17 00:00:00 2001 From: Craig Weber Date: Wed, 13 Jul 2022 21:26:44 -0500 Subject: [PATCH 1/6] fixed Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 58d7536..3214e02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rustlang/rust:stable +FROM rust:1.62.0 WORKDIR /workspace From dbde9b17bb70bc902d1e6f64b7ef1afe5a13ace3 Mon Sep 17 00:00:00 2001 From: Craig Weber Date: Tue, 21 Feb 2023 09:32:00 -0600 Subject: [PATCH 2/6] stupid --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4f2b4d8..6edd62a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,7 +201,7 @@ dependencies = [ [[package]] name = "futhorc" -version = "0.1.12" +version = "0.1.13" dependencies = [ "atom_syndication", "chrono", From 8fdcc7d97653bcf56dfaf510e27884f52a0fda40 Mon Sep 17 00:00:00 2001 From: Craig Weber Date: Tue, 21 Feb 2023 06:04:22 -0600 Subject: [PATCH 3/6] Post bundle WIP --- Cargo.lock | 30 +++++++++++++++++++ Cargo.toml | 1 + src/build.rs | 6 +++- src/markdown.rs | 23 +++++++++++++++ src/url.rs | 78 +++++++++++++++++++++++++++++++++++++++---------- src/write.rs | 30 +++++++++++++++---- 6 files changed, 146 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6edd62a..e0bc3b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,6 +214,7 @@ dependencies = [ "serde_yaml", "slug", "url", + "walkdir", ] [[package]] @@ -391,6 +392,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.124" @@ -551,6 +561,17 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -573,6 +594,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 40d4e77..3f476b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ clap = "2.33.3" atom_syndication = "0.11.0" chrono = "0.4.19" url = { version = "2.2.2", features = ["serde"] } +walkdir = "2.3.2" [features] fail-on-warnings = [] diff --git a/src/build.rs b/src/build.rs index b3db7be..d9cd1fa 100644 --- a/src/build.rs +++ b/src/build.rs @@ -25,7 +25,8 @@ pub fn build_site(config: Config) -> Result<()> { ); // collect all posts - let posts = post_parser.parse_posts(&config.posts_source_directory)?; + let (posts, static_files) = + post_parser.parse_posts(&config.posts_source_directory)?; // Parse the template files. let index_template = parse_template(config.index_template.iter())?; @@ -56,6 +57,9 @@ pub fn build_site(config: Config) -> Result<()> { }; writer.write_posts(&posts)?; + // write the static files + writer.write_static_files(&static_files)?; + // copy static directory copy_dir( &config.static_source_directory, diff --git a/src/markdown.rs b/src/markdown.rs index 3bbbed1..18549e5 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -56,6 +56,29 @@ impl<'a> EventConverter<'a> { // intercepting heading tags and returning the tag size + 2. Tag::Heading(s) => Tag::Heading(s + 2), + // Internal image links (links from blog posts, pages, and assets + // *to* posts, pages, and assets) need to be converted from their + // input formats to their output formats (e.g., a post linking to + // another post as `foo.md` will need to be converted to an + // equivalent link ending in `foo.html`). + Tag::Image( + link @ (LinkType::Inline + | LinkType::Reference + | LinkType::ReferenceUnknown + | LinkType::Shortcut + | LinkType::Autolink + | LinkType::Collapsed + | LinkType::CollapsedUnknown), + url, + title, + ) => Tag::Image( + link, + CowStr::Boxed( + self.link_converter.convert(&url)?.into_boxed_str(), + ), + title, + ), + // Internal links (links from blog posts, pages, and assets *to* // posts, pages, and assets) need to be converted from their input // formats to their output formats (e.g., a post linking to another diff --git a/src/url.rs b/src/url.rs index 9e12e79..85bde68 100644 --- a/src/url.rs +++ b/src/url.rs @@ -23,18 +23,32 @@ impl<'a> Converter<'a> { }) } + fn parse_bundle_base(normalized: &str) -> Option<&str> { + let base = normalized.trim_end_matches("/index.md"); + if base == normalized || base.contains('/') { + None + } else { + Some(base) + } + } + fn convert_absolute(&self, absolute: Url) -> Result { if let Some(relative) = self.posts_root.make_relative(&absolute) { if !relative.starts_with("../") && relative.ends_with(MARKDOWN_EXTENSION) { - let abs = absolute.to_string(); - return Ok(Url::parse(&format!( - "{}{}", - &abs[..abs.len() - MARKDOWN_EXTENSION.len()], - HTML_EXTENSION, - )) - .unwrap()); // this should never fail + return Ok(self + .posts_root + .join(&format!( + "{}{}", + match Self::parse_bundle_base(&relative) { + Some(base) => base, + None => + relative.trim_end_matches(MARKDOWN_EXTENSION), + }, + HTML_EXTENSION, + )) + .unwrap()); } } Ok(absolute) @@ -68,10 +82,7 @@ mod test { #[test] fn test_convert_relative_post_leading_dotslash() -> Result<()> { - fixture_basic( - "https://example.org/posts/relative.html", - "./relative.md", - ) + fixture_basic("https://example.org/posts/relative.html", "relative.md") } #[test] @@ -103,6 +114,40 @@ mod test { ) } + #[test] + fn test_convert_relative_bundle() -> Result<()> { + fixture_basic( + "https://example.org/posts/relative.html", + "relative/index.md", + ) + } + + #[test] + fn test_convert_relative_bundle_leading_dotslash() -> Result<()> { + fixture_basic( + "https://example.org/posts/relative.html", + "./relative.md", + ) + } + + #[test] + fn test_convert_relative_bundle_asset() -> Result<()> { + fixture( + "relative/index.md", + "https://example.org/posts/relative/image.jpg", + "image.jpg", + ) + } + + #[test] + fn test_convert_relative_bundle_asset_leading_dotslash() -> Result<()> { + fixture( + "relative/index.md", + "https://example.org/posts/relative/image.jpg", + "./image.jpg", + ) + } + #[test] fn test_convert_absolute_post() -> Result<()> { fixture_basic( @@ -144,13 +189,14 @@ mod test { } fn fixture_basic(wanted: &str, target: &str) -> Result<()> { + fixture("index.html", wanted, target) + } + + fn fixture(base: &str, wanted: &str, target: &str) -> Result<()> { assert_eq!( wanted, - Converter::new( - &Url::parse("https://example.org/posts/")?, - "index.html" - )? - .convert(target)?, + Converter::new(&Url::parse("https://example.org/posts/")?, base)? + .convert(target)?, ); Ok(()) } diff --git a/src/write.rs b/src/write.rs index 576bb0c..3fb9596 100644 --- a/src/write.rs +++ b/src/write.rs @@ -3,6 +3,7 @@ use crate::post::*; use gtmpl::{Template, Value}; +use std::collections::HashSet; use std::fmt; use std::io; use std::path::{Path, PathBuf}; @@ -78,7 +79,6 @@ impl Writer<'_> { /// Takes a slice of [`Post`], indexes it by tag, and writes post and index /// pages to disk. pub fn write_posts(&self, posts: &[Post]) -> Result<()> { - use std::collections::HashSet; let mut seen_dirs: HashSet = HashSet::new(); pages( posts, @@ -89,13 +89,33 @@ impl Writer<'_> { self.index_template, ) .try_for_each(|page| { - let dir = page.file_path.parent().unwrap(); // there should always be a dir - if seen_dirs.insert(dir.to_owned()) { - std::fs::create_dir_all(dir)?; - } + Self::make_parent_dir(&mut seen_dirs, &page.file_path)?; self.write_page(&page) }) } + + pub fn write_static_files( + &self, + static_files: &[StaticFile], + ) -> Result<()> { + let mut seen_dirs: HashSet = HashSet::new(); + for (src, dst) in static_files { + Self::make_parent_dir(&mut seen_dirs, dst)?; + std::fs::hard_link(src, dst)?; + } + Ok(()) + } + + fn make_parent_dir<'a>( + seen_dirs: &mut HashSet, + path: &Path, + ) -> Result<()> { + let parent = path.parent().unwrap(); + if seen_dirs.insert(parent.to_owned()) { + std::fs::create_dir_all(&parent)?; + } + Ok(()) + } } /// An object representing an output HTML file. A [`Page`] can be converted to From 36ddfec9a0bf06055fe6056e842f210040c4a331 Mon Sep 17 00:00:00 2001 From: Craig Weber Date: Tue, 21 Feb 2023 08:31:29 -0600 Subject: [PATCH 4/6] wip test data --- testdata/posts/bundle-with-asset/asset.jpg | Bin 0 -> 17945 bytes testdata/posts/bundle-with-asset/index.md | 8 ++++++++ .../asset.jpg | Bin 0 -> 17834 bytes .../index.md | 10 ++++++++++ testdata/posts/simple.md | 6 ++++++ 5 files changed, 24 insertions(+) create mode 100644 testdata/posts/bundle-with-asset/asset.jpg create mode 100644 testdata/posts/bundle-with-asset/index.md create mode 100644 testdata/posts/bundle-with-different-asset-same-name/asset.jpg create mode 100644 testdata/posts/bundle-with-different-asset-same-name/index.md create mode 100644 testdata/posts/simple.md diff --git a/testdata/posts/bundle-with-asset/asset.jpg b/testdata/posts/bundle-with-asset/asset.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a696a271fbf18d2ec1f6b2af2f724cd10eee3037 GIT binary patch literal 17945 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 S1r$+01r$+01r$+0AOG2igdlPN literal 0 HcmV?d00001 diff --git a/testdata/posts/bundle-with-asset/index.md b/testdata/posts/bundle-with-asset/index.md new file mode 100644 index 0000000..11b2ca3 --- /dev/null +++ b/testdata/posts/bundle-with-asset/index.md @@ -0,0 +1,8 @@ +--- +Title: Bundle with an asset +Date: 2023-02-21 +--- + +This is a bundle with an asset. This asset was sourced from +[Wikipedia](https://upload.wikimedia.org/wikipedia/commons/a/a8/Anastasius_I_%28emperor%29.jpg). +![The asset](./asset.jpg) \ No newline at end of file diff --git a/testdata/posts/bundle-with-different-asset-same-name/asset.jpg b/testdata/posts/bundle-with-different-asset-same-name/asset.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a22f727e0f2069b8710a604938f73c96dee34993 GIT binary patch literal 17834 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

}6tUDVIfIO6 zO%8(U+G*G(#Iw-7S57i{EX?zUQ1(`^vX*MaZ6Z0{5k%*@9{)5?Xwdd})6ml4(b*m^ zokuqw((QXx%(vlHAK&8G+n9`{MUz-3f&_`v{8RosduJo61`^zB683P&{`^! z-?4)~SmcfIR^4&!+TP#j`L}gUibZLg%OWw!$P!CJ0O|2|O{*3mJ(l}VJleojYIyys@-ia5ji~`{kQzv!F6dJb<9e_JVVDcn`b2MCz>TpvwhjFy!_bP(DZ@) z_d7&{hD2#yZ8oE^il=6~FSQ(}e%=L+B^>M!G^)S>2DFjuf4z>?cE$CRU{$L>GwoYI zosMQLQE88-RfM#&oeV(a%wz3e2pZbL`mlQj>w=I6^PIS^`WtbI|AN1bC51$J8%|wF zp=VT;oG4LCg&~!_KCcgkxN4F|MUyVY_SYVkcQE2@h9U`x3|Y*s?pM_)l8!w6A-uQM zT3HKZdyB*DelkfW5?;%Fa}L1`?-n%x)d2KuUv{lotR{tP^=Y1A`zRs0{R@k`d$`c_ z+Un(zRUDeAU|4Zto?2FSwjLxF)UvW(9hVkcq?eGzEXa=>Qd~TXGw>=Ztr$bW>&P`Y z(|xstQ{>Xug5!OnMZ`6fw-90R`?LKtjm|Q6=38a)-lesptCePCx2_b3@7tZLhQ7GB z!sar4)tY$tm~CQ)<~8*P5;<`6^{uemmeAm$FUAx+yz1y&uaJDIGTj!hSoyqfgThxE1 zy(((0Rt^_t-a|^1@$vTSK}qY&K#b@E$T05|uN*lY4SG<8fHrWCGtCX%(D493Vi0Z*4qv(OM6Q~sGuT5a2XqBR@@)MNMb&F?JsWLvs`10nA_*^k|rTe^{Mj|@;mF3XNOC0ZB6#C zQE}JrZ*4joc`b4wN4|bXpGHIYh&@yNP9!8n~&%;1u|Jl95qk8}V literal 0 HcmV?d00001 diff --git a/testdata/posts/bundle-with-different-asset-same-name/index.md b/testdata/posts/bundle-with-different-asset-same-name/index.md new file mode 100644 index 0000000..eec3a96 --- /dev/null +++ b/testdata/posts/bundle-with-different-asset-same-name/index.md @@ -0,0 +1,10 @@ +--- +Title: Bundle with different asset same name +Date: 2023-02-21 +--- + +This is a post bundle with a different asset (compared to that of [the previous +post](../bundle-with-asset/index.md)) but sharing the same name (asset.jpg). +This asset was also sourced from +[Wikipedia](https://upload.wikimedia.org/wikipedia/commons/8/89/Tremissis_Avitus-RIC_2402.jpg). +![The asset](./asset.jpg). \ No newline at end of file diff --git a/testdata/posts/simple.md b/testdata/posts/simple.md new file mode 100644 index 0000000..b4d009c --- /dev/null +++ b/testdata/posts/simple.md @@ -0,0 +1,6 @@ +--- +Title: The title +Date: 0000-01-01 +--- + +Today is the first day of the Common Era. \ No newline at end of file From b53334eecab64a3dfb942c8e4917e6baab8782e4 Mon Sep 17 00:00:00 2001 From: Craig Weber Date: Tue, 21 Feb 2023 09:53:59 -0600 Subject: [PATCH 5/6] extract parser stuff --- src/parser.rs | 180 +++++++++++++++++++++++++++++++++++++++++++++----- src/write.rs | 1 + 2 files changed, 165 insertions(+), 16 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index ad8787b..645909d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,7 @@ use std::{ collections::HashSet, fmt, fs::{read_dir, File}, - path::Path, + path::{Path, PathBuf}, }; use serde::Deserialize; @@ -47,21 +47,63 @@ impl<'a> Parser<'a> { } } + fn parse_post_bundle( + &self, + posts_source_directory: &Path, + relative_path: &Path, + static_files: &mut Vec, + ) -> Result { + // We want to make sure we can parse a post before we mutate + // `static_files` + let post = self.parse_post( + posts_source_directory, + &relative_path.join("index.md"), + )?; + + // Mutate `static_files` only after we've confirmed that we've parsed a + // valid post. + use walkdir::WalkDir; + let abs = posts_source_directory.join(relative_path); + for result in WalkDir::new(&abs) { + let entry = result?; + if entry.file_type().is_file() && entry.file_name() != "index.md" { + static_files.push(( + entry.path().to_owned(), + self.posts_directory + .join(relative_path.file_name().unwrap()) + // strip_prefix shouldn't fail since `abs` is always an + // ancestor of `entry_path` + .join(entry.path().strip_prefix(&abs).unwrap()), + )); + } + } + + Ok(post) + } + /// Parses a single [`Post`] from an `id` and `input` strings. The `id` is /// the path of the file relative to the `posts_source_directory` less the /// extension (e.g., the ID for a post whose source file is /// `{posts_source_directory}/foo/bar.md` is `foo/bar`). - fn parse_post(&self, id: &str, input: &str) -> Result { - match self._parse_post(id, input) { + fn parse_post( + &self, + posts_source_directory: &Path, + relative_path: &Path, + ) -> Result { + match self._parse_post(posts_source_directory, relative_path) { Ok(p) => Ok(p), Err(e) => Err(Error::Annotated( - format!("parsing post `{}`", id), + format!("parsing post `{:?}`", relative_path), Box::new(e), )), } } - fn _parse_post(&self, id: &str, input: &str) -> Result { + fn _parse_post( + &self, + posts_source_directory: &Path, + relative_path: &Path, + ) -> Result { fn frontmatter_indices(input: &str) -> Result<(usize, usize, usize)> { const FENCE: &str = "---"; if !input.starts_with(FENCE) { @@ -77,15 +119,34 @@ impl<'a> Parser<'a> { } } + use std::io::Read; + let mut contents = String::new(); + File::open(posts_source_directory.join(relative_path))? + .read_to_string(&mut contents)?; + let input: &str = &contents; + let (yaml_start, yaml_stop, body_start) = frontmatter_indices(input)?; let frontmatter: Frontmatter = serde_yaml::from_str(&input[yaml_start..yaml_stop])?; - let file_name = format!("{}.html", id); + + let with_extension = if relative_path.ends_with("index.md") { + relative_path.parent().unwrap() + } else { + relative_path + } + .with_extension("html"); + + let file_name = with_extension + .file_name() + .ok_or_else(|| InvalidFileNameError(relative_path.to_owned()))? + .to_str() + .ok_or_else(|| InvalidFileNameError(relative_path.to_owned()))?; + let mut post = Post { title: frontmatter.title, date: frontmatter.date, file_path: self.posts_directory.join(&file_name), - url: self.posts_url.join(&file_name)?, + url: self.posts_url.join(file_name)?, tags: frontmatter .tags .iter() @@ -116,7 +177,7 @@ impl<'a> Parser<'a> { markdown::to_html( &mut post.body, self.posts_url, - id, + file_name, &input[body_start..], post.url.as_str(), )?; @@ -144,25 +205,38 @@ impl<'a> Parser<'a> { /// /// World /// ``` - pub fn parse_posts(&self, source_directory: &Path) -> Result> { - use std::io::Read; + pub fn parse_posts(&self, source_directory: &Path) -> Result { const MARKDOWN_EXTENSION: &str = ".md"; let mut posts = Vec::new(); + let mut static_files = Vec::new(); for result in read_dir(source_directory)? { let entry = result?; let os_file_name = entry.file_name(); let file_name = os_file_name.to_string_lossy(); - if file_name.ends_with(MARKDOWN_EXTENSION) { - let base_name = file_name.trim_end_matches(MARKDOWN_EXTENSION); - let mut contents = String::new(); - File::open(entry.path())?.read_to_string(&mut contents)?; - posts.push(self.parse_post(base_name, &contents)?); + if Self::is_bundle(&entry)? { + posts.push(self.parse_post_bundle( + source_directory, + // strip_prefix() should never fail + entry.path().strip_prefix(source_directory).unwrap(), + &mut static_files, + )?) + } else if file_name.ends_with(MARKDOWN_EXTENSION) { + posts.push(self.parse_post( + source_directory, + // should never fail + entry.path().strip_prefix(source_directory).unwrap(), + )?); } } posts.sort_by(|a, b| b.date.cmp(&a.date)); - Ok(posts) + Ok((posts, static_files)) + } + + fn is_bundle(entry: &std::fs::DirEntry) -> std::io::Result { + Ok(entry.file_type()?.is_dir() + && entry.path().join("index.md").is_file()) } } @@ -181,6 +255,27 @@ struct Frontmatter { pub tags: HashSet, } +#[derive(Debug)] +pub struct InvalidFileNameError(PathBuf); + +impl fmt::Display for InvalidFileNameError { + /// Displays an [`InvalidFileNameError`] as human-readable text. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid file name: {:?}", &self.0) + } +} + +impl std::error::Error for InvalidFileNameError { + /// Implements the [`std::error::Error`] trait for [`InvalidFileNameError`]. + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } +} + +pub type Posts = (Vec, Vec); + +pub type StaticFile = (PathBuf, PathBuf); + /// Represents the result of a [`Post`]-parse operation. pub type Result = std::result::Result; @@ -205,6 +300,12 @@ pub enum Error { /// Returned for other I/O errors. Io(std::io::Error), + /// Returned for WalkDir I/O errors. + WalkDir(walkdir::Error), + + /// Returned when a source file isn't valid UTF-8. + InvalidFileName(InvalidFileNameError), + /// An error with an annotation. Annotated(String, Box), } @@ -222,6 +323,8 @@ impl fmt::Display for Error { Error::DeserializeYaml(err) => err.fmt(f), Error::UrlParse(err) => err.fmt(f), Error::Io(err) => err.fmt(f), + Error::WalkDir(err) => err.fmt(f), + Error::InvalidFileName(err) => err.fmt(f), Error::Annotated(annotation, err) => { write!(f, "{}: {}", &annotation, err) } @@ -238,11 +341,19 @@ impl std::error::Error for Error { Error::DeserializeYaml(err) => Some(err), Error::UrlParse(err) => Some(err), Error::Io(err) => Some(err), + Error::WalkDir(err) => Some(err), + Error::InvalidFileName(err) => Some(err), Error::Annotated(_, err) => Some(err), } } } +impl From for Error { + fn from(err: InvalidFileNameError) -> Error { + Error::InvalidFileName(err) + } +} + impl From for Error { fn from(err: markdown::Error) -> Error { match err { @@ -268,6 +379,14 @@ impl From for Error { } } +impl From for Error { + /// Converts a [`walkdir::Error`] into an [`Error`]. It allows us to + // use the `?` operator for fallible I/O functions. + fn from(err: walkdir::Error) -> Error { + Error::WalkDir(err) + } +} + impl From for Error { /// Converts a [`std::io::Error`] into an [`Error`]. It allows us to // use the `?` operator for fallible I/O functions. @@ -275,3 +394,32 @@ impl From for Error { Error::Io(err) } } + +#[cfg(test)] +mod test { + use std::path::PathBuf; + + use super::*; + + #[test] + fn test_parse_posts() -> Result<()> { + let index_url = Url::parse("https://example.com")?; + let posts_url = Url::parse("https://example.com/posts/")?; + let posts_directory = Path::new("./testdata/posts/"); + let parser = Parser::new(&index_url, &posts_url, &posts_directory); + let (posts, static_files) = + parser.parse_posts(Path::new("./testdata/posts/"))?; + + let wanted_posts = vec![Post { + file_path: PathBuf::from("./testdata/posts/"), + title: String::from("Simple"), + url: Url::parse("https://example.com/posts/simple.html")?, + date: String::from("0000-01-01"), + body: String::from("Today is the first day of the Common Era."), + tags: HashSet::new(), + }]; + + // assert_eq!(wanted_posts, posts); + Ok(()) + } +} diff --git a/src/write.rs b/src/write.rs index 3fb9596..7e8dd89 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,6 +1,7 @@ //! Takes [`Post`] objects created by the [`crate::post`] module and turns them //! into index and post HTML files on the file system. +use crate::parser::StaticFile; use crate::post::*; use gtmpl::{Template, Value}; use std::collections::HashSet; From 4ad5fdc435533dd6f116b2f076112805d34bcaba Mon Sep 17 00:00:00 2001 From: Craig Weber Date: Tue, 21 Feb 2023 10:00:14 -0600 Subject: [PATCH 6/6] make `Post` comparable via `assert_eq!()` --- src/parser.rs | 5 ++++- src/post.rs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 645909d..e2b8ba6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -419,7 +419,10 @@ mod test { tags: HashSet::new(), }]; - // assert_eq!(wanted_posts, posts); + let wanted_static_files: Vec = Vec::new(); + + assert_eq!(wanted_posts, posts); + assert_eq!(wanted_static_files, static_files); Ok(()) } } diff --git a/src/post.rs b/src/post.rs index e678cf4..cc6a58f 100644 --- a/src/post.rs +++ b/src/post.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use url::Url; /// Represents a blog post. -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub struct Post { /// The output path where the final post file will be rendered. pub file_path: PathBuf,