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:
parent
6950759eda
commit
92b5b4b3a5
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -19,7 +19,7 @@ Any non-Markdown file in a section directory is added to the `assets` collection
|
|||
Markdown file using relative links.
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -91,7 +91,7 @@ render = true
|
|||
# Useful for the same reason as `render` but when you don't want a 404 when
|
||||
# landing on the root section page.
|
||||
# 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`.
|
||||
# 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`.
|
||||
|
||||
## 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,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)
|
||||
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 sections is a bit less flexible: sections can only be sorted by `weight`,
|
||||
|
|
Loading…
Reference in a new issue