Merge branch 'pages'
rust-test Build rust-test has succeeded Details
rust-clippy Build rust-clippy has succeeded Details
format Build format has succeeded Details
build-natter Build build-natter has succeeded Details

main
Tom Alexander 4 months ago
commit 5228851c0e
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE

@ -10,6 +10,7 @@
{#.page_header}{>page_header/}{/.page_header}
<main class="main_content">
{@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}

@ -0,0 +1,19 @@
<article class="page">
{?.title}<h1 class="blog_post_title"><span>{.title}</span></h1>{/.title}
{! TODO: date? !}
{! TODO: Table of contents? !}
<div class="blog_post_body">
{#.children}
{>document_element/}
{/.children}
{?.footnotes}
<h2>Footnotes:</h2>
{#.footnotes}
{>real_footnote_definition/}
{/.footnotes}
{/.footnotes}
</div>
</article>

@ -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<BlogPost>,
stylesheets: Vec<Stylesheet>,
pages: Vec<IPage>,
}
impl SiteRenderer {
@ -34,11 +37,13 @@ impl SiteRenderer {
output_directory: P,
blog_posts: Vec<BlogPost>,
stylesheets: Vec<Stylesheet>,
pages: Vec<IPage>,
) -> 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()?;

@ -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<Vec<BlogPost>, CustomError>
Ok(blog_posts)
}
async fn load_pages(config: &Config) -> Result<Vec<IPage>, 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<Vec<Stylesheet>, CustomError> {
let sources: Vec<_> = DEFAULT_STYLESHEETS
.files()

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

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

@ -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<PageHeader>,
/// The title that will be shown visibly on the page.
title: Option<String>,
self_link: Option<String>,
children: Vec<RenderDocumentElement>,
footnotes: Vec<RenderRealFootnoteDefinition>,
}
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)
});

@ -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<P: AsRef<Path>>(
pub(crate) 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)

@ -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<P: Into<PathBuf>>(
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<String>,
pub(crate) date: Option<String>,
pub(crate) children: Vec<IDocumentElement>,
pub(crate) footnotes: Vec<IRealFootnoteDefinition>,
}
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<String> {
organic::types::AstNode::from(document)
.iter_all_ast_nodes()
.filter_map(|node| match node {
organic::types::AstNode::Keyword(kw) if kw.key.eq_ignore_ascii_case("title") => {
Some(kw)
}
_ => None,
})
.last()
.map(|kw| kw.value.to_owned())
}
pub(crate) fn get_date(document: &organic::types::Document<'_>) -> Option<String> {
organic::types::AstNode::from(document)
.iter_all_ast_nodes()
.filter_map(|node| match node {
organic::types::AstNode::Keyword(kw) if kw.key.eq_ignore_ascii_case("date") => Some(kw),
_ => None,
})
.last()
.map(|kw| kw.value.to_owned())
}

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

@ -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<P: Into<PathBuf>>(
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<String>,
#[allow(dead_code)]
pub(crate) date: Option<String>,
pub(crate) children: Vec<IDocumentElement>,
@ -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<String> {
organic::types::AstNode::from(document)
.iter_all_ast_nodes()
.filter_map(|node| match node {
organic::types::AstNode::Keyword(kw) if kw.key.eq_ignore_ascii_case("title") => {
Some(kw)
}
_ => None,
})
.last()
.map(|kw| kw.value.to_owned())
#[derive(Debug)]
pub(crate) struct PageInput<'b, 'parse> {
path: PathBuf,
document: &'b organic::types::Document<'parse>,
}
fn get_date(document: &organic::types::Document<'_>) -> Option<String> {
organic::types::AstNode::from(document)
.iter_all_ast_nodes()
.filter_map(|node| match node {
organic::types::AstNode::Keyword(kw) if kw.key.eq_ignore_ascii_case("date") => Some(kw),
_ => None,
})
.last()
.map(|kw| kw.value.to_owned())
impl<'b, 'parse> PageInput<'b, 'parse> {
pub(crate) fn new<P: Into<PathBuf>>(
path: P,
document: &'b organic::types::Document<'parse>,
) -> PageInput<'b, 'parse> {
PageInput {
path: path.into(),
document,
}
}
}

Loading…
Cancel
Save