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 .
+ page_title: Option,
+ css_files: Vec,
+ js_files: Vec,
+}
+
+impl GlobalSettings {
+ pub(crate) fn new(
+ page_title: Option,
+ css_files: Vec,
+ js_files: Vec,
+ ) -> 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,
+}
+
+impl RenderHeading {
+ pub(crate) fn new, F: AsRef>(
+ config: &Config,
+ output_directory: D,
+ output_file: F,
+ heading: &IHeading,
+ ) -> Result {
+ let title = heading
+ .title
+ .iter()
+ .map(|obj| RenderObject::new(config, &output_directory, &output_file, obj))
+ .collect::, _>>()?;
+ 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, F: AsRef>(
+ config: &Config,
+ output_directory: D,
+ output_file: F,
+ keyword: &IKeyword,
+ ) -> Result {
+ 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, F: AsRef>(
+ config: &Config,
+ output_directory: D,
+ output_file: F,
+ object: &IObject,
+ ) -> Result {
+ 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,
+}
+
+impl RenderParagraph {
+ pub(crate) fn new, F: AsRef>(
+ config: &Config,
+ output_directory: D,
+ output_file: F,
+ paragraph: &IParagraph,
+ ) -> Result {
+ let children = paragraph
+ .children
+ .iter()
+ .map(|obj| RenderObject::new(config, &output_directory, &output_file, obj))
+ .collect::, _>>()?;
+ 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, F: AsRef>(
+ config: &Config,
+ output_directory: D,
+ output_file: F,
+ heading: &IPlainText,
+ ) -> Result {
+ 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,
+}
+
+impl RenderSection {
+ pub(crate) fn new, F: AsRef>(
+ config: &Config,
+ output_directory: D,
+ output_file: F,
+ section: &ISection,
+ ) -> Result {
+ let children = section
+ .children
+ .iter()
+ .map(|obj| RenderElement::new(config, &output_directory, &output_file, obj))
+ .collect::, _>>()?;
+
+ 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, F: AsRef>(
+ config: &Config,
+ output_directory: D,
+ output_file: F,
+ target: &ITarget,
+ ) -> Result {
+ 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 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 for CustomError {
+ fn from(value: toml::ser::Error) -> Self {
+ CustomError::TomlSerialize(value)
+ }
+}
+
+impl From for CustomError {
+ fn from(value: toml::de::Error) -> Self {
+ CustomError::TomlDeserialize(value)
+ }
+}
+
+impl From for CustomError {
+ fn from(value: walkdir::Error) -> Self {
+ CustomError::WalkDir(value)
+ }
+}
+
+impl From for CustomError {
+ fn from(value: tokio::task::JoinError) -> Self {
+ CustomError::Tokio(value)
+ }
+}
+
+impl From for CustomError {
+ fn from(value: serde_json::Error) -> Self {
+ CustomError::Serde(value)
+ }
+}
+
+impl From for CustomError {
+ fn from(value: Utf8Error) -> Self {
+ CustomError::Utf8(value)
+ }
+}
+
+impl From for CustomError {
+ fn from(value: FromUtf8Error) -> Self {
+ CustomError::FromUtf8(value)
+ }
+}
+
+impl From for CustomError {
+ fn from(value: duster::renderer::CompileError) -> Self {
+ CustomError::DusterCompile(value)
+ }
+}
+
+impl From for CustomError {
+ fn from(value: duster::renderer::RenderError) -> Self {
+ CustomError::DusterRender(value)
+ }
+}
+
+impl From 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 {
+ 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, F: AsRef>(
+ config: &Config,
+ output_directory: D,
+ output_file: F,
+ _post: &BlogPost,
+ page: &BlogPostPage,
+) -> Result {
+ 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, F: AsRef, P: AsRef>(
+ config: &Config,
+ output_directory: D,
+ containing_file: F,
+ path_from_web_root: P,
+) -> Result {
+ 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::();
+ // 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- > {
+ 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::from("")
+ );
+ assert_eq!(
+ get_shared_steps(Path::new("foo.txt"), Path::new("foo.txt")).collect::(),
+ PathBuf::from("foo.txt")
+ );
+ assert_eq!(
+ get_shared_steps(Path::new("cat/foo.txt"), Path::new("dog/foo.txt"))
+ .collect::(),
+ PathBuf::from("")
+ );
+ assert_eq!(
+ get_shared_steps(
+ Path::new("foo/bar/baz/lorem.txt"),
+ Path::new("foo/bar/ipsum/dolar.txt")
+ )
+ .collect::(),
+ 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,
+}
+
+impl BlogPost {
+ pub(crate) async fn load_blog_post, R: AsRef>(
+ root_dir: R,
+ post_dir: P,
+ ) -> Result {
+ async fn inner(_root_dir: &Path, post_dir: &Path) -> Result {
+ 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>(
+ root_dir: P,
+) -> Result>>, 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::, _>>()?;
+ 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 {
+ 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,
+}
+
+impl IHeading {
+ pub(crate) async fn new<'parse>(
+ registry: &mut Registry<'parse>,
+ heading: &organic::types::Heading<'parse>,
+ ) -> Result {
+ 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 {
+ 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 {
+ 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,
+
+ pub(crate) children: Vec,
+}
+
+impl BlogPostPage {
+ pub(crate) async fn new<'parse, P: Into>(
+ path: P,
+ registry: &mut Registry<'parse>,
+ document: &organic::types::Document<'parse>,
+ ) -> Result {
+ 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 {
+ 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,
+}
+
+impl IParagraph {
+ pub(crate) async fn new<'parse>(
+ registry: &mut Registry<'parse>,
+ paragraph: &organic::types::Paragraph<'parse>,
+ ) -> Result {
+ 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 {
+ 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,
+}
+
+impl ISection {
+ pub(crate) async fn new<'parse>(
+ registry: &mut Registry<'parse>,
+ section: &organic::types::Section<'parse>,
+ ) -> Result {
+ 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 {
+ 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> {
+fn main() -> Result {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let main_body_result = main_body().await;
@@ -18,7 +23,7 @@ fn main() -> Result> {
})
}
-async fn main_body() -> Result> {
+async fn main_body() -> Result {
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(&self, context: C) -> Result
+ 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(&self, context: C) -> Result
+ where
+ C: Serialize;
+}