diff --git a/Cargo.lock b/Cargo.lock index 6983727..44e485f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "backtrace" version = "0.3.69" @@ -80,6 +86,24 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "bytes" version = "1.5.0" @@ -146,12 +170,28 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "duster" +version = "0.1.1" +source = "git+https://code.fizz.buzz/talexander/duster.git?branch=master#3428a3f5097c7d2cc252d1bfd9aae7771553ab69" +dependencies = [ + "nom 6.1.2", + "serde", + "serde_json", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "gimli" version = "0.28.0" @@ -176,6 +216,25 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "include_dir" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.0.2" @@ -186,6 +245,25 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.149" @@ -213,6 +291,19 @@ dependencies = [ "adler", ] +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -245,10 +336,8 @@ dependencies = [ [[package]] name = "organic" version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f0f8a2a6d31c3cac7ebf543d8cb2e8f648300462fc2f6b1a09cac10daf0387" dependencies = [ - "nom", + "nom 7.1.3", "walkdir", ] @@ -276,12 +365,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + [[package]] name = "same-file" version = "1.0.6" @@ -311,6 +412,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.3" @@ -320,6 +432,12 @@ dependencies = [ "serde", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.38" @@ -331,6 +449,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tokio" version = "1.33.0" @@ -389,6 +513,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "walkdir" version = "2.4.0" @@ -510,8 +640,18 @@ name = "writer" version = "0.0.1" dependencies = [ "clap", + "duster", + "include_dir", "organic", "serde", + "serde_json", "tokio", "toml", + "walkdir", ] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/Cargo.toml b/Cargo.toml index 44b8f2f..4026897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -# error-context, suggestions, usage | env clap = { version = "4.4.6", default-features = false, features = ["std", "color", "help", "derive"] } -organic = "0.1.12" -# | alloc, rc, serde_derive, unstable +duster = { git = "https://code.fizz.buzz/talexander/duster.git", branch = "master" } +include_dir = "0.7.3" +# TODO: This is temporary to work on the latest organic code. Eventually switch back to using the published crate. +organic = { path = "../organic" } +# organic = "0.1.12" serde = { version = "1.0.189", default-features = false, features = ["std", "derive"] } +serde_json = "1.0.107" tokio = { version = "1.30.0", default-features = false, features = ["rt", "rt-multi-thread", "fs", "io-util"] } -# display, parse | indexmap, preserve_order toml = "0.8.2" +walkdir = "2.4.0" diff --git a/default_environment/templates/html/blog_post_page.dust b/default_environment/templates/html/blog_post_page.dust new file mode 100644 index 0000000..6b431c4 --- /dev/null +++ b/default_environment/templates/html/blog_post_page.dust @@ -0,0 +1,11 @@ +
+
+ {?.title}{?.self_link}{.title}{:else}
{.title}
{/.self_link}{/.title} + {! TODO: date? !} +
+ + {! TODO: Table of contents? !} + +
+
+
diff --git a/default_environment/templates/html/main.dust b/default_environment/templates/html/main.dust new file mode 100644 index 0000000..77faec1 --- /dev/null +++ b/default_environment/templates/html/main.dust @@ -0,0 +1,18 @@ + + + + + {#global_settings.css_files}{/global_settings.css_files} + {#global_settings.js_files}{/global_settings.js_files} + {?global_settings.page_title}{global_settings.page_title}{/global_settings.page_title} + + + {! TODO: Header bar with links? !} +
+ {@select key=.type} + {@eq value="blog_post_page"}{>blog_post_page/}{/eq} + {@none}{!TODO: make this panic!}ERROR: Unrecognized page content type{/none} + {/select} +
+ + diff --git a/src/command/build/mod.rs b/src/command/build/mod.rs index 2b7c076..8edcb15 100644 --- a/src/command/build/mod.rs +++ b/src/command/build/mod.rs @@ -1,3 +1,4 @@ +mod render; mod runner; pub(crate) use runner::build_site; diff --git a/src/command/build/render.rs b/src/command/build/render.rs new file mode 100644 index 0000000..853028a --- /dev/null +++ b/src/command/build/render.rs @@ -0,0 +1,98 @@ +use std::ffi::OsStr; +use std::path::PathBuf; + +use include_dir::include_dir; +use include_dir::Dir; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::convert_blog_post_page_to_render_context; +use crate::intermediate::BlogPost; +use crate::render::DusterRenderer; +use crate::render::RendererIntegration; + +static MAIN_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/default_environment/templates/html"); + +pub(crate) struct SiteRenderer { + output_directory: PathBuf, + blog_posts: Vec, +} + +impl SiteRenderer { + pub(crate) fn new>( + output_directory: P, + blog_posts: Vec, + ) -> SiteRenderer { + SiteRenderer { + output_directory: output_directory.into(), + blog_posts, + } + } + pub(crate) async fn render_blog_posts(&self, config: &Config) -> Result<(), CustomError> { + let mut renderer_integration = DusterRenderer::new(); + + let sources: Vec<_> = MAIN_TEMPLATES + .files() + .filter(|f| f.path().extension() == Some(OsStr::new("dust"))) + .collect(); + if sources + .iter() + .filter(|f| f.path().file_stem() == Some(OsStr::new("main"))) + .count() + != 1 + { + return Err("Expect exactly 1 main.dust template file.".into()); + } + + let decoded_templates = { + let mut decoded_templates = Vec::with_capacity(sources.len()); + for entry in sources { + decoded_templates.push(build_name_contents_pairs(entry)?); + } + decoded_templates + }; + + for (name, contents) in decoded_templates { + renderer_integration.load_template(name, contents)?; + } + + for blog_post in &self.blog_posts { + for blog_post_page in &blog_post.pages { + let output_path = self + .output_directory + .join("posts") + .join(&blog_post.id) + .join(blog_post_page.get_output_path()); + + let render_context = convert_blog_post_page_to_render_context( + config, + &self.output_directory, + &output_path, + blog_post, + blog_post_page, + )?; + let rendered_output = renderer_integration.render(render_context)?; + let parent_directory = output_path + .parent() + .ok_or("Output file should have a containing directory.")?; + tokio::fs::create_dir_all(parent_directory).await?; + tokio::fs::write(output_path, rendered_output).await?; + } + } + + Ok(()) + } +} + +fn build_name_contents_pairs<'a>( + entry: &'a include_dir::File<'_>, +) -> Result<(&'a str, &'a str), CustomError> { + let path = entry.path(); + let name = path + .file_stem() + .ok_or("All templates should have a stem.")? + .to_str() + .ok_or("All template filenames should be valid utf-8.")?; + let contents = std::str::from_utf8(entry.contents())?; + Ok((name, contents)) +} diff --git a/src/command/build/runner.rs b/src/command/build/runner.rs index c9183a3..13dc846 100644 --- a/src/command/build/runner.rs +++ b/src/command/build/runner.rs @@ -1,7 +1,60 @@ -use crate::cli::parameters::BuildArgs; -use crate::config::Config; +use std::path::PathBuf; + +use crate::cli::parameters::BuildArgs; +use crate::command::build::render::SiteRenderer; +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::BlogPost; + +pub(crate) async fn build_site(args: BuildArgs) -> Result<(), CustomError> { + let config = Config::load_from_file(args.config).await?; + let blog_posts = load_blog_posts(&config).await?; + let renderer = SiteRenderer::new(get_output_directory(&config).await?, blog_posts); + renderer.render_blog_posts(&config).await?; -pub(crate) async fn build_site(args: BuildArgs) -> Result<(), Box> { - let _config = Config::load_from_file(args.config).await?; Ok(()) } + +/// Delete everything inside the output directory and return the path to that directory. +async fn get_output_directory(config: &Config) -> Result { + let output_directory = config.get_output_directory(); + if !output_directory.exists() { + tokio::fs::create_dir(&output_directory).await?; + } else { + let mut existing_entries = tokio::fs::read_dir(&output_directory).await?; + while let Some(entry) = existing_entries.next_entry().await? { + let file_type = entry.file_type().await?; + if file_type.is_dir() { + tokio::fs::remove_dir_all(entry.path()).await?; + } else { + tokio::fs::remove_file(entry.path()).await?; + } + } + } + Ok(output_directory) +} + +async fn get_post_directories(config: &Config) -> Result, CustomError> { + let mut ret = Vec::new(); + let mut entries = tokio::fs::read_dir(config.get_posts_directory()).await?; + while let Some(entry) = entries.next_entry().await? { + let file_type = entry.file_type().await?; + if file_type.is_dir() { + ret.push(entry.path()); + } + } + Ok(ret) +} + +async fn load_blog_posts(config: &Config) -> Result, CustomError> { + let root_directory = config.get_root_directory().to_owned(); + let post_directories = get_post_directories(&config).await?; + let load_jobs = post_directories + .into_iter() + .map(|path| tokio::spawn(BlogPost::load_blog_post(root_directory.clone(), path))); + let mut blog_posts = Vec::new(); + for job in load_jobs { + blog_posts.push(job.await??); + } + Ok(blog_posts) +} diff --git a/src/command/init/runner.rs b/src/command/init/runner.rs index 976f7a2..40d1871 100644 --- a/src/command/init/runner.rs +++ b/src/command/init/runner.rs @@ -1,7 +1,8 @@ use crate::cli::parameters::InitArgs; use crate::config::Config; +use crate::error::CustomError; -pub(crate) async fn init_writer_folder(args: InitArgs) -> Result<(), Box> { +pub(crate) async fn init_writer_folder(args: InitArgs) -> Result<(), CustomError> { if args.path.exists() && !args.path.is_dir() { return Err("The supplied path exists but is not a directory. Aborting.".into()); } diff --git a/src/config/full.rs b/src/config/full.rs index 49a0e8f..0f962aa 100644 --- a/src/config/full.rs +++ b/src/config/full.rs @@ -3,6 +3,8 @@ use std::path::PathBuf; use tokio::fs::File; use tokio::io::AsyncWriteExt; +use crate::error::CustomError; + use super::raw::RawConfig; /// This is the config struct used by most of the code, which is an interpreted version of the RawConfig struct which is the raw disk-representation of the config. @@ -12,8 +14,8 @@ pub(crate) struct Config { } impl Config { - pub(crate) fn new>(root_dir: P) -> Result> { - fn inner(root_dir: &Path) -> Result> { + pub(crate) fn new>(root_dir: P) -> Result { + fn inner(root_dir: &Path) -> Result { let file_path = root_dir.join("writer.toml"); Ok(Config { raw: RawConfig::default(), @@ -23,10 +25,8 @@ impl Config { inner(root_dir.as_ref()) } - pub(crate) async fn load_from_file>( - path: P, - ) -> Result> { - async fn inner(path: PathBuf) -> Result> { + pub(crate) async fn load_from_file>(path: P) -> Result { + async fn inner(path: PathBuf) -> Result { let contents = tokio::fs::read_to_string(&path).await?; let parsed_contents: RawConfig = toml::from_str(contents.as_str())?; Ok(Config { @@ -37,11 +37,34 @@ impl Config { inner(path.into()).await } - pub(crate) async fn write_to_disk(&self) -> Result<(), Box> { + pub(crate) async fn write_to_disk(&self) -> Result<(), CustomError> { let mut config_file = File::create(&self.config_path).await?; config_file .write_all(toml::to_string(&self.raw)?.as_bytes()) .await?; Ok(()) } + + pub(crate) fn get_root_directory(&self) -> &Path { + &self + .config_path + .parent() + .expect("Config file must exist inside a directory.") + } + + pub(crate) fn get_posts_directory(&self) -> PathBuf { + self.get_root_directory().join("posts") + } + + pub(crate) fn get_output_directory(&self) -> PathBuf { + self.get_root_directory().join("output") + } + + pub(crate) fn use_relative_paths(&self) -> bool { + self.raw.use_relative_paths.unwrap_or(true) + } + + pub(crate) fn get_web_root(&self) -> Option<&str> { + self.raw.web_root.as_deref() + } } diff --git a/src/config/raw.rs b/src/config/raw.rs index 3d43bd5..b0d95cf 100644 --- a/src/config/raw.rs +++ b/src/config/raw.rs @@ -7,6 +7,8 @@ pub(crate) struct RawConfig { site_title: String, author: Option, email: Option, + pub(super) use_relative_paths: Option, + pub(super) web_root: Option, } impl Default for RawConfig { @@ -15,6 +17,8 @@ impl Default for RawConfig { site_title: "My super awesome website".to_owned(), author: None, email: None, + use_relative_paths: None, + web_root: None, } } } diff --git a/src/context/blog_post_page.rs b/src/context/blog_post_page.rs new file mode 100644 index 0000000..9f607ce --- /dev/null +++ b/src/context/blog_post_page.rs @@ -0,0 +1,34 @@ +use serde::Serialize; + +use super::GlobalSettings; +use super::RenderDocumentElement; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "blog_post_page")] +pub(crate) struct RenderBlogPostPage { + global_settings: GlobalSettings, + + /// The title that will be shown visibly on the page. + title: Option, + + self_link: Option, + + children: Vec, +} + +impl RenderBlogPostPage { + pub(crate) fn new( + global_settings: GlobalSettings, + title: Option, + self_link: Option, + children: Vec, + ) -> RenderBlogPostPage { + RenderBlogPostPage { + global_settings, + title, + self_link, + children, + } + } +} diff --git a/src/context/comment.rs b/src/context/comment.rs new file mode 100644 index 0000000..713c220 --- /dev/null +++ b/src/context/comment.rs @@ -0,0 +1,23 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::IComment; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "comment")] +pub(crate) struct RenderComment {} + +impl RenderComment { + pub(crate) fn new, F: AsRef>( + config: &Config, + output_directory: D, + output_file: F, + comment: &IComment, + ) -> Result { + Ok(RenderComment {}) + } +} diff --git a/src/context/document_element.rs b/src/context/document_element.rs new file mode 100644 index 0000000..6b11e0b --- /dev/null +++ b/src/context/document_element.rs @@ -0,0 +1,11 @@ +use serde::Serialize; + +use super::RenderHeading; +use super::RenderSection; + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub(crate) enum RenderDocumentElement { + Heading(RenderHeading), + Section(RenderSection), +} diff --git a/src/context/element.rs b/src/context/element.rs new file mode 100644 index 0000000..5144938 --- /dev/null +++ b/src/context/element.rs @@ -0,0 +1,49 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::IElement; + +use super::comment::RenderComment; +use super::keyword::RenderKeyword; +use super::paragraph::RenderParagraph; + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub(crate) enum RenderElement { + Paragraph(RenderParagraph), + Keyword(RenderKeyword), + Comment(RenderComment), +} + +impl RenderElement { + pub(crate) fn new, F: AsRef>( + config: &Config, + output_directory: D, + output_file: F, + element: &IElement, + ) -> Result { + match element { + IElement::Paragraph(inner) => Ok(RenderElement::Paragraph(RenderParagraph::new( + config, + output_directory, + output_file, + inner, + )?)), + IElement::Keyword(inner) => Ok(RenderElement::Keyword(RenderKeyword::new( + config, + output_directory, + output_file, + inner, + )?)), + IElement::Comment(inner) => Ok(RenderElement::Comment(RenderComment::new( + config, + output_directory, + output_file, + inner, + )?)), + } + } +} diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs new file mode 100644 index 0000000..53e3eda --- /dev/null +++ b/src/context/global_settings.rs @@ -0,0 +1,24 @@ +use serde::Serialize; + +/// The settings that a "global" to a single dustjs render. +#[derive(Debug, Serialize)] +pub(crate) struct GlobalSettings { + /// The title that goes in the html tag in the <head>. + page_title: Option<String>, + css_files: Vec<String>, + js_files: Vec<String>, +} + +impl GlobalSettings { + pub(crate) fn new( + page_title: Option<String>, + css_files: Vec<String>, + js_files: Vec<String>, + ) -> GlobalSettings { + GlobalSettings { + page_title, + css_files, + js_files, + } + } +} diff --git a/src/context/heading.rs b/src/context/heading.rs new file mode 100644 index 0000000..c609c5a --- /dev/null +++ b/src/context/heading.rs @@ -0,0 +1,36 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::IHeading; + +use super::RenderObject; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "heading")] +pub(crate) struct RenderHeading { + level: organic::types::HeadlineLevel, + title: Vec<RenderObject>, +} + +impl RenderHeading { + pub(crate) fn new<D: AsRef<Path>, F: AsRef<Path>>( + config: &Config, + output_directory: D, + output_file: F, + heading: &IHeading, + ) -> Result<RenderHeading, CustomError> { + let title = heading + .title + .iter() + .map(|obj| RenderObject::new(config, &output_directory, &output_file, obj)) + .collect::<Result<Vec<_>, _>>()?; + Ok(RenderHeading { + level: heading.level, + title, + }) + } +} diff --git a/src/context/keyword.rs b/src/context/keyword.rs new file mode 100644 index 0000000..c9e7a10 --- /dev/null +++ b/src/context/keyword.rs @@ -0,0 +1,23 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::IKeyword; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "keyword")] +pub(crate) struct RenderKeyword {} + +impl RenderKeyword { + pub(crate) fn new<D: AsRef<Path>, F: AsRef<Path>>( + config: &Config, + output_directory: D, + output_file: F, + keyword: &IKeyword, + ) -> Result<RenderKeyword, CustomError> { + Ok(RenderKeyword {}) + } +} diff --git a/src/context/mod.rs b/src/context/mod.rs new file mode 100644 index 0000000..96a8811 --- /dev/null +++ b/src/context/mod.rs @@ -0,0 +1,20 @@ +mod blog_post_page; +mod comment; +mod document_element; +mod element; +mod global_settings; +mod heading; +mod keyword; +mod object; +mod paragraph; +mod plain_text; +mod section; +mod target; + +pub(crate) use blog_post_page::RenderBlogPostPage; +pub(crate) use document_element::RenderDocumentElement; +pub(crate) use element::RenderElement; +pub(crate) use global_settings::GlobalSettings; +pub(crate) use heading::RenderHeading; +pub(crate) use object::RenderObject; +pub(crate) use section::RenderSection; diff --git a/src/context/object.rs b/src/context/object.rs new file mode 100644 index 0000000..c54a619 --- /dev/null +++ b/src/context/object.rs @@ -0,0 +1,41 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::IObject; + +use super::plain_text::RenderPlainText; +use super::target::RenderTarget; + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub(crate) enum RenderObject { + PlainText(RenderPlainText), + Target(RenderTarget), +} + +impl RenderObject { + pub(crate) fn new<D: AsRef<Path>, F: AsRef<Path>>( + config: &Config, + output_directory: D, + output_file: F, + object: &IObject, + ) -> Result<RenderObject, CustomError> { + match object { + IObject::PlainText(inner) => Ok(RenderObject::PlainText(RenderPlainText::new( + config, + output_directory, + output_file, + inner, + )?)), + IObject::Target(inner) => Ok(RenderObject::Target(RenderTarget::new( + config, + output_directory, + output_file, + inner, + )?)), + } + } +} diff --git a/src/context/paragraph.rs b/src/context/paragraph.rs new file mode 100644 index 0000000..2570f93 --- /dev/null +++ b/src/context/paragraph.rs @@ -0,0 +1,32 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::IParagraph; + +use super::RenderObject; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "paragraph")] +pub(crate) struct RenderParagraph { + children: Vec<RenderObject>, +} + +impl RenderParagraph { + pub(crate) fn new<D: AsRef<Path>, F: AsRef<Path>>( + config: &Config, + output_directory: D, + output_file: F, + paragraph: &IParagraph, + ) -> Result<RenderParagraph, CustomError> { + let children = paragraph + .children + .iter() + .map(|obj| RenderObject::new(config, &output_directory, &output_file, obj)) + .collect::<Result<Vec<_>, _>>()?; + Ok(RenderParagraph { children }) + } +} diff --git a/src/context/plain_text.rs b/src/context/plain_text.rs new file mode 100644 index 0000000..76f3d78 --- /dev/null +++ b/src/context/plain_text.rs @@ -0,0 +1,23 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::IPlainText; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "plain_text")] +pub(crate) struct RenderPlainText {} + +impl RenderPlainText { + pub(crate) fn new<D: AsRef<Path>, F: AsRef<Path>>( + config: &Config, + output_directory: D, + output_file: F, + heading: &IPlainText, + ) -> Result<RenderPlainText, CustomError> { + Ok(RenderPlainText {}) + } +} diff --git a/src/context/section.rs b/src/context/section.rs new file mode 100644 index 0000000..d614638 --- /dev/null +++ b/src/context/section.rs @@ -0,0 +1,33 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::ISection; + +use super::RenderElement; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "section")] +pub(crate) struct RenderSection { + children: Vec<RenderElement>, +} + +impl RenderSection { + pub(crate) fn new<D: AsRef<Path>, F: AsRef<Path>>( + config: &Config, + output_directory: D, + output_file: F, + section: &ISection, + ) -> Result<RenderSection, CustomError> { + let children = section + .children + .iter() + .map(|obj| RenderElement::new(config, &output_directory, &output_file, obj)) + .collect::<Result<Vec<_>, _>>()?; + + Ok(RenderSection { children }) + } +} diff --git a/src/context/target.rs b/src/context/target.rs new file mode 100644 index 0000000..ebdb69c --- /dev/null +++ b/src/context/target.rs @@ -0,0 +1,27 @@ +use std::path::Path; + +use serde::Serialize; + +use crate::config::Config; +use crate::error::CustomError; +use crate::intermediate::ITarget; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "target")] +pub(crate) struct RenderTarget { + id: String, +} + +impl RenderTarget { + pub(crate) fn new<D: AsRef<Path>, F: AsRef<Path>>( + config: &Config, + output_directory: D, + output_file: F, + target: &ITarget, + ) -> Result<RenderTarget, CustomError> { + Ok(RenderTarget { + id: target.id.clone(), + }) + } +} diff --git a/src/error/error.rs b/src/error/error.rs new file mode 100644 index 0000000..9707bdb --- /dev/null +++ b/src/error/error.rs @@ -0,0 +1,90 @@ +use std::str::Utf8Error; +use std::string::FromUtf8Error; + +#[derive(Debug)] +pub(crate) enum CustomError { + Static(&'static str), + IO(std::io::Error), + TomlSerialize(toml::ser::Error), + TomlDeserialize(toml::de::Error), + WalkDir(walkdir::Error), + Tokio(tokio::task::JoinError), + Serde(serde_json::Error), + Utf8(Utf8Error), + FromUtf8(FromUtf8Error), + DusterCompile(duster::renderer::CompileError), + DusterRender(duster::renderer::RenderError), + PathStripPrefix(std::path::StripPrefixError), +} + +impl From<std::io::Error> for CustomError { + fn from(value: std::io::Error) -> Self { + CustomError::IO(value) + } +} + +impl From<&'static str> for CustomError { + fn from(value: &'static str) -> Self { + CustomError::Static(value) + } +} + +impl From<toml::ser::Error> for CustomError { + fn from(value: toml::ser::Error) -> Self { + CustomError::TomlSerialize(value) + } +} + +impl From<toml::de::Error> for CustomError { + fn from(value: toml::de::Error) -> Self { + CustomError::TomlDeserialize(value) + } +} + +impl From<walkdir::Error> for CustomError { + fn from(value: walkdir::Error) -> Self { + CustomError::WalkDir(value) + } +} + +impl From<tokio::task::JoinError> for CustomError { + fn from(value: tokio::task::JoinError) -> Self { + CustomError::Tokio(value) + } +} + +impl From<serde_json::Error> for CustomError { + fn from(value: serde_json::Error) -> Self { + CustomError::Serde(value) + } +} + +impl From<Utf8Error> for CustomError { + fn from(value: Utf8Error) -> Self { + CustomError::Utf8(value) + } +} + +impl From<FromUtf8Error> for CustomError { + fn from(value: FromUtf8Error) -> Self { + CustomError::FromUtf8(value) + } +} + +impl From<duster::renderer::CompileError> for CustomError { + fn from(value: duster::renderer::CompileError) -> Self { + CustomError::DusterCompile(value) + } +} + +impl From<duster::renderer::RenderError> for CustomError { + fn from(value: duster::renderer::RenderError) -> Self { + CustomError::DusterRender(value) + } +} + +impl From<std::path::StripPrefixError> for CustomError { + fn from(value: std::path::StripPrefixError) -> Self { + CustomError::PathStripPrefix(value) + } +} diff --git a/src/error/mod.rs b/src/error/mod.rs new file mode 100644 index 0000000..84e7c7c --- /dev/null +++ b/src/error/mod.rs @@ -0,0 +1,3 @@ +#[allow(clippy::module_inception)] +mod error; +pub(crate) use error::CustomError; diff --git a/src/intermediate/comment.rs b/src/intermediate/comment.rs new file mode 100644 index 0000000..70649f2 --- /dev/null +++ b/src/intermediate/comment.rs @@ -0,0 +1,16 @@ +use crate::error::CustomError; + +use super::registry::Registry; + +/// Essentially a no-op since the comment is not rendered. +#[derive(Debug)] +pub(crate) struct IComment {} + +impl IComment { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + comment: &organic::types::Comment<'parse>, + ) -> Result<IComment, CustomError> { + Ok(IComment {}) + } +} diff --git a/src/intermediate/convert.rs b/src/intermediate/convert.rs new file mode 100644 index 0000000..f28ac59 --- /dev/null +++ b/src/intermediate/convert.rs @@ -0,0 +1,168 @@ +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; + +use crate::config::Config; +use crate::context::GlobalSettings; +use crate::context::RenderBlogPostPage; +use crate::context::RenderDocumentElement; +use crate::context::RenderHeading; +use crate::context::RenderSection; +use crate::error::CustomError; + +use super::BlogPost; +use super::BlogPostPage; +use super::IDocumentElement; + +pub(crate) fn convert_blog_post_page_to_render_context<D: AsRef<Path>, F: AsRef<Path>>( + config: &Config, + output_directory: D, + output_file: F, + _post: &BlogPost, + page: &BlogPostPage, +) -> Result<RenderBlogPostPage, CustomError> { + let output_directory = output_directory.as_ref(); + let output_file = output_file.as_ref(); + let css_files = vec![get_web_path( + config, + output_directory, + output_file, + "main.css", + )?]; + let js_files = vec![get_web_path( + config, + output_directory, + output_file, + "blog_post.js", + )?]; + let global_settings = GlobalSettings::new(page.title.clone(), css_files, js_files); + let link_to_blog_post = get_web_path( + config, + output_directory, + output_file, + output_file.strip_prefix(output_directory)?, + )?; + + let children = { + let mut children = Vec::new(); + + for child in page.children.iter() { + match child { + IDocumentElement::Heading(heading) => { + children.push(RenderDocumentElement::Heading(RenderHeading::new( + config, + output_directory, + output_file, + heading, + )?)); + } + IDocumentElement::Section(section) => { + children.push(RenderDocumentElement::Section(RenderSection::new( + config, + output_directory, + output_file, + section, + )?)); + } + } + } + + children + }; + + let ret = RenderBlogPostPage::new( + global_settings, + page.title.clone(), + Some(link_to_blog_post), + children, + ); + Ok(ret) +} + +fn get_web_path<D: AsRef<Path>, F: AsRef<Path>, P: AsRef<Path>>( + config: &Config, + output_directory: D, + containing_file: F, + path_from_web_root: P, +) -> Result<String, CustomError> { + let path_from_web_root = path_from_web_root.as_ref(); + if config.use_relative_paths() { + let output_directory = output_directory.as_ref(); + let containing_file = containing_file.as_ref(); + let containing_file_relative_to_output_directory = + containing_file.strip_prefix(output_directory)?; + let shared_stem = get_shared_steps( + containing_file_relative_to_output_directory + .parent() + .ok_or("File should exist in a folder.")?, + path_from_web_root + .parent() + .ok_or("File should exist in a folder.")?, + ) + .collect::<PathBuf>(); + // Subtracting 1 from the depth to "remove" the file name. + let depth_from_shared_stem = containing_file_relative_to_output_directory + .strip_prefix(&shared_stem)? + .components() + .count() + - 1; + let final_path = PathBuf::from("../".repeat(depth_from_shared_stem)) + .join(path_from_web_root.strip_prefix(shared_stem)?); + let final_string = final_path + .as_path() + .to_str() + .map(str::to_string) + .ok_or("Path should be valid utf-8.")?; + Ok(final_string) + } else { + let web_root = config + .get_web_root() + .ok_or("Must either use_relative_paths or set the web_root in the config.")?; + let final_path = PathBuf::from(web_root).join(path_from_web_root); + let final_string = final_path + .as_path() + .to_str() + .map(str::to_string) + .ok_or("Path should be valid utf-8.")?; + Ok(final_string) + } +} + +fn get_shared_steps<'a>(left: &'a Path, right: &'a Path) -> impl Iterator<Item = Component<'a>> { + let shared_stem = left + .components() + .zip(right.components()) + .take_while(|(l, r)| l == r) + .map(|(l, _r)| l); + shared_stem +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_shared_steps() { + assert_eq!( + get_shared_steps(Path::new(""), Path::new("")).collect::<PathBuf>(), + PathBuf::from("") + ); + assert_eq!( + get_shared_steps(Path::new("foo.txt"), Path::new("foo.txt")).collect::<PathBuf>(), + PathBuf::from("foo.txt") + ); + assert_eq!( + get_shared_steps(Path::new("cat/foo.txt"), Path::new("dog/foo.txt")) + .collect::<PathBuf>(), + PathBuf::from("") + ); + assert_eq!( + get_shared_steps( + Path::new("foo/bar/baz/lorem.txt"), + Path::new("foo/bar/ipsum/dolar.txt") + ) + .collect::<PathBuf>(), + PathBuf::from("foo/bar") + ); + } +} diff --git a/src/intermediate/definition.rs b/src/intermediate/definition.rs new file mode 100644 index 0000000..2f62ca7 --- /dev/null +++ b/src/intermediate/definition.rs @@ -0,0 +1,111 @@ +use std::path::Path; +use std::path::PathBuf; + +use tokio::task::JoinHandle; +use walkdir::WalkDir; + +use crate::error::CustomError; +use crate::intermediate::registry::Registry; + +use super::BlogPostPage; + +#[derive(Debug)] +pub(crate) struct BlogPost { + pub(crate) id: String, + pub(crate) pages: Vec<BlogPostPage>, +} + +impl BlogPost { + pub(crate) async fn load_blog_post<P: AsRef<Path>, R: AsRef<Path>>( + root_dir: R, + post_dir: P, + ) -> Result<BlogPost, CustomError> { + async fn inner(_root_dir: &Path, post_dir: &Path) -> Result<BlogPost, CustomError> { + let post_id = post_dir + .file_name() + .expect("The post directory should have a name."); + + let org_files = { + let mut ret = Vec::new(); + let org_files_iter = get_org_files(post_dir)?; + for entry in org_files_iter { + ret.push(entry.await??); + } + ret + }; + let parsed_org_files = { + let mut ret = Vec::new(); + for (path, contents) in org_files.iter() { + let parsed = organic::parser::parse_file(contents.as_str(), Some(path)) + .map_err(|_| CustomError::Static("Failed to parse org-mode document."))?; + ret.push((path, contents, parsed)); + } + ret + }; + + let mut registry = Registry::new(); + + // Assign IDs to the targets + for (_real_path, _contents, parsed_document) in parsed_org_files.iter() { + organic::types::AstNode::from(parsed_document) + .iter_all_ast_nodes() + .for_each(|node| match node { + organic::types::AstNode::Target(target) => { + registry.get_target(target.value); + } + _ => {} + }); + } + + let pages = { + let mut ret = Vec::new(); + for (real_path, _contents, parsed_document) in parsed_org_files.iter() { + let relative_to_post_dir_path = real_path.strip_prefix(post_dir)?; + ret.push( + BlogPostPage::new( + relative_to_post_dir_path, + &mut registry, + parsed_document, + ) + .await?, + ); + } + ret + }; + + Ok(BlogPost { + id: post_id.to_string_lossy().into_owned(), + pages, + }) + } + inner(root_dir.as_ref(), post_dir.as_ref()).await + } +} + +async fn read_file(path: PathBuf) -> std::io::Result<(PathBuf, String)> { + let contents = tokio::fs::read_to_string(&path).await?; + Ok((path, contents)) +} + +fn get_org_files<P: AsRef<Path>>( + root_dir: P, +) -> Result<impl Iterator<Item = JoinHandle<std::io::Result<(PathBuf, String)>>>, walkdir::Error> { + let org_files = WalkDir::new(root_dir) + .into_iter() + .filter(|e| match e { + Ok(dir_entry) => { + dir_entry.file_type().is_file() + && Path::new(dir_entry.file_name()) + .extension() + .map(|ext| ext.to_ascii_lowercase() == "org") + .unwrap_or(false) + } + Err(_) => true, + }) + .collect::<Result<Vec<_>, _>>()?; + let org_files = org_files + .into_iter() + .map(walkdir::DirEntry::into_path) + .map(|path| tokio::spawn(read_file(path))); + Ok(org_files) +} diff --git a/src/intermediate/document_element.rs b/src/intermediate/document_element.rs new file mode 100644 index 0000000..3f35270 --- /dev/null +++ b/src/intermediate/document_element.rs @@ -0,0 +1,8 @@ +use super::IHeading; +use super::ISection; + +#[derive(Debug)] +pub(crate) enum IDocumentElement { + Heading(IHeading), + Section(ISection), +} diff --git a/src/intermediate/element.rs b/src/intermediate/element.rs new file mode 100644 index 0000000..fd2b4f1 --- /dev/null +++ b/src/intermediate/element.rs @@ -0,0 +1,53 @@ +use crate::error::CustomError; + +use super::comment::IComment; +use super::keyword::IKeyword; +use super::registry::Registry; +use super::IParagraph; + +#[derive(Debug)] +pub(crate) enum IElement { + Paragraph(IParagraph), + Keyword(IKeyword), + Comment(IComment), +} + +impl IElement { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + elem: &organic::types::Element<'parse>, + ) -> Result<IElement, CustomError> { + match elem { + organic::types::Element::Paragraph(inner) => { + Ok(IElement::Paragraph(IParagraph::new(registry, inner).await?)) + } + organic::types::Element::PlainList(_) => todo!(), + organic::types::Element::CenterBlock(_) => todo!(), + organic::types::Element::QuoteBlock(_) => todo!(), + organic::types::Element::SpecialBlock(_) => todo!(), + organic::types::Element::DynamicBlock(_) => todo!(), + organic::types::Element::FootnoteDefinition(_) => todo!(), + organic::types::Element::Comment(inner) => { + Ok(IElement::Comment(IComment::new(registry, inner).await?)) + } + organic::types::Element::Drawer(_) => todo!(), + organic::types::Element::PropertyDrawer(_) => todo!(), + organic::types::Element::Table(_) => todo!(), + organic::types::Element::VerseBlock(_) => todo!(), + organic::types::Element::CommentBlock(_) => todo!(), + organic::types::Element::ExampleBlock(_) => todo!(), + organic::types::Element::ExportBlock(_) => todo!(), + organic::types::Element::SrcBlock(_) => todo!(), + organic::types::Element::Clock(_) => todo!(), + organic::types::Element::DiarySexp(_) => todo!(), + organic::types::Element::Planning(_) => todo!(), + organic::types::Element::FixedWidthArea(_) => todo!(), + organic::types::Element::HorizontalRule(_) => todo!(), + organic::types::Element::Keyword(inner) => { + Ok(IElement::Keyword(IKeyword::new(registry, inner).await?)) + } + organic::types::Element::BabelCall(_) => todo!(), + organic::types::Element::LatexEnvironment(_) => todo!(), + } + } +} diff --git a/src/intermediate/heading.rs b/src/intermediate/heading.rs new file mode 100644 index 0000000..17cc680 --- /dev/null +++ b/src/intermediate/heading.rs @@ -0,0 +1,29 @@ +use crate::error::CustomError; + +use super::registry::Registry; +use super::IObject; + +#[derive(Debug)] +pub(crate) struct IHeading { + pub(crate) level: organic::types::HeadlineLevel, + pub(crate) title: Vec<IObject>, +} + +impl IHeading { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + heading: &organic::types::Heading<'parse>, + ) -> Result<IHeading, CustomError> { + let title = { + let mut ret = Vec::new(); + for obj in heading.title.iter() { + ret.push(IObject::new(registry, obj).await?); + } + ret + }; + Ok(IHeading { + title, + level: heading.level, + }) + } +} diff --git a/src/intermediate/keyword.rs b/src/intermediate/keyword.rs new file mode 100644 index 0000000..bd26939 --- /dev/null +++ b/src/intermediate/keyword.rs @@ -0,0 +1,16 @@ +use crate::error::CustomError; + +use super::registry::Registry; + +/// Essentially a no-op since the keyword is not rendered and any relevant impact on other elements is pulled from the parsed form of keyword. +#[derive(Debug)] +pub(crate) struct IKeyword {} + +impl IKeyword { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + keyword: &organic::types::Keyword<'parse>, + ) -> Result<IKeyword, CustomError> { + Ok(IKeyword {}) + } +} diff --git a/src/intermediate/mod.rs b/src/intermediate/mod.rs new file mode 100644 index 0000000..b363853 --- /dev/null +++ b/src/intermediate/mod.rs @@ -0,0 +1,28 @@ +mod comment; +mod convert; +mod definition; +mod document_element; +mod element; +mod heading; +mod keyword; +mod object; +mod page; +mod paragraph; +mod plain_text; +mod registry; +mod section; +mod target; +mod util; +pub(crate) use comment::IComment; +pub(crate) use convert::convert_blog_post_page_to_render_context; +pub(crate) use definition::BlogPost; +pub(crate) use document_element::IDocumentElement; +pub(crate) use element::IElement; +pub(crate) use heading::IHeading; +pub(crate) use keyword::IKeyword; +pub(crate) use object::IObject; +pub(crate) use page::BlogPostPage; +pub(crate) use paragraph::IParagraph; +pub(crate) use plain_text::IPlainText; +pub(crate) use section::ISection; +pub(crate) use target::ITarget; diff --git a/src/intermediate/object.rs b/src/intermediate/object.rs new file mode 100644 index 0000000..3506a00 --- /dev/null +++ b/src/intermediate/object.rs @@ -0,0 +1,52 @@ +use crate::error::CustomError; + +use super::plain_text::IPlainText; +use super::registry::Registry; +use super::ITarget; + +#[derive(Debug)] +pub(crate) enum IObject { + PlainText(IPlainText), + Target(ITarget), +} + +impl IObject { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + obj: &organic::types::Object<'parse>, + ) -> Result<IObject, CustomError> { + match obj { + organic::types::Object::Bold(_) => todo!(), + organic::types::Object::Italic(_) => todo!(), + organic::types::Object::Underline(_) => todo!(), + organic::types::Object::StrikeThrough(_) => todo!(), + organic::types::Object::Code(_) => todo!(), + organic::types::Object::Verbatim(_) => todo!(), + organic::types::Object::PlainText(inner) => { + Ok(IObject::PlainText(IPlainText::new(registry, inner).await?)) + } + organic::types::Object::RegularLink(_) => todo!(), + organic::types::Object::RadioLink(_) => todo!(), + organic::types::Object::RadioTarget(_) => todo!(), + organic::types::Object::PlainLink(_) => todo!(), + organic::types::Object::AngleLink(_) => todo!(), + organic::types::Object::OrgMacro(_) => todo!(), + organic::types::Object::Entity(_) => todo!(), + organic::types::Object::LatexFragment(_) => todo!(), + organic::types::Object::ExportSnippet(_) => todo!(), + organic::types::Object::FootnoteReference(_) => todo!(), + organic::types::Object::Citation(_) => todo!(), + organic::types::Object::CitationReference(_) => todo!(), + organic::types::Object::InlineBabelCall(_) => todo!(), + organic::types::Object::InlineSourceBlock(_) => todo!(), + organic::types::Object::LineBreak(_) => todo!(), + organic::types::Object::Target(inner) => { + Ok(IObject::Target(ITarget::new(registry, inner).await?)) + } + organic::types::Object::StatisticsCookie(_) => todo!(), + organic::types::Object::Subscript(_) => todo!(), + organic::types::Object::Superscript(_) => todo!(), + organic::types::Object::Timestamp(_) => todo!(), + } + } +} diff --git a/src/intermediate/page.rs b/src/intermediate/page.rs new file mode 100644 index 0000000..c512d46 --- /dev/null +++ b/src/intermediate/page.rs @@ -0,0 +1,65 @@ +use std::path::PathBuf; + +use crate::error::CustomError; + +use super::registry::Registry; +use super::IDocumentElement; +use super::IHeading; +use super::ISection; + +#[derive(Debug)] +pub(crate) struct BlogPostPage { + /// Relative path from the root of the blog post. + pub(crate) path: PathBuf, + + pub(crate) title: Option<String>, + + pub(crate) children: Vec<IDocumentElement>, +} + +impl BlogPostPage { + pub(crate) async fn new<'parse, P: Into<PathBuf>>( + path: P, + registry: &mut Registry<'parse>, + document: &organic::types::Document<'parse>, + ) -> Result<BlogPostPage, CustomError> { + let path = path.into(); + let mut children = Vec::new(); + if let Some(section) = document.zeroth_section.as_ref() { + children.push(IDocumentElement::Section( + ISection::new(registry, section).await?, + )); + } + for heading in document.children.iter() { + children.push(IDocumentElement::Heading( + IHeading::new(registry, heading).await?, + )); + } + + Ok(BlogPostPage { + path, + title: get_title(&document), + children, + }) + } + + /// Get the output path relative to the post directory. + pub(crate) fn get_output_path(&self) -> PathBuf { + let mut ret = self.path.clone(); + ret.set_extension("html"); + ret + } +} + +fn get_title(document: &organic::types::Document<'_>) -> Option<String> { + organic::types::AstNode::from(document) + .iter_all_ast_nodes() + .filter_map(|node| match node { + organic::types::AstNode::Keyword(kw) if kw.key.eq_ignore_ascii_case("title") => { + Some(kw) + } + _ => None, + }) + .last() + .map(|kw| kw.value.to_owned()) +} diff --git a/src/intermediate/paragraph.rs b/src/intermediate/paragraph.rs new file mode 100644 index 0000000..09cf1be --- /dev/null +++ b/src/intermediate/paragraph.rs @@ -0,0 +1,26 @@ +use crate::error::CustomError; + +use super::registry::Registry; +use super::IObject; + +#[derive(Debug)] +pub(crate) struct IParagraph { + pub(crate) children: Vec<IObject>, +} + +impl IParagraph { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + paragraph: &organic::types::Paragraph<'parse>, + ) -> Result<IParagraph, CustomError> { + let children = { + let mut ret = Vec::new(); + for obj in paragraph.children.iter() { + ret.push(IObject::new(registry, obj).await?); + } + ret + }; + + Ok(IParagraph { children }) + } +} diff --git a/src/intermediate/plain_text.rs b/src/intermediate/plain_text.rs new file mode 100644 index 0000000..490c843 --- /dev/null +++ b/src/intermediate/plain_text.rs @@ -0,0 +1,20 @@ +use crate::error::CustomError; +use crate::intermediate::util::coalesce_whitespace; + +use super::registry::Registry; + +#[derive(Debug)] +pub(crate) struct IPlainText { + source: String, +} + +impl IPlainText { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + plain_text: &organic::types::PlainText<'parse>, + ) -> Result<IPlainText, CustomError> { + Ok(IPlainText { + source: coalesce_whitespace(plain_text.source).into_owned(), + }) + } +} diff --git a/src/intermediate/registry.rs b/src/intermediate/registry.rs new file mode 100644 index 0000000..d53e2ba --- /dev/null +++ b/src/intermediate/registry.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +type IdCounter = u16; + +pub(crate) struct Registry<'parse> { + id_counter: IdCounter, + targets: HashMap<&'parse str, String>, +} + +impl<'parse> Registry<'parse> { + pub(crate) fn new() -> Registry<'parse> { + Registry { + id_counter: 0, + targets: HashMap::new(), + } + } + + pub(crate) fn get_target<'b>(&'b mut self, body: &'parse str) -> &'b String { + self.targets.entry(body).or_insert_with(|| { + self.id_counter += 1; + format!("target_{}", self.id_counter) + }) + } +} diff --git a/src/intermediate/section.rs b/src/intermediate/section.rs new file mode 100644 index 0000000..c9426e6 --- /dev/null +++ b/src/intermediate/section.rs @@ -0,0 +1,26 @@ +use crate::error::CustomError; + +use super::registry::Registry; +use super::IElement; + +#[derive(Debug)] +pub(crate) struct ISection { + pub(crate) children: Vec<IElement>, +} + +impl ISection { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + section: &organic::types::Section<'parse>, + ) -> Result<ISection, CustomError> { + let children = { + let mut ret = Vec::new(); + for elem in section.children.iter() { + ret.push(IElement::new(registry, elem).await?); + } + ret + }; + + Ok(ISection { children }) + } +} diff --git a/src/intermediate/target.rs b/src/intermediate/target.rs new file mode 100644 index 0000000..1a594de --- /dev/null +++ b/src/intermediate/target.rs @@ -0,0 +1,23 @@ +use crate::error::CustomError; +use crate::intermediate::util::coalesce_whitespace; + +use super::registry::Registry; + +#[derive(Debug)] +pub(crate) struct ITarget { + pub(crate) id: String, + value: String, +} + +impl ITarget { + pub(crate) async fn new<'parse>( + registry: &mut Registry<'parse>, + target: &organic::types::Target<'parse>, + ) -> Result<ITarget, CustomError> { + let id = registry.get_target(target.value); + Ok(ITarget { + id: id.clone(), + value: target.value.to_owned(), + }) + } +} diff --git a/src/intermediate/util.rs b/src/intermediate/util.rs new file mode 100644 index 0000000..b482b98 --- /dev/null +++ b/src/intermediate/util.rs @@ -0,0 +1,48 @@ +use std::borrow::Cow; + +/// Removes all whitespace from a string. +/// +/// Example: "foo bar" => "foobar" and "foo \n bar" => "foobar". +#[allow(dead_code)] +pub(crate) fn coalesce_whitespace(input: &str) -> Cow<'_, str> { + let mut state = CoalesceWhitespace::Normal; + for (offset, c) in input.char_indices() { + match (&mut state, c) { + (CoalesceWhitespace::Normal, ' ' | '\t' | '\r' | '\n') => { + let mut ret = String::with_capacity(input.len()); + ret.push_str(&input[..offset]); + ret.push(' '); + state = CoalesceWhitespace::HasWhitespace { + in_whitespace: true, + ret, + }; + } + (CoalesceWhitespace::Normal, _) => {} + ( + CoalesceWhitespace::HasWhitespace { in_whitespace, ret }, + ' ' | '\t' | '\r' | '\n', + ) => { + if !*in_whitespace { + *in_whitespace = true; + ret.push(' '); + } + } + (CoalesceWhitespace::HasWhitespace { in_whitespace, ret }, _) => { + *in_whitespace = false; + ret.push(c); + } + } + } + match state { + CoalesceWhitespace::Normal => Cow::Borrowed(input), + CoalesceWhitespace::HasWhitespace { + in_whitespace: _, + ret, + } => Cow::Owned(ret), + } +} + +enum CoalesceWhitespace { + Normal, + HasWhitespace { in_whitespace: bool, ret: String }, +} diff --git a/src/main.rs b/src/main.rs index 76d095c..d2d3edd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,16 @@ use self::cli::parameters::Cli; use self::cli::parameters::Commands; use self::command::build::build_site; use self::command::init::init_writer_folder; +use self::error::CustomError; mod cli; mod command; mod config; +mod context; +mod error; +mod intermediate; +mod render; -fn main() -> Result<ExitCode, Box<dyn std::error::Error>> { +fn main() -> Result<ExitCode, CustomError> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async { let main_body_result = main_body().await; @@ -18,7 +23,7 @@ fn main() -> Result<ExitCode, Box<dyn std::error::Error>> { }) } -async fn main_body() -> Result<ExitCode, Box<dyn std::error::Error>> { +async fn main_body() -> Result<ExitCode, CustomError> { let args = Cli::parse(); match args.command { Commands::Init(args) => { diff --git a/src/render/duster_renderer.rs b/src/render/duster_renderer.rs new file mode 100644 index 0000000..e5078a9 --- /dev/null +++ b/src/render/duster_renderer.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +use crate::error::CustomError; + +use super::renderer_integration::RendererIntegration; +use serde::Serialize; + +pub(crate) struct DusterRenderer<'a> { + templates: HashMap<&'a str, duster::parser::Template<'a>>, +} + +impl<'a> DusterRenderer<'a> { + pub(crate) fn new() -> DusterRenderer<'a> { + DusterRenderer { + templates: HashMap::new(), + } + } +} + +impl<'a> RendererIntegration<'a> for DusterRenderer<'a> { + fn load_template(&mut self, name: &'a str, contents: &'a str) -> Result<(), CustomError> { + let compiled_template = duster::renderer::compile_template(contents.as_ref())?; + self.templates.insert(name, compiled_template); + Ok(()) + } + + fn render<C>(&self, context: C) -> Result<String, CustomError> + where + C: Serialize, + { + let mut dust_renderer = duster::renderer::DustRenderer::new(); + for (name, compiled_template) in self.templates.iter() { + dust_renderer.load_source(compiled_template, (*name).to_owned()); + } + // TODO: This is horribly inefficient. I am converting from a serialize type to json and back again so I can use the existing implementation of IntoContextElement. Honestly, I probably need to rework a lot of duster now that I've improved in rust over the years. + let json_context = serde_json::to_string(&context)?; + println!("{}", json_context); + let parsed_context: serde_json::Value = serde_json::from_str(json_context.as_str())?; + let rendered_output = dust_renderer.render("main", Some(&parsed_context))?; + Ok(rendered_output) + } +} diff --git a/src/render/mod.rs b/src/render/mod.rs new file mode 100644 index 0000000..b6bd3a1 --- /dev/null +++ b/src/render/mod.rs @@ -0,0 +1,4 @@ +mod duster_renderer; +mod renderer_integration; +pub(crate) use duster_renderer::DusterRenderer; +pub(crate) use renderer_integration::RendererIntegration; diff --git a/src/render/renderer_integration.rs b/src/render/renderer_integration.rs new file mode 100644 index 0000000..ebf103c --- /dev/null +++ b/src/render/renderer_integration.rs @@ -0,0 +1,11 @@ +use serde::Serialize; + +use crate::error::CustomError; + +pub(crate) trait RendererIntegration<'a> { + fn load_template(&mut self, name: &'a str, contents: &'a str) -> Result<(), CustomError>; + + fn render<C>(&self, context: C) -> Result<String, CustomError> + where + C: Serialize; +}