diff --git a/Cargo.toml b/Cargo.toml index 07e4932..402e48b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,23 +23,23 @@ required-features = ["cli"] [dependencies] chrono = { version = "0.4.38", features = ["serde", "unstable-locales"] } -clap = { version = "4.5.4", features = ["derive", "cargo"], optional = true } -comrak = { version = "0.22.0", features = ["syntect", "shortcodes"], default-features = false } +clap = { version = "4.5.6", features = ["derive", "cargo"], optional = true } +comrak = { version = "0.24.1", features = ["syntect", "shortcodes"], default-features = false } daggy = { version = "0.8.0", features = ["stable_dag"] } -toml = "0.8.12" -liquid = "0.26.4" -liquid-core = "0.26.4" -liquid-lib = { version = "0.26.4", features = ["all", "stdlib", "jekyll", "shopify", "extra"] } -serde = "1.0.198" +toml = "0.8.14" +liquid = "0.26.6" +liquid-core = "0.26.6" +liquid-lib = { version = "0.26.6", features = ["all", "stdlib", "jekyll", "shopify", "extra"] } +serde = "1.0.203" sys-locale = "0.3.1" latex2mathml = "0.2.3" ahash = { version = "0.8.11", features = ["std", "serde", "runtime-rng"] } -mimalloc = { version = "0.1.41", optional = true } +mimalloc = { version = "0.1.42", optional = true } ticky = { version = "1.0.2", optional = true } miette = { version = "7.2.0", features = ["fancy", "syntect-highlighter"] } -thiserror = "1.0.59" -glob = { version = "0.3.1" } -tokio = { version = "1.37.0", features = ["full"], optional = true } +thiserror = "1.0.61" +glob = "0.3.1" +tokio = { version = "1.38.0", features = ["full"], optional = true } futures = "0.3.30" tracing-subscriber = { version = "0.3.18", optional = true } tracing = "0.1.40" diff --git a/site/diary/collections_pagination.vox b/site/diary/collections_pagination.vox new file mode 100644 index 0000000..057c052 --- /dev/null +++ b/site/diary/collections_pagination.vox @@ -0,0 +1,57 @@ +--- +title = "Collections & Pagination" +date = 2024-06-07 +layout = "post" +permalink = "date" +--- + +{% markdown %} + +## Goals +- Supporting pages in multiple collections at once with path nesting. +- If a build has failed, don't immediately retry. +- Upgrading all direct dependencies. +- Implement pagination. +- Modify log output to be more helpful for end-users. + +### Collections + +To support this, `collections` must be renamed to `depends`, and `collection` to `collections`. + +The rule: one collection per path component, and collections including each successive path component. +- Collection names must be represented as Liquid identifiers so they can be used in templating in dependent pages. +- Example: `books/fantasy/page.vox` is in `books`, `fantasy`, and `books_fantasy`. +- Example: `movies/fantasy/page.vox` is in `movies`, `fantasy`, and `movies_fantasy`. + +### Retrying Builds + +This was always the intended behaviour. The mistake was in accidentally checking for a [`JoinError`](https://docs.rs/tokio/latest/tokio/task/struct.JoinError.html) when the build thread had completed, rather than checking for errors from the build thread. + +### Upgrading Dependencies + +Notably, this brought my implementation of Jekyll's sorting filter, which was merged into the Liquid Rust implementation. + +### Pagination + +Pages may have a `pagination` frontmatter value, containing: +* `pagination.collection`: the name of a collection in a page's `depends` list +* `pagination.page_size`: the maximum number of collection pages per page + +Pages using pagination are copied in memory, with each copy being supplied different `pagination.page_number` values. +The `pagination.page_number` is used in a page's `permalink`, and can be used with `pagination.page_size`: +- to calculate the starting index in the collection for the current page +- to calculate the total number of pages +- to determine the URL of any page +- to calculate the remaining number of pages + - This requires the length of the paginated collection as well + +--- + +## Future Goals +- Parallelising as much as possible. +- Removing code that was commented out. +- Documenting the CLI code. +- Creating a logo for Vox. +- Incorporating snippets into DAG construction. + +{% endmarkdown %} \ No newline at end of file diff --git a/site/diary/inheritance_guide.vox b/site/diary/inheritance_guide.vox index bea1436..8341508 100644 --- a/site/diary/inheritance_guide.vox +++ b/site/diary/inheritance_guide.vox @@ -11,7 +11,7 @@ permalink = "date" This site is used to debug changes to Vox. First, the `global.url` of the site is changed, then this command is used to rebuild & serve: ```sh -clear; rm -rf ./output; ./prebuild.sh; vox serve -d -w -vv +clear; rm -rf ./output; ./prebuild.sh; vox serve -d -s -w -vv ``` --- @@ -23,7 +23,7 @@ Today's agenda: - Implementing partial date-times. - Redesigning layout inheritance. - In layout inheritance, the lowest layout should include chained parent contexts up to the first non-layout page. -- Fixing the `{% raw %}{{ markdown }}{% endraw %}` block. +- Fixing the `{% raw %}{% markdown %}{% endraw %}` block. - Finishing the user guide. - Modifying code according to lints. - Improving syntax highlighting. diff --git a/site/global.toml b/site/global.toml index 29651ab..a207ba4 100644 --- a/site/global.toml +++ b/site/global.toml @@ -3,4 +3,4 @@ description = "A performant static site generator built to scale." author = "Emil Sayahi" locale = "en_US" url = "https://emmyoh.github.io/vox" -# url = "http://localhost:80" \ No newline at end of file +# url = "http://localhost" \ No newline at end of file diff --git a/site/guide/frontmatter.vox b/site/guide/frontmatter.vox index f762d97..433248d 100644 --- a/site/guide/frontmatter.vox +++ b/site/guide/frontmatter.vox @@ -42,11 +42,11 @@ There are a variety of shorthand options for the `permalink` field: | Shorthand | Expanded | |------------|--------------------------------------------------------------------------------------------------------------------------| -| `date` | `/{{ page.collection }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}.html` | -| `pretty` | `/{{ page.collection }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}/index.html` | -| `ordinal` | `/{{ page.collection }}/{{ page.date.year }}/{{ page.date.y_day }}/{{ page.data.title }}.html` | -| `weekdate` | `/{{ page.collection }}/{{ page.date.year }}/W{{ page.date.week }}/{{ page.date.short_day }}/{{ page.data.title }}.html` | -| `none` | `/{{ page.collection }}/{{ page.data.title }}.html` | +| `date` | `{{ page.collections.last }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}.html` | +| `pretty` | `{{ page.collections.last }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}/index.html` | +| `ordinal` | `{{ page.collections.last }}/{{ page.date.year }}/{{ page.date.y_day }}/{{ page.data.title }}.html` | +| `weekdate` | `{{ page.collections.last }}/{{ page.date.year }}/W{{ page.date.week }}/{{ page.date.short_day }}/{{ page.data.title }}.html` | +| `none` | `{{ page.collections.last }}/{{ page.data.title }}.html` | {% endraw %} @@ -58,13 +58,17 @@ Suppose you're trying to build an index page for your blog. Its frontmatter will ```toml --- layout = "default" -collections = ["posts"] +depends = ["posts"] permalink = "index.html" --- ``` {% endraw %} -The `collections` property indicates the page collections that this page depends on. A templating context is provided for each requested collection; in this example, there will be a `posts` context containing all pages in the `posts` collection. +The `depends` property indicates the page collections that this page depends on. A templating context is provided for each requested collection; in this example, there will be a `posts` context containing all pages in the `posts` collection. + +The collections a page is in is defined by a page's path, with one collection per path component, and collections including each successive path component as well. For example: +* `books/fantasy/page.vox` is in `books`, `fantasy`, and `books_fantasy`. +* `movies/fantasy/page.vox` is in `movies`, `fantasy`, and `movies_fantasy`. ## Data All other fields fall under a page's `data` property. diff --git a/site/layouts/default.vox b/site/layouts/default.vox index d72e377..12d673f 100644 --- a/site/layouts/default.vox +++ b/site/layouts/default.vox @@ -11,6 +11,14 @@ {{ global.title }} {{ global.description }} + {% assign output_name = page.url | split: "/" | last %} + {% if output_name != 'index.html' %} + {% if page.collections.first %} +

+ ⟵ {{ page.collections.first | capitalize }} +

+ {% endif %} + {% endif %}
{{ layouts | map: "rendered" | first }} diff --git a/site/pages/blog_atom.vox b/site/pages/blog_atom.vox index 43ab6e7..31156a3 100644 --- a/site/pages/blog_atom.vox +++ b/site/pages/blog_atom.vox @@ -1,5 +1,5 @@ --- -collections = ["blog"] +depends = ["blog"] permalink = "blog/atom.xml" --- {% include atom.voxs posts = blog %} \ No newline at end of file diff --git a/site/pages/blog_index.vox b/site/pages/blog_index.vox index 579856d..1c0c96c 100644 --- a/site/pages/blog_index.vox +++ b/site/pages/blog_index.vox @@ -1,7 +1,7 @@ --- layout = "default" -collections = ["blog"] +depends = ["blog"] permalink = "blog/index.html" --- -{% assign posts = blog | sort: "date" %} +{% assign posts = blog | sort: "date.rfc_3339" | reverse %} {% include index.voxs posts = posts minimal = true %} \ No newline at end of file diff --git a/site/pages/diary_atom.vox b/site/pages/diary_atom.vox index ffe9cf1..80698c8 100644 --- a/site/pages/diary_atom.vox +++ b/site/pages/diary_atom.vox @@ -1,5 +1,5 @@ --- -collections = ["diary"] +depends = ["diary"] permalink = "diary/atom.xml" --- {% include atom.voxs posts = diary %} \ No newline at end of file diff --git a/site/pages/diary_index.vox b/site/pages/diary_index.vox index 1faacdc..651391f 100644 --- a/site/pages/diary_index.vox +++ b/site/pages/diary_index.vox @@ -1,7 +1,7 @@ --- layout = "default" -collections = ["diary"] +depends = ["diary"] permalink = "diary/index.html" --- -{% assign posts = diary | sort: "date" %} +{% assign posts = diary | sort: "date.rfc_3339" | reverse %} {% include index.voxs posts = posts minimal = true %} \ No newline at end of file diff --git a/site/pages/guide_index.vox b/site/pages/guide_index.vox index 39165c8..c9bc71a 100644 --- a/site/pages/guide_index.vox +++ b/site/pages/guide_index.vox @@ -1,6 +1,6 @@ --- layout = "default" -collections = ["guide"] +depends = ["guide"] permalink = "guide/index.html" --- {% assign posts = guide | sort: "data.title" | reverse %} diff --git a/site/snippets/atom.voxs b/site/snippets/atom.voxs index 70f0671..1541503 100644 --- a/site/snippets/atom.voxs +++ b/site/snippets/atom.voxs @@ -2,9 +2,10 @@ {{ global.title }} + {{ meta.date.rfc_3339 }} - {{ global.url }} + {{ global.url }}/ {{ global.author }} @@ -12,11 +13,11 @@ {% for post in include.posts %} {{ post.data.title | escape }} - + {% if post.date %} - {{ post.date.rfc_2822 }} + {{ post.date.rfc_3339 }} {% endif %} - {{ post.url | escape }} + {{ post.url | url_encode | prepend: "/" | prepend: global.url }} {{ post.rendered | escape }} {% endfor %} diff --git a/site/snippets/index.voxs b/site/snippets/index.voxs index 6463f51..37348e7 100644 --- a/site/snippets/index.voxs +++ b/site/snippets/index.voxs @@ -2,7 +2,7 @@ {% for post in include.posts %}

- + {{ post.data.title }}

@@ -16,7 +16,7 @@ {% for post in include.posts %}

- + {{ post.data.title }}

diff --git a/src/builds.rs b/src/builds.rs index 00a4ce7..46aa3f6 100644 --- a/src/builds.rs +++ b/src/builds.rs @@ -74,7 +74,7 @@ impl Build { if Page::is_layout_path(label.clone())? { node.look.fill_color = Some(Color::fast("#FFDFBA")); } else { - match Page::get_collection_name_from_path(label)? { + match Page::get_collections_from_path(label)? { Some(_) => { node.look.fill_color = Some(Color::fast("#DAFFBA")); } @@ -303,18 +303,20 @@ impl Build { // If the parent page is in a collection this page depends on, make note of it. EdgeType::Collection => { let parent_path = parent_page.to_path_string(); - let collection_name = parent_page.get_collection_name()?.unwrap(); + let collection_names = parent_page.get_collections()?.unwrap(); info!( - "Parent page ({:?}) is in collection: {:?}", - parent_path, collection_name + "Parent page ({:?}) is in collections: {:?}", + parent_path, collection_names ); - if collection_pages.contains_key(&collection_name) { - collection_pages - .get_mut(&collection_name) - .unwrap() - .push(parent.1); - } else { - collection_pages.insert(collection_name.clone(), vec![parent.1]); + for collection_name in collection_names { + if collection_pages.contains_key(&collection_name) { + collection_pages + .get_mut(&collection_name) + .unwrap() + .push(parent.1); + } else { + collection_pages.insert(collection_name.clone(), vec![parent.1]); + } } } } diff --git a/src/error.rs b/src/error.rs index 7ba3546..a0e69ce 100644 --- a/src/error.rs +++ b/src/error.rs @@ -61,11 +61,13 @@ pub struct DateNotValid { #[diagnostic( code(page::invalid_collections_property), url(docsrs), - help("Please ensure that your `collections` property is a list of collections this page depends on.") + help( + "Please ensure that your `depends` property is a list of collections this page depends on." + ) )] /// Invalid list of dependent collections. -pub struct InvalidCollectionsProperty { +pub struct InvalidDependsProperty { #[source_code] - /// The page with the invalid `collections` property. + /// The page with the invalid `depends` property. pub src: NamedSource, } diff --git a/src/main.rs b/src/main.rs index 8473055..35d9b95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,13 +20,14 @@ use std::net::Ipv4Addr; use std::path::Path; use std::sync::mpsc::channel; use std::{fs, path::PathBuf, time::Duration}; +use syntect::highlighting::ThemeSet; +use syntect::html::css_for_theme_with_class_style; use ticky::Stopwatch; use tokio::time::sleep; use toml::Table; use tracing::{debug, error, info, trace, warn, Level}; use vox::builds::EdgeType; use vox::date::{self, Date}; -use vox::templates; use vox::{builds::Build, page::Page, templates::create_liquid_parser}; #[global_allocator] @@ -130,7 +131,7 @@ async fn main() -> miette::Result<()> { let build_loop = tokio::spawn(async move { loop { let building = tokio::spawn(build(watch, visualise_dag, generate_syntax_css)); - match building.await { + match building.await.unwrap() { Ok(_) => { if !watch { break; @@ -180,7 +181,7 @@ async fn main() -> miette::Result<()> { let build_loop = tokio::spawn(async move { loop { let building = tokio::spawn(build(watch, visualise_dag, generate_syntax_css)); - match building.await { + match building.await.unwrap() { Ok(_) => { if !watch { break; @@ -226,7 +227,7 @@ async fn main() -> miette::Result<()> { .run(), ); println!("Serving on {}:{} … ", Ipv4Addr::UNSPECIFIED, port); - match serving.await { + match serving.await.unwrap() { Ok(_) => {} Err(err) => { error!("Serving failed: {:#?}", err); @@ -264,6 +265,7 @@ fn insert_or_update_page( pages: &mut AHashMap, layouts: &mut AHashMap>, collection_dependents: &mut AHashMap>, + collection_members: &mut AHashMap>, locale: Locale, ) -> miette::Result<()> { let entry = fs::canonicalize(entry).into_diagnostic()?; @@ -299,8 +301,9 @@ fn insert_or_update_page( // A page's parents are pages in the collections it depends on. Its layout is a child. let layout = page.layout.clone(); let collections = page.collections.clone(); + let depends = page.depends.clone(); debug!("Layout used: {:?} … ", layout); - debug!("Collections used: {:?} … ", collections); + debug!("Collections used: {:?} … ", depends); if let Some(layout) = layout { // Layouts are inserted multiple times, once for each page that uses them. let layout_path = fs::canonicalize(format!("layouts/{}.vox", layout)) @@ -346,6 +349,17 @@ fn insert_or_update_page( } if let Some(collections) = collections { for collection in collections { + if let Some(collection_members) = collection_members.get_mut(&collection) { + collection_members.insert(index); + } else { + let mut new_set = HashSet::new(); + new_set.insert(index); + collection_members.insert(collection.clone(), new_set); + } + } + } + if let Some(depends) = depends { + for collection in depends { if let Some(collection_dependents) = collection_dependents.get_mut(&collection) { collection_dependents.insert(index); } else { @@ -353,29 +367,29 @@ fn insert_or_update_page( new_set.insert(index); collection_dependents.insert(collection.clone(), new_set); } - let collection = fs::canonicalize(collection).into_diagnostic()?; - for entry in - glob(&format!("{}/**/*.vox", collection.to_string_lossy())).into_diagnostic()? - { - let entry = fs::canonicalize(entry.into_diagnostic()?).into_diagnostic()?; - if pages.get(&entry).is_none() { - info!("Inserting collection page: {:?} … ", entry); - let collection_page = path_to_page(entry.clone(), locale)?; - debug!("{:#?}", collection_page); - let collection_page_index = - dag.add_parent(index, EdgeType::Collection, collection_page); - pages.insert(entry, collection_page_index.1); - } else { - info!( - "Setting collection page ({:?}) as parent of {:?} … ", - entry, - page.to_path_string() - ); - let collection_page_index = pages[&entry]; - dag.add_edge(collection_page_index, index, EdgeType::Collection) - .into_diagnostic()?; - } - } + // let collection = fs::canonicalize(collection).into_diagnostic()?; + // for entry in + // glob(&format!("{}/**/*.vox", collection.to_string_lossy())).into_diagnostic()? + // { + // let entry = fs::canonicalize(entry.into_diagnostic()?).into_diagnostic()?; + // if pages.get(&entry).is_none() { + // info!("Inserting collection page: {:?} … ", entry); + // let collection_page = path_to_page(entry.clone(), locale)?; + // debug!("{:#?}", collection_page); + // let collection_page_index = + // dag.add_parent(index, EdgeType::Collection, collection_page); + // pages.insert(entry, collection_page_index.1); + // } else { + // info!( + // "Setting collection page ({:?}) as parent of {:?} … ", + // entry, + // page.to_path_string() + // ); + // let collection_page_index = pages[&entry]; + // dag.add_edge(collection_page_index, index, EdgeType::Collection) + // .into_diagnostic()?; + // } + // } } } @@ -390,6 +404,7 @@ async fn build(watch: bool, visualise_dag: bool, generate_syntax_css: bool) -> m // let mut layouts_by_index: AHashMap = AHashMap::new(); let mut layouts: AHashMap> = AHashMap::new(); let mut collection_dependents: AHashMap> = AHashMap::new(); + let mut collection_members: AHashMap> = AHashMap::new(); // Initial DAG construction. info!("Constructing DAG … "); @@ -406,6 +421,7 @@ async fn build(watch: bool, visualise_dag: bool, generate_syntax_css: bool) -> m &mut pages, &mut layouts, &mut collection_dependents, + &mut collection_members, global.1, )?; } @@ -420,10 +436,22 @@ async fn build(watch: bool, visualise_dag: bool, generate_syntax_css: bool) -> m &mut pages, &mut layouts, &mut collection_dependents, + &mut collection_members, global.1, )?; } } + // We construct edges between collection members and dependents. + for (collection, members) in collection_members { + if let Some(dependents) = collection_dependents.get(&collection) { + for member in members { + for dependent in dependents { + dag.add_edge(member, *dependent, EdgeType::Collection) + .into_diagnostic()?; + } + } + } + } // Write the initial site to the output directory. info!("Performing initial build … "); @@ -492,6 +520,7 @@ async fn build(watch: bool, visualise_dag: bool, generate_syntax_css: bool) -> m let mut new_layouts: AHashMap> = AHashMap::new(); let mut new_collection_dependents: AHashMap> = AHashMap::new(); + let mut new_collection_members: AHashMap> = AHashMap::new(); // New DAG construction. info!("Constructing DAG … "); @@ -507,6 +536,7 @@ async fn build(watch: bool, visualise_dag: bool, generate_syntax_css: bool) -> m &mut new_pages, &mut new_layouts, &mut new_collection_dependents, + &mut new_collection_members, global.1, )?; } @@ -519,10 +549,22 @@ async fn build(watch: bool, visualise_dag: bool, generate_syntax_css: bool) -> m &mut new_pages, &mut new_layouts, &mut new_collection_dependents, + &mut new_collection_members, global.1, )?; } } + for (collection, members) in new_collection_members { + if let Some(dependents) = new_collection_dependents.get(&collection) { + for member in members { + for dependent in dependents { + new_dag + .add_edge(member, *dependent, EdgeType::Collection) + .into_diagnostic()?; + } + } + } + } // 2. Obtain the difference between the old and new DAGs; ie, calculate the set of added or modified nodes. // - A node is modified if it has the same label, but its page is different (not comparing `url` or `rendered`). @@ -758,7 +800,7 @@ async fn build(watch: bool, visualise_dag: bool, generate_syntax_css: bool) -> m .into_diagnostic()?; } if generate_syntax_css { - templates::generate_syntax_css()?; + generate_syntax_stylesheets()?; } timer.stop(); println!( @@ -858,7 +900,7 @@ async fn generate_site( .into_diagnostic()?; } if generate_syntax_css { - templates::generate_syntax_css()?; + generate_syntax_stylesheets()?; } timer.stop(); println!( @@ -904,3 +946,27 @@ fn get_global_context() -> miette::Result<(Object, Locale)> { locale, )) } + +/// Generate stylesheets for syntax highlighting. +fn generate_syntax_stylesheets() -> miette::Result<()> { + let css_path = PathBuf::from("output/css/"); + let dark_css_path = css_path.join("dark-code.css"); + let light_css_path = css_path.join("light-code.css"); + let code_css_path = css_path.join("code.css"); + std::fs::create_dir_all(css_path).into_diagnostic()?; + + let ts = ThemeSet::load_defaults(); + let dark_theme = &ts.themes["base16-ocean.dark"]; + let css_dark = css_for_theme_with_class_style(dark_theme, syntect::html::ClassStyle::Spaced) + .into_diagnostic()?; + std::fs::write(dark_css_path, css_dark).into_diagnostic()?; + + let light_theme = &ts.themes["base16-ocean.light"]; + let css_light = css_for_theme_with_class_style(light_theme, syntect::html::ClassStyle::Spaced) + .into_diagnostic()?; + std::fs::write(light_css_path, css_light).into_diagnostic()?; + + let css = r#"@import url("light-code.css") (prefers-color-scheme: light);@import url("dark-code.css") (prefers-color-scheme: dark);"#; + std::fs::write(code_css_path, css).into_diagnostic()?; + Ok(()) +} diff --git a/src/markdown_block.rs b/src/markdown_block.rs index 01cdb0a..fd7682a 100644 --- a/src/markdown_block.rs +++ b/src/markdown_block.rs @@ -1,6 +1,3 @@ -use std::io::BufWriter; -use std::io::Write; - use comrak::markdown_to_html_with_plugins; use comrak::plugins::syntect::SyntectAdapter; use comrak::ComrakPlugins; @@ -14,6 +11,8 @@ use liquid_core::Result; use liquid_core::Runtime; use liquid_core::Template; use liquid_core::{BlockReflection, ParseBlock, TagBlock, TagTokenIter}; +use std::io::BufWriter; +use std::io::Write; /// Render Markdown as HTML /// diff --git a/src/page.rs b/src/page.rs index b4e31f6..81d0e11 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,6 +1,6 @@ use crate::{ date::Date, - error::{DateNotValid, FrontmatterNotFound, InvalidCollectionsProperty}, + error::{DateNotValid, FrontmatterNotFound, InvalidDependsProperty}, }; use chrono::Locale; use liquid::{Object, Parser}; @@ -27,9 +27,13 @@ pub struct Page { /// A page's date-time metadata, formatted per the RFC 3339 standard. /// This is defined in a page's frontmatter. pub date: Option, + /// The collections a page belongs to. + /// This is defined by a page's path, with one collection per path component, and collections including each successive path component as well. + /// Example: `books/fantasy/page.vox` is in `books`, `fantasy`, and `books_fantasy`. + pub collections: Option>, /// The collections a page depends on. /// This is defined in a page's frontmatter. - pub collections: Option>, + pub depends: Option>, /// The layout a page uses. /// This is defined in a page's frontmatter. pub layout: Option, @@ -37,8 +41,6 @@ pub struct Page { pub directory: String, /// The page's base filename. pub name: String, - /// The collection a page belongs to. - pub collection: Option, /// Whether or not a page is a layout. pub is_layout: bool, /// The output path of a file; a processed `permalink` value. @@ -73,7 +75,7 @@ impl Page { Ok(path_difference.starts_with("layouts/")) } - /// Get the name of the collection a page belongs to based on its path. + /// Get the names of the collections a page belongs to based on its path. /// /// # Arguments /// @@ -81,10 +83,10 @@ impl Page { /// /// # Returns /// - /// The name of the collection a page belongs to, or `None` if the page does not belong to a collection. - pub fn get_collection_name_from_path>( + /// The names of the collections a page belongs to, or `None` if the page does not belong to a collection. + pub fn get_collections_from_path>( path: P, - ) -> miette::Result> { + ) -> miette::Result>> { let current_directory = fs::canonicalize(std::env::current_dir().into_diagnostic()?).into_diagnostic()?; let page_path = fs::canonicalize(path).into_diagnostic()?; @@ -102,7 +104,23 @@ impl Page { if Path::new(first_path_component.as_str()).is_file() { return Ok(None); } - Ok(Some(first_path_component)) + let mut results = Vec::new(); + let mut path_builder = Vec::new(); + for path_component in path_components { + if Path::new(&path_builder.join("/")) + .join(&path_component) + .is_file() + { + break; + } + results.push(path_component.clone()); + path_builder.push(path_component.clone()); + let current_path = path_builder.join("_"); + if path_component != current_path { + results.push(path_builder.join("_")) + } + } + Ok(Some(results)) } /// Determine if two pages are equivalent despite their rendered content. @@ -122,10 +140,10 @@ impl Page { && lhs.permalink == rhs.permalink && lhs.date == rhs.date && lhs.collections == rhs.collections + && lhs.depends == rhs.depends && lhs.layout == rhs.layout && lhs.directory == rhs.directory && lhs.name == rhs.name - && lhs.collection == rhs.collection && lhs.is_layout == rhs.is_layout } @@ -158,12 +176,12 @@ impl Page { Page::is_layout_path(self.to_path_string()) } - /// Get the name of the collection a page belongs to. + /// Get the names of the collections a page belongs to. /// /// # Returns /// - /// The name of the collection a page belongs to, or `None` if the page does not belong to a collection. - pub fn get_collection_name(&self) -> miette::Result> { + /// The names of the collections a page belongs to, or `None` if the page does not belong to a collection. + pub fn get_collections(&self) -> miette::Result>> { // let current_directory = // fs::canonicalize(std::env::current_dir().into_diagnostic()?).into_diagnostic()?; // let page_path = fs::canonicalize(self.to_path_string()).into_diagnostic()?; @@ -175,7 +193,7 @@ impl Page { // .map(|c| c.as_os_str().to_string_lossy().to_string()) // .collect(); // Ok(Some(path_components[0].clone())) - Page::get_collection_name_from_path(self.to_path_string()) + Page::get_collections_from_path(self.to_path_string()) } /// Renders a page's content and URL. @@ -217,20 +235,20 @@ impl Page { pub fn render_url(&mut self, contexts: &Object, parser: &Parser) -> miette::Result { let expanded_permalink = match self.permalink.as_str() { "date" => { - "/{{ page.collection }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}.html".to_owned() + "{{ page.collections.last }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}.html".to_owned() } "pretty" => { - "/{{ page.collection }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}/index.html".to_owned() + "{{ page.collections.last }}/{{ page.date.year }}/{{ page.date.month }}/{{ page.date.day }}/{{ page.data.title }}/index.html".to_owned() } "ordinal" => { - "/{{ page.collection }}/{{ page.date.year }}/{{ page.date.y_day }}/{{ page.data.title }}.html" + "{{ page.collections.last }}/{{ page.date.year }}/{{ page.date.y_day }}/{{ page.data.title }}.html" .to_owned() } "weekdate" => { - "/{{ page.collection }}/{{ page.date.year }}/W{{ page.date.week }}/{{ page.date.short_day }}/{{ page.data.title }}.html".to_owned() + "{{ page.collections.last }}/{{ page.date.year }}/W{{ page.date.week }}/{{ page.date.short_day }}/{{ page.data.title }}.html".to_owned() } "none" => { - "/{{ page.collection }}/{{ page.data.title }}.html".to_owned() + "{{ page.collections.last }}/{{ page.data.title }}.html".to_owned() } _ => { self.permalink.to_owned() @@ -339,18 +357,18 @@ impl Page { let permalink = frontmatter_data_clone .get("permalink") .map(|p| p.as_str().unwrap().to_string()); - let collections = match frontmatter_data_clone.get("collections") { - Some(collections) => Some( - collections + let depends = match frontmatter_data_clone.get("depends") { + Some(depends) => Some( + depends .as_array() - .ok_or(InvalidCollectionsProperty { + .ok_or(InvalidDependsProperty { src: NamedSource::new(path.to_string_lossy(), frontmatter.clone()), }) .into_diagnostic()? .iter() .map(|x| { x.as_str() - .ok_or(InvalidCollectionsProperty { + .ok_or(InvalidDependsProperty { src: NamedSource::new(path.to_string_lossy(), frontmatter.clone()), }) .unwrap() @@ -366,7 +384,7 @@ impl Page { permalink: permalink.unwrap_or_default(), date, layout, - collections, + depends, directory: path .parent() .unwrap_or(&PathBuf::new()) @@ -377,7 +395,7 @@ impl Page { .unwrap_or(&OsString::new()) .to_string_lossy() .to_string(), - collection: Page::get_collection_name_from_path(path.clone())?, + collections: Page::get_collections_from_path(path.clone())?, is_layout: Page::is_layout_path(path)?, url: String::new(), rendered: String::new(), diff --git a/src/templates.rs b/src/templates.rs index cee65cb..b7dded0 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,12 +1,10 @@ +use crate::{markdown_block::MarkdownBlock, math_block::MathBlock}; use glob::glob; use miette::IntoDiagnostic; use std::{ borrow::Cow, path::{Path, PathBuf}, }; -use syntect::{highlighting::ThemeSet, html::css_for_theme}; - -use crate::{markdown_block::MarkdownBlock, math_block::MathBlock}; #[derive(Eq, PartialEq, PartialOrd, Clone, Default, Debug)] /// A source of snippets for Liquid templates. @@ -89,6 +87,7 @@ pub fn create_liquid_parser() -> miette::Result { .filter(liquid_lib::jekyll::Shift) .filter(liquid_lib::jekyll::Slugify) .filter(liquid_lib::jekyll::Unshift) + .filter(liquid_lib::jekyll::Sort) .filter(liquid_lib::shopify::Pluralize) .filter(liquid_lib::extra::DateInTz) .block(MathBlock) @@ -97,25 +96,3 @@ pub fn create_liquid_parser() -> miette::Result { .build() .into_diagnostic() } - -/// Generate stylesheets for syntax highlighting. -pub fn generate_syntax_css() -> miette::Result<()> { - let css_path = PathBuf::from("output/css/"); - let dark_css_path = css_path.join("dark-code.css"); - let light_css_path = css_path.join("light-code.css"); - let code_css_path = css_path.join("code.css"); - std::fs::create_dir_all(css_path).into_diagnostic()?; - - let ts = ThemeSet::load_defaults(); - let dark_theme = &ts.themes["base16-ocean.dark"]; - let css_dark = css_for_theme(dark_theme); - std::fs::write(dark_css_path, css_dark).into_diagnostic()?; - - let light_theme = &ts.themes["base16-ocean.light"]; - let css_light = css_for_theme(light_theme); - std::fs::write(light_css_path, css_light).into_diagnostic()?; - - let css = r#"@import url("light-code.css") (prefers-color-scheme: light);@import url("dark-code.css") (prefers-color-scheme: dark);"#; - std::fs::write(code_css_path, css).into_diagnostic()?; - Ok(()) -}