From 8905c9356b845441e8da7c1b14978460a47be1b0 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 23 Dec 2023 16:37:19 -0500 Subject: [PATCH 1/2] Add a build for regular non-blog-post pages from org source. --- src/command/build/render.rs | 28 +++++++ src/command/build/runner.rs | 66 ++++++++++++++++ src/config/full.rs | 4 + src/context/mod.rs | 2 + src/context/page.rs | 102 ++++++++++++++++++++++++ src/intermediate/blog_post.rs | 4 +- src/intermediate/blog_post_page.rs | 121 +++++++++++++++++++++++++++++ src/intermediate/mod.rs | 7 +- src/intermediate/page.rs | 76 +++++++----------- 9 files changed, 357 insertions(+), 53 deletions(-) create mode 100644 src/context/page.rs create mode 100644 src/intermediate/blog_post_page.rs diff --git a/src/command/build/render.rs b/src/command/build/render.rs index d17b188..530fba1 100644 --- a/src/command/build/render.rs +++ b/src/command/build/render.rs @@ -13,9 +13,11 @@ use crate::context::RenderBlogPostPageInput; use crate::context::RenderBlogStream; use crate::context::RenderBlogStreamInput; use crate::context::RenderContext; +use crate::context::RenderPage; use crate::error::CustomError; use crate::intermediate::get_web_path; use crate::intermediate::BlogPost; +use crate::intermediate::IPage; use crate::render::DusterRenderer; use crate::render::RendererIntegration; @@ -27,6 +29,7 @@ pub(crate) struct SiteRenderer { output_directory: PathBuf, blog_posts: Vec, stylesheets: Vec, + pages: Vec, } impl SiteRenderer { @@ -34,11 +37,13 @@ impl SiteRenderer { output_directory: P, blog_posts: Vec, stylesheets: Vec, + pages: Vec, ) -> SiteRenderer { SiteRenderer { output_directory: output_directory.into(), blog_posts, stylesheets, + pages, } } @@ -73,6 +78,29 @@ impl SiteRenderer { Ok(renderer_integration) } + pub(crate) async fn render_pages(&self, config: &Config) -> Result<(), CustomError> { + let renderer_integration = self.init_renderer_integration()?; + + for page in &self.pages { + let output_path = self.output_directory.join(page.get_output_path()); + let render_context = RenderContext::new( + config, + self.output_directory.as_path(), + output_path.as_path(), + None, + )?; + let render_context = RenderPage::new(render_context, 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(()) + } + pub(crate) async fn render_blog_posts(&self, config: &Config) -> Result<(), CustomError> { let renderer_integration = self.init_renderer_integration()?; diff --git a/src/command/build/runner.rs b/src/command/build/runner.rs index 391d0a1..de9edbb 100644 --- a/src/command/build/runner.rs +++ b/src/command/build/runner.rs @@ -1,12 +1,19 @@ use std::ffi::OsStr; use std::path::PathBuf; +use std::sync::Arc; +use std::sync::Mutex; use super::stylesheet::Stylesheet; use crate::cli::parameters::BuildArgs; use crate::command::build::render::SiteRenderer; use crate::config::Config; use crate::error::CustomError; +use crate::intermediate::get_org_files; use crate::intermediate::BlogPost; +use crate::intermediate::IPage; +use crate::intermediate::IntermediateContext; +use crate::intermediate::PageInput; +use crate::intermediate::Registry; use include_dir::include_dir; use include_dir::Dir; @@ -17,13 +24,16 @@ 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 stylesheets = load_stylesheets().await?; + let pages = load_pages(&config).await?; let renderer = SiteRenderer::new( get_output_directory(&config).await?, blog_posts, stylesheets, + pages, ); renderer.render_blog_posts(&config).await?; renderer.render_blog_stream(&config).await?; + renderer.render_pages(&config).await?; renderer.render_stylesheets().await?; renderer.copy_static_files(&config).await?; @@ -74,6 +84,62 @@ async fn load_blog_posts(config: &Config) -> Result, CustomError> Ok(blog_posts) } +async fn load_pages(config: &Config) -> Result, CustomError> { + let pages_source = config + .get_root_directory() + .join(config.get_relative_path_to_pages()); + if !pages_source.exists() { + return Ok(Vec::new()); + } + let page_files = get_org_files(&pages_source)?; + let org_files = { + let mut ret = Vec::new(); + for page in page_files { + ret.push(page.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 pages = { + let mut ret = Vec::new(); + for (real_path, _contents, parsed_document) in parsed_org_files.iter() { + let mut registry = Registry::new(); + + // Assign IDs to the targets + organic::types::AstNode::from(parsed_document) + .iter_all_ast_nodes() + .for_each(|node| { + if let organic::types::AstNode::Target(target) = node { + registry.get_target(target.value); + } + }); + + let registry = Arc::new(Mutex::new(registry)); + let intermediate_context = IntermediateContext::new(registry)?; + let relative_to_pages_dir_path = real_path.strip_prefix(&pages_source)?; + ret.push( + IPage::new( + intermediate_context, + PageInput::new(relative_to_pages_dir_path, parsed_document), + ) + .await?, + ); + } + ret + }; + + Ok(pages) +} + async fn load_stylesheets() -> Result, CustomError> { let sources: Vec<_> = DEFAULT_STYLESHEETS .files() diff --git a/src/config/full.rs b/src/config/full.rs index 5c7d3af..87c6b69 100644 --- a/src/config/full.rs +++ b/src/config/full.rs @@ -92,4 +92,8 @@ impl Config { pub(crate) fn get_relative_path_to_static_files(&self) -> PathBuf { Path::new("static").into() } + + pub(crate) fn get_relative_path_to_pages(&self) -> PathBuf { + Path::new("pages").into() + } } diff --git a/src/context/mod.rs b/src/context/mod.rs index 8594f5d..cea8689 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -36,6 +36,7 @@ mod line_break; mod macros; mod object; mod org_macro; +mod page; mod page_header; mod paragraph; mod plain_link; @@ -77,6 +78,7 @@ pub(crate) use footnote_definition::RenderRealFootnoteDefinition; pub(crate) use global_settings::GlobalSettings; pub(crate) use heading::RenderHeading; pub(crate) use object::RenderObject; +pub(crate) use page::RenderPage; pub(crate) use page_header::PageHeader; pub(crate) use render_context::RenderContext; pub(crate) use section::RenderSection; diff --git a/src/context/page.rs b/src/context/page.rs new file mode 100644 index 0000000..d87d91e --- /dev/null +++ b/src/context/page.rs @@ -0,0 +1,102 @@ +use super::footnote_definition::RenderRealFootnoteDefinition; +use super::macros::render; +use super::render_context::RenderContext; +use super::GlobalSettings; +use super::PageHeader; +use super::RenderDocumentElement; +use crate::error::CustomError; +use crate::intermediate::get_web_path; +use crate::intermediate::IPage; +use serde::Serialize; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +#[serde(rename = "page")] +pub(crate) struct RenderPage { + global_settings: GlobalSettings, + + page_header: Option, + + /// The title that will be shown visibly on the page. + title: Option, + + self_link: Option, + + children: Vec, + + footnotes: Vec, +} + +render!(RenderPage, IPage, original, render_context, { + let css_files = vec![ + get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "stylesheet/reset.css", + )?, + get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "stylesheet/main.css", + )?, + ]; + let js_files = vec![get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "blog_post.js", + )?]; + let global_settings = GlobalSettings::new(original.title.clone(), css_files, js_files); + let page_header = PageHeader::new( + render_context.config.get_site_title().map(str::to_string), + Some(get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "", + )?), + ); + let link_to_blog_post = get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + render_context + .output_file + .strip_prefix(render_context.output_root_directory)?, + )?; + + let children = { + let mut children = Vec::new(); + + for child in original.children.iter() { + children.push(RenderDocumentElement::new(render_context.clone(), child)?); + } + + children + }; + + let footnotes = { + let mut ret = Vec::new(); + + for footnote in original.footnotes.iter() { + ret.push(RenderRealFootnoteDefinition::new( + render_context.clone(), + footnote, + )?); + } + + ret + }; + + let ret = RenderPage { + global_settings, + page_header: Some(page_header), + title: original.title.clone(), + self_link: Some(link_to_blog_post), + children, + footnotes, + }; + Ok(ret) +}); diff --git a/src/intermediate/blog_post.rs b/src/intermediate/blog_post.rs index 2d66ca9..da3a374 100644 --- a/src/intermediate/blog_post.rs +++ b/src/intermediate/blog_post.rs @@ -7,7 +7,7 @@ use tokio::task::JoinHandle; use walkdir::WalkDir; use crate::error::CustomError; -use crate::intermediate::page::BlogPostPageInput; +use crate::intermediate::blog_post_page::BlogPostPageInput; use crate::intermediate::registry::Registry; use crate::intermediate::IntermediateContext; @@ -119,7 +119,7 @@ async fn read_file(path: PathBuf) -> std::io::Result<(PathBuf, String)> { Ok((path, contents)) } -fn get_org_files>( +pub(crate) fn get_org_files>( root_dir: P, ) -> Result>>, walkdir::Error> { let org_files = WalkDir::new(root_dir) diff --git a/src/intermediate/blog_post_page.rs b/src/intermediate/blog_post_page.rs new file mode 100644 index 0000000..dec8273 --- /dev/null +++ b/src/intermediate/blog_post_page.rs @@ -0,0 +1,121 @@ +use std::path::PathBuf; + +use crate::error::CustomError; + +use super::footnote_definition::IRealFootnoteDefinition; + +use super::macros::intermediate; +use super::IDocumentElement; +use super::IHeading; +use super::ISection; + +#[derive(Debug)] +pub(crate) struct BlogPostPageInput<'b, 'parse> { + path: PathBuf, + document: &'b organic::types::Document<'parse>, +} + +impl<'b, 'parse> BlogPostPageInput<'b, 'parse> { + pub(crate) fn new>( + path: P, + document: &'b organic::types::Document<'parse>, + ) -> BlogPostPageInput<'b, 'parse> { + BlogPostPageInput { + path: path.into(), + document, + } + } +} + +#[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) date: Option, + + pub(crate) children: Vec, + + pub(crate) footnotes: Vec, +} + +intermediate!( + BlogPostPage, + BlogPostPageInput<'orig, 'parse>, + original, + intermediate_context, + { + let mut children = Vec::new(); + if let Some(section) = original.document.zeroth_section.as_ref() { + children.push(IDocumentElement::Section( + ISection::new(intermediate_context.clone(), section).await?, + )); + } + for heading in original.document.children.iter() { + children.push(IDocumentElement::Heading( + IHeading::new(intermediate_context.clone(), heading).await?, + )); + } + + let footnotes = { + let footnote_definitions: Vec<_> = { + let registry = intermediate_context.registry.lock().unwrap(); + let ret = registry + .get_footnote_ids() + .map(|(id, def)| (id, def.clone())) + .collect(); + ret + }; + let mut ret = Vec::new(); + for (id, def) in footnote_definitions.into_iter() { + ret.push( + IRealFootnoteDefinition::new(intermediate_context.clone(), id, def).await?, + ); + } + ret + }; + + Ok(BlogPostPage { + path: original.path, + title: get_title(original.document), + date: get_date(original.document), + children, + footnotes, + }) + } +); + +impl BlogPostPage { + /// 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 + } +} + +pub(crate) 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()) +} + +pub(crate) fn get_date(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("date") => Some(kw), + _ => None, + }) + .last() + .map(|kw| kw.value.to_owned()) +} diff --git a/src/intermediate/mod.rs b/src/intermediate/mod.rs index 2d6e34f..042e971 100644 --- a/src/intermediate/mod.rs +++ b/src/intermediate/mod.rs @@ -2,6 +2,7 @@ mod angle_link; mod ast_node; mod babel_call; mod blog_post; +mod blog_post_page; mod bold; mod center_block; mod citation; @@ -70,7 +71,9 @@ mod verse_block; pub(crate) use angle_link::IAngleLink; pub(crate) use ast_node::IAstNode; pub(crate) use babel_call::IBabelCall; +pub(crate) use blog_post::get_org_files; pub(crate) use blog_post::BlogPost; +pub(crate) use blog_post_page::BlogPostPage; pub(crate) use bold::IBold; pub(crate) use center_block::ICenterBlock; pub(crate) use citation::ICitation; @@ -105,7 +108,8 @@ pub(crate) use latex_fragment::ILatexFragment; pub(crate) use line_break::ILineBreak; pub(crate) use object::IObject; pub(crate) use org_macro::IOrgMacro; -pub(crate) use page::BlogPostPage; +pub(crate) use page::IPage; +pub(crate) use page::PageInput; pub(crate) use paragraph::IParagraph; pub(crate) use plain_link::IPlainLink; pub(crate) use plain_list::IPlainList; @@ -117,6 +121,7 @@ pub(crate) use property_drawer::IPropertyDrawer; pub(crate) use quote_block::IQuoteBlock; pub(crate) use radio_link::IRadioLink; pub(crate) use radio_target::IRadioTarget; +pub(crate) use registry::Registry; pub(crate) use regular_link::IRegularLink; pub(crate) use section::ISection; pub(crate) use special_block::ISpecialBlock; diff --git a/src/intermediate/page.rs b/src/intermediate/page.rs index bc0603d..45e042d 100644 --- a/src/intermediate/page.rs +++ b/src/intermediate/page.rs @@ -1,39 +1,21 @@ -use std::path::PathBuf; - -use crate::error::CustomError; - +use super::blog_post_page::get_date; +use super::blog_post_page::get_title; use super::footnote_definition::IRealFootnoteDefinition; - use super::macros::intermediate; use super::IDocumentElement; use super::IHeading; use super::ISection; +use crate::error::CustomError; +use std::path::PathBuf; #[derive(Debug)] -pub(crate) struct BlogPostPageInput<'b, 'parse> { - path: PathBuf, - document: &'b organic::types::Document<'parse>, -} - -impl<'b, 'parse> BlogPostPageInput<'b, 'parse> { - pub(crate) fn new>( - path: P, - document: &'b organic::types::Document<'parse>, - ) -> BlogPostPageInput<'b, 'parse> { - BlogPostPageInput { - path: path.into(), - document, - } - } -} - -#[derive(Debug)] -pub(crate) struct BlogPostPage { - /// Relative path from the root of the blog post. +pub(crate) struct IPage { + /// Relative path from the root of the pages directory. pub(crate) path: PathBuf, pub(crate) title: Option, + #[allow(dead_code)] pub(crate) date: Option, pub(crate) children: Vec, @@ -42,8 +24,8 @@ pub(crate) struct BlogPostPage { } intermediate!( - BlogPostPage, - BlogPostPageInput<'orig, 'parse>, + IPage, + PageInput<'orig, 'parse>, original, intermediate_context, { @@ -77,7 +59,7 @@ intermediate!( ret }; - Ok(BlogPostPage { + Ok(IPage { path: original.path, title: get_title(original.document), date: get_date(original.document), @@ -87,8 +69,8 @@ intermediate!( } ); -impl BlogPostPage { - /// Get the output path relative to the post directory. +impl IPage { + /// Get the output path relative to the pages directory. pub(crate) fn get_output_path(&self) -> PathBuf { let mut ret = self.path.clone(); ret.set_extension("html"); @@ -96,26 +78,20 @@ impl BlogPostPage { } } -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()) +#[derive(Debug)] +pub(crate) struct PageInput<'b, 'parse> { + path: PathBuf, + document: &'b organic::types::Document<'parse>, } -fn get_date(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("date") => Some(kw), - _ => None, - }) - .last() - .map(|kw| kw.value.to_owned()) +impl<'b, 'parse> PageInput<'b, 'parse> { + pub(crate) fn new>( + path: P, + document: &'b organic::types::Document<'parse>, + ) -> PageInput<'b, 'parse> { + PageInput { + path: path.into(), + document, + } + } } From 4fc08f4375f36d0af14c252392c548a549be95e8 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 23 Dec 2023 16:50:06 -0500 Subject: [PATCH 2/2] Add a template for static pages. --- default_environment/templates/html/main.dust | 1 + default_environment/templates/html/page.dust | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 default_environment/templates/html/page.dust diff --git a/default_environment/templates/html/main.dust b/default_environment/templates/html/main.dust index d183145..4c3a4f2 100644 --- a/default_environment/templates/html/main.dust +++ b/default_environment/templates/html/main.dust @@ -10,6 +10,7 @@ {#.page_header}{>page_header/}{/.page_header}
{@select key=.type} + {@eq value="page"}{>page/}{/eq} {@eq value="blog_post_page"}{>blog_post_page/}{/eq} {@eq value="blog_stream"}{>blog_stream/}{/eq} {@none}{!TODO: make this panic!}ERROR: Unrecognized page content type{/none} diff --git a/default_environment/templates/html/page.dust b/default_environment/templates/html/page.dust new file mode 100644 index 0000000..45b389b --- /dev/null +++ b/default_environment/templates/html/page.dust @@ -0,0 +1,19 @@ +
+ {?.title}

{.title}

{/.title} + {! TODO: date? !} + + {! TODO: Table of contents? !} + +
+ {#.children} + {>document_element/} + {/.children} + + {?.footnotes} +

Footnotes:

+ {#.footnotes} + {>real_footnote_definition/} + {/.footnotes} + {/.footnotes} +
+