jekyll-paginate-v3 0.1.0.alpha.2 → 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 +23 -6
- data/lib/jekyll-paginate-v3/config/normaliser/legacy_compatibility.rb +9 -9
- data/lib/jekyll-paginate-v3/config/normaliser/site_and_templates.rb +60 -36
- data/lib/jekyll-paginate-v3/generators/pagination_generator.rb +2 -2
- data/lib/jekyll-paginate-v3/pagination/model/core.rb +29 -11
- data/lib/jekyll-paginate-v3/pagination/model/group_navigation_and_pages.rb +80 -20
- data/lib/jekyll-paginate-v3/pagination/model/pagination_pipeline.rb +123 -49
- data/lib/jekyll-paginate-v3/pagination/pages/document.rb +26 -11
- 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/pagination/paginator/core.rb +27 -17
- data/lib/jekyll-paginate-v3/query/parser.rb +18 -8
- 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/builder/core.rb +1 -1
- data/lib/jekyll-paginate-v3/templates/builder/definition_normalisation.rb +9 -12
- data/lib/jekyll-paginate-v3/templates/builder/token_metadata.rb +2 -2
- data/lib/jekyll-paginate-v3/templates/document_template.rb +16 -7
- data/lib/jekyll-paginate-v3/templates/page_template.rb +14 -6
- 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 +204 -0
- data/lib/jekyll-paginate-v3/version.rb +1 -1
- data/lib/jekyll-paginate-v3.rb +2 -0
- data/readme.md +21 -12
- metadata +18 -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
|
|
@@ -27,6 +27,24 @@ module Config
|
|
|
27
27
|
'items' => 'items'
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
30
|
+
# Internal collection-mode identifiers. These never appear in user
|
|
31
|
+
# configuration, which lets a renamed keyword release its original word for
|
|
32
|
+
# use as a real Jekyll collection label.
|
|
33
|
+
COLLECTION_TARGET_PAGES = '__paginate_v3_collection_target_pages__'.freeze
|
|
34
|
+
COLLECTION_TARGET_SELF = '__paginate_v3_collection_target_self__'.freeze
|
|
35
|
+
COLLECTION_TARGET_SHADOW = '__paginate_v3_collection_target_shadow__'.freeze
|
|
36
|
+
COLLECTION_TARGET_CLONE = '__paginate_v3_collection_target_clone__'.freeze
|
|
37
|
+
COLLECTION_TARGET_BY_KEY = {
|
|
38
|
+
'pages' => COLLECTION_TARGET_PAGES,
|
|
39
|
+
'self' => COLLECTION_TARGET_SELF,
|
|
40
|
+
'shadow' => COLLECTION_TARGET_SHADOW,
|
|
41
|
+
'clone' => COLLECTION_TARGET_CLONE
|
|
42
|
+
}.freeze
|
|
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
|
+
|
|
30
48
|
DEFAULTS = {
|
|
31
49
|
'enabled' => true,
|
|
32
50
|
'compatibility' => nil,
|
|
@@ -41,15 +59,15 @@ module Config
|
|
|
41
59
|
['category', 'categories']
|
|
42
60
|
],
|
|
43
61
|
'items' => nil,
|
|
44
|
-
'collection' => [
|
|
62
|
+
'collection' => [COLLECTION_TARGET_SELF, COLLECTION_TARGET_SHADOW],
|
|
45
63
|
'filters' => nil,
|
|
46
64
|
'sort' => 'date desc',
|
|
47
65
|
'per_page' => 10,
|
|
48
66
|
'limit' => 0,
|
|
49
67
|
'offset' => 0,
|
|
50
68
|
'trail' => 5,
|
|
51
|
-
'title' => '
|
|
52
|
-
'permalink' => '
|
|
69
|
+
'title' => '{{ title }} - {{ num }}',
|
|
70
|
+
'permalink' => '{{ num }}',
|
|
53
71
|
'layout' => nil,
|
|
54
72
|
'layouts' => [],
|
|
55
73
|
'group' => nil,
|
|
@@ -58,7 +76,7 @@ module Config
|
|
|
58
76
|
'lowercase' => true
|
|
59
77
|
},
|
|
60
78
|
'templates' => {
|
|
61
|
-
'location' =>
|
|
79
|
+
'location' => nil,
|
|
62
80
|
'generate' => []
|
|
63
81
|
}
|
|
64
82
|
}.freeze
|
|
@@ -82,7 +100,7 @@ module Config
|
|
|
82
100
|
},
|
|
83
101
|
'items' => 'posts',
|
|
84
102
|
'title' => ':title - page :num',
|
|
85
|
-
'permalink' => '
|
|
103
|
+
'permalink' => 'page/:num/',
|
|
86
104
|
'trail' => {
|
|
87
105
|
'before' => 2,
|
|
88
106
|
'after' => 2
|
|
@@ -95,7 +113,6 @@ module Config
|
|
|
95
113
|
},
|
|
96
114
|
'items' => 'posts',
|
|
97
115
|
'templates' => {
|
|
98
|
-
'location' => 'pages',
|
|
99
116
|
'generate' => []
|
|
100
117
|
}
|
|
101
118
|
}
|
|
@@ -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
|
|
|
@@ -72,26 +72,28 @@ class Normaliser
|
|
|
72
72
|
autopages = Utils.safe_hash(raw_autopages)
|
|
73
73
|
return if autopages.empty? || autopages['enabled'] == false
|
|
74
74
|
|
|
75
|
+
# Use the effective search keyword because compatibility profiles may rename `all` and release the original word for use as a collection label.
|
|
76
|
+
all_items_keyword = config.dig('keywords', 'all')
|
|
75
77
|
migrated = []
|
|
76
78
|
|
|
77
79
|
migrated.concat(migrate_v2_autopage_group(
|
|
78
80
|
raw_group: autopages['tags'],
|
|
79
81
|
index_key: 'tag',
|
|
80
|
-
items:
|
|
82
|
+
items: all_items_keyword,
|
|
81
83
|
defaults: V2_AUTOPAGE_DEFAULTS['tags'],
|
|
82
84
|
split_delimiter: config.dig('syntax', 'split')
|
|
83
85
|
))
|
|
84
86
|
migrated.concat(migrate_v2_autopage_group(
|
|
85
87
|
raw_group: autopages['categories'],
|
|
86
88
|
index_key: 'category',
|
|
87
|
-
items:
|
|
89
|
+
items: all_items_keyword,
|
|
88
90
|
defaults: V2_AUTOPAGE_DEFAULTS['categories'],
|
|
89
91
|
split_delimiter: config.dig('syntax', 'split')
|
|
90
92
|
))
|
|
91
93
|
migrated.concat(migrate_v2_autopage_group(
|
|
92
94
|
raw_group: autopages['collections'],
|
|
93
95
|
index_key: 'collection',
|
|
94
|
-
items:
|
|
96
|
+
items: all_items_keyword,
|
|
95
97
|
defaults: V2_AUTOPAGE_DEFAULTS['collections'],
|
|
96
98
|
split_delimiter: config.dig('syntax', 'split')
|
|
97
99
|
))
|
|
@@ -116,11 +118,9 @@ class Normaliser
|
|
|
116
118
|
permalink = group['permalink']
|
|
117
119
|
permalink = defaults['permalink'] unless present_config_value?(permalink)
|
|
118
120
|
|
|
119
|
-
slugify =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
Utils.deep_copy(defaults['slugify'])
|
|
123
|
-
end
|
|
121
|
+
slugify = normalise_slugify_config(
|
|
122
|
+
group.key?('slugify') ? group['slugify'] : Utils.deep_copy(defaults['slugify'])
|
|
123
|
+
)
|
|
124
124
|
|
|
125
125
|
[
|
|
126
126
|
{
|
|
@@ -58,7 +58,8 @@ class Normaliser
|
|
|
58
58
|
config['equivalents'] = normalise_equivalents(config['equivalents'], split_delimiter)
|
|
59
59
|
config['templates'] = normalise_templates(
|
|
60
60
|
config['templates'],
|
|
61
|
-
split_delimiter: split_delimiter
|
|
61
|
+
split_delimiter: split_delimiter,
|
|
62
|
+
keywords: config['keywords']
|
|
62
63
|
)
|
|
63
64
|
|
|
64
65
|
normalised_template_defaults = normalise_template_defaults(
|
|
@@ -188,17 +189,19 @@ class Normaliser
|
|
|
188
189
|
# Connects to: the surrounding pagination flow in this file.
|
|
189
190
|
# Params: `raw_templates`, `split_delimiter`.
|
|
190
191
|
# Returns: a value consumed by the next pipeline step.
|
|
191
|
-
def normalise_templates(raw_templates, split_delimiter:)
|
|
192
|
+
def normalise_templates(raw_templates, split_delimiter:, keywords:)
|
|
192
193
|
defaults = Utils.deep_copy(DEFAULTS['templates'])
|
|
193
194
|
source_hash = Utils.safe_hash(raw_templates)
|
|
194
195
|
source = defaults.merge(source_hash)
|
|
195
196
|
source.delete('defaults')
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
# Default to the currently configured site-pages keyword so changing it
|
|
199
|
+
# does not make the default location collide with a collection label.
|
|
200
|
+
source['location'] = keywords['pages'] if source['location'].nil? || source['location'].to_s.strip.empty?
|
|
198
201
|
source['generate'] = if source['generate'].is_a?(Array)
|
|
199
|
-
source['generate'].map { |entry|
|
|
202
|
+
source['generate'].map { |entry| normalise_generated_template_definition(entry) }
|
|
200
203
|
elsif source['generate'].is_a?(Hash)
|
|
201
|
-
[
|
|
204
|
+
[normalise_generated_template_definition(source['generate'])]
|
|
202
205
|
else
|
|
203
206
|
[]
|
|
204
207
|
end
|
|
@@ -209,6 +212,17 @@ class Normaliser
|
|
|
209
212
|
source
|
|
210
213
|
end
|
|
211
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
|
+
|
|
212
226
|
# Returns site-level defaults that are inherited by explicit and
|
|
213
227
|
# generated templates.
|
|
214
228
|
def extract_site_template_defaults(site_config)
|
|
@@ -344,7 +358,7 @@ class Normaliser
|
|
|
344
358
|
def normalise_collection_targets(raw_collection, split_delimiter:, keywords:)
|
|
345
359
|
default_targets = Utils.deep_copy(DEFAULTS['collection'])
|
|
346
360
|
entries = Utils.delimited_array(raw_collection, delimiter: split_delimiter)
|
|
347
|
-
|
|
361
|
+
return default_targets if entries.empty?
|
|
348
362
|
|
|
349
363
|
normalised = entries.map do |entry|
|
|
350
364
|
normalise_collection_target_entry(entry, keywords)
|
|
@@ -363,21 +377,12 @@ class Normaliser
|
|
|
363
377
|
entry = raw_entry.to_s.strip
|
|
364
378
|
return '' if entry.empty?
|
|
365
379
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
'self' => keywords['self'],
|
|
369
|
-
'shadow' => keywords['shadow'],
|
|
370
|
-
'clone' => keywords['clone']
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
keyword_map.each do |canonical, keyword|
|
|
380
|
+
COLLECTION_TARGET_BY_KEY.each do |key, internal_target|
|
|
381
|
+
keyword = keywords[key]
|
|
374
382
|
next if keyword.to_s.empty?
|
|
375
|
-
return
|
|
383
|
+
return internal_target if entry == keyword
|
|
376
384
|
end
|
|
377
385
|
|
|
378
|
-
canonical = entry.downcase
|
|
379
|
-
return canonical if %w[pages self shadow clone].include?(canonical)
|
|
380
|
-
|
|
381
386
|
entry
|
|
382
387
|
end
|
|
383
388
|
|
|
@@ -388,7 +393,7 @@ class Normaliser
|
|
|
388
393
|
def build_page_templates(page2_title, page2_permalink)
|
|
389
394
|
{
|
|
390
395
|
'page1' => {
|
|
391
|
-
'title' => '
|
|
396
|
+
'title' => '{{ title }}',
|
|
392
397
|
'permalink' => ''
|
|
393
398
|
},
|
|
394
399
|
'page2' => {
|
|
@@ -460,7 +465,7 @@ class Normaliser
|
|
|
460
465
|
# Preserves legacy `sort_field` + `sort_reverse` behaviour when the
|
|
461
466
|
# caller did not provide an explicit `sort` override.
|
|
462
467
|
def normalise_sort(raw_sort, raw_sort_field, raw_sort_reverse, split_delimiter, sort_explicitly_set: false)
|
|
463
|
-
sort_entries =
|
|
468
|
+
sort_entries = placeholder_aware_sort_entries(raw_sort, split_delimiter)
|
|
464
469
|
sort_field = raw_sort_field.to_s.strip
|
|
465
470
|
|
|
466
471
|
if !sort_explicitly_set && !sort_field.empty?
|
|
@@ -472,13 +477,25 @@ class Normaliser
|
|
|
472
477
|
|
|
473
478
|
if sort_field.empty?
|
|
474
479
|
fallback_sort = DEFAULTS['sort']
|
|
475
|
-
return
|
|
480
|
+
return placeholder_aware_sort_entries(fallback_sort, split_delimiter)
|
|
476
481
|
end
|
|
477
482
|
|
|
478
483
|
direction = boolean_config_value(raw_sort_reverse) ? 'desc' : 'asc'
|
|
479
484
|
["#{sort_field} #{direction}"]
|
|
480
485
|
end
|
|
481
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
|
+
|
|
482
499
|
# Normalises `pagination.layout` / `pagination.layouts` into a
|
|
483
500
|
# canonical string array.
|
|
484
501
|
def normalise_layout_overrides(config, split_delimiter:)
|
|
@@ -505,9 +522,17 @@ class Normaliser
|
|
|
505
522
|
[raw_group]
|
|
506
523
|
end
|
|
507
524
|
|
|
508
|
-
raw_entries.map do |raw_entry|
|
|
525
|
+
entries = raw_entries.map do |raw_entry|
|
|
509
526
|
normalise_group_entry(raw_entry)
|
|
510
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
|
|
511
536
|
end
|
|
512
537
|
|
|
513
538
|
# Normalises legacy `index` + `group` + `filter` config into modern
|
|
@@ -577,26 +602,25 @@ class Normaliser
|
|
|
577
602
|
{ 'on' => on_key }
|
|
578
603
|
end
|
|
579
604
|
|
|
580
|
-
# Normalises slugify config
|
|
581
|
-
#
|
|
582
|
-
# 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.
|
|
583
607
|
def normalise_slugify_config(raw_slugify)
|
|
584
|
-
|
|
585
|
-
mode = raw_slugify.to_s.strip
|
|
586
|
-
mode = 'default' if mode.empty?
|
|
587
|
-
return {
|
|
588
|
-
'mode' => mode,
|
|
589
|
-
'lowercase' => true
|
|
590
|
-
}
|
|
591
|
-
end
|
|
592
|
-
|
|
593
|
-
slugify = Utils.safe_hash(raw_slugify)
|
|
608
|
+
slugify = raw_slugify.is_a?(String) ? { 'mode' => raw_slugify } : Utils.safe_hash(raw_slugify)
|
|
594
609
|
mode = slugify['mode'].to_s.strip
|
|
595
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
|
|
596
620
|
|
|
597
621
|
{
|
|
598
622
|
'mode' => mode,
|
|
599
|
-
'lowercase' =>
|
|
623
|
+
'lowercase' => lowercase
|
|
600
624
|
}
|
|
601
625
|
end
|
|
602
626
|
|
|
@@ -149,7 +149,7 @@ class PaginationGenerator < Jekyll::Generator
|
|
|
149
149
|
# Converts a generated-template destination location into readable text.
|
|
150
150
|
def generated_template_location_label(collection_targets)
|
|
151
151
|
targets = Utils.arrayify(collection_targets).map(&:to_s).map(&:strip).reject(&:empty?)
|
|
152
|
-
targets = [
|
|
152
|
+
targets = [Config::COLLECTION_TARGET_PAGES] if targets.empty?
|
|
153
153
|
return target_label(targets.first) if targets.length == 1
|
|
154
154
|
|
|
155
155
|
"page1=#{target_label(targets.first)} page2+=#{target_label(targets[1])}"
|
|
@@ -157,7 +157,7 @@ class PaginationGenerator < Jekyll::Generator
|
|
|
157
157
|
|
|
158
158
|
# Converts one collection target token to a readable log label.
|
|
159
159
|
def target_label(target)
|
|
160
|
-
return 'pages (site root)' if target ==
|
|
160
|
+
return 'pages (site root)' if target == Config::COLLECTION_TARGET_PAGES
|
|
161
161
|
|
|
162
162
|
"collection '#{target}'"
|
|
163
163
|
end
|
|
@@ -28,7 +28,7 @@ class Model
|
|
|
28
28
|
@nested_separator = site_config.dig('syntax', 'separator')
|
|
29
29
|
@split_delimiter = site_config.dig('syntax', 'split')
|
|
30
30
|
@equivalents = site_config['equivalents']
|
|
31
|
-
@item_keyword = site_config.dig('keywords', 'items') || 'items'
|
|
31
|
+
@item_keyword = site_config.dig('keywords', 'items') || Config::KEYWORD_DEFAULTS.fetch('items')
|
|
32
32
|
@generated_index_sets = {}
|
|
33
33
|
@clone_collection_cache = {}
|
|
34
34
|
@template_search_reports = []
|
|
@@ -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.
|
|
@@ -490,13 +508,13 @@ class Model
|
|
|
490
508
|
[Utils.item_collection_label(item).to_s, Utils.relative_item_path(item)]
|
|
491
509
|
end
|
|
492
510
|
|
|
493
|
-
# Resolves one parsed search entry (
|
|
511
|
+
# Resolves one parsed search entry (a private source type or collection label).
|
|
494
512
|
def resolve_entry(entry)
|
|
495
513
|
type = entry['type']
|
|
496
514
|
paths = entry['paths']
|
|
497
515
|
source_items = source_items_for_entry_type(type)
|
|
498
516
|
if source_items.nil?
|
|
499
|
-
log("Search entry type='#{type}' did not match
|
|
517
|
+
log("Search entry type='#{type}' did not match a configured search keyword or known collection; resolved 0 items.", 'warn')
|
|
500
518
|
return []
|
|
501
519
|
end
|
|
502
520
|
|
|
@@ -512,11 +530,11 @@ class Model
|
|
|
512
530
|
# Resolves source items for one parsed search entry type.
|
|
513
531
|
def source_items_for_entry_type(type)
|
|
514
532
|
case type
|
|
515
|
-
when
|
|
533
|
+
when Query::Parser::SEARCH_TYPE_PAGES
|
|
516
534
|
@item_resolution_pages || @site.pages
|
|
517
|
-
when
|
|
535
|
+
when Query::Parser::SEARCH_TYPE_ALL
|
|
518
536
|
all_collection_documents
|
|
519
|
-
when
|
|
537
|
+
when Query::Parser::SEARCH_TYPE_EVERYTHING
|
|
520
538
|
(@item_resolution_pages || @site.pages) + all_collection_documents
|
|
521
539
|
else
|
|
522
540
|
return @item_resolution_documents_by_collection[type] if @item_resolution_documents_by_collection&.key?(type)
|
|
@@ -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.
|