Merge branch 'build_site'

This commit is contained in:
Tom Alexander 2023-10-27 16:19:39 -04:00
commit 860b601f62
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
45 changed files with 1617 additions and 21 deletions

146
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -0,0 +1,11 @@
<div class="blog_post">
<div class="blog_post_intro">
{?.title}{?.self_link}<a class="blog_post_title" href="{.link}">{.title}</a>{:else}<div class="blog_post_title">{.title}</div>{/.self_link}{/.title}
{! TODO: date? !}
</div>
{! TODO: Table of contents? !}
<div class="blog_post_body">
</div>
</div>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
{#global_settings.css_files}<link rel="stylesheet" href="{.}">{/global_settings.css_files}
{#global_settings.js_files}<script type="text/javascript" src="{.}"></script>{/global_settings.js_files}
{?global_settings.page_title}<title>{global_settings.page_title}</title>{/global_settings.page_title}
</head>
<body>
{! TODO: Header bar with links? !}
<div class="main_content">
{@select key=.type}
{@eq value="blog_post_page"}{>blog_post_page/}{/eq}
{@none}{!TODO: make this panic!}ERROR: Unrecognized page content type{/none}
{/select}
</div>
</body>
</html>

View File

@ -1,3 +1,4 @@
mod render;
mod runner;
pub(crate) use runner::build_site;

View File

@ -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<BlogPost>,
}
impl SiteRenderer {
pub(crate) fn new<P: Into<PathBuf>>(
output_directory: P,
blog_posts: Vec<BlogPost>,
) -> 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))
}

View File

@ -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<dyn std::error::Error>> {
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<PathBuf, CustomError> {
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<Vec<PathBuf>, 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<Vec<BlogPost>, 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)
}

View File

@ -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<dyn std::error::Error>> {
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());
}

View File

@ -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<P: AsRef<Path>>(root_dir: P) -> Result<Config, Box<dyn std::error::Error>> {
fn inner(root_dir: &Path) -> Result<Config, Box<dyn std::error::Error>> {
pub(crate) fn new<P: AsRef<Path>>(root_dir: P) -> Result<Config, CustomError> {
fn inner(root_dir: &Path) -> Result<Config, CustomError> {
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<P: Into<PathBuf>>(
path: P,
) -> Result<Config, Box<dyn std::error::Error>> {
async fn inner(path: PathBuf) -> Result<Config, Box<dyn std::error::Error>> {
pub(crate) async fn load_from_file<P: Into<PathBuf>>(path: P) -> Result<Config, CustomError> {
async fn inner(path: PathBuf) -> Result<Config, CustomError> {
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<dyn std::error::Error>> {
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()
}
}

View File

@ -7,6 +7,8 @@ pub(crate) struct RawConfig {
site_title: String,
author: Option<String>,
email: Option<String>,
pub(super) use_relative_paths: Option<bool>,
pub(super) web_root: Option<String>,
}
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,
}
}
}

View File

@ -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<String>,
self_link: Option<String>,
children: Vec<RenderDocumentElement>,
}
impl RenderBlogPostPage {
pub(crate) fn new(
global_settings: GlobalSettings,
title: Option<String>,
self_link: Option<String>,
children: Vec<RenderDocumentElement>,
) -> RenderBlogPostPage {
RenderBlogPostPage {
global_settings,
title,
self_link,
children,
}
}
}

23
src/context/comment.rs Normal file
View File

@ -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<D: AsRef<Path>, F: AsRef<Path>>(
config: &Config,
output_directory: D,
output_file: F,
comment: &IComment,
) -> Result<RenderComment, CustomError> {
Ok(RenderComment {})
}
}

View File

@ -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),
}

49
src/context/element.rs Normal file
View File

@ -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<D: AsRef<Path>, F: AsRef<Path>>(
config: &Config,
output_directory: D,
output_file: F,
element: &IElement,
) -> Result<RenderElement, CustomError> {
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,
)?)),
}
}
}

View File

@ -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 <title> 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,
}
}
}

36
src/context/heading.rs Normal file
View File

@ -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,
})
}
}

23
src/context/keyword.rs Normal file
View File

@ -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 {})
}
}

20
src/context/mod.rs Normal file
View File

@ -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;

41
src/context/object.rs Normal file
View File

@ -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,
)?)),
}
}
}

32
src/context/paragraph.rs Normal file
View File

@ -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 })
}
}

23
src/context/plain_text.rs Normal file
View File

@ -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 {})
}
}

33
src/context/section.rs Normal file
View File

@ -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 })
}
}

27
src/context/target.rs Normal file
View File

@ -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(),
})
}
}

90
src/error/error.rs Normal file
View File

@ -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)
}
}

3
src/error/mod.rs Normal file
View File

@ -0,0 +1,3 @@
#[allow(clippy::module_inception)]
mod error;
pub(crate) use error::CustomError;

View File

@ -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 {})
}
}

168
src/intermediate/convert.rs Normal file
View File

@ -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")
);
}
}

View File

@ -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)
}