cobalt/pagination/
mod.rs

1use std::cmp::Ordering;
2
3use liquid::ValueView;
4
5use crate::cobalt_model::SortOrder;
6use crate::cobalt_model::pagination::Include;
7use crate::cobalt_model::pagination::PaginationConfig;
8use crate::cobalt_model::permalink;
9use crate::cobalt_model::slug;
10
11use crate::document;
12use crate::document::Document;
13use crate::error::Result;
14
15mod categories;
16mod dates;
17mod helpers;
18mod paginator;
19mod tags;
20
21use paginator::Paginator;
22
23pub(crate) fn generate_paginators(
24    doc: &mut Document,
25    posts_data: &[liquid::model::Value],
26) -> Result<Vec<Paginator>> {
27    let config = doc
28        .front
29        .pagination
30        .as_ref()
31        .expect("Front should have pagination here.");
32    let mut all_posts: Vec<_> = posts_data.iter().collect();
33    match config.include {
34        Include::All => {
35            sort_posts(&mut all_posts, config);
36            create_all_paginators(&all_posts, doc, config, None)
37        }
38        Include::Tags => tags::create_tags_paginators(&all_posts, doc, config),
39        Include::Categories => categories::create_categories_paginators(&all_posts, doc, config),
40        Include::Dates => dates::create_dates_paginators(&all_posts, doc, config),
41        Include::None => {
42            unreachable!("PaginationConfigBuilder should have lead to a None for pagination.")
43        }
44    }
45}
46
47fn create_all_paginators(
48    all_posts: &[&liquid::model::Value],
49    doc: &Document,
50    pagination_cfg: &PaginationConfig,
51    index_title: Option<&liquid::model::Value>,
52) -> Result<Vec<Paginator>> {
53    let total_pages = all_posts.len();
54    // f32 used here in order to not lose information to ceil the result,
55    // otherwise we can lose an index
56    let total_indexes = (total_pages as f32 / pagination_cfg.per_page as f32).ceil() as usize;
57    let paginators: Result<Vec<_>> = all_posts
58        .chunks(pagination_cfg.per_page as usize)
59        .enumerate()
60        .map(|(i, chunk)| {
61            paginator::create_paginator(
62                i,
63                total_indexes,
64                total_pages,
65                pagination_cfg,
66                doc,
67                chunk,
68                index_title,
69            )
70        })
71        .collect();
72    paginators
73}
74
75// sort posts by multiple criteria
76fn sort_posts(posts: &mut [&liquid::model::Value], config: &PaginationConfig) {
77    let order: fn(liquid::model::ScalarCow<'_>, liquid::model::ScalarCow<'_>) -> Ordering =
78        match config.order {
79            SortOrder::Desc => {
80                |a, b: liquid::model::ScalarCow<'_>| b.partial_cmp(&a).unwrap_or(Ordering::Equal)
81            }
82            SortOrder::Asc => {
83                |a: liquid::model::ScalarCow<'_>, b| a.partial_cmp(&b).unwrap_or(Ordering::Equal)
84            }
85            SortOrder::None => {
86                // when built, order is set like this:
87                // `order.unwrap_or(SortOrder::Desc);` so it's unreachable
88                unreachable!(
89                    "Sort order should have default value when constructing PaginationConfig"
90                )
91            }
92        };
93    posts.sort_by(|a, b| {
94        let keys = &config.sort_by;
95        let mut cmp = Ordering::Less;
96        for k in keys {
97            cmp = match (
98                helpers::extract_scalar(a.as_view(), k),
99                helpers::extract_scalar(b.as_view(), k),
100            ) {
101                (Some(a), Some(b)) => order(a, b),
102                (None, None) => Ordering::Equal,
103                (_, None) => Ordering::Greater,
104                (None, _) => Ordering::Less,
105            };
106            if cmp != Ordering::Equal {
107                return cmp;
108            }
109        }
110        cmp
111    });
112}
113
114fn pagination_attributes(page_num: i32) -> liquid::Object {
115    let attributes: liquid::Object = vec![("num".into(), liquid::model::Value::scalar(page_num))]
116        .into_iter()
117        .collect();
118    attributes
119}
120
121fn index_to_string(index: &liquid::model::Value) -> String {
122    if let Some(index) = index.as_array() {
123        // categories
124        let mut s: String = index
125            .values()
126            .map(|i| {
127                let mut s = slug::slugify(i.to_kstr().into_string());
128                s.push('/');
129                s
130            })
131            .collect();
132        s.pop(); // remove last '/'
133        s
134    } else {
135        slug::slugify(index.to_kstr().into_string())
136    }
137}
138
139fn interpret_permalink(
140    config: &PaginationConfig,
141    doc: &Document,
142    page_num: usize,
143    index: Option<&liquid::model::Value>,
144) -> Result<String> {
145    let mut attributes = document::permalink_attributes(&doc.front, &doc.file_path);
146    let permalink = permalink::explode_permalink(&config.front_permalink, &attributes)?;
147    let permalink_path = std::path::Path::new(&permalink);
148    let pagination_root = permalink_path
149        .extension()
150        .map(|os_str| {
151            permalink
152                .trim_end_matches(&format!(".{}", os_str.to_string_lossy()))
153                .to_string()
154        })
155        .unwrap_or_else(|| permalink.clone());
156    let interpreted_permalink = if page_num == 1 {
157        index
158            .map(|index| {
159                if pagination_root.is_empty() {
160                    index_to_string(index)
161                } else {
162                    format!("{}/{}", pagination_root, index_to_string(index))
163                }
164            })
165            .unwrap_or_else(|| doc.url_path.clone())
166    } else {
167        let pagination_attr = pagination_attributes(page_num as i32);
168        attributes.extend(pagination_attr);
169        let index = index.map(index_to_string).unwrap_or_else(|| {
170            if config.include != Include::All {
171                unreachable!("Include is not All and no index");
172            }
173            "all".to_string()
174        });
175        if pagination_root.is_empty() {
176            format!(
177                "{}/{}",
178                index,
179                permalink::explode_permalink(&config.permalink_suffix, &attributes)?
180            )
181        } else {
182            format!(
183                "{}/{}/{}",
184                pagination_root,
185                index,
186                permalink::explode_permalink(&config.permalink_suffix, &attributes)?
187            )
188        }
189    };
190    Ok(interpreted_permalink)
191}