Add sort_by title (#1315)

* Add sort_by=title

* Remove old comment.

* Remove println! debugging

* Minor: text spacing

* Use lexical_sort crate for sort_by title

Co-authored-by: David James <davidcjames@gmail.com>
This commit is contained in:
David James 2021-01-20 09:35:25 -05:00 committed by GitHub
parent 6950759eda
commit 92b5b4b3a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 157 additions and 11 deletions

16
Cargo.lock generated
View file

@ -51,6 +51,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "any_ascii"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.5.2" version = "0.5.2"
@ -1160,6 +1166,15 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "lexical-sort"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a"
dependencies = [
"any_ascii",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.82" version = "0.2.82"
@ -1176,6 +1191,7 @@ dependencies = [
"front_matter", "front_matter",
"globset", "globset",
"lazy_static", "lazy_static",
"lexical-sort",
"rayon", "rayon",
"regex", "regex",
"rendering", "rendering",

View file

@ -46,6 +46,8 @@ impl RawFrontMatter<'_> {
pub enum SortBy { pub enum SortBy {
/// Most recent to oldest /// Most recent to oldest
Date, Date,
/// Sort by title
Title,
/// Lower weight comes first /// Lower weight comes first
Weight, Weight,
/// No sorting /// No sorting

View file

@ -13,6 +13,7 @@ serde = "1"
serde_derive = "1" serde_derive = "1"
regex = "1" regex = "1"
lazy_static = "1" lazy_static = "1"
lexical-sort = "0.3"
front_matter = { path = "../front_matter" } front_matter = { path = "../front_matter" }
config = { path = "../config" } config = { path = "../config" }

View file

@ -62,6 +62,10 @@ pub struct Page {
pub earlier: Option<DefaultKey>, pub earlier: Option<DefaultKey>,
/// The later page, for pages sorted by date /// The later page, for pages sorted by date
pub later: Option<DefaultKey>, pub later: Option<DefaultKey>,
/// The previous page, for pages sorted by title
pub title_prev: Option<DefaultKey>,
/// The next page, for pages sorted by title
pub title_next: Option<DefaultKey>,
/// The lighter page, for pages sorted by weight /// The lighter page, for pages sorted by weight
pub lighter: Option<DefaultKey>, pub lighter: Option<DefaultKey>,
/// The heavier page, for pages sorted by weight /// The heavier page, for pages sorted by weight

View file

@ -92,6 +92,8 @@ pub struct SerializingPage<'a> {
heavier: Option<Box<SerializingPage<'a>>>, heavier: Option<Box<SerializingPage<'a>>>,
earlier: Option<Box<SerializingPage<'a>>>, earlier: Option<Box<SerializingPage<'a>>>,
later: Option<Box<SerializingPage<'a>>>, later: Option<Box<SerializingPage<'a>>>,
title_prev: Option<Box<SerializingPage<'a>>>,
title_next: Option<Box<SerializingPage<'a>>>,
translations: Vec<TranslatedContent<'a>>, translations: Vec<TranslatedContent<'a>>,
} }
@ -119,6 +121,12 @@ impl<'a> SerializingPage<'a> {
let later = page let later = page
.later .later
.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library)))); .map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library))));
let title_prev = page
.title_prev
.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library))));
let title_next = page
.title_next
.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library))));
let ancestors = page let ancestors = page
.ancestors .ancestors
.iter() .iter()
@ -155,6 +163,8 @@ impl<'a> SerializingPage<'a> {
heavier, heavier,
earlier, earlier,
later, later,
title_prev,
title_next,
translations, translations,
} }
} }
@ -217,6 +227,8 @@ impl<'a> SerializingPage<'a> {
heavier: None, heavier: None,
earlier: None, earlier: None,
later: None, later: None,
title_prev: None,
title_next: None,
translations, translations,
} }
} }

View file

@ -6,7 +6,7 @@ use slotmap::{DefaultKey, DenseSlotMap};
use front_matter::SortBy; use front_matter::SortBy;
use crate::content::{Page, Section}; use crate::content::{Page, Section};
use crate::sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight}; use crate::sorting::{find_siblings, sort_pages_by_date, sort_pages_by_title, sort_pages_by_weight};
use config::Config; use config::Config;
// Like vec! but for HashSet // Like vec! but for HashSet
@ -282,6 +282,21 @@ impl Library {
sort_pages_by_date(data) sort_pages_by_date(data)
} }
SortBy::Title => {
let data = section
.pages
.iter()
.map(|k| {
if let Some(page) = self.pages.get(*k) {
(k, page.meta.title.as_deref(), page.permalink.as_ref())
} else {
unreachable!("Sorting got an unknown page")
}
})
.collect();
sort_pages_by_title(data)
}
SortBy::Weight => { SortBy::Weight => {
let data = section let data = section
.pages .pages
@ -312,6 +327,10 @@ impl Library {
page.earlier = val2; page.earlier = val2;
page.later = val1; page.later = val1;
} }
SortBy::Title => {
page.title_prev = val1;
page.title_next = val2;
}
SortBy::Weight => { SortBy::Weight => {
page.lighter = val1; page.lighter = val1;
page.heavier = val2; page.heavier = val2;

View file

@ -1,6 +1,7 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use lexical_sort::natural_lexical_cmp;
use rayon::prelude::*; use rayon::prelude::*;
use slotmap::DefaultKey; use slotmap::DefaultKey;
@ -39,6 +40,28 @@ pub fn sort_pages_by_date(
(can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect()) (can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect())
} }
/// Takes a list of (page key, title, permalink) and sort them by title if possible.
/// Uses the a natural lexical comparison as defined by the lexical_sort crate.
/// Pages without title will be put in the unsortable bucket.
/// The permalink is used to break ties.
pub fn sort_pages_by_title(
pages: Vec<(&DefaultKey, Option<&str>, &str)>,
) -> (Vec<DefaultKey>, Vec<DefaultKey>) {
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) =
pages.into_par_iter().partition(|page| page.1.is_some());
can_be_sorted.par_sort_unstable_by(|a, b| {
let ord = natural_lexical_cmp(a.1.unwrap(), b.1.unwrap());
if ord == Ordering::Equal {
a.2.cmp(&b.2)
} else {
ord
}
});
(can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect())
}
/// Takes a list of (page key, weight, permalink) and sort them by weight if possible /// Takes a list of (page key, weight, permalink) and sort them by weight if possible
/// Pages without weight will be put in the unsortable bucket /// Pages without weight will be put in the unsortable bucket
/// The permalink is used to break ties /// The permalink is used to break ties
@ -60,7 +83,8 @@ pub fn sort_pages_by_weight(
(can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect()) (can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect())
} }
/// Find the lighter/heavier and earlier/later pages for all pages having a date/weight /// Find the lighter/heavier, earlier/later, and title_prev/title_next
/// pages for all pages having a date/weight/title
pub fn find_siblings( pub fn find_siblings(
sorted: &[DefaultKey], sorted: &[DefaultKey],
) -> Vec<(DefaultKey, Option<DefaultKey>, Option<DefaultKey>)> { ) -> Vec<(DefaultKey, Option<DefaultKey>, Option<DefaultKey>)> {
@ -71,12 +95,12 @@ pub fn find_siblings(
let mut with_siblings = (*key, None, None); let mut with_siblings = (*key, None, None);
if i > 0 { if i > 0 {
// lighter / later // lighter / later / title_prev
with_siblings.1 = Some(sorted[i - 1]); with_siblings.1 = Some(sorted[i - 1]);
} }
if i < length - 1 { if i < length - 1 {
// heavier/earlier // heavier / earlier / title_next
with_siblings.2 = Some(sorted[i + 1]); with_siblings.2 = Some(sorted[i + 1]);
} }
res.push(with_siblings); res.push(with_siblings);
@ -90,7 +114,7 @@ mod tests {
use slotmap::DenseSlotMap; use slotmap::DenseSlotMap;
use std::path::PathBuf; use std::path::PathBuf;
use super::{find_siblings, sort_pages_by_date, sort_pages_by_weight}; use super::{find_siblings, sort_pages_by_date, sort_pages_by_title, sort_pages_by_weight};
use crate::content::Page; use crate::content::Page;
use front_matter::PageFrontMatter; use front_matter::PageFrontMatter;
@ -101,6 +125,12 @@ mod tests {
Page::new("content/hello.md", front_matter, &PathBuf::new()) Page::new("content/hello.md", front_matter, &PathBuf::new())
} }
fn create_page_with_title(title: &str) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.title = Some(title.to_string());
Page::new("content/hello.md", front_matter, &PathBuf::new())
}
fn create_page_with_weight(weight: usize) -> Page { fn create_page_with_weight(weight: usize) -> Page {
let mut front_matter = PageFrontMatter::default(); let mut front_matter = PageFrontMatter::default();
front_matter.weight = Some(weight); front_matter.weight = Some(weight);
@ -129,6 +159,49 @@ mod tests {
assert_eq!(pages[2], key2); assert_eq!(pages[2], key2);
} }
#[test]
fn can_sort_by_titles() {
let titles = vec![
"bagel",
"track_3",
"microkernel",
"métro",
"BART",
"Underground",
"track_13",
"μ-kernel",
"meter",
"track_1",
];
let pages: Vec<Page> = titles.iter().map(
|title| create_page_with_title(title)
).collect();
let mut dense = DenseSlotMap::new();
let keys: Vec<_> = pages.iter().map(
|p| dense.insert(p)
).collect();
let input: Vec<_> = pages.iter().enumerate().map(
|(i, page)| (&keys[i], page.meta.title.as_deref(), page.permalink.as_ref())
).collect();
let (sorted, _) = sort_pages_by_title(input);
// Should be sorted by title
let sorted_titles: Vec<_> = sorted.iter().map(
|key| dense.get(*key).unwrap().meta.title.as_ref().unwrap()
).collect();
assert_eq!(sorted_titles, vec![
"bagel",
"BART",
"μ-kernel",
"meter",
"métro",
"microkernel",
"track_1",
"track_3",
"track_13",
"Underground",
]);
}
#[test] #[test]
fn can_sort_by_weight() { fn can_sort_by_weight() {
let mut dense = DenseSlotMap::new(); let mut dense = DenseSlotMap::new();

View file

@ -19,7 +19,7 @@ Any non-Markdown file in a section directory is added to the `assets` collection
Markdown file using relative links. Markdown file using relative links.
## Drafting ## Drafting
Just like pages sections can be drafted by setting the `draft` option in the front matter. By default this is not done. When a section is drafted it's descendants like pages, subsections and assets will not be processed unless the `--drafts` flag is passed. Note that even pages that don't have a `draft` status will not be processed if one of their parent sections is drafted. Just like pages sections can be drafted by setting the `draft` option in the front matter. By default this is not done. When a section is drafted it's descendants like pages, subsections and assets will not be processed unless the `--drafts` flag is passed. Note that even pages that don't have a `draft` status will not be processed if one of their parent sections is drafted.
## Front matter ## Front matter
@ -48,7 +48,7 @@ description = ""
# A draft section is only loaded if the `--drafts` flag is passed to `zola build`, `zola serve` or `zola check`. # A draft section is only loaded if the `--drafts` flag is passed to `zola build`, `zola serve` or `zola check`.
draft = false draft = false
# Used to sort pages by "date", "weight" or "none". See below for more information. # Used to sort pages by "date", "title, "weight", or "none". See below for more information.
sort_by = "none" sort_by = "none"
# Used by the parent section to order its subsections. # Used by the parent section to order its subsections.
@ -91,7 +91,7 @@ render = true
# Useful for the same reason as `render` but when you don't want a 404 when # Useful for the same reason as `render` but when you don't want a 404 when
# landing on the root section page. # landing on the root section page.
# Example: redirect_to = "documentation/content/overview" # Example: redirect_to = "documentation/content/overview"
redirect_to = redirect_to =
# If set to "true", the section will pass its pages on to the parent section. Defaults to `false`. # If set to "true", the section will pass its pages on to the parent section. Defaults to `false`.
# Useful when the section shouldn't split up the parent section, like # Useful when the section shouldn't split up the parent section, like
@ -124,6 +124,7 @@ You can also change the pagination path (the word displayed while paginated in t
by setting the `paginate_path` variable, which defaults to `page`. by setting the `paginate_path` variable, which defaults to `page`.
## Sorting ## Sorting
It is very common for Zola templates to iterate over pages or sections It is very common for Zola templates to iterate over pages or sections
to display all pages/sections in a given directory. Consider a very simple to display all pages/sections in a given directory. Consider a very simple
example: a `blog` directory with three files: `blog/Post_1.md`, example: a `blog` directory with three files: `blog/Post_1.md`,
@ -139,7 +140,7 @@ create a list of links to the posts, a simple template might look like this:
This would iterate over the posts in the order specified This would iterate over the posts in the order specified
by the `sort_by` variable set in the `_index.md` page for the corresponding by the `sort_by` variable set in the `_index.md` page for the corresponding
section. The `sort_by` variable can be given one of three values: `date`, section. The `sort_by` variable can be given one of three values: `date`,
`weight` or `none`. If `sort_by` is not set, the pages will be `title`, `weight` or `none`. If `sort_by` is not set, the pages will be
sorted in the `none` order, which is not intended for sorted content. sorted in the `none` order, which is not intended for sorted content.
Any page that is missing the data it needs to be sorted will be ignored and Any page that is missing the data it needs to be sorted will be ignored and
@ -159,6 +160,20 @@ top of the list) to the oldest (at the bottom of the list). Each page will
get `page.earlier` and `page.later` variables that contain the pages with get `page.earlier` and `page.later` variables that contain the pages with
earlier and later dates, respectively. earlier and later dates, respectively.
### `title`
This will sort all pages by their `title` field in natural lexical order, as
defined by `natural_lexical_cmp` in the [lexical-sort] crate. Each page will
get `page.title_prev` and `page.title_next` variables that contain the pages
with previous and next titles, respectively.
For example, here is a natural lexical ordering: "bachata, BART, bolero,
μ-kernel, meter, Métro, Track-2, Track-3, Track-13, underground". Notice how
special characters and numbers are sorted reasonably. This is better than
the standard sorting: "BART, Métro, Track-13, Track-2, Track-3, bachata,
bolero, meter, underground, μ-kernel".
[lexical-sort]: https://docs.rs/lexical-sort
### `weight` ### `weight`
This will be sort all pages by their `weight` field, from lightest weight This will be sort all pages by their `weight` field, from lightest weight
(at the top of the list) to heaviest (at the bottom of the list). Each (at the top of the list) to heaviest (at the bottom of the list). Each
@ -172,9 +187,13 @@ pages sorted by weight will be sorted from lightest (at the top) to heaviest
(at the bottom); pages sorted by date will be sorted from oldest (at the top) (at the bottom); pages sorted by date will be sorted from oldest (at the top)
to newest (at the bottom). to newest (at the bottom).
`reverse` has no effect on `page.later`/`page.earlier` or `page.heavier`/`page.lighter`. `reverse` has no effect on:
If the section is paginated the `paginate_reversed=true` in the front matter of the relevant section should be set instead of using the filter. * `page.later` / `page.earlier`,
* `page.title_prev` / `page.title_next`, or
* `page.heavier` / `page.lighter`.
If the section is paginated the `paginate_reversed=true` in the front matter of the relevant section should be set instead of using the filter.
## Sorting subsections ## Sorting subsections
Sorting sections is a bit less flexible: sections can only be sorted by `weight`, Sorting sections is a bit less flexible: sections can only be sorted by `weight`,