jekyll-collection-pages 0.1.0 → 0.1.1
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/.devcontainer/devcontainer.json +11 -3
- data/.github/workflows/release_draft.yml +1 -1
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -2
- data/.vscode/settings.json +0 -3
- data/Gemfile +3 -1
- data/README.md +3 -3
- data/VERSION +1 -1
- data/demo/_config.yml +1 -2
- data/demo/_docs/configuration-guide.md +9 -7
- data/demo/_docs/examples.md +1 -6
- data/demo/_docs/generated-data.md +1 -1
- data/demo/_docs/troubleshooting.md +2 -2
- data/demo/index.md +2 -2
- data/lib/jekyll/collection_pages/collection_config.rb +54 -0
- data/lib/jekyll/collection_pages/collection_pager.rb +95 -0
- data/lib/jekyll/collection_pages/path_template.rb +90 -0
- data/lib/jekyll/collection_pages/tag_page.rb +32 -0
- data/lib/jekyll/collection_pages/tag_pager.rb +58 -0
- data/lib/jekyll/collection_pages/tag_path.rb +86 -0
- data/lib/jekyll/collection_pages/version.rb +7 -0
- data/lib/jekyll/collection_pages.rb +16 -354
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fd1f1722ec16a60ac61dd1779dad45c8107720762e03c2e9ae623784d3696ce
|
|
4
|
+
data.tar.gz: 7ce8708d2ec0759f9ec478ec89691f102b6204da925cb25ff7374390f4aeee88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9fed8faba1a44eb009b4a83d6c0844a1ef321faf1cdbd115a7997124080cfc112157147b4c0a07405372a048327084032f4abfef6b93865fd855a5f6bca4da0d
|
|
7
|
+
data.tar.gz: 45b2ef4c09b213f5c376472adb1acf1ad9be7811b1d5686cd3b78ef69b417400e2984c0c70809536d7ed572442e32d4e99460ec673c22966515d3c68a730d9d8
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
|
6
6
|
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
|
|
7
7
|
"features": {
|
|
8
|
-
"ghcr.io/devcontainers/features/ruby:
|
|
8
|
+
"ghcr.io/devcontainers/features/ruby:2": {
|
|
9
9
|
"version": "3.3.4"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
@@ -19,9 +19,17 @@
|
|
|
19
19
|
35729
|
|
20
20
|
],
|
|
21
21
|
// Use 'postCreateCommand' to run commands after the container is created.
|
|
22
|
-
"postCreateCommand": ".devcontainer/post-create.sh"
|
|
22
|
+
"postCreateCommand": ".devcontainer/post-create.sh",
|
|
23
23
|
// Configure tool-specific properties.
|
|
24
|
-
|
|
24
|
+
"customizations": {
|
|
25
|
+
"vscode": {
|
|
26
|
+
"settings": {
|
|
27
|
+
"rubyLsp.rubyVersionManager": {
|
|
28
|
+
"identifier": "none"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
25
33
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
26
34
|
// "remoteUser": "root"
|
|
27
35
|
}
|
|
@@ -51,7 +51,7 @@ jobs:
|
|
|
51
51
|
run: bundle install
|
|
52
52
|
|
|
53
53
|
- name: Create Draft Pull Request
|
|
54
|
-
uses: peter-evans/create-pull-request@
|
|
54
|
+
uses: peter-evans/create-pull-request@v8
|
|
55
55
|
with:
|
|
56
56
|
commit-message: "Bump version to ${{ needs.draft-release.outputs.tag_name }}"
|
|
57
57
|
branch: release/draft
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -20,14 +20,14 @@ Layout/LineLength:
|
|
|
20
20
|
# Metrics cops
|
|
21
21
|
# ----------------------------------------
|
|
22
22
|
Metrics/AbcSize:
|
|
23
|
-
Max:
|
|
23
|
+
Max: 24
|
|
24
24
|
Metrics/BlockLength:
|
|
25
25
|
Exclude:
|
|
26
26
|
- 'spec/**/*'
|
|
27
27
|
Metrics/ClassLength:
|
|
28
28
|
Max: 240
|
|
29
29
|
Metrics/CyclomaticComplexity:
|
|
30
|
-
Max:
|
|
30
|
+
Max: 12
|
|
31
31
|
Metrics/MethodLength:
|
|
32
32
|
Max: 20
|
|
33
33
|
Metrics/ModuleLength:
|
data/.vscode/settings.json
CHANGED
data/Gemfile
CHANGED
|
@@ -9,11 +9,13 @@ gem 'webrick', '~> 1.8'
|
|
|
9
9
|
|
|
10
10
|
group :jekyll_plugins do
|
|
11
11
|
gem 'jekyll-feed'
|
|
12
|
+
gem 'jekyll-octicons'
|
|
12
13
|
gem 'jekyll-paginate'
|
|
13
14
|
gem 'jekyll-redirect-from'
|
|
14
15
|
gem 'jekyll-relative-links'
|
|
15
16
|
gem 'jekyll-sitemap'
|
|
16
|
-
gem 'jekyll-theme-
|
|
17
|
+
gem 'jekyll-theme-primerpages' # needed for themeing
|
|
18
|
+
gem 'jekyll-toc'
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
group :development do
|
data/README.md
CHANGED
|
@@ -42,6 +42,7 @@ collection_pages:
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Key options:
|
|
45
|
+
|
|
45
46
|
- `collection`: collection label (matches `collections` config).
|
|
46
47
|
- `field`: front-matter key to group documents (string or list values).
|
|
47
48
|
- `path`: base folder for generated pages (relative to site root).
|
|
@@ -72,7 +73,6 @@ Run `bundle exec jekyll serve` and visit the generated paths, e.g. `/docs/catego
|
|
|
72
73
|
- `page.posts`: documents in that field bucket.
|
|
73
74
|
- `page.paginator`: pagination data when `paginate` is set (same shape as Jekyll paginator).
|
|
74
75
|
|
|
75
|
-
|
|
76
76
|
```html
|
|
77
77
|
{% raw %}
|
|
78
78
|
---
|
|
@@ -91,8 +91,8 @@ layout: default
|
|
|
91
91
|
|
|
92
92
|
Every build populates a hash at `site.data.collection_pages[collection][field]` that contains:
|
|
93
93
|
|
|
94
|
-
- `template` → the
|
|
95
|
-
- `permalink` → the sanitized template for the index with placeholders intact (e.g. `/docs/category/:field/`)
|
|
94
|
+
- `template` → the sanitized template the generator used (`/docs/category/:field/index.html` for unpaginated configs, `/docs/category/:field/page:num/index.html` when `paginate` is positive).
|
|
95
|
+
- `permalink` → the sanitized template for the index with placeholders intact (`:num` never appears here because it always points to page 1, e.g. `/docs/category/:field/`)
|
|
96
96
|
- `pages` → documents grouped by label (`{ label => [documents...] }`)
|
|
97
97
|
- `labels`: metadata describing the generated index pages
|
|
98
98
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.1
|
data/demo/_config.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
theme: jekyll-theme-
|
|
1
|
+
theme: jekyll-theme-primerpages
|
|
2
2
|
style: topbar
|
|
3
3
|
title: Jekyll Collection Pages plugin
|
|
4
4
|
repository: PrimerPages/jekyll-collection-pages
|
|
@@ -75,7 +75,6 @@ relative_links:
|
|
|
75
75
|
plugins:
|
|
76
76
|
- jekyll-paginate
|
|
77
77
|
- jekyll-collection-pages
|
|
78
|
-
- jekyll-github-metadata
|
|
79
78
|
- jekyll-octicons
|
|
80
79
|
- jekyll-relative-links
|
|
81
80
|
- jekyll-seo-tag
|
|
@@ -44,7 +44,7 @@ collection_pages:
|
|
|
44
44
|
|
|
45
45
|
If you only need a single entry, `collection_pages` can also be a hash (the plugin normalises it internally). The array form keeps things consistent once you add more targets.
|
|
46
46
|
|
|
47
|
-
Each `path` above is treated as a template—directory values automatically expand to
|
|
47
|
+
Each `path` above is treated as a template—directory and can take `:field` and `:num` as placeholders. It is recommended to set only set the directory since values automatically expand to `<path>/:field/page:num/index.html`. The first page, and when pagination is off the generator will create pages in `<path>/:field/index.html`.
|
|
48
48
|
|
|
49
49
|
## Configuration options
|
|
50
50
|
|
|
@@ -66,13 +66,15 @@ Make sure every document you expect to index sets this field. When the field hol
|
|
|
66
66
|
|
|
67
67
|
- Type: `String`
|
|
68
68
|
- Required: ✔
|
|
69
|
-
- Description: Path template relative to the site source. It
|
|
69
|
+
- Description: Path template relative to the site source. It can also include a `:field` and a `:num` placeholder which are appended to the path if they are ommitted `:field/page:num/index.html` (recommended).
|
|
70
|
+
- `:field`: This placeholder is for the value of the field for the page.
|
|
71
|
+
- `:num`: This placeholder is for the page number of the pagination.
|
|
70
72
|
|
|
71
73
|
Rules enforced by the generator:
|
|
72
74
|
|
|
73
|
-
- `:
|
|
74
|
-
- Paths ending in `.html`/`.htm` must include
|
|
75
|
-
- Leaving `path` blank defaults to `<collection>/:field/page:num/index.html
|
|
75
|
+
- When `:num` is present it must appear after `:field`, and the two placeholders cannot live in the same segment.
|
|
76
|
+
- Paths ending in `.html`/`.htm` must include the required placeholders explicitly.
|
|
77
|
+
- Leaving `path` blank defaults to `<collection>/:field/index.html` (or `<collection>/:field/page:num/index.html` when paginated).
|
|
76
78
|
|
|
77
79
|
### `layout`
|
|
78
80
|
|
|
@@ -94,8 +96,8 @@ Set `paginate` to `nil`, omit the key, or use a non-positive number to render a
|
|
|
94
96
|
|
|
95
97
|
At build time the plugin exports `site.data.collection_pages[collection_name][field]`, which contains:
|
|
96
98
|
|
|
97
|
-
- `template` → the
|
|
98
|
-
- `permalink` → the sanitized template for the index with placeholders intact (e.g. `/docs/category/:field/`)
|
|
99
|
+
- `template` → the sanitized template used to create each index. Directory values inherit `<path>/:field/index.html` and add `/page:num/` only when pagination is enabled (e.g. `/docs/category/:field/index.html` or `/docs/category/:field/page:num/index.html`).
|
|
100
|
+
- `permalink` → the sanitized template for the index with placeholders intact (no `:num`, since it always points to page one, e.g. `/docs/category/:field/`)
|
|
99
101
|
- `pages` → documents grouped by label (`{ label => [documents...] }`)
|
|
100
102
|
- `labels`: metadata describing the generated index pages
|
|
101
103
|
|
data/demo/_docs/examples.md
CHANGED
|
@@ -16,14 +16,9 @@ collection_pages:
|
|
|
16
16
|
path: blog/category
|
|
17
17
|
layout: category_page.html
|
|
18
18
|
paginate: 10
|
|
19
|
-
- collection: posts
|
|
20
|
-
field: category
|
|
21
|
-
path: blog/category/:field-page:num.html
|
|
22
|
-
layout: category_page.html
|
|
23
|
-
paginate: 10
|
|
24
19
|
```
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
This configuration uses folder-style pagination such as `/blog/category/reference/` and `/blog/category/reference/page2/`. Stick with this pattern for the most compatible URLs.
|
|
27
22
|
|
|
28
23
|
Render a category listing with the exported data registry:
|
|
29
24
|
|
|
@@ -45,7 +45,7 @@ The structure is intentionally compatible with existing theme includes that expe
|
|
|
45
45
|
`labels` exposes metadata about each generated index:
|
|
46
46
|
|
|
47
47
|
- `index`: the first generated index page (`index.html`)
|
|
48
|
-
- `pages`: array of all `
|
|
48
|
+
- `pages`: array of all generated `TagPage` objects
|
|
49
49
|
|
|
50
50
|
Example:
|
|
51
51
|
|
|
@@ -41,8 +41,8 @@ Having trouble seeing generated pages or data? Start here.
|
|
|
41
41
|
|
|
42
42
|
## Path template errors
|
|
43
43
|
|
|
44
|
-
- `path` values must resolve to exactly one `:field` placeholder
|
|
45
|
-
- `:
|
|
44
|
+
- `path` values must resolve to exactly one `:field` placeholder. Add a single `:num` placeholder only when the configuration sets a positive `paginate` value. Directory-style paths (no `.html`/`.htm`) automatically become `<path>/:field/index.html`, but Explicit filenames must include the required placeholders.
|
|
45
|
+
- Whenever `:num` is present it must appear after `:field`, and they cannot live in the same path segment.
|
|
46
46
|
- When the build fails with an error referencing these placeholders, trim any trailing slashes and adjust the order. The generator logs the sanitized template in debug mode so you can confirm the final value.
|
|
47
47
|
|
|
48
48
|
## Liquid include expects `site.tags`
|
data/demo/index.md
CHANGED
|
@@ -28,7 +28,7 @@ See how `jekyll-collection-pages` organizes technical documentation:
|
|
|
28
28
|
|
|
29
29
|
See how `jekyll-collection-pages` can organize collections like posts:
|
|
30
30
|
|
|
31
|
-
- [
|
|
31
|
+
- [Generated pages directory](directory.md)
|
|
32
32
|
- [Tag index](tags.md)
|
|
33
33
|
- [Tag gallery](gallery.md)
|
|
34
34
|
|
|
@@ -52,7 +52,7 @@ Ready to use `jekyll-collection-pages` in your own project?
|
|
|
52
52
|
{%- include post-card.html border="border-top" %}
|
|
53
53
|
{%- endif %}
|
|
54
54
|
{% endfor %}
|
|
55
|
-
</div
|
|
55
|
+
</div>
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module CollectionPages
|
|
5
|
+
class CollectionConfig
|
|
6
|
+
attr_reader :collection_name, :tag_field, :tag_base_path, :tag_layout, :per_page
|
|
7
|
+
|
|
8
|
+
def initialize(config)
|
|
9
|
+
unless config.is_a?(Hash)
|
|
10
|
+
Jekyll.logger.error('CollectionPages:', "Invalid collection config entry: #{config.inspect}.")
|
|
11
|
+
raise ArgumentError, "Invalid collection_pages config entry. #{config.inspect}."
|
|
12
|
+
end
|
|
13
|
+
@collection_name = config['collection']
|
|
14
|
+
@tag_field = config['field']
|
|
15
|
+
@tag_base_path = config['path'] || @collection_name
|
|
16
|
+
@tag_layout = normalize_layout(config['layout'] || 'collection_layout')
|
|
17
|
+
@per_page = normalize_paginate_value(config['paginate'])
|
|
18
|
+
|
|
19
|
+
validate_config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def validate_config
|
|
25
|
+
missing_keys = []
|
|
26
|
+
missing_keys << 'collection' unless @collection_name
|
|
27
|
+
missing_keys << 'field' unless @tag_field
|
|
28
|
+
return if missing_keys.empty?
|
|
29
|
+
|
|
30
|
+
Jekyll.logger.error('CollectionPages:', "Missing required config keys: #{missing_keys.join(', ')}.")
|
|
31
|
+
raise ArgumentError, "Invalid collection_pages config entry. Missing: #{missing_keys.join(', ')}."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def normalize_paginate_value(value)
|
|
35
|
+
return nil if value.nil?
|
|
36
|
+
|
|
37
|
+
per_page = Integer(value)
|
|
38
|
+
return per_page if per_page.positive?
|
|
39
|
+
|
|
40
|
+
Jekyll.logger.warn('CollectionPages:',
|
|
41
|
+
"Non-positive paginate value #{value.inspect} for collection '#{@collection_name}' field '#{@tag_field}'. " \
|
|
42
|
+
'Falling back to single page generation.')
|
|
43
|
+
nil
|
|
44
|
+
rescue ArgumentError, TypeError
|
|
45
|
+
raise ArgumentError,
|
|
46
|
+
"Invalid paginate value #{value.inspect} for collection '#{@collection_name}' field '#{@tag_field}'. Expected a numeric value."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def normalize_layout(layout)
|
|
50
|
+
layout.to_s.sub(/\.[^.]+\z/, '').sub(%r{\A_layouts/}, '')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module CollectionPages
|
|
5
|
+
class CollectionPager
|
|
6
|
+
def initialize(site, config)
|
|
7
|
+
@site = site
|
|
8
|
+
@config = config
|
|
9
|
+
@tags_with_docs = sorted_tags(site, config.collection_name, config.tag_field)
|
|
10
|
+
path_template = PathTemplate.new(raw_template: config.tag_base_path, tag_field: config.tag_field, collection_name: config.collection_name,
|
|
11
|
+
require_num: !config.per_page.nil? && config.per_page.positive?)
|
|
12
|
+
@template = path_template.template
|
|
13
|
+
@permalink = TagPath.new(@template, ':field', slugify_value: false).url_for(1)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_pages
|
|
17
|
+
documents_map = {}
|
|
18
|
+
metadata_map = {}
|
|
19
|
+
@tags_with_docs.each do |tag, posts_with_tag|
|
|
20
|
+
page_count = TagPager.calculate_pages(posts_with_tag, @config.per_page)
|
|
21
|
+
tag_pages = []
|
|
22
|
+
(1..page_count).each do |page_num|
|
|
23
|
+
tag_page = build_page(@site, tag, posts_with_tag, page_num)
|
|
24
|
+
@site.pages << tag_page
|
|
25
|
+
tag_pages << tag_page
|
|
26
|
+
end
|
|
27
|
+
documents_map[tag] = posts_with_tag
|
|
28
|
+
metadata_map[tag] = {
|
|
29
|
+
'pages' => tag_pages,
|
|
30
|
+
'index' => tag_pages.first
|
|
31
|
+
}
|
|
32
|
+
Jekyll.logger.info('CollectionPages:',
|
|
33
|
+
"Generated #{tag_pages.size} page(s) for tag '#{tag}' in collection '#{@config.collection_name}'.")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
set_metadata(documents_map, metadata_map)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def set_metadata(documents_map, metadata_map)
|
|
42
|
+
@site.data['collection_pages'] ||= {}
|
|
43
|
+
collection_registry = @site.data['collection_pages'][@config.collection_name] ||= {}
|
|
44
|
+
collection_registry[@config.tag_field] = {
|
|
45
|
+
'template' => @template,
|
|
46
|
+
'permalink' => @permalink,
|
|
47
|
+
'labels' => metadata_map,
|
|
48
|
+
'pages' => documents_map
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_page(site, tag, posts_with_tag, page_num)
|
|
53
|
+
tag_path = TagPath.new(@template, tag)
|
|
54
|
+
paginator = TagPager.new(page_num, @config.per_page, posts_with_tag)
|
|
55
|
+
|
|
56
|
+
paginator_liquid = paginator.to_liquid.merge(
|
|
57
|
+
'previous_page_path' => tag_path.url_for(paginator.previous_page),
|
|
58
|
+
'next_page_path' => tag_path.url_for(paginator.next_page)
|
|
59
|
+
)
|
|
60
|
+
TagPage.new(
|
|
61
|
+
site,
|
|
62
|
+
{
|
|
63
|
+
dir: tag_path.dir_for(page_num),
|
|
64
|
+
name: tag_path.filename_for(page_num),
|
|
65
|
+
title: tag,
|
|
66
|
+
tag: tag_path.tag,
|
|
67
|
+
layout: @config.tag_layout,
|
|
68
|
+
posts: paginator.posts,
|
|
69
|
+
page_num: page_num,
|
|
70
|
+
paginator: @config.per_page ? paginator_liquid : nil
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sorted_tags(site, collection_name, tag_field)
|
|
76
|
+
tags = {}
|
|
77
|
+
collection = site.collections[collection_name]
|
|
78
|
+
return [] unless collection
|
|
79
|
+
|
|
80
|
+
Jekyll.logger.debug('CollectionPages:', "Found collection '#{collection_name}' with #{collection.docs.size} entries.")
|
|
81
|
+
collection.docs.each do |doc|
|
|
82
|
+
doc_tags = doc.data[tag_field]
|
|
83
|
+
next unless doc_tags
|
|
84
|
+
|
|
85
|
+
doc_tags = [doc_tags] if doc_tags.is_a?(String)
|
|
86
|
+
doc_tags.each do |tag|
|
|
87
|
+
tags[tag] ||= []
|
|
88
|
+
tags[tag] << doc
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
tags.keys.sort.map { |tag| [tag, tags[tag]] }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module CollectionPages
|
|
5
|
+
INDEXFILE = 'index.html' unless const_defined?(:INDEXFILE)
|
|
6
|
+
|
|
7
|
+
class PathTemplate
|
|
8
|
+
attr_reader :tag_field, :collection_name
|
|
9
|
+
|
|
10
|
+
def initialize(raw_template:, tag_field:, collection_name:, require_num: true)
|
|
11
|
+
@tag_field = tag_field
|
|
12
|
+
@collection_name = collection_name
|
|
13
|
+
@require_num = require_num
|
|
14
|
+
@template = build_effective_template(raw_template)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def template
|
|
18
|
+
"/#{@template}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_effective_template(raw)
|
|
24
|
+
sanitized = sanitize_path(raw)
|
|
25
|
+
sanitized = default_placeholder if sanitized.empty?
|
|
26
|
+
sanitized = add_field_placeholder(sanitized)
|
|
27
|
+
sanitized = add_num_placeholder(sanitized) if @require_num
|
|
28
|
+
sanitized = add_index(sanitized)
|
|
29
|
+
Jekyll.logger.debug('CollectionPages:', "Using path template '#{sanitized}' for collection '#{@collection_name}'.")
|
|
30
|
+
validate_template(sanitized)
|
|
31
|
+
|
|
32
|
+
sanitized
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sanitize_path(path)
|
|
36
|
+
path.to_s.strip.sub(%r{^/+}, '').sub(%r{/+\z}, '')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def default_placeholder
|
|
40
|
+
@collection_name.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_field_placeholder(path)
|
|
44
|
+
return path if path.include?(':field')
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, "Path template '#{path}' must include a ':field' placeholder." if path.end_with?('.html') || path.end_with?('.htm')
|
|
47
|
+
|
|
48
|
+
"#{path}/:field"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_num_placeholder(path)
|
|
52
|
+
return path if path.include?(':num')
|
|
53
|
+
|
|
54
|
+
raise ArgumentError, "Path template '#{path}' must include a ':num' placeholder." if path.end_with?('.html') || path.end_with?('.htm')
|
|
55
|
+
|
|
56
|
+
"#{path}/page:num"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_index(path)
|
|
60
|
+
return path if path.end_with?('.html') || path.end_with?('.htm')
|
|
61
|
+
|
|
62
|
+
"#{path}/#{INDEXFILE}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_template(path)
|
|
66
|
+
field_count = path.scan(':field').size
|
|
67
|
+
num_count = path.scan(':num').size
|
|
68
|
+
|
|
69
|
+
error_msg = ''
|
|
70
|
+
error_msg += "Path template '#{path}' must include exactly one ':field' placeholder. " if field_count != 1
|
|
71
|
+
error_msg += "Path template '#{path}' must include exactly one ':num' placeholder. " if @require_num && num_count != 1
|
|
72
|
+
|
|
73
|
+
if num_count.positive? && field_count.positive?
|
|
74
|
+
field_idx = path.index(':field')
|
|
75
|
+
num_idx = path.index(':num')
|
|
76
|
+
error_msg += "In path template '#{path}', ':field' must come before ':num'. " if num_idx < field_idx
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
segments = path.split('/').reject(&:empty?)
|
|
80
|
+
segments.each do |segment|
|
|
81
|
+
if segment.include?(':field') && segment.include?(':num')
|
|
82
|
+
error_msg += "In path template '#{path}', ':field' and ':num' cannot be in the same file segment. "
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
raise ArgumentError, error_msg unless error_msg.empty?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module CollectionPages
|
|
5
|
+
class TagPage < PageWithoutAFile
|
|
6
|
+
def initialize(site, attributes)
|
|
7
|
+
dir = attributes[:dir]
|
|
8
|
+
name = attributes[:name]
|
|
9
|
+
tag = attributes[:tag]
|
|
10
|
+
title = attributes[:title]
|
|
11
|
+
layout = attributes[:layout]
|
|
12
|
+
posts = attributes[:posts]
|
|
13
|
+
page_num = attributes[:page_num]
|
|
14
|
+
paginator = attributes[:paginator]
|
|
15
|
+
|
|
16
|
+
# This sets up a page that has no source file on disk.
|
|
17
|
+
super(site, site.source, dir, name) # also calls process(name) internally
|
|
18
|
+
|
|
19
|
+
self.content = '' # virtual page body (optional)
|
|
20
|
+
|
|
21
|
+
self.data = {
|
|
22
|
+
'layout' => layout, # layout NAME (relative path, no _layouts prefix)
|
|
23
|
+
'tag' => tag.to_s,
|
|
24
|
+
'title' => title.to_s,
|
|
25
|
+
'posts' => posts,
|
|
26
|
+
'page_num' => page_num
|
|
27
|
+
}
|
|
28
|
+
data['paginator'] = paginator if paginator
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module CollectionPages
|
|
5
|
+
class TagPager
|
|
6
|
+
attr_reader :page, :per_page, :posts, :total_posts, :total_pages,
|
|
7
|
+
:previous_page, :next_page
|
|
8
|
+
|
|
9
|
+
LIQUID_MAP = {
|
|
10
|
+
'page' => :page, # the current page number
|
|
11
|
+
'per_page' => :per_page, # the number of posts per page
|
|
12
|
+
'posts' => :posts, # the paginated posts for this page
|
|
13
|
+
'total_posts' => :total_posts, # the total number of posts being paginated
|
|
14
|
+
'total_pages' => :total_pages, # the total number of pages
|
|
15
|
+
'previous_page' => :previous_page, # the previous page number, or nil
|
|
16
|
+
'next_page' => :next_page # the next page number, or nil
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def self.calculate_pages(all_posts, per_page)
|
|
20
|
+
per_page_value = per_page.to_i
|
|
21
|
+
return 1 if per_page_value <= 0
|
|
22
|
+
|
|
23
|
+
(all_posts.size.to_f / per_page_value).ceil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(page_num, per_page, all_posts)
|
|
27
|
+
@page = page_num
|
|
28
|
+
@per_page = per_page.to_i.positive? ? per_page.to_i : 0
|
|
29
|
+
@total_posts = all_posts.size
|
|
30
|
+
@total_pages = self.class.calculate_pages(all_posts, @per_page)
|
|
31
|
+
@posts = slice_posts(all_posts)
|
|
32
|
+
@previous_page = previous_page_number
|
|
33
|
+
@next_page = next_page_number(total_pages)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_liquid
|
|
37
|
+
LIQUID_MAP.transform_values { |reader| public_send(reader) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def slice_posts(all_posts)
|
|
43
|
+
return all_posts if @per_page <= 0
|
|
44
|
+
|
|
45
|
+
start_index = (@page - 1) * @per_page
|
|
46
|
+
all_posts.slice(start_index, @per_page) || []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def previous_page_number
|
|
50
|
+
@page > 1 ? @page - 1 : nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def next_page_number(total_pages)
|
|
54
|
+
@page < total_pages ? @page + 1 : nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module CollectionPages
|
|
5
|
+
class TagPath
|
|
6
|
+
attr_reader :tag
|
|
7
|
+
|
|
8
|
+
def initialize(template, field_value, slugify_value: true)
|
|
9
|
+
@template = template
|
|
10
|
+
@tag = slugify_value ? Utils.slugify(field_value.to_s) : field_value.to_s
|
|
11
|
+
@segments = template.split('/').reject(&:empty?)
|
|
12
|
+
@explicit_file = file_segment?(@segments.last) && @segments.last.include?(':field')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def dir_for(page_number)
|
|
16
|
+
segments = @segments
|
|
17
|
+
segments = apply_field_value(segments)
|
|
18
|
+
segments = page_number == 1 ? remove_paginated_segments(segments) : apply_page_number(segments, page_number)
|
|
19
|
+
segments = drop_file_segment(segments)
|
|
20
|
+
|
|
21
|
+
File.join(*segments)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def filename_for(page_number)
|
|
25
|
+
segments = @segments.last(1)
|
|
26
|
+
segments = apply_field_value(segments)
|
|
27
|
+
|
|
28
|
+
if page_number == 1
|
|
29
|
+
return segments.join if @explicit_file
|
|
30
|
+
|
|
31
|
+
return INDEXFILE
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
segments = apply_page_number(segments, page_number)
|
|
35
|
+
segments.join
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def url_for(page_number)
|
|
39
|
+
return nil if page_number.nil?
|
|
40
|
+
|
|
41
|
+
dir = dir_for(page_number)
|
|
42
|
+
filename = filename_for(page_number)
|
|
43
|
+
return formatted_index_path(dir) if filename == INDEXFILE
|
|
44
|
+
|
|
45
|
+
dir.empty? ? "/#{filename}" : "/#{dir}/#{filename}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def apply_field_value(segments)
|
|
51
|
+
segments.map do |segment|
|
|
52
|
+
segment.include?(':field') ? segment.gsub(':field', @tag) : segment
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def apply_page_number(segments, page_number)
|
|
57
|
+
segments.map do |segment|
|
|
58
|
+
segment.include?(':num') ? segment.gsub(':num', page_number.to_s) : segment
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def drop_file_segment(segments)
|
|
63
|
+
if file_segment?(segments.last)
|
|
64
|
+
segments[0...-1]
|
|
65
|
+
else
|
|
66
|
+
segments
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def file_segment?(segment)
|
|
71
|
+
segment && (segment.end_with?('.html') || segment.end_with?('.htm'))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def remove_paginated_segments(segments)
|
|
75
|
+
number_index = segments.index { |segment| segment.include?(':num') }
|
|
76
|
+
number_index ? segments[0...number_index] : segments
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def formatted_index_path(dir)
|
|
80
|
+
return '/' if dir.empty?
|
|
81
|
+
|
|
82
|
+
"/#{dir}/"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -1,165 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'collection_pages/collection_config'
|
|
4
|
+
require_relative 'collection_pages/collection_pager'
|
|
5
|
+
require_relative 'collection_pages/path_template'
|
|
6
|
+
require_relative 'collection_pages/tag_page'
|
|
7
|
+
require_relative 'collection_pages/tag_pager'
|
|
8
|
+
require_relative 'collection_pages/tag_path'
|
|
9
|
+
require_relative 'collection_pages/version'
|
|
10
|
+
|
|
3
11
|
module Jekyll
|
|
4
12
|
module CollectionPages
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class PathTemplate
|
|
8
|
-
def initialize(raw_template:, tag_field:, collection_name:)
|
|
9
|
-
@tag_field = tag_field
|
|
10
|
-
@collection_name = collection_name
|
|
11
|
-
@template = build_effective_template(raw_template)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def for_tag(tag_value)
|
|
15
|
-
TagPathResolver.new(@template, tag_value)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def permalink
|
|
19
|
-
TagPathResolver.new(@template, ':field', slugify_value: false).url_for(1)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def template
|
|
23
|
-
"/#{@template}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def build_effective_template(raw)
|
|
29
|
-
sanitized = sanitize_path(raw)
|
|
30
|
-
sanitized = default_placeholder if sanitized.empty?
|
|
31
|
-
sanitized = add_field_placeholder(sanitized)
|
|
32
|
-
sanitized = add_num_placeholder(sanitized)
|
|
33
|
-
sanitized = add_index(sanitized)
|
|
34
|
-
Jekyll.logger.debug('CollectionPages:', "Using path template '#{sanitized}' for collection '#{@collection_name}'.")
|
|
35
|
-
validate_template(sanitized)
|
|
36
|
-
|
|
37
|
-
sanitized
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def sanitize_path(path)
|
|
41
|
-
path.to_s.strip.sub(%r{^/+}, '').sub(%r{/+\z}, '')
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def default_placeholder
|
|
45
|
-
@collection_name.to_s
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def add_field_placeholder(path)
|
|
49
|
-
return path if path.include?(':field')
|
|
50
|
-
|
|
51
|
-
raise ArgumentError, "Path template '#{path}' must include a ':field' placeholder." if path.end_with?('.html') || path.end_with?('.htm')
|
|
52
|
-
|
|
53
|
-
"#{path}/:field"
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def add_num_placeholder(path)
|
|
57
|
-
return path if path.include?(':num')
|
|
58
|
-
|
|
59
|
-
raise ArgumentError, "Path template '#{path}' must include a ':num' placeholder." if path.end_with?('.html') || path.end_with?('.htm')
|
|
60
|
-
|
|
61
|
-
"#{path}/page:num"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def add_index(path)
|
|
65
|
-
return path if path.end_with?('.html') || path.end_with?('.htm')
|
|
66
|
-
|
|
67
|
-
"#{path}/#{CollectionPages::INDEXFILE}"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def validate_template(path)
|
|
71
|
-
field_count = path.scan(':field').size
|
|
72
|
-
num_count = path.scan(':num').size
|
|
73
|
-
|
|
74
|
-
error_msg = ''
|
|
75
|
-
error_msg += "Path template '#{path}' must include exactly one ':field' placeholder. " if field_count != 1
|
|
76
|
-
error_msg += "Path template '#{path}' must include exactly one ':num' placeholder. " if num_count != 1
|
|
77
|
-
|
|
78
|
-
if num_count.positive? && field_count.positive?
|
|
79
|
-
field_idx = path.index(':field')
|
|
80
|
-
num_idx = path.index(':num')
|
|
81
|
-
error_msg += "In path template '#{path}', ':field' must come before ':num'. " if num_idx < field_idx
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
segments = path.split('/').reject(&:empty?)
|
|
85
|
-
segments.each do |segment|
|
|
86
|
-
if segment.include?(':field') && segment.include?(':num')
|
|
87
|
-
error_msg += "In path template '#{path}', ':field' and ':num' cannot be in the same file segment. "
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
raise ArgumentError, error_msg unless error_msg.empty?
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
class TagPathResolver
|
|
95
|
-
def initialize(template, field_value, slugify_value: true)
|
|
96
|
-
@template = template
|
|
97
|
-
@value = slugify_value ? Utils.slugify(field_value.to_s) : field_value.to_s
|
|
98
|
-
@segments = template.split('/').reject(&:empty?)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def dir_for(page_number)
|
|
102
|
-
segments = @segments
|
|
103
|
-
segments = apply_field_value(segments)
|
|
104
|
-
segments = page_number == 1 ? remove_paginated_segments(segments) : apply_page_number(segments, page_number)
|
|
105
|
-
segments = drop_file_segment(segments)
|
|
106
|
-
|
|
107
|
-
File.join(*segments)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def filename_for(page_number)
|
|
111
|
-
return CollectionPages::INDEXFILE if page_number == 1
|
|
112
|
-
|
|
113
|
-
segments = @segments.last(1)
|
|
114
|
-
segments = apply_field_value(segments)
|
|
115
|
-
segments = apply_page_number(segments, page_number)
|
|
116
|
-
segments.join
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def url_for(page_number)
|
|
120
|
-
dir = dir_for(page_number)
|
|
121
|
-
filename = filename_for(page_number)
|
|
122
|
-
return formatted_index_path(dir) if filename == CollectionPages::INDEXFILE
|
|
123
|
-
|
|
124
|
-
dir.empty? ? "/#{filename}" : "/#{dir}/#{filename}"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
private
|
|
128
|
-
|
|
129
|
-
def apply_field_value(segments)
|
|
130
|
-
segments.map do |segment|
|
|
131
|
-
segment.include?(':field') ? segment.gsub(':field', @value) : segment
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def apply_page_number(segments, page_number)
|
|
136
|
-
segments.map do |segment|
|
|
137
|
-
segment.include?(':num') ? segment.gsub(':num', page_number.to_s) : segment
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def drop_file_segment(segments)
|
|
142
|
-
if segments.last.end_with?('.html') || segments.last.end_with?('.htm')
|
|
143
|
-
segments[0...-1]
|
|
144
|
-
else
|
|
145
|
-
segments
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def remove_paginated_segments(segments)
|
|
150
|
-
number_index = segments.index { |segment| segment.include?(':num') }
|
|
151
|
-
number_index ? segments[0...number_index] : segments
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def formatted_index_path(dir)
|
|
155
|
-
return '/' if dir.empty?
|
|
156
|
-
|
|
157
|
-
"/#{dir}/"
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
class TagPagination < Generator
|
|
13
|
+
class CollectionPages < Generator
|
|
163
14
|
safe true
|
|
164
15
|
priority :lowest
|
|
165
16
|
|
|
@@ -176,208 +27,19 @@ module Jekyll
|
|
|
176
27
|
config.each do |collection_config|
|
|
177
28
|
generate_for_config(site, collection_config)
|
|
178
29
|
end
|
|
30
|
+
else
|
|
31
|
+
Jekyll.logger.error('CollectionPages:', 'Invalid configuration.')
|
|
179
32
|
end
|
|
180
33
|
Jekyll.logger.debug('CollectionPages:', "Generation complete. Total pages: #{site.pages.size}")
|
|
181
34
|
end
|
|
182
35
|
|
|
183
36
|
def generate_for_config(site, config)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
Jekyll.logger.debug('CollectionPages:', "Generating pages for collection: #{collection_name}::#{tag_field}")
|
|
190
|
-
|
|
191
|
-
path_template = PathTemplate.new(raw_template: tag_base_path, tag_field: tag_field, collection_name: collection_name)
|
|
192
|
-
|
|
193
|
-
site.data['collection_pages'] ||= {}
|
|
194
|
-
|
|
195
|
-
documents_map, metadata_map = generate_paginated_tags(site, path_template, tag_layout, collection_name, tag_field, per_page)
|
|
196
|
-
|
|
197
|
-
collection_registry = site.data['collection_pages'][collection_name] ||= {}
|
|
198
|
-
collection_registry[tag_field] = {
|
|
199
|
-
'template' => path_template.template,
|
|
200
|
-
'permalink' => path_template.permalink,
|
|
201
|
-
'labels' => metadata_map,
|
|
202
|
-
'pages' => documents_map
|
|
203
|
-
}
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def sorted_tags(site, collection_name, tag_field)
|
|
207
|
-
tags = {}
|
|
208
|
-
collection = site.collections[collection_name]
|
|
209
|
-
return [] unless collection
|
|
210
|
-
|
|
211
|
-
Jekyll.logger.debug('CollectionPages:', "Found collection '#{collection_name}' with #{collection.docs.size} entries.")
|
|
212
|
-
collection.docs.each do |doc|
|
|
213
|
-
doc_tags = doc.data[tag_field]
|
|
214
|
-
next unless doc_tags
|
|
215
|
-
|
|
216
|
-
doc_tags = [doc_tags] if doc_tags.is_a?(String)
|
|
217
|
-
doc_tags.each do |tag|
|
|
218
|
-
tags[tag] ||= []
|
|
219
|
-
tags[tag] << doc
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
tags.keys.sort.map { |tag| [tag, tags[tag]] }
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def generate_paginated_tags(site, path_template, tag_layout, collection_name, tag_field, per_page)
|
|
226
|
-
tags_with_docs = sorted_tags(site, collection_name, tag_field)
|
|
227
|
-
|
|
228
|
-
documents_map = {}
|
|
229
|
-
metadata_map = {}
|
|
230
|
-
|
|
231
|
-
tags_with_docs.each do |tag, posts_with_tag|
|
|
232
|
-
tag_path = path_template.for_tag(tag)
|
|
233
|
-
|
|
234
|
-
page_count = TagPager.calculate_pages(posts_with_tag, per_page)
|
|
235
|
-
tag_pages = []
|
|
236
|
-
(1..page_count).each do |page_num|
|
|
237
|
-
paginator = TagPager.new(page_num, per_page, posts_with_tag, tag_path) if per_page
|
|
238
|
-
posts_for_page = paginator ? paginator.posts : posts_with_tag
|
|
239
|
-
page_dir = tag_path.dir_for(page_num)
|
|
240
|
-
page_filename = tag_path.filename_for(page_num)
|
|
241
|
-
tag_page = build_page(site, page_dir, page_filename, tag, tag_layout, posts_for_page, page_num, paginator)
|
|
242
|
-
site.pages << tag_page
|
|
243
|
-
tag_pages << tag_page
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
documents_map[tag] = posts_with_tag
|
|
247
|
-
metadata_map[tag] = {
|
|
248
|
-
'pages' => tag_pages,
|
|
249
|
-
'index' => tag_pages.first
|
|
250
|
-
}
|
|
251
|
-
Jekyll.logger.info('CollectionPages:',
|
|
252
|
-
"Generated #{tag_pages.size} page(s) for tag '#{tag}' in collection '#{collection_name}'.")
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
[documents_map, metadata_map]
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def build_page(site, dir, page_filename, tag, layout, posts, page_num, paginator = nil)
|
|
259
|
-
TagIndexPage.new(
|
|
260
|
-
site,
|
|
261
|
-
{
|
|
262
|
-
dir: dir,
|
|
263
|
-
name: page_filename,
|
|
264
|
-
tag: tag,
|
|
265
|
-
layout: layout,
|
|
266
|
-
posts: posts,
|
|
267
|
-
page_num: page_num,
|
|
268
|
-
paginator: paginator
|
|
269
|
-
}
|
|
270
|
-
)
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
private
|
|
274
|
-
|
|
275
|
-
def normalize_paginate_value(value, collection_name, tag_field)
|
|
276
|
-
return nil if value.nil?
|
|
277
|
-
|
|
278
|
-
per_page = Integer(value)
|
|
279
|
-
return per_page if per_page.positive?
|
|
280
|
-
|
|
281
|
-
Jekyll.logger.warn('CollectionPages:',
|
|
282
|
-
"Non-positive paginate value #{value.inspect} for collection '#{collection_name}' field '#{tag_field}'. " \
|
|
283
|
-
'Falling back to single page generation.')
|
|
284
|
-
nil
|
|
285
|
-
rescue ArgumentError, TypeError
|
|
286
|
-
raise ArgumentError,
|
|
287
|
-
"Invalid paginate value #{value.inspect} for collection '#{collection_name}' field '#{tag_field}'. Expected a numeric value."
|
|
37
|
+
collection_config = CollectionConfig.new(config)
|
|
38
|
+
Jekyll.logger.debug('CollectionPages:',
|
|
39
|
+
"Generating pages for collection: #{collection_config.collection_name}::#{collection_config.tag_field}")
|
|
40
|
+
collection_pager = CollectionPager.new(site, collection_config)
|
|
41
|
+
collection_pager.create_pages
|
|
288
42
|
end
|
|
289
43
|
end
|
|
290
44
|
end
|
|
291
|
-
|
|
292
|
-
class TagIndexPage < PageWithoutAFile
|
|
293
|
-
def initialize(site, attributes)
|
|
294
|
-
dir = attributes[:dir]
|
|
295
|
-
name = attributes[:name]
|
|
296
|
-
tag = attributes[:tag]
|
|
297
|
-
layout = attributes[:layout]
|
|
298
|
-
posts = attributes[:posts]
|
|
299
|
-
page_num = attributes[:page_num]
|
|
300
|
-
paginator = attributes[:paginator]
|
|
301
|
-
|
|
302
|
-
# This sets up a page that has no source file on disk.
|
|
303
|
-
super(site, site.source, dir, name) # also calls process(name) internally
|
|
304
|
-
|
|
305
|
-
self.content = '' # virtual page body (optional)
|
|
306
|
-
|
|
307
|
-
self.data = {
|
|
308
|
-
'layout' => File.basename(layout, '.*'), # layout NAME (no path)
|
|
309
|
-
'tag' => tag,
|
|
310
|
-
'title' => tag.to_s,
|
|
311
|
-
'posts' => posts,
|
|
312
|
-
'page_num' => page_num
|
|
313
|
-
}
|
|
314
|
-
data['paginator'] = paginator if paginator
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
class TagPager
|
|
319
|
-
attr_reader :page, :per_page, :posts, :total_posts, :total_pages,
|
|
320
|
-
:previous_page, :previous_page_path, :next_page, :next_page_path
|
|
321
|
-
|
|
322
|
-
LIQUID_MAP = {
|
|
323
|
-
'page' => :page, # the current page number
|
|
324
|
-
'per_page' => :per_page, # the number of posts per page
|
|
325
|
-
'posts' => :posts, # the paginated posts for this page
|
|
326
|
-
'total_posts' => :total_posts, # the total number of posts being paginated
|
|
327
|
-
'total_pages' => :total_pages, # the total number of pages
|
|
328
|
-
'previous_page' => :previous_page, # the previous page number, or nil
|
|
329
|
-
'previous_page_path' => :previous_page_path, # the previous page path, or nil
|
|
330
|
-
'next_page' => :next_page, # the next page number, or nil
|
|
331
|
-
'next_page_path' => :next_page_path # the next page path, or nil
|
|
332
|
-
}.freeze
|
|
333
|
-
|
|
334
|
-
def self.calculate_pages(all_posts, per_page)
|
|
335
|
-
per_page_value = per_page.to_i
|
|
336
|
-
return 1 if per_page_value <= 0
|
|
337
|
-
|
|
338
|
-
(all_posts.size.to_f / per_page_value).ceil
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def initialize(page_num, per_page, all_posts, path_resolver)
|
|
342
|
-
@page = page_num
|
|
343
|
-
@per_page = per_page.to_i.positive? ? per_page.to_i : 0
|
|
344
|
-
@total_posts = all_posts.size
|
|
345
|
-
@total_pages = self.class.calculate_pages(all_posts, @per_page)
|
|
346
|
-
@posts = slice_posts(all_posts)
|
|
347
|
-
@path_resolver = path_resolver
|
|
348
|
-
@previous_page = previous_page_number
|
|
349
|
-
@next_page = next_page_number(total_pages)
|
|
350
|
-
@previous_page_path = page_path(@previous_page)
|
|
351
|
-
@next_page_path = page_path(@next_page)
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def to_liquid
|
|
355
|
-
LIQUID_MAP.transform_values { |reader| public_send(reader) }
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
private
|
|
359
|
-
|
|
360
|
-
attr_reader :path_resolver
|
|
361
|
-
|
|
362
|
-
def slice_posts(all_posts)
|
|
363
|
-
return all_posts if @per_page <= 0
|
|
364
|
-
|
|
365
|
-
start_index = (@page - 1) * @per_page
|
|
366
|
-
all_posts.slice(start_index, @per_page) || []
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
def previous_page_number
|
|
370
|
-
@page > 1 ? @page - 1 : nil
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
def next_page_number(total_pages)
|
|
374
|
-
@page < total_pages ? @page + 1 : nil
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def page_path(target_page)
|
|
378
|
-
return unless target_page && target_page <= total_pages
|
|
379
|
-
|
|
380
|
-
path_resolver.url_for(target_page)
|
|
381
|
-
end
|
|
382
|
-
end
|
|
383
45
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-collection-pages
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- allison@allisonthackston.com
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-07-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -96,6 +96,13 @@ files:
|
|
|
96
96
|
- jekyll-collection-pages.gemspec
|
|
97
97
|
- lib/jekyll-collection-pages.rb
|
|
98
98
|
- lib/jekyll/collection_pages.rb
|
|
99
|
+
- lib/jekyll/collection_pages/collection_config.rb
|
|
100
|
+
- lib/jekyll/collection_pages/collection_pager.rb
|
|
101
|
+
- lib/jekyll/collection_pages/path_template.rb
|
|
102
|
+
- lib/jekyll/collection_pages/tag_page.rb
|
|
103
|
+
- lib/jekyll/collection_pages/tag_pager.rb
|
|
104
|
+
- lib/jekyll/collection_pages/tag_path.rb
|
|
105
|
+
- lib/jekyll/collection_pages/version.rb
|
|
99
106
|
homepage: https://github.com/PrimerPages/jekyll-collection-pages
|
|
100
107
|
licenses:
|
|
101
108
|
- MIT
|