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",
]
[[package]]
name = "any_ascii"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e"
[[package]]
name = "arrayvec"
version = "0.5.2"
@ -1160,6 +1166,15 @@ dependencies = [
"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]]
name = "libc"
version = "0.2.82"
@ -1176,6 +1191,7 @@ dependencies = [
"front_matter",
"globset",
"lazy_static",
"lexical-sort",
"rayon",
"regex",
"rendering",

View file

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

View file

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

View file

@ -62,6 +62,10 @@ pub struct Page {
pub earlier: Option<DefaultKey>,
/// The later page, for pages sorted by date
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
pub lighter: Option<DefaultKey>,
/// The heavier page, for pages sorted by weight

View file

@ -92,6 +92,8 @@ pub struct SerializingPage<'a> {
heavier: Option<Box<SerializingPage<'a>>>,
earlier: 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>>,
}
@ -119,6 +121,12 @@ impl<'a> SerializingPage<'a> {
let later = page
.later
.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
.ancestors
.iter()
@ -155,6 +163,8 @@ impl<'a> SerializingPage<'a> {
heavier,
earlier,
later,
title_prev,
title_next,
translations,
}
}
@ -217,6 +227,8 @@ impl<'a> SerializingPage<'a> {
heavier: None,
earlier: None,
later: None,
title_prev: None,
title_next: None,
translations,
}
}

View file

@ -6,7 +6,7 @@ use slotmap::{DefaultKey, DenseSlotMap};
use front_matter::SortBy;
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;
// Like vec! but for HashSet
@ -282,6 +282,21 @@ impl Library {
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 => {
let data = section
.pages
@ -312,6 +327,10 @@ impl Library {
page.earlier = val2;
page.later = val1;
}
SortBy::Title => {
page.title_prev = val1;
page.title_next = val2;
}
SortBy::Weight => {
page.lighter = val1;
page.heavier = val2;

View file

@ -1,6 +1,7 @@
use std::cmp::Ordering;
use chrono::NaiveDateTime;
use lexical_sort::natural_lexical_cmp;
use rayon::prelude::*;
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())
}
/// 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
/// Pages without weight will be put in the unsortable bucket
/// 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())
}
/// 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(
sorted: &[DefaultKey],
) -> Vec<(DefaultKey, Option<DefaultKey>, Option<DefaultKey>)> {
@ -71,12 +95,12 @@ pub fn find_siblings(
let mut with_siblings = (*key, None, None);
if i > 0 {
// lighter / later
// lighter / later / title_prev
with_siblings.1 = Some(sorted[i - 1]);
}
if i < length - 1 {
// heavier/earlier
// heavier / earlier / title_next
with_siblings.2 = Some(sorted[i + 1]);
}
res.push(with_siblings);
@ -90,7 +114,7 @@ mod tests {
use slotmap::DenseSlotMap;
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 front_matter::PageFrontMatter;
@ -101,6 +125,12 @@ mod tests {
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 {
let mut front_matter = PageFrontMatter::default();
front_matter.weight = Some(weight);
@ -129,6 +159,49 @@ mod tests {
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]
fn can_sort_by_weight() {
let mut dense = DenseSlotMap::new();

View file

@ -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`.
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"
# Used by the parent section to order its subsections.
@ -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`.
## Sorting
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
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
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`,
`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.
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
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`
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
@ -172,7 +187,11 @@ 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)
to newest (at the bottom).
`reverse` has no effect on `page.later`/`page.earlier` or `page.heavier`/`page.lighter`.
`reverse` has no effect on:
* `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.