jekyll-paginate-v3 0.1.0.alpha.3 → 0.2.0.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/jekyll-paginate-v3/config/defaults.rb +7 -3
- data/lib/jekyll-paginate-v3/config/normaliser/legacy_compatibility.rb +4 -6
- data/lib/jekyll-paginate-v3/config/normaliser/site_and_templates.rb +50 -20
- data/lib/jekyll-paginate-v3/pagination/model/core.rb +23 -5
- data/lib/jekyll-paginate-v3/pagination/model/group_navigation_and_pages.rb +80 -20
- data/lib/jekyll-paginate-v3/pagination/model/pagination_pipeline.rb +108 -35
- data/lib/jekyll-paginate-v3/pagination/pages/document.rb +3 -2
- data/lib/jekyll-paginate-v3/pagination/pages/page.rb +3 -2
- data/lib/jekyll-paginate-v3/pagination/pages/pager_support.rb +20 -0
- data/lib/jekyll-paginate-v3/query/parser.rb +3 -2
- data/lib/jekyll-paginate-v3/query/sorter.rb +66 -11
- data/lib/jekyll-paginate-v3/support/frontmatter_path.rb +20 -5
- data/lib/jekyll-paginate-v3/support/placeholder_template.rb +502 -0
- data/lib/jekyll-paginate-v3/templates/variant_expander.rb +316 -46
- data/lib/jekyll-paginate-v3/utils/core.rb +13 -1
- data/lib/jekyll-paginate-v3/utils/formatting.rb +65 -25
- data/lib/jekyll-paginate-v3/utils/paths.rb +109 -0
- data/lib/jekyll-paginate-v3/version.rb +1 -1
- data/lib/jekyll-paginate-v3.rb +2 -0
- data/readme.md +21 -12
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 800113e7a994aa5e6b1a85ec42a7af7a2d14af9c47e8ad9db98de54763b4b6db
|
|
4
|
+
data.tar.gz: d1e054fdd4621e09fb37f36d98c7009d6d8891e12ce485ef3d6fc0bb3aa7aa99
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a955153959b071277e958f5b6d24518a49295b401edd2fc15ee74f282e85e4f3ca5af6ae1be61a5b8af0c72b2a9163d49e6ee4214df11886609c56c29363e9b
|
|
7
|
+
data.tar.gz: 56c0a857ce7e8275c5f4081b427bf6dd6e79ff2ab571cb6446a4d8901c5cc0229c416ed263a9ed497d1737149556dc181b8ef37337209fec743dfa347287f776
|
|
@@ -41,6 +41,10 @@ module Config
|
|
|
41
41
|
'clone' => COLLECTION_TARGET_CLONE
|
|
42
42
|
}.freeze
|
|
43
43
|
|
|
44
|
+
# Slugification modes exposed by pagination config. Every supported mode
|
|
45
|
+
# must produce a route key rather than bypassing character filtering.
|
|
46
|
+
SLUGIFY_MODES = %w[default ascii latin].freeze
|
|
47
|
+
|
|
44
48
|
DEFAULTS = {
|
|
45
49
|
'enabled' => true,
|
|
46
50
|
'compatibility' => nil,
|
|
@@ -62,8 +66,8 @@ module Config
|
|
|
62
66
|
'limit' => 0,
|
|
63
67
|
'offset' => 0,
|
|
64
68
|
'trail' => 5,
|
|
65
|
-
'title' => '
|
|
66
|
-
'permalink' => '
|
|
69
|
+
'title' => '{{ title }} - {{ num }}',
|
|
70
|
+
'permalink' => '{{ num }}',
|
|
67
71
|
'layout' => nil,
|
|
68
72
|
'layouts' => [],
|
|
69
73
|
'group' => nil,
|
|
@@ -96,7 +100,7 @@ module Config
|
|
|
96
100
|
},
|
|
97
101
|
'items' => 'posts',
|
|
98
102
|
'title' => ':title - page :num',
|
|
99
|
-
'permalink' => '
|
|
103
|
+
'permalink' => 'page/:num/',
|
|
100
104
|
'trail' => {
|
|
101
105
|
'before' => 2,
|
|
102
106
|
'after' => 2
|
|
@@ -43,7 +43,7 @@ class Normaliser
|
|
|
43
43
|
extension = override_hash.key?('extension') ? override_hash['extension'].to_s : 'html'
|
|
44
44
|
|
|
45
45
|
template_config['page_templates'] ||= build_page_templates(template_config['title'], template_config['permalink'])
|
|
46
|
-
template_config['page_templates']['page1']['permalink'] = Utils.ensure_full_path(
|
|
46
|
+
template_config['page_templates']['page1']['permalink'] = Utils.ensure_full_path(index_name, index_name, extension)
|
|
47
47
|
template_config['page_templates']['page2']['permalink'] = Utils.ensure_full_path(template_config['permalink'], index_name, extension)
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -118,11 +118,9 @@ class Normaliser
|
|
|
118
118
|
permalink = group['permalink']
|
|
119
119
|
permalink = defaults['permalink'] unless present_config_value?(permalink)
|
|
120
120
|
|
|
121
|
-
slugify =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Utils.deep_copy(defaults['slugify'])
|
|
125
|
-
end
|
|
121
|
+
slugify = normalise_slugify_config(
|
|
122
|
+
group.key?('slugify') ? group['slugify'] : Utils.deep_copy(defaults['slugify'])
|
|
123
|
+
)
|
|
126
124
|
|
|
127
125
|
[
|
|
128
126
|
{
|
|
@@ -199,9 +199,9 @@ class Normaliser
|
|
|
199
199
|
# does not make the default location collide with a collection label.
|
|
200
200
|
source['location'] = keywords['pages'] if source['location'].nil? || source['location'].to_s.strip.empty?
|
|
201
201
|
source['generate'] = if source['generate'].is_a?(Array)
|
|
202
|
-
source['generate'].map { |entry|
|
|
202
|
+
source['generate'].map { |entry| normalise_generated_template_definition(entry) }
|
|
203
203
|
elsif source['generate'].is_a?(Hash)
|
|
204
|
-
[
|
|
204
|
+
[normalise_generated_template_definition(source['generate'])]
|
|
205
205
|
else
|
|
206
206
|
[]
|
|
207
207
|
end
|
|
@@ -212,6 +212,17 @@ class Normaliser
|
|
|
212
212
|
source
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
+
# Validates settings that generated templates otherwise would not
|
|
216
|
+
# normalise until their in-memory template objects are processed.
|
|
217
|
+
def normalise_generated_template_definition(raw_definition)
|
|
218
|
+
definition = Utils.safe_hash(raw_definition)
|
|
219
|
+
if definition.key?('slugify')
|
|
220
|
+
definition['slugify'] = normalise_slugify_config(definition['slugify'])
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
definition
|
|
224
|
+
end
|
|
225
|
+
|
|
215
226
|
# Returns site-level defaults that are inherited by explicit and
|
|
216
227
|
# generated templates.
|
|
217
228
|
def extract_site_template_defaults(site_config)
|
|
@@ -382,7 +393,7 @@ class Normaliser
|
|
|
382
393
|
def build_page_templates(page2_title, page2_permalink)
|
|
383
394
|
{
|
|
384
395
|
'page1' => {
|
|
385
|
-
'title' => '
|
|
396
|
+
'title' => '{{ title }}',
|
|
386
397
|
'permalink' => ''
|
|
387
398
|
},
|
|
388
399
|
'page2' => {
|
|
@@ -454,7 +465,7 @@ class Normaliser
|
|
|
454
465
|
# Preserves legacy `sort_field` + `sort_reverse` behaviour when the
|
|
455
466
|
# caller did not provide an explicit `sort` override.
|
|
456
467
|
def normalise_sort(raw_sort, raw_sort_field, raw_sort_reverse, split_delimiter, sort_explicitly_set: false)
|
|
457
|
-
sort_entries =
|
|
468
|
+
sort_entries = placeholder_aware_sort_entries(raw_sort, split_delimiter)
|
|
458
469
|
sort_field = raw_sort_field.to_s.strip
|
|
459
470
|
|
|
460
471
|
if !sort_explicitly_set && !sort_field.empty?
|
|
@@ -466,13 +477,25 @@ class Normaliser
|
|
|
466
477
|
|
|
467
478
|
if sort_field.empty?
|
|
468
479
|
fallback_sort = DEFAULTS['sort']
|
|
469
|
-
return
|
|
480
|
+
return placeholder_aware_sort_entries(fallback_sort, split_delimiter)
|
|
470
481
|
end
|
|
471
482
|
|
|
472
483
|
direction = boolean_config_value(raw_sort_reverse) ? 'desc' : 'asc'
|
|
473
484
|
["#{sort_field} #{direction}"]
|
|
474
485
|
end
|
|
475
486
|
|
|
487
|
+
# Splits sort lists without treating a canonical placeholder filter pipe
|
|
488
|
+
# as the configured list separator.
|
|
489
|
+
def placeholder_aware_sort_entries(raw_sort, split_delimiter)
|
|
490
|
+
raw_entries = raw_sort.is_a?(Array) ? raw_sort.flatten : [raw_sort]
|
|
491
|
+
raw_entries.flat_map do |raw_entry|
|
|
492
|
+
Support::PlaceholderTemplate.split_source(
|
|
493
|
+
raw_entry,
|
|
494
|
+
delimiter: split_delimiter
|
|
495
|
+
)
|
|
496
|
+
end.map(&:to_s).map(&:strip).reject(&:empty?)
|
|
497
|
+
end
|
|
498
|
+
|
|
476
499
|
# Normalises `pagination.layout` / `pagination.layouts` into a
|
|
477
500
|
# canonical string array.
|
|
478
501
|
def normalise_layout_overrides(config, split_delimiter:)
|
|
@@ -499,9 +522,17 @@ class Normaliser
|
|
|
499
522
|
[raw_group]
|
|
500
523
|
end
|
|
501
524
|
|
|
502
|
-
raw_entries.map do |raw_entry|
|
|
525
|
+
entries = raw_entries.map do |raw_entry|
|
|
503
526
|
normalise_group_entry(raw_entry)
|
|
504
527
|
end.compact
|
|
528
|
+
|
|
529
|
+
keys = entries.map { |entry| entry['on'].to_s }
|
|
530
|
+
duplicate_keys = keys.group_by(&:itself).select { |_, matches| matches.length > 1 }.keys
|
|
531
|
+
unless duplicate_keys.empty?
|
|
532
|
+
raise ArgumentError, "Duplicate pagination group key(s): #{duplicate_keys.sort.join(', ')}. Each `group.on` key must be unique."
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
entries
|
|
505
536
|
end
|
|
506
537
|
|
|
507
538
|
# Normalises legacy `index` + `group` + `filter` config into modern
|
|
@@ -571,26 +602,25 @@ class Normaliser
|
|
|
571
602
|
{ 'on' => on_key }
|
|
572
603
|
end
|
|
573
604
|
|
|
574
|
-
# Normalises slugify config
|
|
575
|
-
#
|
|
576
|
-
# string shorthand where the value maps directly to `mode`.
|
|
605
|
+
# Normalises slugify config into the route-key policy shared by grouping
|
|
606
|
+
# and slugified placeholder representations.
|
|
577
607
|
def normalise_slugify_config(raw_slugify)
|
|
578
|
-
|
|
579
|
-
mode = raw_slugify.to_s.strip
|
|
580
|
-
mode = 'default' if mode.empty?
|
|
581
|
-
return {
|
|
582
|
-
'mode' => mode,
|
|
583
|
-
'lowercase' => true
|
|
584
|
-
}
|
|
585
|
-
end
|
|
586
|
-
|
|
587
|
-
slugify = Utils.safe_hash(raw_slugify)
|
|
608
|
+
slugify = raw_slugify.is_a?(String) ? { 'mode' => raw_slugify } : Utils.safe_hash(raw_slugify)
|
|
588
609
|
mode = slugify['mode'].to_s.strip
|
|
589
610
|
mode = 'default' if mode.empty?
|
|
611
|
+
unless SLUGIFY_MODES.include?(mode)
|
|
612
|
+
raise ArgumentError, "`slugify.mode` must be one of #{SLUGIFY_MODES.join(', ')}; received '#{mode}'."
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
lowercase = if slugify.key?('lowercase')
|
|
616
|
+
boolean_config_value(slugify['lowercase'])
|
|
617
|
+
else
|
|
618
|
+
true
|
|
619
|
+
end
|
|
590
620
|
|
|
591
621
|
{
|
|
592
622
|
'mode' => mode,
|
|
593
|
-
'lowercase' =>
|
|
623
|
+
'lowercase' => lowercase
|
|
594
624
|
}
|
|
595
625
|
end
|
|
596
626
|
|
|
@@ -72,8 +72,6 @@ class Model
|
|
|
72
72
|
end
|
|
73
73
|
next
|
|
74
74
|
end
|
|
75
|
-
validate_required_template_config!(template, template_config)
|
|
76
|
-
|
|
77
75
|
enabled_templates << [template, template_config, template_pagination_source, template_log_lambda]
|
|
78
76
|
end
|
|
79
77
|
|
|
@@ -177,9 +175,28 @@ class Model
|
|
|
177
175
|
)
|
|
178
176
|
end
|
|
179
177
|
|
|
178
|
+
# Keeps the captured item-resolution view immutable when a source
|
|
179
|
+
# template itself is retained and mutated into pagination page one.
|
|
180
|
+
# Later templates should still see the source template metadata rather
|
|
181
|
+
# than the emitted index metadata attached to the retained object.
|
|
182
|
+
def replace_item_resolution_source!(source_item, source_snapshot)
|
|
183
|
+
@item_resolution_pages&.map! do |item|
|
|
184
|
+
item.equal?(source_item) ? source_snapshot : item
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
@item_resolution_documents_by_collection&.each_value do |documents|
|
|
188
|
+
documents.map! do |item|
|
|
189
|
+
item.equal?(source_item) ? source_snapshot : item
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
180
194
|
# Discovers all pages/documents configured as pagination templates.
|
|
181
195
|
def discover_templates
|
|
182
196
|
search_entries = Query::Parser.parse(@site_config.dig('templates', 'location'), @site_config['keywords'], split_delimiter: @split_delimiter)
|
|
197
|
+
# Report site pages before collection sources regardless of configuration order.
|
|
198
|
+
page_search_entries, collection_search_entries = search_entries.partition { |entry| entry['type'] == Query::Parser::SEARCH_TYPE_PAGES }
|
|
199
|
+
search_entries = page_search_entries + collection_search_entries
|
|
183
200
|
reset_template_search_reporting_state
|
|
184
201
|
|
|
185
202
|
combined_candidates = []
|
|
@@ -321,10 +338,11 @@ class Model
|
|
|
321
338
|
def find_layout(layout_name)
|
|
322
339
|
layout = @site.layouts[layout_name]
|
|
323
340
|
return layout unless layout.nil?
|
|
324
|
-
return nil unless layout_name.include?('.')
|
|
325
341
|
|
|
326
|
-
|
|
327
|
-
|
|
342
|
+
normalised_layout_name = Utils.normalise_layout_name(layout_name)
|
|
343
|
+
return nil if normalised_layout_name == layout_name
|
|
344
|
+
|
|
345
|
+
@site.layouts[normalised_layout_name]
|
|
328
346
|
end
|
|
329
347
|
|
|
330
348
|
# Resolves template compatibility mode from local and site config.
|
|
@@ -94,9 +94,14 @@ class Model
|
|
|
94
94
|
def grouped_set_sort_direction(config, index_key)
|
|
95
95
|
return 'asc' if index_key.to_s.strip.empty?
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
sort_instructions
|
|
99
|
-
|
|
97
|
+
sort_instructions = config['_sort_instructions']
|
|
98
|
+
if sort_instructions.nil?
|
|
99
|
+
split_delimiter = config.key?('split') ? config['split'] : @split_delimiter
|
|
100
|
+
sort_instructions = Query::Sorter.parse(config['sort'], split_delimiter: split_delimiter)
|
|
101
|
+
end
|
|
102
|
+
sort_entry = sort_instructions.find do |entry|
|
|
103
|
+
(entry['source_field'] || entry['field']) == index_key
|
|
104
|
+
end
|
|
100
105
|
return 'asc' if sort_entry.nil?
|
|
101
106
|
|
|
102
107
|
sort_entry['direction']
|
|
@@ -115,33 +120,61 @@ class Model
|
|
|
115
120
|
|
|
116
121
|
# Applies configured page title templates.
|
|
117
122
|
def assign_generated_page_title!(generated, template, config, current_page, total_pages)
|
|
118
|
-
page_template = page_template_config(config, current_page)
|
|
119
123
|
base_title = template.data['title'] || @site.config['title']
|
|
120
|
-
|
|
124
|
+
pattern = page_placeholder_template(config, current_page, 'title')
|
|
125
|
+
generated.data['title'] = Utils.format_page_title(
|
|
126
|
+
pattern,
|
|
127
|
+
base_title,
|
|
128
|
+
current_page,
|
|
129
|
+
total_pages,
|
|
130
|
+
slugifier: placeholder_slugifier(config)
|
|
131
|
+
)
|
|
121
132
|
end
|
|
122
133
|
|
|
123
134
|
# Applies configured page permalink templates.
|
|
124
135
|
def assign_generated_page_permalink!(generated, template, config, current_page, total_pages)
|
|
125
|
-
|
|
136
|
+
page_route_fragment = resolved_page_route_fragment(config, current_page, total_pages)
|
|
137
|
+
page_route_path = Utils.join_route_fragments(page_route_fragment)
|
|
138
|
+
resolved_permalink = resolved_page_permalink(template, config, current_page, page_route_fragment)
|
|
139
|
+
reset_cached_item_url!(generated)
|
|
126
140
|
|
|
127
141
|
if resolved_permalink.nil?
|
|
128
142
|
generated.data.delete('permalink') if current_page > 1
|
|
129
|
-
return
|
|
143
|
+
return page_route_path
|
|
130
144
|
end
|
|
131
145
|
|
|
132
|
-
|
|
146
|
+
context = "pagination page #{current_page} for template '#{Utils.relative_item_path(template)}'"
|
|
147
|
+
generated.data['permalink'] = Utils.validate_resolved_permalink!(resolved_permalink, context: context)
|
|
148
|
+
Utils.validate_output_destination!(generated, site: @site, context: context)
|
|
149
|
+
page_route_path
|
|
133
150
|
end
|
|
134
151
|
|
|
135
|
-
# Resolves
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
# Resolves the relative route fragment contributed by the current index.
|
|
153
|
+
def resolved_page_route_fragment(config, current_page, total_pages)
|
|
154
|
+
pattern = page_placeholder_template(config, current_page, 'permalink')
|
|
155
|
+
Utils.format_page_number(
|
|
156
|
+
pattern,
|
|
157
|
+
current_page,
|
|
158
|
+
total_pages,
|
|
159
|
+
slugifier: placeholder_slugifier(config)
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Resolves one complete page permalink from the template route and already
|
|
164
|
+
# interpolated page fragment.
|
|
165
|
+
def resolved_page_permalink(template, config, current_page, page_route_fragment)
|
|
166
|
+
return Utils.ensure_leading_slash(page_route_fragment) if v1_absolute_paginate_path?(config, current_page)
|
|
140
167
|
|
|
141
168
|
first_page_url = template_first_page_url(template)
|
|
142
|
-
return first_page_url if
|
|
169
|
+
return first_page_url if page_route_fragment.to_s.strip.empty?
|
|
170
|
+
|
|
171
|
+
join_url(first_page_url, page_route_fragment)
|
|
172
|
+
end
|
|
143
173
|
|
|
144
|
-
|
|
174
|
+
# Clears Jekyll's memoised URL before a retained source object receives its
|
|
175
|
+
# final permalink. Generated adapters already start with an empty cache.
|
|
176
|
+
def reset_cached_item_url!(item)
|
|
177
|
+
item.instance_variable_set(:@url, nil) if item.instance_variable_defined?(:@url)
|
|
145
178
|
end
|
|
146
179
|
|
|
147
180
|
# Determines whether this page should use v1-style absolute paginate_path.
|
|
@@ -161,7 +194,7 @@ class Model
|
|
|
161
194
|
|
|
162
195
|
if current_page == 1
|
|
163
196
|
{
|
|
164
|
-
'title' => '
|
|
197
|
+
'title' => '{{ title }}',
|
|
165
198
|
'permalink' => ''
|
|
166
199
|
}
|
|
167
200
|
else
|
|
@@ -172,15 +205,42 @@ class Model
|
|
|
172
205
|
end
|
|
173
206
|
end
|
|
174
207
|
|
|
175
|
-
#
|
|
208
|
+
# Returns the already-parsed pattern for one page-template field, falling
|
|
209
|
+
# back to its public scalar for compatibility callers without variant state.
|
|
210
|
+
def page_placeholder_template(config, current_page, field)
|
|
211
|
+
key = current_page == 1 ? 'page1' : 'page2'
|
|
212
|
+
parsed = config.dig('_placeholder_templates', 'page_templates', key, field)
|
|
213
|
+
return parsed unless parsed.nil?
|
|
214
|
+
|
|
215
|
+
page_template_config(config, current_page)[field]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Builds the representation filter used by system values in this template.
|
|
219
|
+
def placeholder_slugifier(config)
|
|
220
|
+
slugify = Utils.safe_hash(config['slugify'])
|
|
221
|
+
mode = slugify['mode'].to_s.strip
|
|
222
|
+
mode = 'default' if mode.empty?
|
|
223
|
+
lowercase = !!slugify['lowercase']
|
|
224
|
+
lambda do |value|
|
|
225
|
+
Jekyll::Utils.slugify(value.to_s, mode: mode, cased: !lowercase)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Ensures multi-page pagination outputs include a page-number placeholder
|
|
176
230
|
# templates so each generated index resolves to a unique destination path.
|
|
177
231
|
def validate_numbered_permalink_template!(template, config, total_pages)
|
|
178
232
|
return unless total_pages > 1
|
|
179
233
|
|
|
180
234
|
page2_permalink = page_template_config(config, 2)['permalink'].to_s
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
235
|
+
parsed_permalink = page_placeholder_template(config, 2, 'permalink')
|
|
236
|
+
parsed_permalink = Utils.placeholder_template(
|
|
237
|
+
page2_permalink,
|
|
238
|
+
allowed: %w[num max],
|
|
239
|
+
context: 'pagination page2 permalink'
|
|
240
|
+
) unless parsed_permalink.is_a?(Support::PlaceholderTemplate)
|
|
241
|
+
return if parsed_permalink.include_placeholder?('num')
|
|
242
|
+
|
|
243
|
+
raise ArgumentError, "Template '#{Utils.relative_item_path(template)}' paginates to #{total_pages} pages but page2 permalink template '#{page2_permalink}' (from `pagination.page_templates.page2.permalink` or fallback `pagination.permalink`) does not include '{{ num }}' or ':num'. Add a page-number placeholder so generated indexes have unique permalinks."
|
|
184
244
|
end
|
|
185
245
|
|
|
186
246
|
# Determines the canonical URL for the first pagination page of a template.
|
|
@@ -14,9 +14,17 @@ class Model
|
|
|
14
14
|
|
|
15
15
|
def paginate_template(template, config, template_pagination_source)
|
|
16
16
|
item_exclusions = pagination_item_exclusions_for_template(template)
|
|
17
|
-
|
|
17
|
+
template_route = template_first_page_url(template)
|
|
18
|
+
variants = expand_template_variants(
|
|
19
|
+
template,
|
|
20
|
+
config,
|
|
21
|
+
template_route: template_route,
|
|
22
|
+
template_pagination_source: template_pagination_source,
|
|
23
|
+
item_exclusions: item_exclusions
|
|
24
|
+
)
|
|
18
25
|
if variants.empty?
|
|
19
|
-
|
|
26
|
+
@remove_item_lambda.call(template)
|
|
27
|
+
log("Template '#{Utils.relative_item_path(template)}': grouping/layout expansion produced no variants; consumed template without emitting indexes.", 'debug')
|
|
20
28
|
return {
|
|
21
29
|
'paginated_items' => 0,
|
|
22
30
|
'indexes' => 0
|
|
@@ -26,6 +34,7 @@ class Model
|
|
|
26
34
|
total_paginated_items = 0
|
|
27
35
|
total_indexes = 0
|
|
28
36
|
collection_replacement_state = nil
|
|
37
|
+
retain_page_one = variants.one? && retain_source_template_as_page_one?(template, variants.first['config'])
|
|
29
38
|
|
|
30
39
|
variants.each_with_index do |variant, variant_index|
|
|
31
40
|
variant_template = variant['template']
|
|
@@ -33,7 +42,10 @@ class Model
|
|
|
33
42
|
variant_report = paginate_template_variant(
|
|
34
43
|
variant_template,
|
|
35
44
|
variant_config,
|
|
36
|
-
remove_source_template: variant_index.zero
|
|
45
|
+
remove_source_template: variant_index.zero? && !retain_page_one,
|
|
46
|
+
retain_page_one: retain_page_one,
|
|
47
|
+
route_base: variant['route_base'],
|
|
48
|
+
route_path: variant['route_path'],
|
|
37
49
|
item_exclusions: item_exclusions,
|
|
38
50
|
collection_replacement_state: collection_replacement_state
|
|
39
51
|
)
|
|
@@ -56,34 +68,28 @@ class Model
|
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
# Expands one template into grouped/layout variants before pagination.
|
|
59
|
-
def expand_template_variants(template, config, template_pagination_source:, item_exclusions:)
|
|
71
|
+
def expand_template_variants(template, config, template_route:, template_pagination_source:, item_exclusions:)
|
|
60
72
|
expander = Templates::VariantExpander.new(
|
|
61
73
|
site: @site,
|
|
62
74
|
site_config: @site_config,
|
|
63
75
|
template: template,
|
|
76
|
+
template_route: template_route,
|
|
64
77
|
template_config: config,
|
|
65
78
|
template_pagination_source: template_pagination_source,
|
|
66
79
|
merge_template_pagination_lambda: method(:merged_template_pagination_config),
|
|
67
80
|
normalise_template_config_lambda: lambda { |pagination| Config::Normaliser.normalise_template_config(@site_config, pagination) },
|
|
81
|
+
validate_template_config_lambda: method(:validate_required_template_config!),
|
|
68
82
|
resolve_items_lambda: lambda { |raw_search| resolve_items(raw_search, exclude_items: item_exclusions) },
|
|
69
83
|
log_lambda: @active_log_lambda
|
|
70
84
|
)
|
|
71
|
-
|
|
72
|
-
return variants unless variants.empty?
|
|
73
|
-
|
|
74
|
-
template.data['pagination'] = merged_template_pagination_config(template, template_pagination_source)
|
|
75
|
-
|
|
76
|
-
[
|
|
77
|
-
{
|
|
78
|
-
'template' => template,
|
|
79
|
-
'config' => config
|
|
80
|
-
}
|
|
81
|
-
]
|
|
85
|
+
expander.expand
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
# Runs pagination for one already-expanded template variant.
|
|
85
|
-
def paginate_template_variant(template, config, remove_source_template:, item_exclusions:, collection_replacement_state:)
|
|
89
|
+
def paginate_template_variant(template, config, remove_source_template:, retain_page_one:, route_base:, route_path:, item_exclusions:, collection_replacement_state:)
|
|
86
90
|
template_path = Utils.relative_item_path(template)
|
|
91
|
+
emission_template = retain_page_one ? clone_template_for_emission(template) : template
|
|
92
|
+
replace_item_resolution_source!(template, emission_template) if retain_page_one
|
|
87
93
|
split_delimiter = config.key?('split') ? config['split'] : @split_delimiter
|
|
88
94
|
nested_separator = config['separator'] || @nested_separator
|
|
89
95
|
all_items = resolve_items(config['items'], exclude_items: item_exclusions)
|
|
@@ -108,7 +114,8 @@ class Model
|
|
|
108
114
|
config['sort'],
|
|
109
115
|
nested_separator: nested_separator,
|
|
110
116
|
equivalents: @equivalents,
|
|
111
|
-
split_delimiter: split_delimiter
|
|
117
|
+
split_delimiter: split_delimiter,
|
|
118
|
+
instructions: config['_sort_instructions']
|
|
112
119
|
)
|
|
113
120
|
log("Template '#{template_path}': sorted #{sorted_items.length} item(s) by #{config['sort']} before offset.", 'debug')
|
|
114
121
|
log_item_path_sample("Template '#{template_path}': sorted item sample", sorted_items)
|
|
@@ -127,20 +134,23 @@ class Model
|
|
|
127
134
|
|
|
128
135
|
page_windows = Utils.build_pagination_windows(sorted_items.length, config['per_page'])
|
|
129
136
|
total_pages = page_windows.length
|
|
130
|
-
validate_numbered_permalink_template!(
|
|
137
|
+
validate_numbered_permalink_template!(emission_template, config, total_pages)
|
|
131
138
|
|
|
132
139
|
log("Template '#{template_path}': generating #{total_pages} page(s) with per_page=#{config['per_page']} limit=#{config['limit']}.", 'debug')
|
|
133
140
|
page_emission = emit_paginated_pages(
|
|
134
|
-
|
|
141
|
+
emission_template,
|
|
135
142
|
config,
|
|
136
143
|
sorted_items,
|
|
137
144
|
page_windows,
|
|
138
145
|
remove_template: remove_source_template,
|
|
146
|
+
retained_page_one: retain_page_one ? template : nil,
|
|
147
|
+
route_base: route_base,
|
|
148
|
+
route_path: route_path,
|
|
139
149
|
collection_replacement_state: collection_replacement_state
|
|
140
150
|
)
|
|
141
151
|
generated_pages = page_emission['pages']
|
|
142
152
|
collection_replacement_state = page_emission['collection_replacement_state']
|
|
143
|
-
register_grouped_set_if_applicable(
|
|
153
|
+
register_grouped_set_if_applicable(emission_template, config, generated_pages)
|
|
144
154
|
{
|
|
145
155
|
'paginated_items' => sorted_items.length,
|
|
146
156
|
'indexes' => generated_pages.length,
|
|
@@ -149,10 +159,12 @@ class Model
|
|
|
149
159
|
end
|
|
150
160
|
|
|
151
161
|
# Replaces a template with one synthetic page/document per page number.
|
|
152
|
-
def emit_paginated_pages(template, config, items, page_windows, remove_template: true, collection_replacement_state: nil)
|
|
162
|
+
def emit_paginated_pages(template, config, items, page_windows, remove_template: true, retained_page_one: nil, route_base:, route_path:, collection_replacement_state: nil)
|
|
153
163
|
if remove_template
|
|
154
164
|
removed_item_state = @remove_item_lambda.call(template)
|
|
155
165
|
collection_replacement_state = build_collection_replacement_state(template, removed_item_state)
|
|
166
|
+
elsif !retained_page_one.nil?
|
|
167
|
+
collection_replacement_state = build_retained_collection_insertion_state(retained_page_one)
|
|
156
168
|
end
|
|
157
169
|
|
|
158
170
|
new_pages = []
|
|
@@ -164,13 +176,18 @@ class Model
|
|
|
164
176
|
|
|
165
177
|
page_windows.each do |page_window|
|
|
166
178
|
current_page = page_window['num']
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
retaining_current_page = current_page == 1 && !retained_page_one.nil?
|
|
180
|
+
generated = if retaining_current_page
|
|
181
|
+
prepare_retained_page_one(retained_page_one, current_page: current_page, total_pages: total_pages)
|
|
182
|
+
else
|
|
183
|
+
build_generated_item(
|
|
184
|
+
template: template,
|
|
185
|
+
config: config,
|
|
186
|
+
current_page: current_page,
|
|
187
|
+
total_pages: total_pages,
|
|
188
|
+
index_file: index_file
|
|
189
|
+
)
|
|
190
|
+
end
|
|
174
191
|
|
|
175
192
|
generated.pager = Paginator.new(
|
|
176
193
|
per_page: config['per_page'],
|
|
@@ -199,13 +216,18 @@ class Model
|
|
|
199
216
|
generated.data['autogen'] = 'jekyll-paginate-v2' if config['compatibility'] == 'v2'
|
|
200
217
|
|
|
201
218
|
assign_generated_page_title!(generated, template, config, current_page, total_pages)
|
|
202
|
-
assign_generated_page_permalink!(generated, template, config, current_page, total_pages)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
page_route_path = assign_generated_page_permalink!(generated, template, config, current_page, total_pages)
|
|
220
|
+
generated.data['pagination']['base'] = Utils.normalise_route(route_base)
|
|
221
|
+
generated.data['pagination']['path'] = Utils.join_route_fragments(route_path, page_route_path)
|
|
222
|
+
|
|
223
|
+
unless retaining_current_page
|
|
224
|
+
@add_item_lambda.call(
|
|
225
|
+
generated,
|
|
226
|
+
collection_index: collection_insertion_index_for_generated_item(generated, collection_replacement_state)
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
action = retaining_current_page ? 'Retained' : 'Emitted'
|
|
230
|
+
log("#{action} pagination page #{current_page}/#{total_pages} at '#{generated.url}' for template '#{Utils.relative_item_path(template)}'.", 'debug')
|
|
209
231
|
new_pages << generated
|
|
210
232
|
end
|
|
211
233
|
|
|
@@ -217,6 +239,32 @@ class Model
|
|
|
217
239
|
}
|
|
218
240
|
end
|
|
219
241
|
|
|
242
|
+
# Reuses an eligible source page/document while adding the small runtime
|
|
243
|
+
# interface and metadata normally supplied by generated subclasses.
|
|
244
|
+
def prepare_retained_page_one(item, current_page:, total_pages:)
|
|
245
|
+
item.extend(Pages::PagerSupport) unless item.respond_to?(:pager=)
|
|
246
|
+
item.data['pagination_info'] = {
|
|
247
|
+
'curr_page' => current_page,
|
|
248
|
+
'total_pages' => total_pages
|
|
249
|
+
}
|
|
250
|
+
item
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Clones mutable template presentation state before page one is updated in
|
|
254
|
+
# place, keeping all later page emission based on the original variant.
|
|
255
|
+
def clone_template_for_emission(template)
|
|
256
|
+
cloned_template = template.dup
|
|
257
|
+
# Keep the retained source independent from the snapshot used to emit its later pages.
|
|
258
|
+
cloned_data = Utils.deep_copy(Utils.safe_hash(template.data))
|
|
259
|
+
if cloned_template.respond_to?(:data=)
|
|
260
|
+
cloned_template.data = cloned_data
|
|
261
|
+
else
|
|
262
|
+
cloned_template.instance_variable_set(:@data, cloned_data)
|
|
263
|
+
end
|
|
264
|
+
cloned_template.content = template.content.to_s if cloned_template.respond_to?(:content=)
|
|
265
|
+
cloned_template
|
|
266
|
+
end
|
|
267
|
+
|
|
220
268
|
# Builds insertion state for collection-template replacement.
|
|
221
269
|
#
|
|
222
270
|
# The state stores a moving insertion cursor so any generated
|
|
@@ -235,6 +283,21 @@ class Model
|
|
|
235
283
|
}
|
|
236
284
|
end
|
|
237
285
|
|
|
286
|
+
# Starts insertion immediately after a retained source document so later
|
|
287
|
+
# self-targeted indexes preserve collection ordering without remove/re-add.
|
|
288
|
+
def build_retained_collection_insertion_state(template)
|
|
289
|
+
return nil unless collection_template?(template)
|
|
290
|
+
|
|
291
|
+
collection_documents = template.collection.docs
|
|
292
|
+
template_index = collection_documents.index { |document| document.equal?(template) }
|
|
293
|
+
return nil if template_index.nil?
|
|
294
|
+
|
|
295
|
+
{
|
|
296
|
+
'source_collection_label' => template.collection.label.to_s,
|
|
297
|
+
'next_index' => template_index + 1
|
|
298
|
+
}
|
|
299
|
+
end
|
|
300
|
+
|
|
238
301
|
# Returns the next insertion index for a generated document that
|
|
239
302
|
# belongs in the same collection as the template it replaced.
|
|
240
303
|
def collection_insertion_index_for_generated_item(generated, collection_replacement_state)
|
|
@@ -313,6 +376,16 @@ class Model
|
|
|
313
376
|
Config::COLLECTION_TARGET_SHADOW
|
|
314
377
|
end
|
|
315
378
|
|
|
379
|
+
# Determines whether page one can keep the source object's exact identity
|
|
380
|
+
# while honouring the effective output target.
|
|
381
|
+
def retain_source_template_as_page_one?(template, config)
|
|
382
|
+
target_mode = collection_target_mode_for_page(template, config, 1)
|
|
383
|
+
return target_mode == Config::COLLECTION_TARGET_SELF if template.is_a?(Jekyll::Document)
|
|
384
|
+
return target_mode == Config::COLLECTION_TARGET_PAGES if template.is_a?(Jekyll::Page)
|
|
385
|
+
|
|
386
|
+
false
|
|
387
|
+
end
|
|
388
|
+
|
|
316
389
|
# Returns true when a template is a collection document.
|
|
317
390
|
def collection_template?(template)
|
|
318
391
|
template.is_a?(Jekyll::Document)
|