jekyll-paginate-v3 0.1.0.alpha → 0.1.0.alpha.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfe3b140435a9e8b5011cc8ff234547046323329bf1fba18dc0cdbc649fb1266
4
- data.tar.gz: '039c94ad587c94ce7ebf66a828aa856b0092cf9f28a489da2aedc0e7102c2ed7'
3
+ metadata.gz: 036fbbfba18f61ae73a069e4330fce833a1cb7549704c9bf396961ddd262569f
4
+ data.tar.gz: 061d674f1bfd29de53bb79bc5a64feb8076a80af38f59be877f3cc77443173ff
5
5
  SHA512:
6
- metadata.gz: cf32cbe5e10d028b59b8f9807ae3d52bb63421eb0ffa57d3f104b7bdc953f483da24d1453d5974bfcba186e969674ebf1ee1ae39a8e8993456f1033661a873da
7
- data.tar.gz: 2278e72f4b899f3cfaf21e62eb55c776fff719bd1e88c2374206f627a070e4dc35cf6bca7613c189590e8be3c5657c40f0e2ad2fd21b4776f48deb7c6b90aa92
6
+ metadata.gz: e6561b1d072672dabdbace597b3b7d310a3f357f7d9b05a1f6815d7b29e634029faf7828ca8105edf6cc41fd1cab276e215de8343358c53e42b482e005f64235
7
+ data.tar.gz: 259bd8d452c747203d316a977de290141b11bc1061a39386e0bac81733f1ace2b08ef0b9c1d8092212e232544bb6f8f758e2e49327b6779f54116e555213a7d4
@@ -39,23 +39,14 @@ class PaginationGenerator < Jekyll::Generator
39
39
 
40
40
  # Abstract site mutation so the model can add pages or documents
41
41
  # without knowing where Jekyll stores each item type.
42
- add_item_lambda = lambda do |item|
43
- if item.is_a?(Jekyll::Document)
44
- site.collections[item.collection.label].docs << item
45
- else
46
- site.pages << item
47
- end
48
- item
42
+ add_item_lambda = lambda do |item, collection_index: nil|
43
+ add_site_item(site, item, collection_index: collection_index)
49
44
  end
50
45
 
51
46
  # Mirror add_item_lambda for replacing template pages with generated
52
47
  # paginated siblings.
53
48
  remove_item_lambda = lambda do |item|
54
- if item.is_a?(Jekyll::Document)
55
- site.collections[item.collection.label].docs.delete_if { |doc| doc == item }
56
- else
57
- site.pages.delete_if { |page| page == item }
58
- end
49
+ remove_site_item(site, item)
59
50
  end
60
51
 
61
52
  model = Pagination::Model.new(
@@ -87,6 +78,60 @@ class PaginationGenerator < Jekyll::Generator
87
78
 
88
79
  private
89
80
 
81
+ # Adds one generated page/document to the appropriate site collection.
82
+ #
83
+ # `collection_index` is optional and is only used when pagination needs
84
+ # to replace a source collection document without disturbing the
85
+ # ordering of surrounding documents.
86
+ def add_site_item(site, item, collection_index: nil)
87
+ if item.is_a?(Jekyll::Document)
88
+ add_collection_document(site, item, collection_index: collection_index)
89
+ else
90
+ site.pages << item
91
+ end
92
+
93
+ item
94
+ end
95
+
96
+ # Inserts one generated collection document either at a precise source
97
+ # position or at the end of the collection when no preserved position
98
+ # is required.
99
+ def add_collection_document(site, item, collection_index: nil)
100
+ collection_docs = site.collections[item.collection.label].docs
101
+ if collection_index.nil?
102
+ collection_docs << item
103
+ return
104
+ end
105
+
106
+ safe_collection_index = [[collection_index.to_i, 0].max, collection_docs.length].min
107
+ collection_docs.insert(safe_collection_index, item)
108
+ end
109
+
110
+ # Removes one item from the site and returns collection position
111
+ # metadata when the removed item came from a collection.
112
+ def remove_site_item(site, item)
113
+ if item.is_a?(Jekyll::Document)
114
+ return remove_collection_document(site, item)
115
+ end
116
+
117
+ site.pages.delete_if { |page| page.equal?(item) }
118
+ nil
119
+ end
120
+
121
+ # Removes one collection document by exact array position so the
122
+ # generated replacement can later reuse the same slot.
123
+ def remove_collection_document(site, item)
124
+ collection_docs = site.collections[item.collection.label].docs
125
+ collection_index = collection_docs.index { |document| document.equal?(item) }
126
+ return nil if collection_index.nil?
127
+
128
+ collection_docs.delete_at(collection_index)
129
+ {
130
+ 'collection_label' => item.collection.label.to_s,
131
+ 'index' => collection_index
132
+ }
133
+ end
134
+
90
135
  # Logs one info-level summary line for each configured generate entry.
91
136
  def log_generate_report(logger, generated_template_report)
92
137
  report_entries = Utils.arrayify(generated_template_report['entries'])
@@ -160,6 +160,9 @@ class Model
160
160
  # Captures the source items used by later `items` searches so
161
161
  # pagination item resolution remains stable even after templates are
162
162
  # removed and replaced by generated index pages.
163
+ #
164
+ # This frozen snapshot is the invariant that stops emitted pagination
165
+ # pages from feeding back into later item searches.
163
166
  def capture_item_resolution_sources!
164
167
  @item_resolution_pages = @site.pages.dup
165
168
  @item_resolution_documents_by_collection = {}
@@ -436,9 +439,12 @@ class Model
436
439
  # applies generic inclusion/exclusion flags.
437
440
  #
438
441
  # `exclude_items` is used by the pagination pipeline to remove the
439
- # active source template from its own item set without also excluding
440
- # other templates that the user may legitimately want to paginate over.
441
- def resolve_items(raw_search, include_templates: false, include_generated_indexes: false, include_hidden: false, exclude_items: nil)
442
+ # active source template from its own item set while leaving other
443
+ # pagination templates eligible for ordinary item searches.
444
+ #
445
+ # Emitted pagination indexes are kept out by the frozen source items
446
+ # captured earlier in the pipeline, rather than by ad hoc filtering.
447
+ def resolve_items(raw_search, include_hidden: false, exclude_items: nil)
442
448
  entries = Query::Parser.parse(raw_search, @site_config['keywords'], split_delimiter: @split_delimiter)
443
449
  if entries.empty?
444
450
  log("Resolving items from search=#{raw_search.inspect} produced no parsed entries.", 'debug')
@@ -446,7 +452,7 @@ class Model
446
452
  end
447
453
 
448
454
  excluded_item_keys = excluded_item_identity_keys(exclude_items)
449
- log("Resolving items from search=#{raw_search.inspect} (entries=#{entries.length}, include_templates=#{include_templates}, include_generated_indexes=#{include_generated_indexes}, include_hidden=#{include_hidden}, exclude_items=#{excluded_item_keys.length}).", 'debug')
455
+ log("Resolving items from search=#{raw_search.inspect} (entries=#{entries.length}, include_hidden=#{include_hidden}, exclude_items=#{excluded_item_keys.length}).", 'debug')
450
456
  resolved = []
451
457
  entries.each do |entry|
452
458
  resolved.concat(resolve_entry(entry))
@@ -457,18 +463,14 @@ class Model
457
463
  log("Resolved #{resolved.length} unique item(s) before exclusion filters.", 'debug')
458
464
  log_item_path_sample('Resolved item sample before exclusions', resolved)
459
465
 
460
- excluded_generated_indexes = include_generated_indexes ? 0 : resolved.count { |item| Utils.generated_index?(item) }
461
- excluded_templates = include_templates ? 0 : resolved.count { |item| Utils.pagination_template?(item) }
462
466
  excluded_hidden = include_hidden ? 0 : resolved.count { |item| item['hidden'] }
463
467
  excluded_explicit_items = excluded_item_keys.empty? ? 0 : resolved.count { |item| excluded_item_keys.include?(item_identity_key(item)) }
464
468
 
465
- resolved.select! { |item| !Utils.generated_index?(item) } unless include_generated_indexes
466
- resolved.select! { |item| !Utils.pagination_template?(item) } unless include_templates
467
469
  resolved.select! { |item| !item['hidden'] } unless include_hidden
468
470
  resolved.select! { |item| !excluded_item_keys.include?(item_identity_key(item)) } unless excluded_item_keys.empty?
469
471
 
470
- if excluded_generated_indexes.positive? || excluded_templates.positive? || excluded_hidden.positive? || excluded_explicit_items.positive?
471
- log("Excluded generated_indexes=#{excluded_generated_indexes} templates=#{excluded_templates} hidden=#{excluded_hidden} explicit=#{excluded_explicit_items} from resolved items.", 'debug')
472
+ if excluded_hidden.positive? || excluded_explicit_items.positive?
473
+ log("Excluded hidden=#{excluded_hidden} explicit=#{excluded_explicit_items} from resolved items.", 'debug')
472
474
  end
473
475
  log("Resolved #{resolved.length} item(s) after exclusion filters.", 'debug')
474
476
  log_item_path_sample('Resolved item sample after exclusions', resolved)
@@ -25,6 +25,7 @@ class Model
25
25
 
26
26
  total_paginated_items = 0
27
27
  total_indexes = 0
28
+ collection_replacement_state = nil
28
29
 
29
30
  variants.each_with_index do |variant, variant_index|
30
31
  variant_template = variant['template']
@@ -33,8 +34,10 @@ class Model
33
34
  variant_template,
34
35
  variant_config,
35
36
  remove_source_template: variant_index.zero?,
36
- item_exclusions: item_exclusions
37
+ item_exclusions: item_exclusions,
38
+ collection_replacement_state: collection_replacement_state
37
39
  )
40
+ collection_replacement_state = variant_report['collection_replacement_state']
38
41
  total_paginated_items += variant_report['paginated_items'].to_i
39
42
  total_indexes += variant_report['indexes'].to_i
40
43
  end
@@ -46,9 +49,8 @@ class Model
46
49
  end
47
50
 
48
51
  # Returns the per-template item exclusions required to prevent a
49
- # template from paginating over itself when the active item object is
50
- # still otherwise eligible during resolution. Other pagination
51
- # templates remain excluded by normal item-resolution rules.
52
+ # template from paginating over itself when pagination templates are
53
+ # otherwise eligible during item resolution.
52
54
  def pagination_item_exclusions_for_template(template)
53
55
  [template]
54
56
  end
@@ -80,7 +82,7 @@ class Model
80
82
  end
81
83
 
82
84
  # Runs pagination for one already-expanded template variant.
83
- def paginate_template_variant(template, config, remove_source_template:, item_exclusions:)
85
+ def paginate_template_variant(template, config, remove_source_template:, item_exclusions:, collection_replacement_state:)
84
86
  template_path = Utils.relative_item_path(template)
85
87
  split_delimiter = config.key?('split') ? config['split'] : @split_delimiter
86
88
  nested_separator = config['separator'] || @nested_separator
@@ -128,23 +130,30 @@ class Model
128
130
  validate_numbered_permalink_template!(template, config, total_pages)
129
131
 
130
132
  log("Template '#{template_path}': generating #{total_pages} page(s) with per_page=#{config['per_page']} limit=#{config['limit']}.", 'debug')
131
- generated_pages = emit_paginated_pages(
133
+ page_emission = emit_paginated_pages(
132
134
  template,
133
135
  config,
134
136
  sorted_items,
135
137
  page_windows,
136
- remove_template: remove_source_template
138
+ remove_template: remove_source_template,
139
+ collection_replacement_state: collection_replacement_state
137
140
  )
141
+ generated_pages = page_emission['pages']
142
+ collection_replacement_state = page_emission['collection_replacement_state']
138
143
  register_grouped_set_if_applicable(template, config, generated_pages)
139
144
  {
140
145
  'paginated_items' => sorted_items.length,
141
- 'indexes' => generated_pages.length
146
+ 'indexes' => generated_pages.length,
147
+ 'collection_replacement_state' => collection_replacement_state
142
148
  }
143
149
  end
144
150
 
145
151
  # Replaces a template with one synthetic page/document per page number.
146
- def emit_paginated_pages(template, config, items, page_windows, remove_template: true)
147
- @remove_item_lambda.call(template) if remove_template
152
+ def emit_paginated_pages(template, config, items, page_windows, remove_template: true, collection_replacement_state: nil)
153
+ if remove_template
154
+ removed_item_state = @remove_item_lambda.call(template)
155
+ collection_replacement_state = build_collection_replacement_state(template, removed_item_state)
156
+ end
148
157
 
149
158
  new_pages = []
150
159
  total_pages = page_windows.length
@@ -192,14 +201,50 @@ class Model
192
201
  assign_generated_page_title!(generated, template, config, current_page, total_pages)
193
202
  assign_generated_page_permalink!(generated, template, config, current_page, total_pages)
194
203
 
195
- @add_item_lambda.call(generated)
204
+ @add_item_lambda.call(
205
+ generated,
206
+ collection_index: collection_insertion_index_for_generated_item(generated, collection_replacement_state)
207
+ )
196
208
  log("Emitted pagination page #{current_page}/#{total_pages} at '#{generated.url}' for template '#{Utils.relative_item_path(template)}'.", 'debug')
197
209
  new_pages << generated
198
210
  end
199
211
 
200
212
  bind_paginator_references(new_pages)
201
213
  apply_page_trail(new_pages, config)
202
- new_pages
214
+ {
215
+ 'pages' => new_pages,
216
+ 'collection_replacement_state' => collection_replacement_state
217
+ }
218
+ end
219
+
220
+ # Builds insertion state for collection-template replacement.
221
+ #
222
+ # The state stores a moving insertion cursor so any generated
223
+ # collection documents can occupy the template's original source slot
224
+ # and then continue immediately after it.
225
+ def build_collection_replacement_state(template, removed_item_state)
226
+ return nil unless collection_template?(template)
227
+
228
+ removed_item_state = Utils.safe_hash(removed_item_state)
229
+ return nil if removed_item_state.empty?
230
+ return nil if removed_item_state['collection_label'].to_s != template.collection.label.to_s
231
+
232
+ {
233
+ 'source_collection_label' => template.collection.label.to_s,
234
+ 'next_index' => removed_item_state['index'].to_i
235
+ }
236
+ end
237
+
238
+ # Returns the next insertion index for a generated document that
239
+ # belongs in the same collection as the template it replaced.
240
+ def collection_insertion_index_for_generated_item(generated, collection_replacement_state)
241
+ return nil if collection_replacement_state.nil?
242
+ return nil unless generated.is_a?(Jekyll::Document)
243
+ return nil if generated.collection.label.to_s != collection_replacement_state['source_collection_label'].to_s
244
+
245
+ insertion_index = collection_replacement_state['next_index'].to_i
246
+ collection_replacement_state['next_index'] = insertion_index + 1
247
+ insertion_index
203
248
  end
204
249
 
205
250
  # Builds one generated index object according to configured collection mode.
@@ -4,7 +4,7 @@ module Jekyll
4
4
  module Plugins
5
5
 
6
6
  module PaginateV3
7
- VERSION = '0.1.0.alpha'
7
+ VERSION = '0.1.0.alpha.2'
8
8
  end
9
9
 
10
10
  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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Convincible
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-24 00:00:00.000000000 Z
11
+ date: 2026-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll