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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d3adc84d250c4ff140df5b9cc9db266896fdfce92fd899789fd943ce8ac4659
4
- data.tar.gz: 474d788dea7851796fb189732c9bb75d118666177f4cf81df70d8361f4310872
3
+ metadata.gz: c03095737ae31747d865ba734912bb725e2d88586405dd847c63d340b42bbfa3
4
+ data.tar.gz: 764bc60f9ef00c00e33391511a7cb5680438660dd5a622af0801add004670f31
5
5
  SHA512:
6
- metadata.gz: d9e76b0be8485c3870366cce0bc816fc55f3dee7ea2b17ec40602f2c70dccbf985e2d7be6da7911e54a7f830c6fd3c4030dbb985689d50c84cb9226c04cd0f2e
7
- data.tar.gz: 6853fe1a1d0edd4066d45187a4cddd67258a753afd849284615383fb9cae4844a24aabff188681a73066e2808170bfc1e28730832e68d976c34400899d2cea88
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 a closed English
1205
- vocabulary: `Aesthetics`, `AI`, `Data`, `DevOps`, `Mathematics`,
1206
- `Robotics`, `Social`. All 22 carry them.
1207
- * **Rascunhos** classify on `tags` too, by subject and in Portuguese, **zero to
1208
- three**: `arte`, `brasilidade`, `carme`, `devaneio`, `engraçadinho`,
1209
- `música`, `pesquisa`, `provocação`, `técnico`, `trabalho`. Unlike articles
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), '" alt="', escapeAttr(alt || ''),
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">' +
@@ -140,6 +140,7 @@
140
140
  description: entry.description,
141
141
  excerpt: entry.excerpt,
142
142
  thumbnail: entry.thumbnail,
143
+ thumbnail_srcset: entry.thumbnail_srcset,
143
144
  language: entry.language,
144
145
  date: entry.date,
145
146
  last_updated: entry.last_updated,
@@ -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 poster = runtime.renderPosterMedia(fm.thumbnail, projectName(item));
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.11.0"
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, taxonomies)
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, taxonomies)
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, taxonomies)
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, taxonomies)
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 apply_taxonomy!(entry, collection_name, taxonomies)
197
- tax = taxonomies[collection_name]
198
- return unless tax.is_a?(Hash)
273
+ def resolve_taxonomies(config)
274
+ return {} unless config.is_a?(Hash)
199
275
 
200
- Array(entry["fields"]).each do |field|
201
- next unless field.is_a?(Hash)
202
- next unless field["name"].to_s == "tags"
276
+ vocab = {}
277
+ pages = Array(config.dig("pandorga", "pages")).select { |p| p.is_a?(Hash) }
278
+ taxonomies = config.dig("pandorga", "taxonomies")
203
279
 
204
- field["options"] ||= {}
205
- next unless field["options"].is_a?(Hash)
280
+ if taxonomies.is_a?(Hash)
281
+ taxonomies.each do |collection, spec|
282
+ next unless spec.is_a?(Hash)
206
283
 
207
- tags = tax["tags"]
208
- field["options"]["values"] = tags if tags.is_a?(Array) && !tags.empty?
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
- def apply_overrides!(schema, extra)
215
- if extra["media"].is_a?(Hash)
216
- schema["media"] = deep_merge(schema["media"] || {}, extra["media"])
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
- if extra["components"].is_a?(Hash)
220
- schema["components"] = deep_merge(schema["components"] || {}, extra["components"])
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
- if extra["collections"].is_a?(Hash)
224
- extra["collections"].each do |name, patch|
225
- next unless patch.is_a?(Hash)
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
- item = find_content_item(schema, name.to_s)
228
- if item
229
- merged = deep_merge(item, patch)
230
- item.replace(merged)
231
- end
232
- if schema["collections"].is_a?(Hash) && schema["collections"][name.to_s]
233
- schema["collections"][name.to_s] =
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
- return unless extra["content"].is_a?(Array)
311
+ vocab
312
+ end
240
313
 
241
- schema["content"] ||= []
242
- extra["content"].each do |group|
243
- next unless group.is_a?(Hash)
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
- existing = schema["content"].find { |g| g["name"].to_s == group["name"].to_s }
246
- if existing
247
- existing.replace(deep_merge(existing, group))
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
- schema["content"] << deep_dup(group)
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[]`"