jekyll-paginate-v3 0.1.0.alpha.2 → 0.1.0.alpha.3
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 +16 -3
- data/lib/jekyll-paginate-v3/config/normaliser/legacy_compatibility.rb +5 -3
- data/lib/jekyll-paginate-v3/config/normaliser/site_and_templates.rb +10 -16
- data/lib/jekyll-paginate-v3/generators/pagination_generator.rb +2 -2
- data/lib/jekyll-paginate-v3/pagination/model/core.rb +6 -6
- data/lib/jekyll-paginate-v3/pagination/model/pagination_pipeline.rb +15 -14
- data/lib/jekyll-paginate-v3/pagination/pages/document.rb +23 -9
- data/lib/jekyll-paginate-v3/pagination/paginator/core.rb +27 -17
- data/lib/jekyll-paginate-v3/query/parser.rb +15 -6
- 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/utils/paths.rb +95 -0
- data/lib/jekyll-paginate-v3/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4167a9c3e76eb08e45275f31be52227e30a6b6d453f1bceef0c901097d4250f7
|
|
4
|
+
data.tar.gz: e8e96586a43ff094865264446fc5389555438c74e87dbeb38f5c0c818b2ed47e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 457fc67c2920f51aac38003b395720365df59f5786a9fb71f1d34c6d68d5a279abb32f10608b3d4c2c66b93934f8108e26175165795bb4a253c43898eb89d12d
|
|
7
|
+
data.tar.gz: a44bb5a0591d319502f0f574d5fa0493a4636477e7a8301ba9a9c5fc712b74f1564b169617b15c950512f52f29d2a23d3b6207c86b8d09cddbd182f81e4fdca3
|
|
@@ -27,6 +27,20 @@ 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
|
+
|
|
30
44
|
DEFAULTS = {
|
|
31
45
|
'enabled' => true,
|
|
32
46
|
'compatibility' => nil,
|
|
@@ -41,7 +55,7 @@ module Config
|
|
|
41
55
|
['category', 'categories']
|
|
42
56
|
],
|
|
43
57
|
'items' => nil,
|
|
44
|
-
'collection' => [
|
|
58
|
+
'collection' => [COLLECTION_TARGET_SELF, COLLECTION_TARGET_SHADOW],
|
|
45
59
|
'filters' => nil,
|
|
46
60
|
'sort' => 'date desc',
|
|
47
61
|
'per_page' => 10,
|
|
@@ -58,7 +72,7 @@ module Config
|
|
|
58
72
|
'lowercase' => true
|
|
59
73
|
},
|
|
60
74
|
'templates' => {
|
|
61
|
-
'location' =>
|
|
75
|
+
'location' => nil,
|
|
62
76
|
'generate' => []
|
|
63
77
|
}
|
|
64
78
|
}.freeze
|
|
@@ -95,7 +109,6 @@ module Config
|
|
|
95
109
|
},
|
|
96
110
|
'items' => 'posts',
|
|
97
111
|
'templates' => {
|
|
98
|
-
'location' => 'pages',
|
|
99
112
|
'generate' => []
|
|
100
113
|
}
|
|
101
114
|
}
|
|
@@ -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
|
))
|
|
@@ -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,13 +189,15 @@ 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
202
|
source['generate'].map { |entry| Utils.safe_hash(entry) }
|
|
200
203
|
elsif source['generate'].is_a?(Hash)
|
|
@@ -344,7 +347,7 @@ class Normaliser
|
|
|
344
347
|
def normalise_collection_targets(raw_collection, split_delimiter:, keywords:)
|
|
345
348
|
default_targets = Utils.deep_copy(DEFAULTS['collection'])
|
|
346
349
|
entries = Utils.delimited_array(raw_collection, delimiter: split_delimiter)
|
|
347
|
-
|
|
350
|
+
return default_targets if entries.empty?
|
|
348
351
|
|
|
349
352
|
normalised = entries.map do |entry|
|
|
350
353
|
normalise_collection_target_entry(entry, keywords)
|
|
@@ -363,21 +366,12 @@ class Normaliser
|
|
|
363
366
|
entry = raw_entry.to_s.strip
|
|
364
367
|
return '' if entry.empty?
|
|
365
368
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
'self' => keywords['self'],
|
|
369
|
-
'shadow' => keywords['shadow'],
|
|
370
|
-
'clone' => keywords['clone']
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
keyword_map.each do |canonical, keyword|
|
|
369
|
+
COLLECTION_TARGET_BY_KEY.each do |key, internal_target|
|
|
370
|
+
keyword = keywords[key]
|
|
374
371
|
next if keyword.to_s.empty?
|
|
375
|
-
return
|
|
372
|
+
return internal_target if entry == keyword
|
|
376
373
|
end
|
|
377
374
|
|
|
378
|
-
canonical = entry.downcase
|
|
379
|
-
return canonical if %w[pages self shadow clone].include?(canonical)
|
|
380
|
-
|
|
381
375
|
entry
|
|
382
376
|
end
|
|
383
377
|
|
|
@@ -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 = []
|
|
@@ -490,13 +490,13 @@ class Model
|
|
|
490
490
|
[Utils.item_collection_label(item).to_s, Utils.relative_item_path(item)]
|
|
491
491
|
end
|
|
492
492
|
|
|
493
|
-
# Resolves one parsed search entry (
|
|
493
|
+
# Resolves one parsed search entry (a private source type or collection label).
|
|
494
494
|
def resolve_entry(entry)
|
|
495
495
|
type = entry['type']
|
|
496
496
|
paths = entry['paths']
|
|
497
497
|
source_items = source_items_for_entry_type(type)
|
|
498
498
|
if source_items.nil?
|
|
499
|
-
log("Search entry type='#{type}' did not match
|
|
499
|
+
log("Search entry type='#{type}' did not match a configured search keyword or known collection; resolved 0 items.", 'warn')
|
|
500
500
|
return []
|
|
501
501
|
end
|
|
502
502
|
|
|
@@ -512,11 +512,11 @@ class Model
|
|
|
512
512
|
# Resolves source items for one parsed search entry type.
|
|
513
513
|
def source_items_for_entry_type(type)
|
|
514
514
|
case type
|
|
515
|
-
when
|
|
515
|
+
when Query::Parser::SEARCH_TYPE_PAGES
|
|
516
516
|
@item_resolution_pages || @site.pages
|
|
517
|
-
when
|
|
517
|
+
when Query::Parser::SEARCH_TYPE_ALL
|
|
518
518
|
all_collection_documents
|
|
519
|
-
when
|
|
519
|
+
when Query::Parser::SEARCH_TYPE_EVERYTHING
|
|
520
520
|
(@item_resolution_pages || @site.pages) + all_collection_documents
|
|
521
521
|
else
|
|
522
522
|
return @item_resolution_documents_by_collection[type] if @item_resolution_documents_by_collection&.key?(type)
|
|
@@ -253,9 +253,9 @@ class Model
|
|
|
253
253
|
target_collection = collection_target_for_page(template, target_mode)
|
|
254
254
|
|
|
255
255
|
case target_mode
|
|
256
|
-
when
|
|
256
|
+
when Config::COLLECTION_TARGET_PAGES
|
|
257
257
|
Pages::Page.new(template, current_page, total_pages, index_file)
|
|
258
|
-
when
|
|
258
|
+
when Config::COLLECTION_TARGET_SHADOW
|
|
259
259
|
Pages::ShadowPage.new(
|
|
260
260
|
template,
|
|
261
261
|
current_page,
|
|
@@ -281,7 +281,7 @@ class Model
|
|
|
281
281
|
# Resolves one normalised collection mode for a generated page number.
|
|
282
282
|
def collection_target_mode_for_page(template, config, current_page)
|
|
283
283
|
targets = Utils.arrayify(config['collection']).map(&:to_s).map(&:strip).reject(&:empty?)
|
|
284
|
-
targets = [
|
|
284
|
+
targets = [Config::COLLECTION_TARGET_SELF, Config::COLLECTION_TARGET_SHADOW] if targets.empty?
|
|
285
285
|
target = current_page == 1 ? targets.first : targets.last
|
|
286
286
|
|
|
287
287
|
normalise_collection_target_for_template(template, target)
|
|
@@ -290,11 +290,11 @@ class Model
|
|
|
290
290
|
# Resolves one collection object for collection-targeted modes.
|
|
291
291
|
def collection_target_for_page(template, target_mode)
|
|
292
292
|
case target_mode
|
|
293
|
-
when
|
|
293
|
+
when Config::COLLECTION_TARGET_SELF
|
|
294
294
|
return template.collection if collection_template?(template)
|
|
295
|
-
when
|
|
295
|
+
when Config::COLLECTION_TARGET_CLONE
|
|
296
296
|
return clone_collection_for(template.collection) if collection_template?(template)
|
|
297
|
-
when
|
|
297
|
+
when Config::COLLECTION_TARGET_PAGES, Config::COLLECTION_TARGET_SHADOW
|
|
298
298
|
return nil
|
|
299
299
|
end
|
|
300
300
|
|
|
@@ -304,13 +304,13 @@ class Model
|
|
|
304
304
|
# Converts abstract target modes to executable runtime modes.
|
|
305
305
|
def normalise_collection_target_for_template(template, target_mode)
|
|
306
306
|
value = target_mode.to_s.strip
|
|
307
|
-
value =
|
|
308
|
-
return value unless
|
|
309
|
-
return
|
|
310
|
-
return value if value ==
|
|
311
|
-
return value if value ==
|
|
307
|
+
value = Config::COLLECTION_TARGET_PAGES if value.empty?
|
|
308
|
+
return value unless [Config::COLLECTION_TARGET_SELF, Config::COLLECTION_TARGET_SHADOW, Config::COLLECTION_TARGET_CLONE].include?(value)
|
|
309
|
+
return Config::COLLECTION_TARGET_PAGES unless collection_template?(template)
|
|
310
|
+
return value if value == Config::COLLECTION_TARGET_SELF
|
|
311
|
+
return value if value == Config::COLLECTION_TARGET_CLONE
|
|
312
312
|
|
|
313
|
-
|
|
313
|
+
Config::COLLECTION_TARGET_SHADOW
|
|
314
314
|
end
|
|
315
315
|
|
|
316
316
|
# Returns true when a template is a collection document.
|
|
@@ -365,7 +365,8 @@ class Model
|
|
|
365
365
|
@site.config['defaults'] << clone_entry
|
|
366
366
|
end
|
|
367
367
|
|
|
368
|
-
@site.frontmatter_defaults
|
|
368
|
+
frontmatter_defaults = @site.frontmatter_defaults if @site.respond_to?(:frontmatter_defaults)
|
|
369
|
+
frontmatter_defaults.reset if frontmatter_defaults.respond_to?(:reset)
|
|
369
370
|
end
|
|
370
371
|
|
|
371
372
|
# Attaches a compact neighbourhood of page links around each generated
|
|
@@ -437,7 +438,7 @@ class Model
|
|
|
437
438
|
@generated_index_sets[set_id] ||= []
|
|
438
439
|
@generated_index_sets[set_id] << {
|
|
439
440
|
'pages' => generated_pages,
|
|
440
|
-
'count' => generated_pages.first.pager.
|
|
441
|
+
'count' => generated_pages.first.pager.total_item_count,
|
|
441
442
|
'start' => metadata['start'],
|
|
442
443
|
'end' => metadata['end'],
|
|
443
444
|
'order' => metadata['order'].to_i,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'digest'
|
|
4
|
-
|
|
5
3
|
module Jekyll
|
|
6
4
|
module Plugins
|
|
7
5
|
module PaginateV3
|
|
@@ -22,7 +20,8 @@ class Document < Jekyll::Document
|
|
|
22
20
|
def initialize(template_item, current_page, total_pages, _index_filename, collection:)
|
|
23
21
|
target_collection = collection
|
|
24
22
|
source_path = template_path(template_item)
|
|
25
|
-
|
|
23
|
+
extension = template_extname(template_item)
|
|
24
|
+
virtual_path = build_virtual_path(template_item, target_collection, source_path, current_page, extension)
|
|
26
25
|
|
|
27
26
|
initialise_document(template_item.site, target_collection, virtual_path)
|
|
28
27
|
|
|
@@ -32,7 +31,7 @@ class Document < Jekyll::Document
|
|
|
32
31
|
'curr_page' => current_page,
|
|
33
32
|
'total_pages' => total_pages
|
|
34
33
|
}
|
|
35
|
-
@extname =
|
|
34
|
+
@extname = extension
|
|
36
35
|
self.data['path'] = source_path if current_page == 1 && !source_path.empty?
|
|
37
36
|
|
|
38
37
|
trigger_hooks(:post_init)
|
|
@@ -41,10 +40,22 @@ class Document < Jekyll::Document
|
|
|
41
40
|
private
|
|
42
41
|
|
|
43
42
|
# Builds a deterministic synthetic source path inside destination collection.
|
|
44
|
-
def build_virtual_path(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
def build_virtual_path(template_item, collection, source_path, current_page, extension)
|
|
44
|
+
Utils.build_synthetic_source_path(
|
|
45
|
+
site: template_item.site,
|
|
46
|
+
collection: collection,
|
|
47
|
+
extension: extension,
|
|
48
|
+
source_path: source_path,
|
|
49
|
+
role: 'page',
|
|
50
|
+
page_number: current_page,
|
|
51
|
+
signature: {
|
|
52
|
+
'source_path' => source_path,
|
|
53
|
+
'collection' => collection.label.to_s,
|
|
54
|
+
'page_number' => current_page,
|
|
55
|
+
'template_data' => Utils.safe_hash(template_item.data),
|
|
56
|
+
'template_content' => template_item.content.to_s
|
|
57
|
+
}
|
|
58
|
+
)
|
|
48
59
|
end
|
|
49
60
|
|
|
50
61
|
# Resolves source path for compatibility metadata.
|
|
@@ -77,7 +88,10 @@ class Document < Jekyll::Document
|
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
data.default_proc = proc do |_, key|
|
|
80
|
-
|
|
91
|
+
# Jekyll 3 does not expose `type` as a public Document method, while
|
|
92
|
+
# Jekyll 4 does. The internal type is set immediately above and works
|
|
93
|
+
# consistently with both supported Jekyll versions.
|
|
94
|
+
site.frontmatter_defaults.find(relative_path, @type, key)
|
|
81
95
|
end
|
|
82
96
|
end
|
|
83
97
|
end
|
|
@@ -13,15 +13,15 @@ module Pagination
|
|
|
13
13
|
# Used by Pagination::Model for every generated page/document.
|
|
14
14
|
class Paginator < ::Liquid::Drop
|
|
15
15
|
|
|
16
|
-
attr_reader :per_page, :
|
|
16
|
+
attr_reader :per_page, :total_item_count, :total_indexes, :current, :next, :previous, :first, :last, :trail, :groups
|
|
17
17
|
|
|
18
18
|
def initialize(per_page:, items:, current_page:, total_pages:, item_keyword:, compatibility: nil, page_windows: nil)
|
|
19
19
|
@per_page_pattern = Utils.normalise_per_page_pattern(per_page)
|
|
20
20
|
@current_index_number = current_page.to_i
|
|
21
21
|
@item_keyword = normalise_item_keyword(item_keyword)
|
|
22
22
|
@compatibility_mode = normalise_compatibility_mode(compatibility)
|
|
23
|
-
@
|
|
24
|
-
@page_windows = normalise_page_windows(page_windows, @
|
|
23
|
+
@total_item_count = items.size
|
|
24
|
+
@page_windows = normalise_page_windows(page_windows, @total_item_count)
|
|
25
25
|
requested_total_indexes = [total_pages.to_i, 1].max
|
|
26
26
|
|
|
27
27
|
if @current_index_number > requested_total_indexes
|
|
@@ -35,7 +35,7 @@ class Paginator < ::Liquid::Drop
|
|
|
35
35
|
|
|
36
36
|
current_window = window_for_page_number(@current_index_number)
|
|
37
37
|
@per_page = current_window['page_size']
|
|
38
|
-
@
|
|
38
|
+
@page_items = items[current_window['offset_start']...current_window['offset_end']] || []
|
|
39
39
|
@trail = nil
|
|
40
40
|
@groups = []
|
|
41
41
|
|
|
@@ -184,11 +184,12 @@ class Paginator < ::Liquid::Drop
|
|
|
184
184
|
#
|
|
185
185
|
# Liquid rendering uses this Drop instance directly.
|
|
186
186
|
def to_h
|
|
187
|
-
payload =
|
|
188
|
-
payload[@item_keyword] =
|
|
189
|
-
payload["total_#{@item_keyword}"] =
|
|
187
|
+
payload = standard_payload_hash
|
|
188
|
+
payload[@item_keyword] = @page_items
|
|
189
|
+
payload["total_#{@item_keyword}"] = @total_item_count
|
|
190
190
|
|
|
191
191
|
if compatibility_mode?
|
|
192
|
+
add_compatibility_item_keys!(payload)
|
|
192
193
|
payload.merge!(
|
|
193
194
|
'per_page' => per_page,
|
|
194
195
|
'page' => page,
|
|
@@ -212,19 +213,28 @@ class Paginator < ::Liquid::Drop
|
|
|
212
213
|
# Handles dynamic alias keys that are not explicit methods.
|
|
213
214
|
def liquid_method_missing(method_name)
|
|
214
215
|
method_key = method_name.to_s
|
|
215
|
-
return
|
|
216
|
-
return
|
|
216
|
+
return @page_items if method_key == @item_keyword
|
|
217
|
+
return @total_item_count if method_key == "total_#{@item_keyword}"
|
|
217
218
|
|
|
218
219
|
super
|
|
219
220
|
end
|
|
220
221
|
|
|
221
222
|
private
|
|
222
223
|
|
|
223
|
-
#
|
|
224
|
-
|
|
224
|
+
# Restores canonical item keys only for explicitly selected v1/v2
|
|
225
|
+
# compatibility profiles, whose historical paginator APIs require them.
|
|
226
|
+
def add_compatibility_item_keys!(payload)
|
|
227
|
+
canonical_keyword = Config::KEYWORD_DEFAULTS.fetch('items')
|
|
228
|
+
return if @item_keyword == canonical_keyword
|
|
229
|
+
|
|
230
|
+
payload[canonical_keyword] = @page_items
|
|
231
|
+
payload["total_#{canonical_keyword}"] = @total_item_count
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Builds the paginator payload shared by every configured item keyword.
|
|
235
|
+
# Item keys themselves are added by `to_h` using the active keyword only.
|
|
236
|
+
def standard_payload_hash
|
|
225
237
|
{
|
|
226
|
-
'items' => items,
|
|
227
|
-
'total_items' => total_items,
|
|
228
238
|
'total_indexes' => total_indexes,
|
|
229
239
|
'current' => current,
|
|
230
240
|
'next' => self.next,
|
|
@@ -275,8 +285,8 @@ class Paginator < ::Liquid::Drop
|
|
|
275
285
|
'num' => page_number.to_i,
|
|
276
286
|
'page_size' => fallback_size,
|
|
277
287
|
'count' => 0,
|
|
278
|
-
'offset_start' =>
|
|
279
|
-
'offset_end' =>
|
|
288
|
+
'offset_start' => total_item_count,
|
|
289
|
+
'offset_end' => total_item_count,
|
|
280
290
|
'start' => nil,
|
|
281
291
|
'end' => nil
|
|
282
292
|
}
|
|
@@ -310,10 +320,10 @@ class Paginator < ::Liquid::Drop
|
|
|
310
320
|
@page_windows_by_number.values
|
|
311
321
|
end
|
|
312
322
|
|
|
313
|
-
# Normalises item
|
|
323
|
+
# Normalises the configured item keyword for direct Paginator callers.
|
|
314
324
|
def normalise_item_keyword(raw_keyword)
|
|
315
325
|
keyword = raw_keyword.to_s.strip
|
|
316
|
-
keyword.empty? ? 'items' : keyword
|
|
326
|
+
keyword.empty? ? Config::KEYWORD_DEFAULTS.fetch('items') : keyword
|
|
317
327
|
end
|
|
318
328
|
|
|
319
329
|
# Normalises compatibility mode to one supported legacy profile.
|
|
@@ -17,7 +17,16 @@ module Query
|
|
|
17
17
|
# sources into concrete site items.
|
|
18
18
|
class Parser
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# Internal search-source identifiers. Keeping them distinct from collection
|
|
21
|
+
# labels means changing a keyword genuinely releases its former label.
|
|
22
|
+
SEARCH_TYPE_PAGES = '__paginate_v3_search_pages__'.freeze
|
|
23
|
+
SEARCH_TYPE_ALL = '__paginate_v3_search_all__'.freeze
|
|
24
|
+
SEARCH_TYPE_EVERYTHING = '__paginate_v3_search_everything__'.freeze
|
|
25
|
+
SEARCH_TYPE_BY_KEY = {
|
|
26
|
+
'pages' => SEARCH_TYPE_PAGES,
|
|
27
|
+
'all' => SEARCH_TYPE_ALL,
|
|
28
|
+
'everything' => SEARCH_TYPE_EVERYTHING
|
|
29
|
+
}.freeze
|
|
21
30
|
|
|
22
31
|
# Parses a search definition into an array of normalised entries.
|
|
23
32
|
#
|
|
@@ -112,13 +121,13 @@ class Parser
|
|
|
112
121
|
end
|
|
113
122
|
end
|
|
114
123
|
|
|
115
|
-
# Maps
|
|
116
|
-
#
|
|
124
|
+
# Maps only configured keyword aliases to internal search-source types.
|
|
125
|
+
# The former keyword remains an ordinary collection label after a rename.
|
|
117
126
|
def canonical_type(type, keywords)
|
|
118
127
|
string_type = type.to_s.strip
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
SEARCH_TYPE_BY_KEY.each do |key, internal_type|
|
|
129
|
+
return internal_type if string_type == keywords[key]
|
|
130
|
+
end
|
|
122
131
|
|
|
123
132
|
string_type
|
|
124
133
|
end
|
|
@@ -100,7 +100,7 @@ class Builder
|
|
|
100
100
|
pagination_config['enabled'] = true
|
|
101
101
|
|
|
102
102
|
first_target = definition['template_collection'].first
|
|
103
|
-
if first_target ==
|
|
103
|
+
if first_target == Config::COLLECTION_TARGET_PAGES
|
|
104
104
|
Templates::PageTemplate.new(
|
|
105
105
|
site: @site,
|
|
106
106
|
pagination_config: pagination_config,
|
|
@@ -45,32 +45,29 @@ class Builder
|
|
|
45
45
|
entries
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# Normalises one collection target
|
|
48
|
+
# Normalises one generated-template collection target. Generated templates
|
|
49
|
+
# have no source collection, so all collection-mode keywords become pages.
|
|
49
50
|
def normalise_collection_entry(raw_entry)
|
|
50
51
|
entry = raw_entry.to_s.strip
|
|
51
52
|
return '' if entry.empty?
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
self_keyword = @site_config.dig('keywords', 'self').to_s
|
|
55
|
-
shadow_keyword = @site_config.dig('keywords', 'shadow').to_s
|
|
56
|
-
clone_keyword = @site_config.dig('keywords', 'clone').to_s
|
|
54
|
+
return Config::COLLECTION_TARGET_PAGES if Config::COLLECTION_TARGET_BY_KEY.value?(entry)
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return 'pages' if entry == clone_keyword || entry.casecmp('clone').zero?
|
|
56
|
+
Config::COLLECTION_TARGET_BY_KEY.each do |key, _internal_target|
|
|
57
|
+
return Config::COLLECTION_TARGET_PAGES if entry == @site_config.dig('keywords', key).to_s
|
|
58
|
+
end
|
|
62
59
|
|
|
63
60
|
entry
|
|
64
61
|
end
|
|
65
62
|
|
|
66
63
|
# Resolves default generated-template collection targets from site defaults.
|
|
67
64
|
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
65
|
+
# Template-relative modes become the internal pages target during template
|
|
66
|
+
# generation because a generated template has no source collection.
|
|
70
67
|
def default_generation_collection
|
|
71
68
|
raw_collection = @site_config['collection']
|
|
72
69
|
entries = Utils.arrayify(raw_collection).map { |entry| entry.to_s.strip }.reject(&:empty?)
|
|
73
|
-
entries = [
|
|
70
|
+
entries = [Config::COLLECTION_TARGET_PAGES] if entries.empty?
|
|
74
71
|
if entries.length > 2
|
|
75
72
|
raise ArgumentError, 'pagination.collection may contain at most two values.'
|
|
76
73
|
end
|
|
@@ -194,9 +194,9 @@ class Builder
|
|
|
194
194
|
labels = []
|
|
195
195
|
Query::Parser.parse(raw_items, @site_config['keywords'], split_delimiter: @split_delimiter).each do |entry|
|
|
196
196
|
case entry['type']
|
|
197
|
-
when
|
|
197
|
+
when Query::Parser::SEARCH_TYPE_ALL, Query::Parser::SEARCH_TYPE_EVERYTHING
|
|
198
198
|
labels.concat(@site.collections.keys)
|
|
199
|
-
when
|
|
199
|
+
when Query::Parser::SEARCH_TYPE_PAGES
|
|
200
200
|
# pages do not map to collections
|
|
201
201
|
else
|
|
202
202
|
labels << entry['type'] if @site.collections.key?(entry['type'])
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'digest'
|
|
4
|
-
|
|
5
3
|
module Jekyll
|
|
6
4
|
module Plugins
|
|
7
5
|
module PaginateV3
|
|
@@ -18,7 +16,7 @@ class DocumentTemplate < Jekyll::Document
|
|
|
18
16
|
# Creates an in-memory collection document seeded from frontmatter and
|
|
19
17
|
# content supplied by generate config.
|
|
20
18
|
def initialize(site:, collection:, pagination_config:, frontmatter:, content:, generated_metadata:)
|
|
21
|
-
virtual_path = build_virtual_path(site, collection, pagination_config, frontmatter)
|
|
19
|
+
virtual_path = build_virtual_path(site, collection, pagination_config, frontmatter, content)
|
|
22
20
|
generated_metadata_hash = Utils.safe_hash(generated_metadata)
|
|
23
21
|
|
|
24
22
|
initialise_document(site, collection, virtual_path)
|
|
@@ -38,10 +36,21 @@ class DocumentTemplate < Jekyll::Document
|
|
|
38
36
|
|
|
39
37
|
# Builds a deterministic synthetic source path inside destination
|
|
40
38
|
# collection.
|
|
41
|
-
def build_virtual_path(site, collection, pagination_config, frontmatter)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
def build_virtual_path(site, collection, pagination_config, frontmatter, content)
|
|
40
|
+
Utils.build_synthetic_source_path(
|
|
41
|
+
site: site,
|
|
42
|
+
collection: collection,
|
|
43
|
+
extension: '.md',
|
|
44
|
+
source_stem: Utils.derive_synthetic_source_stem_from_frontmatter(frontmatter, fallback: collection.label),
|
|
45
|
+
role: 'template',
|
|
46
|
+
signature: {
|
|
47
|
+
'collection' => collection.label.to_s,
|
|
48
|
+
'pagination' => Utils.safe_hash(pagination_config),
|
|
49
|
+
'frontmatter' => Utils.safe_hash(frontmatter),
|
|
50
|
+
'content' => content.to_s,
|
|
51
|
+
'content_type' => 'document-template'
|
|
52
|
+
}
|
|
53
|
+
)
|
|
45
54
|
end
|
|
46
55
|
|
|
47
56
|
# Adds legacy-friendly fields only when v2 compatibility is active.
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'digest'
|
|
4
|
-
|
|
5
3
|
module Jekyll
|
|
6
4
|
module Plugins
|
|
7
5
|
module PaginateV3
|
|
@@ -20,7 +18,7 @@ class PageTemplate < Jekyll::Page
|
|
|
20
18
|
@dir = '/'
|
|
21
19
|
@name = 'index.html'
|
|
22
20
|
@url = '/'
|
|
23
|
-
@path = build_virtual_path(site, pagination_config, frontmatter)
|
|
21
|
+
@path = build_virtual_path(site, pagination_config, frontmatter, content)
|
|
24
22
|
|
|
25
23
|
process(@name)
|
|
26
24
|
|
|
@@ -42,9 +40,19 @@ class PageTemplate < Jekyll::Page
|
|
|
42
40
|
private
|
|
43
41
|
|
|
44
42
|
# Builds a deterministic virtual source path for this synthetic page.
|
|
45
|
-
def build_virtual_path(site, pagination_config, frontmatter)
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
def build_virtual_path(site, pagination_config, frontmatter, content)
|
|
44
|
+
Utils.build_synthetic_source_path(
|
|
45
|
+
site: site,
|
|
46
|
+
extension: '.md',
|
|
47
|
+
source_stem: Utils.derive_synthetic_source_stem_from_frontmatter(frontmatter, fallback: 'generated-template'),
|
|
48
|
+
role: 'template',
|
|
49
|
+
signature: {
|
|
50
|
+
'pagination' => Utils.safe_hash(pagination_config),
|
|
51
|
+
'frontmatter' => Utils.safe_hash(frontmatter),
|
|
52
|
+
'content' => content.to_s,
|
|
53
|
+
'content_type' => 'page-template'
|
|
54
|
+
}
|
|
55
|
+
)
|
|
48
56
|
end
|
|
49
57
|
|
|
50
58
|
# Adds legacy-friendly fields only when v2 compatibility is active.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
3
5
|
module Jekyll
|
|
4
6
|
module Plugins
|
|
5
7
|
module PaginateV3
|
|
@@ -49,6 +51,99 @@ module Utils
|
|
|
49
51
|
|
|
50
52
|
url
|
|
51
53
|
end
|
|
54
|
+
|
|
55
|
+
# Builds one deterministic synthetic source path for in-memory pages
|
|
56
|
+
# and documents. Filenames remain readable for debugging while a hash
|
|
57
|
+
# suffix preserves practical uniqueness across templates and variants.
|
|
58
|
+
def self.build_synthetic_source_path(site:, extension:, signature:, collection: nil, source_path: nil, source_stem: nil, role:, page_number: nil)
|
|
59
|
+
directory = collection.nil? ? site.source : File.join(site.source, collection.relative_directory)
|
|
60
|
+
File.join(
|
|
61
|
+
directory,
|
|
62
|
+
build_synthetic_filename(
|
|
63
|
+
extension: extension,
|
|
64
|
+
signature: signature,
|
|
65
|
+
source_path: source_path,
|
|
66
|
+
source_stem: source_stem,
|
|
67
|
+
role: role,
|
|
68
|
+
page_number: page_number
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Builds one readable synthetic filename from source identity plus a
|
|
74
|
+
# deterministic safety suffix.
|
|
75
|
+
def self.build_synthetic_filename(extension:, signature:, source_path: nil, source_stem: nil, role:, page_number: nil)
|
|
76
|
+
stem = derive_synthetic_source_stem(source_path: source_path, source_stem: source_stem, fallback: role)
|
|
77
|
+
filename_segments = [stem, role.to_s.strip]
|
|
78
|
+
filename_segments << page_number.to_i.to_s unless page_number.nil?
|
|
79
|
+
filename_segments << Digest::MD5.hexdigest(canonical_signature(signature).inspect)
|
|
80
|
+
"#{filename_segments.reject(&:empty?).join('-')}#{ensure_leading_dot(extension)}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Derives one human-readable synthetic stem from an original source
|
|
84
|
+
# path or fallback label.
|
|
85
|
+
def self.derive_synthetic_source_stem(source_path: nil, source_stem: nil, fallback: 'generated')
|
|
86
|
+
candidate = extract_source_filename_stem(source_path)
|
|
87
|
+
candidate = source_stem.to_s if candidate.empty?
|
|
88
|
+
candidate = fallback.to_s if candidate.to_s.strip.empty?
|
|
89
|
+
sanitise_filename_component(candidate, fallback: fallback)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Derives one readable stem from frontmatter when no real source path
|
|
93
|
+
# exists, preferring permalink and then title.
|
|
94
|
+
def self.derive_synthetic_source_stem_from_frontmatter(frontmatter, fallback: 'generated')
|
|
95
|
+
frontmatter_hash = safe_hash(frontmatter)
|
|
96
|
+
permalink_stem = extract_permalink_stem(frontmatter_hash['permalink'])
|
|
97
|
+
return derive_synthetic_source_stem(source_stem: permalink_stem, fallback: fallback) unless permalink_stem.empty?
|
|
98
|
+
|
|
99
|
+
derive_synthetic_source_stem(source_stem: frontmatter_hash['title'], fallback: fallback)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Extracts one source-style stem from a real path-like value.
|
|
103
|
+
def self.extract_source_filename_stem(source_path)
|
|
104
|
+
path = source_path.to_s
|
|
105
|
+
return '' if path.strip.empty?
|
|
106
|
+
|
|
107
|
+
File.basename(path, File.extname(path))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Extracts one final path segment from a permalink-like value.
|
|
111
|
+
def self.extract_permalink_stem(permalink)
|
|
112
|
+
path = permalink.to_s.strip
|
|
113
|
+
return '' if path.empty?
|
|
114
|
+
|
|
115
|
+
segments = path.split('/').reject(&:empty?)
|
|
116
|
+
return '' if segments.empty?
|
|
117
|
+
|
|
118
|
+
File.basename(segments.last, File.extname(segments.last))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Sanitises one filename component while preserving source readability
|
|
122
|
+
# where the original stem is already filesystem-safe.
|
|
123
|
+
def self.sanitise_filename_component(value, fallback: 'generated')
|
|
124
|
+
component = value.to_s.strip
|
|
125
|
+
component = component.gsub(%r{[\\/]}, '-')
|
|
126
|
+
component = component.gsub(/[<>:"|?*\x00-\x1f]/, '-')
|
|
127
|
+
component = component.gsub(/^-+/, '')
|
|
128
|
+
component = component.gsub(/[. ]+$/, '')
|
|
129
|
+
component = fallback.to_s if component.empty?
|
|
130
|
+
component
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Canonicalises nested signature data so hashes with identical meaning
|
|
134
|
+
# yield the same suffix regardless of insertion order.
|
|
135
|
+
def self.canonical_signature(value)
|
|
136
|
+
case value
|
|
137
|
+
when Hash
|
|
138
|
+
value.each_with_object({}) do |(key, nested_value), canonical|
|
|
139
|
+
canonical[key.to_s] = canonical_signature(nested_value)
|
|
140
|
+
end.sort_by { |key, _| key }.to_h
|
|
141
|
+
when Array
|
|
142
|
+
value.map { |entry| canonical_signature(entry) }
|
|
143
|
+
else
|
|
144
|
+
value
|
|
145
|
+
end
|
|
146
|
+
end
|
|
52
147
|
end
|
|
53
148
|
|
|
54
149
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-paginate-v3
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.alpha.
|
|
4
|
+
version: 0.1.0.alpha.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Convincible
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -44,6 +44,20 @@ dependencies:
|
|
|
44
44
|
- - '='
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
46
|
version: 0.1.0.alpha
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: kramdown-parser-gfm
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.1'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.1'
|
|
47
61
|
- !ruby/object:Gem::Dependency
|
|
48
62
|
name: pry
|
|
49
63
|
requirement: !ruby/object:Gem::Requirement
|