pandorga 1.11.0 → 1.13.0
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/CHANGELOG.md +12 -0
- data/DESIGN.md +6 -8
- data/_includes/content-runtime/43-blocks.html +26 -4
- data/_includes/content-runtime/60-fragments.html +1 -0
- data/_layouts/projects.html +7 -1
- data/lib/pandorga/cli.rb +4 -1
- data/lib/pandorga/commands/install_functions.rb +36 -1
- data/lib/pandorga/commands/new.rb +1 -1
- data/lib/pandorga/commands/studio_schema.rb +162 -54
- data/lib/pandorga/commands/thumbnails.rb +19 -0
- data/lib/pandorga/registry/schema.yml +1 -1
- data/lib/pandorga/thumbnail_variants.rb +267 -0
- data/lib/pandorga/version.rb +1 -1
- data/lib/pandorga.rb +1 -0
- data/scripts/content/export-content-json.rb +22 -1
- data/scripts/test/fixtures/studio-taxonomy/page-local/_config.yml +9 -0
- data/scripts/test/fixtures/studio-taxonomy/top-level/_config.yml +9 -0
- data/scripts/test/test-poster-srcset.mjs +159 -0
- data/scripts/test/test-studio-taxonomy.rb +246 -0
- data/scripts/test/test-thumbnail-variants.rb +380 -0
- data/scripts/validate.sh +7 -0
- data/studio/schema.yml +4 -17
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c03095737ae31747d865ba734912bb725e2d88586405dd847c63d340b42bbfa3
|
|
4
|
+
data.tar.gz: 764bc60f9ef00c00e33391511a7cb5680438660dd5a622af0801add004670f31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 392a4fbde9ea59d1b7dfea8206f45c81f6d104d3b70efcb9a6c67afa21fc9738eefc589b537c8e045bdf7cdb13527ade3ec8c3a1d5683ff0c4f5134a72743dbc
|
|
7
|
+
data.tar.gz: 44b0d25b56e010e2ce1d2106c3a2c688a5b3ee61c322b55f39c89beca4d9821c31f5942de335f351289bab176a6752c809205ae7aae97f517351003eb71ce0a5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.13.0] — 2026-10-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `pandorga export` and the new `pandorga thumbnails [root]` command write 16:9 center-cropped width variants (`variants/<stem>-{480,960,1440}.jpg`, progressive JPEG at quality 82, stripped of metadata, never upscaled) beside each local `/media/...` project thumbnail when ImageMagick is installed (logging one warning otherwise), export `thumbnail_srcset` on project front matter and `writing_index.json`, and render `srcset` and `sizes` on project cards and the home specimen (PLT-AC-44).
|
|
10
|
+
|
|
11
|
+
## [1.12.0] — 2026-10-19
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `pandorga install-functions` fills the Studio tag pickers from the site's `_config.yml` → `pandorga.taxonomies` (with `min` and `max`), falling back to a page's `taxonomy`, then applies `studio.overrides.yml`. The gem's `studio/schema.yml` no longer ships a site's tag vocabulary; a collection without one gets an empty picker and a build warning (PLT-AC-43).
|
|
16
|
+
|
|
5
17
|
## [1.11.0] — 2026-10-19
|
|
6
18
|
|
|
7
19
|
### Fixed
|
data/DESIGN.md
CHANGED
|
@@ -1201,14 +1201,12 @@ discs.
|
|
|
1201
1201
|
An element renders only if a populated field backs it. The classification axes,
|
|
1202
1202
|
as enforced by [`_plugins/lib/tag_validator.rb`](_plugins/lib/tag_validator.rb):
|
|
1203
1203
|
|
|
1204
|
-
* **Articles** classify on `tags`, one to three, from
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
the field may be empty — a notebook holds pages that file nowhere. 29 of 30
|
|
1211
|
-
carry at least one.
|
|
1204
|
+
* **Articles** classify on `tags`, one to three, from the closed vocabulary
|
|
1205
|
+
the site declares in `_config.yml` → `pandorga.taxonomies`.
|
|
1206
|
+
* **Rascunhos** classify on `tags` too, **zero to three**, from the closed
|
|
1207
|
+
vocabulary the site declares in `_config.yml` → `pandorga.taxonomies`.
|
|
1208
|
+
Unlike articles the field may be empty — a notebook holds pages that file
|
|
1209
|
+
nowhere.
|
|
1212
1210
|
* `category` is rejected on both collections — one classification axis, not
|
|
1213
1211
|
two.
|
|
1214
1212
|
* **Projects** carry free `tags` (all 8 do), plus `label`, `status`,
|
|
@@ -300,7 +300,7 @@
|
|
|
300
300
|
*
|
|
301
301
|
* A video URL still renders rather than vanishing, but it downloads
|
|
302
302
|
* nothing until the reader asks for it. */
|
|
303
|
-
function renderPosterMedia(src, alt) {
|
|
303
|
+
function renderPosterMedia(src, alt, options) {
|
|
304
304
|
var url = String(src || '').trim();
|
|
305
305
|
if (!url) return '';
|
|
306
306
|
var isVideo = /\.(mp4|webm|mov)(\?|#|$)/i.test(url);
|
|
@@ -312,9 +312,15 @@
|
|
|
312
312
|
'</div>'
|
|
313
313
|
].join('');
|
|
314
314
|
}
|
|
315
|
+
var opts = options || {};
|
|
316
|
+
var srcset = String(opts.srcset || '').trim();
|
|
317
|
+
var sizes = String(opts.sizes || '').trim();
|
|
315
318
|
return [
|
|
316
319
|
'<div class="ledger-poster">',
|
|
317
|
-
'<img src="', escapeAttr(url), '"
|
|
320
|
+
'<img src="', escapeAttr(url), '"',
|
|
321
|
+
srcset ? ' srcset="' + escapeAttr(srcset) + '"' : '',
|
|
322
|
+
(srcset && sizes) ? ' sizes="' + escapeAttr(sizes) + '"' : '',
|
|
323
|
+
' alt="', escapeAttr(alt || ''),
|
|
318
324
|
'" loading="lazy" decoding="async"',
|
|
319
325
|
// Thumbnails are frequently hotlinked to third-party hosts. When one
|
|
320
326
|
// dies, remove the frame rather than leaving an empty plate behind.
|
|
@@ -324,6 +330,16 @@
|
|
|
324
330
|
].join('');
|
|
325
331
|
}
|
|
326
332
|
|
|
333
|
+
/* Project thumbnail sizes (PLT-AC-44):
|
|
334
|
+
* - Home specimen (home.css PLT-AC-41): >= 900px column is max(35%, 20rem)
|
|
335
|
+
* inside max-width 80rem (1408px at html 110%), minus card padding =>
|
|
336
|
+
* ~431 CSS px at 1440px viewport (31vw = 446px -> 480w at DPR 1, 960w at
|
|
337
|
+
* DPR 2); below 900px full band width (390px at DPR 2 = 780px -> 960w).
|
|
338
|
+
* - Projects grid card (projects.css): 3 cols >= 1024px (~384px -> 28vw),
|
|
339
|
+
* 2 cols >= 700px (~46vw), 1 col below (92vw). */
|
|
340
|
+
var PROJECT_CARD_SIZES = '(min-width: 1024px) 28vw, (min-width: 700px) 46vw, 92vw';
|
|
341
|
+
var HOME_SPECIMEN_SIZES = '(min-width: 900px) 31vw, 92vw';
|
|
342
|
+
|
|
327
343
|
function twoDigits(value) {
|
|
328
344
|
var label = String(Number(value));
|
|
329
345
|
return label.length < 2 ? '0' + label : label;
|
|
@@ -418,7 +434,10 @@
|
|
|
418
434
|
if (options.frameCorners) {
|
|
419
435
|
html += '<span class="ledger-frame-corners" aria-hidden="true"></span>';
|
|
420
436
|
}
|
|
421
|
-
var poster = renderPosterMedia(fm.thumbnail, name
|
|
437
|
+
var poster = renderPosterMedia(fm.thumbnail, name, {
|
|
438
|
+
srcset: fm.thumbnail_srcset,
|
|
439
|
+
sizes: options.sizes || PROJECT_CARD_SIZES
|
|
440
|
+
});
|
|
422
441
|
if (poster) {
|
|
423
442
|
html += poster;
|
|
424
443
|
} else if (options.glyphFallback && fm.icon) {
|
|
@@ -501,7 +520,10 @@
|
|
|
501
520
|
var mark = fm.icon
|
|
502
521
|
? { cls: 'material-symbols-outlined', text: fm.icon }
|
|
503
522
|
: { cls: 'is-initial', text: String(name || '').charAt(0).toUpperCase() };
|
|
504
|
-
var poster = renderPosterMedia(fm.thumbnail, name
|
|
523
|
+
var poster = renderPosterMedia(fm.thumbnail, name, {
|
|
524
|
+
srcset: fm.thumbnail_srcset,
|
|
525
|
+
sizes: options.sizes || HOME_SPECIMEN_SIZES
|
|
526
|
+
});
|
|
505
527
|
html += '<div class="home-specimen-figure">';
|
|
506
528
|
if (poster) {
|
|
507
529
|
html += poster + '<span class="home-specimen-badge ' + mark.cls + '" aria-hidden="true">' +
|
data/_layouts/projects.html
CHANGED
|
@@ -154,7 +154,13 @@ layout: shell
|
|
|
154
154
|
imagery still reads as finished rather than as broken. */
|
|
155
155
|
function plateHtml(item, modifier) {
|
|
156
156
|
var fm = item.front_matter || {};
|
|
157
|
-
var
|
|
157
|
+
var sizes = modifier === 'projects-plate--card'
|
|
158
|
+
? '(min-width: 1024px) 28vw, (min-width: 700px) 46vw, 92vw'
|
|
159
|
+
: '(min-width: 900px) 38vw, 92vw';
|
|
160
|
+
var poster = runtime.renderPosterMedia(fm.thumbnail, projectName(item), {
|
|
161
|
+
srcset: fm.thumbnail_srcset,
|
|
162
|
+
sizes: sizes
|
|
163
|
+
});
|
|
158
164
|
var cls = 'projects-plate' + (modifier ? ' ' + modifier : '');
|
|
159
165
|
if (poster) return '<div class="' + cls + '">' + poster + '</div>';
|
|
160
166
|
if (!fm.icon) return '';
|
data/lib/pandorga/cli.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "commands/publish"
|
|
|
7
7
|
require_relative "commands/install_functions"
|
|
8
8
|
require_relative "commands/studio_schema"
|
|
9
9
|
require_relative "commands/new"
|
|
10
|
+
require_relative "commands/thumbnails"
|
|
10
11
|
|
|
11
12
|
module Pandorga
|
|
12
13
|
# Dispatch for `exe/pandorga`.
|
|
@@ -18,7 +19,8 @@ module Pandorga
|
|
|
18
19
|
"serve" => Commands::Serve,
|
|
19
20
|
"publish" => Commands::Publish,
|
|
20
21
|
"install-functions" => Commands::InstallFunctions,
|
|
21
|
-
"studio-schema" => Commands::StudioSchema
|
|
22
|
+
"studio-schema" => Commands::StudioSchema,
|
|
23
|
+
"thumbnails" => Commands::Thumbnails
|
|
22
24
|
}.freeze
|
|
23
25
|
|
|
24
26
|
module_function
|
|
@@ -52,6 +54,7 @@ module Pandorga
|
|
|
52
54
|
pandorga new PATH Scaffold a site from the minimal example
|
|
53
55
|
pandorga doctor Check site config and environment
|
|
54
56
|
pandorga export [src] [dest] Export content collections to JSON
|
|
57
|
+
pandorga thumbnails [root] Write 16:9 width variants for project thumbnails
|
|
55
58
|
pandorga serve Local Jekyll serve with JSON re-export
|
|
56
59
|
pandorga publish Sync content JSON to R2
|
|
57
60
|
pandorga install-functions Copy functions/ and studio/ into site root
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "date"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "pathname"
|
|
6
|
+
require "yaml"
|
|
7
|
+
require "pandorga/commands/studio_schema"
|
|
5
8
|
|
|
6
9
|
module Pandorga
|
|
7
10
|
module Commands
|
|
8
11
|
# Option A (PLT-0.2): copy functions/ and studio/ from the gem into the site root
|
|
9
|
-
# so Cloudflare Pages can compile Functions from the project root
|
|
12
|
+
# so Cloudflare Pages can compile Functions from the project root, then apply
|
|
13
|
+
# site taxonomies and overrides to studio/schema.yml (PLT-AC-43).
|
|
10
14
|
class InstallFunctions
|
|
11
15
|
STAMP = ".pandorga-version"
|
|
12
16
|
|
|
@@ -22,6 +26,7 @@ module Pandorga
|
|
|
22
26
|
|
|
23
27
|
copy_tree(gem_functions, dest_root.join("functions"))
|
|
24
28
|
copy_tree(gem_studio, dest_root.join("studio")) if gem_studio.directory?
|
|
29
|
+
hydrate_studio_schema(dest_root)
|
|
25
30
|
|
|
26
31
|
stamp = dest_root.join("functions", STAMP)
|
|
27
32
|
stamp.write("#{Pandorga::VERSION}\n")
|
|
@@ -36,6 +41,36 @@ module Pandorga
|
|
|
36
41
|
FileUtils.mkdir_p(dest.dirname)
|
|
37
42
|
FileUtils.cp_r(src, dest)
|
|
38
43
|
end
|
|
44
|
+
|
|
45
|
+
def hydrate_studio_schema(dest_root)
|
|
46
|
+
config_path = dest_root.join("_config.yml")
|
|
47
|
+
schema_path = dest_root.join("studio", "schema.yml")
|
|
48
|
+
return unless config_path.file? && schema_path.file?
|
|
49
|
+
|
|
50
|
+
config = load_yaml(config_path)
|
|
51
|
+
schema = load_yaml(schema_path)
|
|
52
|
+
return unless schema.is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
missing = StudioSchema.apply_taxonomies!(schema, config)
|
|
55
|
+
|
|
56
|
+
overrides_path = dest_root.join("studio.overrides.yml")
|
|
57
|
+
if overrides_path.file?
|
|
58
|
+
extra = load_yaml(overrides_path)
|
|
59
|
+
StudioSchema.apply_overrides!(schema, extra) if extra.is_a?(Hash)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
header = "# Installed by pandorga install-functions #{Pandorga::VERSION}; " \
|
|
63
|
+
"tag vocabularies come from _config.yml pandorga.taxonomies.\n"
|
|
64
|
+
schema_path.write("#{header}#{YAML.dump(schema)}")
|
|
65
|
+
|
|
66
|
+
missing.each do |name|
|
|
67
|
+
warn "pandorga install-functions: warning: no taxonomy for #{name}; its Studio tags picker is empty"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def load_yaml(path)
|
|
72
|
+
YAML.safe_load(path.read, permitted_classes: [Date, Time], aliases: true) || {}
|
|
73
|
+
end
|
|
39
74
|
end
|
|
40
75
|
end
|
|
41
76
|
end
|
|
@@ -24,7 +24,7 @@ module Pandorga
|
|
|
24
24
|
|
|
25
25
|
gem "pandorga", "~> 1.3"
|
|
26
26
|
# Local checkout: gem "pandorga", path: "../pandorga"
|
|
27
|
-
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.
|
|
27
|
+
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.13.0"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
|
@@ -8,7 +8,7 @@ require "date"
|
|
|
8
8
|
module Pandorga
|
|
9
9
|
module Commands
|
|
10
10
|
# Compose studio/schema.yml from template studio.yml files + registry.
|
|
11
|
-
# Refs: PLT-AC-3, docs/spec.md §5.5
|
|
11
|
+
# Refs: PLT-AC-3, PLT-AC-43, docs/spec.md §5.5
|
|
12
12
|
class StudioSchema
|
|
13
13
|
DEFAULT_MEDIA = {
|
|
14
14
|
"name" => "media",
|
|
@@ -28,6 +28,14 @@ module Pandorga
|
|
|
28
28
|
"options" => { "format" => "markdown" }
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
|
+
def self.apply_taxonomies!(schema, config)
|
|
32
|
+
new.apply_taxonomies!(schema, config)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.apply_overrides!(schema, extra)
|
|
36
|
+
new.apply_overrides!(schema, extra)
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
def run(argv)
|
|
32
40
|
root = Pathname.new(argv[0] || Dir.pwd).expand_path
|
|
33
41
|
templates_root = Pathname.new(Pandorga.gem_path("templates"))
|
|
@@ -56,7 +64,6 @@ module Pandorga
|
|
|
56
64
|
# Public for gates: build the schema hash without writing.
|
|
57
65
|
def compose(config, templates_root)
|
|
58
66
|
pages = config.dig("pandorga", "pages") || []
|
|
59
|
-
taxonomies = config.dig("pandorga", "taxonomies") || {}
|
|
60
67
|
groups = {}
|
|
61
68
|
group_order = []
|
|
62
69
|
needs_media = false
|
|
@@ -76,7 +83,7 @@ module Pandorga
|
|
|
76
83
|
next unless fragment.is_a?(Hash)
|
|
77
84
|
|
|
78
85
|
needs_media ||= truthy?(fragment["needs_media"])
|
|
79
|
-
contributions = expand_fragment(fragment, page
|
|
86
|
+
contributions = expand_fragment(fragment, page)
|
|
80
87
|
contributions.each do |contrib|
|
|
81
88
|
group_name = contrib[:group]
|
|
82
89
|
unless groups.key?(group_name)
|
|
@@ -110,12 +117,88 @@ module Pandorga
|
|
|
110
117
|
if needs_markdown
|
|
111
118
|
schema["components"] = { "markdown_body" => deep_dup(DEFAULT_MARKDOWN_BODY) }
|
|
112
119
|
end
|
|
120
|
+
apply_taxonomies!(schema, config)
|
|
113
121
|
schema
|
|
114
122
|
end
|
|
115
123
|
|
|
124
|
+
def apply_taxonomies!(schema, config)
|
|
125
|
+
return [] unless schema.is_a?(Hash)
|
|
126
|
+
|
|
127
|
+
vocabularies = resolve_taxonomies(config)
|
|
128
|
+
missing = []
|
|
129
|
+
|
|
130
|
+
Array(schema["content"]).each do |group|
|
|
131
|
+
next unless group.is_a?(Hash)
|
|
132
|
+
|
|
133
|
+
Array(group["items"]).each do |item|
|
|
134
|
+
next unless item.is_a?(Hash)
|
|
135
|
+
next unless item["type"].to_s == "collection"
|
|
136
|
+
|
|
137
|
+
name = item["name"].to_s
|
|
138
|
+
next if name.empty?
|
|
139
|
+
|
|
140
|
+
had_picker = apply_collection_taxonomy!(item, vocabularies[name])
|
|
141
|
+
missing << name if had_picker && !vocabularies.key?(name)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
index = schema["collections"].is_a?(Hash) ? schema["collections"] : {}
|
|
146
|
+
index.each do |key, entry|
|
|
147
|
+
next unless entry.is_a?(Hash)
|
|
148
|
+
|
|
149
|
+
name = entry["name"].to_s.empty? ? key.to_s : entry["name"].to_s
|
|
150
|
+
next if name.empty?
|
|
151
|
+
|
|
152
|
+
had_picker = apply_collection_taxonomy!(entry, vocabularies[name])
|
|
153
|
+
missing << name if had_picker && !vocabularies.key?(name)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
missing.uniq
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def apply_overrides!(schema, extra)
|
|
160
|
+
if extra["media"].is_a?(Hash)
|
|
161
|
+
schema["media"] = deep_merge(schema["media"] || {}, extra["media"])
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if extra["components"].is_a?(Hash)
|
|
165
|
+
schema["components"] = deep_merge(schema["components"] || {}, extra["components"])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if extra["collections"].is_a?(Hash)
|
|
169
|
+
extra["collections"].each do |name, patch|
|
|
170
|
+
next unless patch.is_a?(Hash)
|
|
171
|
+
|
|
172
|
+
item = find_content_item(schema, name.to_s)
|
|
173
|
+
if item
|
|
174
|
+
merged = deep_merge(item, patch)
|
|
175
|
+
item.replace(merged)
|
|
176
|
+
end
|
|
177
|
+
if schema["collections"].is_a?(Hash) && schema["collections"][name.to_s]
|
|
178
|
+
schema["collections"][name.to_s] =
|
|
179
|
+
deep_merge(schema["collections"][name.to_s], patch)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
return unless extra["content"].is_a?(Array)
|
|
185
|
+
|
|
186
|
+
schema["content"] ||= []
|
|
187
|
+
extra["content"].each do |group|
|
|
188
|
+
next unless group.is_a?(Hash)
|
|
189
|
+
|
|
190
|
+
existing = schema["content"].find { |g| g["name"].to_s == group["name"].to_s }
|
|
191
|
+
if existing
|
|
192
|
+
existing.replace(deep_merge(existing, group))
|
|
193
|
+
else
|
|
194
|
+
schema["content"] << deep_dup(group)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
116
199
|
private
|
|
117
200
|
|
|
118
|
-
def expand_fragment(fragment, page
|
|
201
|
+
def expand_fragment(fragment, page)
|
|
119
202
|
contributions = []
|
|
120
203
|
default_group = fragment["group"].to_s
|
|
121
204
|
default_group = page["template"].to_s if default_group.empty?
|
|
@@ -127,12 +210,7 @@ module Pandorga
|
|
|
127
210
|
if fragment["collection"].is_a?(Hash)
|
|
128
211
|
name = page["collection"].to_s
|
|
129
212
|
unless name.empty?
|
|
130
|
-
items << instantiate_collection(
|
|
131
|
-
fragment["collection"],
|
|
132
|
-
name,
|
|
133
|
-
page,
|
|
134
|
-
taxonomies
|
|
135
|
-
)
|
|
213
|
+
items << instantiate_collection(fragment["collection"], name, page)
|
|
136
214
|
end
|
|
137
215
|
end
|
|
138
216
|
|
|
@@ -145,7 +223,7 @@ module Pandorga
|
|
|
145
223
|
name = slot.to_s if name.empty?
|
|
146
224
|
next if name.empty? || name == "data" || name == "profile"
|
|
147
225
|
|
|
148
|
-
items << instantiate_collection(spec, name, page
|
|
226
|
+
items << instantiate_collection(spec, name, page)
|
|
149
227
|
end
|
|
150
228
|
end
|
|
151
229
|
|
|
@@ -178,7 +256,7 @@ module Pandorga
|
|
|
178
256
|
contributions
|
|
179
257
|
end
|
|
180
258
|
|
|
181
|
-
def instantiate_collection(spec, name, page
|
|
259
|
+
def instantiate_collection(spec, name, page)
|
|
182
260
|
entry = deep_dup(spec)
|
|
183
261
|
entry["name"] = name
|
|
184
262
|
entry["type"] ||= "collection"
|
|
@@ -189,66 +267,96 @@ module Pandorga
|
|
|
189
267
|
end
|
|
190
268
|
entry["path"] ||= "content/collections/#{name}"
|
|
191
269
|
entry["path"] = entry["path"].to_s.gsub("{collection}", name)
|
|
192
|
-
apply_taxonomy!(entry, name, taxonomies)
|
|
193
270
|
entry
|
|
194
271
|
end
|
|
195
272
|
|
|
196
|
-
def
|
|
197
|
-
|
|
198
|
-
return unless tax.is_a?(Hash)
|
|
273
|
+
def resolve_taxonomies(config)
|
|
274
|
+
return {} unless config.is_a?(Hash)
|
|
199
275
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
276
|
+
vocab = {}
|
|
277
|
+
pages = Array(config.dig("pandorga", "pages")).select { |p| p.is_a?(Hash) }
|
|
278
|
+
taxonomies = config.dig("pandorga", "taxonomies")
|
|
203
279
|
|
|
204
|
-
|
|
205
|
-
|
|
280
|
+
if taxonomies.is_a?(Hash)
|
|
281
|
+
taxonomies.each do |collection, spec|
|
|
282
|
+
next unless spec.is_a?(Hash)
|
|
206
283
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
max = tax["max"]
|
|
210
|
-
field["options"]["max"] = max if max
|
|
211
|
-
end
|
|
212
|
-
end
|
|
284
|
+
tags = spec["tags"]
|
|
285
|
+
next unless tags.is_a?(Array) && !tags.empty?
|
|
213
286
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
end
|
|
287
|
+
coll_name = collection.to_s
|
|
288
|
+
page_tax = pages.find { |p| p["collection"].to_s == coll_name }&.fetch("taxonomy", nil)
|
|
289
|
+
page_tax = nil unless page_tax.is_a?(Hash)
|
|
218
290
|
|
|
219
|
-
|
|
220
|
-
|
|
291
|
+
max = spec["max"].nil? ? page_tax&.fetch("max", nil) : spec["max"]
|
|
292
|
+
min = spec["min"].nil? ? page_tax&.fetch("min", nil) : spec["min"]
|
|
293
|
+
|
|
294
|
+
vocab[coll_name] = build_taxonomy_entry(tags, max: max, min: min)
|
|
295
|
+
end
|
|
221
296
|
end
|
|
222
297
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
298
|
+
pages.each do |page|
|
|
299
|
+
coll_name = page["collection"].to_s
|
|
300
|
+
next if coll_name.empty? || vocab.key?(coll_name)
|
|
226
301
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
deep_merge(schema["collections"][name.to_s], patch)
|
|
235
|
-
end
|
|
236
|
-
end
|
|
302
|
+
spec = page["taxonomy"]
|
|
303
|
+
next unless spec.is_a?(Hash)
|
|
304
|
+
|
|
305
|
+
tags = spec["tags"]
|
|
306
|
+
next unless tags.is_a?(Array) && !tags.empty?
|
|
307
|
+
|
|
308
|
+
vocab[coll_name] = build_taxonomy_entry(tags, max: spec["max"], min: spec["min"])
|
|
237
309
|
end
|
|
238
310
|
|
|
239
|
-
|
|
311
|
+
vocab
|
|
312
|
+
end
|
|
240
313
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
314
|
+
def build_taxonomy_entry(tags, max:, min:)
|
|
315
|
+
entry = { "tags" => tags.map(&:to_s) }
|
|
316
|
+
parsed_max = parse_int_or_nil(max)
|
|
317
|
+
parsed_min = parse_int_or_nil(min)
|
|
318
|
+
entry["max"] = parsed_max unless parsed_max.nil?
|
|
319
|
+
entry["min"] = parsed_min unless parsed_min.nil?
|
|
320
|
+
entry
|
|
321
|
+
end
|
|
244
322
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
323
|
+
def parse_int_or_nil(val)
|
|
324
|
+
return nil if val.nil?
|
|
325
|
+
|
|
326
|
+
Integer(val)
|
|
327
|
+
rescue ArgumentError, TypeError
|
|
328
|
+
nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def apply_collection_taxonomy!(entry, tax)
|
|
332
|
+
had_picker = false
|
|
333
|
+
Array(entry["fields"]).each do |field|
|
|
334
|
+
next unless tag_picker_field?(field)
|
|
335
|
+
|
|
336
|
+
had_picker = true
|
|
337
|
+
field["options"] = {} unless field["options"].is_a?(Hash)
|
|
338
|
+
opts = field["options"]
|
|
339
|
+
|
|
340
|
+
if tax.is_a?(Hash)
|
|
341
|
+
opts["values"] = Array(tax["tags"]).map(&:to_s)
|
|
342
|
+
opts["max"] = tax["max"] unless tax["max"].nil?
|
|
343
|
+
if tax["min"].nil?
|
|
344
|
+
opts.delete("min")
|
|
345
|
+
else
|
|
346
|
+
opts["min"] = tax["min"]
|
|
347
|
+
end
|
|
248
348
|
else
|
|
249
|
-
|
|
349
|
+
opts["values"] = []
|
|
350
|
+
opts.delete("min")
|
|
250
351
|
end
|
|
251
352
|
end
|
|
353
|
+
had_picker
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def tag_picker_field?(field)
|
|
357
|
+
field.is_a?(Hash) &&
|
|
358
|
+
field["name"].to_s == "tags" &&
|
|
359
|
+
field["type"].to_s == "select"
|
|
252
360
|
end
|
|
253
361
|
|
|
254
362
|
def find_content_item(schema, name)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require_relative "../thumbnail_variants"
|
|
5
|
+
|
|
6
|
+
module Pandorga
|
|
7
|
+
module Commands
|
|
8
|
+
# Generate 16:9 center-cropped width variants for local project thumbnails.
|
|
9
|
+
# Refs: PLT-AC-44
|
|
10
|
+
class Thumbnails
|
|
11
|
+
def run(argv)
|
|
12
|
+
site_root = Pathname.new(argv[0] || Dir.pwd).expand_path
|
|
13
|
+
summary = Pandorga::ThumbnailVariants.generate(site_root)
|
|
14
|
+
puts "pandorga thumbnails: #{summary[:written]} written, #{summary[:skipped]} skipped (#{summary[:sources]} sources in #{site_root})"
|
|
15
|
+
0
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -79,7 +79,7 @@ taxonomies:
|
|
|
79
79
|
prose: |
|
|
80
80
|
Closed tag vocabularies per collection (`tags`, `max`, `min`). Page-local
|
|
81
81
|
`pandorga.pages[].taxonomy` is also accepted when a collection has no top-level
|
|
82
|
-
entry.
|
|
82
|
+
entry. `pandorga install-functions` copies these vocabularies into the installed Studio schema (`studio/schema.yml`), so the Studio tag pickers always offer this list. A collection with no vocabulary gets an empty picker and a warning.
|
|
83
83
|
|
|
84
84
|
pages:
|
|
85
85
|
title: "`pandorga.pages[]`"
|