pandorga 1.6.7 → 1.6.8

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +3 -2
  4. data/_includes/page/content-runtime.html +3 -0
  5. data/_includes/page/network-graph.html +5 -2
  6. data/_includes/page/timeline-section.html +13 -1
  7. data/_includes/theme/favicons.html +3 -1
  8. data/_layouts/articles.html +5 -2
  9. data/_layouts/bibliography.html +5 -2
  10. data/_layouts/blog.html +5 -2
  11. data/_layouts/cv.html +5 -2
  12. data/_layouts/home.html +10 -3
  13. data/_layouts/projects.html +5 -2
  14. data/_layouts/resources.html +5 -2
  15. data/_layouts/shell.html +6 -1
  16. data/assets/css/ledger/home.css +20 -0
  17. data/assets/css/ledger/rascunhos.css +40 -2
  18. data/assets/icons/apple-touch-icon.png +0 -0
  19. data/assets/icons/favicon-32.png +0 -0
  20. data/assets/icons/favicon.ico +0 -0
  21. data/assets/icons/favicon.svg +23 -0
  22. data/assets/icons/icon-192.png +0 -0
  23. data/assets/icons/icon-32.png +0 -0
  24. data/assets/icons/icon-512.png +0 -0
  25. data/assets/icons/icon-maskable-192.png +0 -0
  26. data/assets/icons/icon-maskable-512.png +0 -0
  27. data/assets/icons/studio-apple-touch-icon.png +0 -0
  28. data/assets/icons/studio-icon-192.png +0 -0
  29. data/assets/icons/studio-icon-512.png +0 -0
  30. data/assets/icons/studio-logo-32.png +0 -0
  31. data/assets/icons/studio-logo.ico +0 -0
  32. data/assets/icons/studio-logo.svg +25 -5
  33. data/assets/icons/studio-maskable-192.png +0 -0
  34. data/assets/icons/studio-maskable-512.png +0 -0
  35. data/lib/pandorga/jekyll/plugins/web_manifest.rb +81 -0
  36. data/lib/pandorga/version.rb +1 -1
  37. data/scripts/check_home_lists.mjs +209 -0
  38. data/scripts/lighthouse-home.sh +6 -0
  39. data/scripts/render-pwa-icons.py +141 -0
  40. data/scripts/test/test-home-articles.rb +30 -0
  41. data/scripts/test/test-home-lists.rb +74 -0
  42. data/scripts/test/test-pwa-icons.rb +280 -0
  43. data/scripts/test/test-rascunhos-licence.rb +43 -0
  44. data/scripts/validate.sh +4 -0
  45. data/studio/index.html +4 -1
  46. data/studio-app/index.html +4 -1
  47. metadata +24 -1
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # PLT-AC-28. The site and Studio launchers share the site stamp. Studio adds
5
+ # one badge. Manifests name maskable icons, and those icons keep their art
6
+ # inside the center 80% circle.
7
+
8
+ require "jekyll"
9
+ require "json"
10
+ require "pathname"
11
+ require "tmpdir"
12
+ require "zlib"
13
+
14
+ ROOT = Pathname.new(__dir__).join("..", "..").expand_path
15
+
16
+ def fail!(message)
17
+ warn "FAIL test-pwa-icons: #{message}"
18
+ exit 1
19
+ end
20
+
21
+ PLATE = [0x5e, 0x5d, 0x59].freeze
22
+ BADGE = [0xb8, 0xb7, 0xae].freeze
23
+ PRIMARY = [0xa4, 0xc8, 0xda].freeze
24
+
25
+ def read_png(path)
26
+ data = File.binread(path)
27
+ fail!("#{path} is not a PNG") unless data.start_with?("\x89PNG\r\n\x1a\n".b)
28
+
29
+ pos = 8
30
+ width = height = nil
31
+ idat = +"".b
32
+ while pos + 12 <= data.bytesize
33
+ length = data[pos, 4].unpack1("N")
34
+ type = data[pos + 4, 4]
35
+ chunk = data[pos + 8, length]
36
+ pos += 12 + length
37
+ case type
38
+ when "IHDR"
39
+ width, height, bit, color, _comp, _filter, interlace = chunk.unpack("NNCCCCC")
40
+ fail!("#{path} is not 8-bit RGB") unless bit == 8 && color == 2 && interlace == 0
41
+ when "IDAT"
42
+ idat << chunk
43
+ when "IEND"
44
+ break
45
+ end
46
+ end
47
+ fail!("#{path} has no image") unless width && height && !idat.empty?
48
+
49
+ raw = Zlib::Inflate.inflate(idat)
50
+ stride = width * 3
51
+ row_start = 0
52
+ prev = Array.new(stride, 0)
53
+ pixels = []
54
+ height.times do
55
+ filter = raw.getbyte(row_start)
56
+ row = raw.byteslice(row_start + 1, stride).bytes
57
+ row_start += stride + 1
58
+ out = Array.new(stride, 0)
59
+ stride.times do |i|
60
+ left = i >= 3 ? out[i - 3] : 0
61
+ up = prev[i]
62
+ up_left = i >= 3 ? prev[i - 3] : 0
63
+ value = row[i]
64
+ out[i] = case filter
65
+ when 0 then value
66
+ when 1 then (value + left) & 255
67
+ when 2 then (value + up) & 255
68
+ when 3 then (value + ((left + up) / 2)) & 255
69
+ when 4
70
+ estimate = left + up - up_left
71
+ nearest = [[estimate - left, left], [estimate - up, up], [estimate - up_left, up_left]]
72
+ nearest.map! { |distance, sample| [distance.abs, sample] }
73
+ (value + nearest.min_by(&:first).last) & 255
74
+ else
75
+ fail!("#{path} uses PNG filter #{filter}")
76
+ end
77
+ end
78
+ prev = out
79
+ width.times { |x| pixels << out[x * 3, 3] }
80
+ end
81
+ [width, height, pixels]
82
+ end
83
+
84
+ def pixel(image, x, y)
85
+ width, _height, pixels = image
86
+ pixels[(y * width) + x]
87
+ end
88
+
89
+ def assert_margin(path, image)
90
+ width, height, = image
91
+ band = (width * 0.10).floor
92
+ samples = [
93
+ [0, 0],
94
+ [width - 1, 0],
95
+ [0, height - 1],
96
+ [width - 1, height - 1],
97
+ [band - 1, height / 2],
98
+ [width - band, height / 2],
99
+ [width / 2, band - 1],
100
+ [width / 2, height - band]
101
+ ]
102
+ samples.each do |x, y|
103
+ got = pixel(image, x, y)
104
+ fail!("#{path} pixel #{x},#{y} is #{got.inspect}, outside the safe zone") unless got == PLATE
105
+ end
106
+ end
107
+
108
+ logo = ROOT.join("_includes/theme/site-logo.html").read
109
+ site_svg = ROOT.join("assets/icons/favicon.svg").read
110
+ studio_svg = ROOT.join("assets/icons/studio-logo.svg").read
111
+ %w[0,0 23,0 15,17 0,20 23,0 32,0 32,32 17,21 15,17 0,20 15,17 17,21 32,32 0,32].each do |point|
112
+ fail!("site stamp lost the point #{point}") unless logo.include?(point) && site_svg.include?(point) && studio_svg.include?(point)
113
+ end
114
+ fail!("Studio icon is still the hexagon") if studio_svg.include?("16,1") || studio_svg.include?("30,9")
115
+ fail!("site icon carries the Studio badge") if site_svg.include?("badge")
116
+ fail!("Studio icon has no badge") unless studio_svg.include?('class="badge"') && studio_svg.include?('x="20.32"')
117
+ fail!("favicon names the owner") if site_svg.match?(/Heinen|Alexandre/i)
118
+
119
+ shell = ROOT.join("_includes/theme/favicons.html").read
120
+ fail!("shell does not link the site manifest") unless shell.include?('href="/site.webmanifest"')
121
+ studio = ROOT.join("studio/index.html").read
122
+ studio_app = ROOT.join("studio-app/index.html").read
123
+ def ink_bounds(image)
124
+ width, height, pixels = image
125
+ min_x, min_y = width, height
126
+ max_x, max_y = -1, -1
127
+ height.times do |y|
128
+ width.times do |x|
129
+ next if pixels[(y * width) + x] == PLATE
130
+
131
+ min_x = x if x < min_x
132
+ min_y = y if y < min_y
133
+ max_x = x if x > max_x
134
+ max_y = y if y > max_y
135
+ end
136
+ end
137
+ [min_x, min_y, max_x, max_y]
138
+ end
139
+
140
+ def ico_sizes(path)
141
+ data = File.binread(path)
142
+ fail!("#{path} is not an ICO") unless data.bytesize >= 6 && data.getbyte(2) == 1
143
+
144
+ count = data[4, 2].unpack1("v")
145
+ count.times.map do |index|
146
+ entry = data[6 + (index * 16), 2]
147
+ width = entry.getbyte(0)
148
+ height = entry.getbyte(1)
149
+ width = 256 if width.zero?
150
+ height = 256 if height.zero?
151
+ [width, height]
152
+ end
153
+ end
154
+
155
+ fail!("site and Studio SVGs use different stamp scales") unless site_svg.include?("translate(7.68 7.68) scale(0.52)") &&
156
+ studio_svg.include?("translate(7.68 7.68) scale(0.52)")
157
+
158
+ [studio, studio_app].each do |html|
159
+ fail!("Studio favicon link is missing") unless html.include?('href="/assets/icons/studio-logo.ico"')
160
+ fail!("Studio does not link its manifest") unless html.include?('href="/studio.webmanifest"')
161
+ fail!("Studio apple touch icon is the 32px file") if html.include?('apple-touch-icon" href="/assets/icons/studio-logo-32.png"')
162
+ fail!("Studio apple touch icon is missing") unless html.include?("/assets/icons/studio-apple-touch-icon.png")
163
+ end
164
+
165
+ def build_manifests(config_yaml)
166
+ Dir.mktmpdir do |dir|
167
+ File.write(File.join(dir, "_config.yml"), config_yaml)
168
+ File.write(File.join(dir, "index.html"), "hello\n")
169
+ config = Jekyll.configuration(
170
+ "source" => dir,
171
+ "destination" => File.join(dir, "_site"),
172
+ "quiet" => true,
173
+ "safe" => false
174
+ )
175
+ site = Jekyll::Site.new(config)
176
+ site.reset
177
+ site.read
178
+ Pandorga::Jekyll::WebManifestGenerator.new.generate(site)
179
+ site.pages.select { |page| page.name.end_with?(".webmanifest") }.to_h do |page|
180
+ fail!("#{page.name} would enter the sitemap") unless page.data["sitemap"] == false
181
+ [page.name, JSON.parse(page.content)]
182
+ end
183
+ end
184
+ end
185
+
186
+ require_relative "../../lib/pandorga/jekyll/plugins/web_manifest"
187
+
188
+ named = build_manifests(<<~YAML)
189
+ title: Fallback Title
190
+ pandorga:
191
+ identity:
192
+ name: Example Person
193
+ short_name: Example
194
+ YAML
195
+ site = named["site.webmanifest"]
196
+ studio_manifest = named["studio.webmanifest"]
197
+ fail!("manifests were not written") unless site && studio_manifest
198
+ fail!("site name ignored identity") unless site["name"] == "Example Person" && site["short_name"] == "Example"
199
+ fail!("site scope is not the origin root") unless site["id"] == "/" && site["scope"] == "/" && site["start_url"] == "/"
200
+ fail!("Studio is not its own app") unless studio_manifest["id"] == "/studio/" && studio_manifest["scope"] == "/studio/"
201
+ fail!("Studio name changed") unless studio_manifest["name"] == "Studio" && studio_manifest["start_url"] == "/studio/"
202
+ fail!("launcher plate changed") unless site["background_color"] == "#5e5d59" && site["theme_color"] == "#5e5d59"
203
+
204
+ {
205
+ "site.webmanifest" => [
206
+ ["/assets/icons/icon-512.png", "512x512", "any"],
207
+ ["/assets/icons/icon-maskable-512.png", "512x512", "maskable"],
208
+ ["/assets/icons/icon-192.png", "192x192", "any"],
209
+ ["/assets/icons/icon-maskable-192.png", "192x192", "maskable"]
210
+ ],
211
+ "studio.webmanifest" => [
212
+ ["/assets/icons/studio-icon-512.png", "512x512", "any"],
213
+ ["/assets/icons/studio-maskable-512.png", "512x512", "maskable"],
214
+ ["/assets/icons/studio-icon-192.png", "192x192", "any"],
215
+ ["/assets/icons/studio-maskable-192.png", "192x192", "maskable"]
216
+ ]
217
+ }.each do |name, expected|
218
+ got = named.fetch(name).fetch("icons").map { |icon| [icon["src"], icon["sizes"], icon["purpose"]] }
219
+ fail!("#{name} icons are #{got.inspect}") unless got == expected
220
+ end
221
+
222
+ fallback = build_manifests("title: \"\"\n")
223
+ fail!("empty identity must still name the app") unless fallback["site.webmanifest"]["name"] == "Site"
224
+
225
+ [
226
+ ["assets/icons/icon-512.png", "assets/icons/studio-icon-512.png", 512],
227
+ ["assets/icons/icon-maskable-512.png", "assets/icons/studio-maskable-512.png", 512],
228
+ ["assets/icons/icon-192.png", "assets/icons/studio-icon-192.png", 192],
229
+ ["assets/icons/icon-maskable-192.png", "assets/icons/studio-maskable-192.png", 192],
230
+ ["assets/icons/apple-touch-icon.png", "assets/icons/studio-apple-touch-icon.png", 180],
231
+ ["assets/icons/favicon-32.png", "assets/icons/studio-logo-32.png", 32],
232
+ ["assets/icons/icon-32.png", "assets/icons/studio-logo-32.png", 32]
233
+ ].each do |site_rel, studio_rel, size|
234
+ site_image = read_png(ROOT.join(site_rel))
235
+ studio_image = read_png(ROOT.join(studio_rel))
236
+ fail!("#{site_rel} is #{site_image[0]}x#{site_image[1]}") unless site_image[0] == size && site_image[1] == size
237
+ fail!("#{studio_rel} canvas is #{studio_image[0]}x#{studio_image[1]}") unless studio_image[0] == size && studio_image[1] == size
238
+
239
+ site_ink = ink_bounds(site_image)
240
+ studio_ink = ink_bounds(studio_image)
241
+ fail!("#{studio_rel} art bounds #{studio_ink.inspect} differ from #{site_ink.inspect}") unless site_ink == studio_ink
242
+
243
+ min_x, min_y, max_x, max_y = site_ink
244
+ pad_right = size - 1 - max_x
245
+ pad_bottom = size - 1 - max_y
246
+ fail!("#{site_rel} padding is uneven #{site_ink.inspect}") unless min_x == min_y && pad_right == min_x && pad_bottom == min_y
247
+
248
+ radius = size * 0.40
249
+ center = size / 2.0
250
+ corner = Math.hypot((max_x + 1) - center, (max_y + 1) - center)
251
+ fail!("#{site_rel} art leaves the safe zone (#{corner} > #{radius})") if corner > radius
252
+ end
253
+
254
+ site_ico = ico_sizes(ROOT.join("assets/icons/favicon.ico"))
255
+ studio_ico = ico_sizes(ROOT.join("assets/icons/studio-logo.ico"))
256
+ fail!("favicon canvases differ: #{site_ico.inspect} vs #{studio_ico.inspect}") unless site_ico == studio_ico && site_ico == [[16, 16], [32, 32]]
257
+
258
+ [512, 192].each do |size|
259
+ site_path = ROOT.join("assets/icons/icon-maskable-#{size}.png")
260
+ studio_path = ROOT.join("assets/icons/studio-maskable-#{size}.png")
261
+ site_image = read_png(site_path)
262
+ studio_image = read_png(studio_path)
263
+ fail!("site maskable is #{site_image[0]} square") unless site_image[0] == size && site_image[1] == size
264
+ fail!("Studio maskable is #{studio_image[0]} square") unless studio_image[0] == size && studio_image[1] == size
265
+ assert_margin(site_path, site_image)
266
+ assert_margin(studio_path, studio_image)
267
+
268
+ mark = (size * 0.24).round + (2.0 / 32 * size * 0.52).round
269
+ fail!("site stamp is not the pastel triad") unless pixel(site_image, mark, mark) == PRIMARY
270
+ fail!("Studio stamp is not the pastel triad") unless pixel(studio_image, mark, mark) == PRIMARY
271
+
272
+ badge_at = (size * 0.68).round
273
+ fail!("site icon has the Studio badge") if pixel(site_image, badge_at, badge_at) == BADGE
274
+ fail!("Studio badge is missing") unless pixel(studio_image, badge_at, badge_at) == BADGE
275
+
276
+ differ = site_image[2].zip(studio_image[2]).count { |left, right| left != right }
277
+ fail!("Studio icon is a different picture (#{differ} pixels)") unless differ.positive? && differ < (size * size * 0.03)
278
+ end
279
+
280
+ puts "PASS test-pwa-icons"
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # PLT-AC-27. Below 1024px the Rascunhos licence follows the entries.
5
+ # writing.css had this order. The per-page sheet split stopped loading that
6
+ # file on the blog layout, and `.rascunhos-body { display: block }` put the
7
+ # licence back above the texts. The winning rule lives in rascunhos.css.
8
+
9
+ require "pathname"
10
+
11
+ ROOT = Pathname.new(__dir__).join("..", "..").expand_path
12
+
13
+ def fail!(message)
14
+ warn "FAIL test-rascunhos-licence: #{message}"
15
+ exit 1
16
+ end
17
+
18
+ shell = ROOT.join("_layouts/shell.html").read
19
+ blog = shell[/when "blog".*?assign _ledger = "([^"]+)"/m, 1]
20
+ fail!("blog layout has no ledger list") unless blog
21
+ sheets = blog.split("|")
22
+ rascunhos_at = sheets.index("rascunhos")
23
+ writing_at = sheets.index("writing")
24
+ fail!("blog layout does not load rascunhos.css") unless rascunhos_at
25
+ fail!("blog layout does not load writing.css after rascunhos.css") unless writing_at && writing_at > rascunhos_at
26
+
27
+ css = ROOT.join("assets/css/ledger/rascunhos.css").read
28
+ block_at = css.index(".rascunhos-body {\n display: block;")
29
+ rule_at = css.index(".rascunhos-main .rascunhos-body {\n display: flex;")
30
+ licence_at = css.index(".rascunhos-main .writing-rail-licence {")
31
+ ledger_at = css.index(".rascunhos-main .rascunhos-body>.rascunhos-ledger {")
32
+ hidden_at = css.index(".rascunhos-main .rascunhos-body.hidden {\n display: none;")
33
+
34
+ fail!("rascunhos body is no longer display:block at rest") unless block_at
35
+ fail!("mobile column is missing from rascunhos.css") unless rule_at && rule_at > block_at
36
+ fail!("mobile column is not limited to the disclosure width") unless css.index("@media (max-width: 1023px)")&.< rule_at
37
+ fail!("licence has no colophon order") unless licence_at && css[licence_at, 180].include?("order: 2")
38
+ fail!("ledger is not ordered ahead of the licence") unless ledger_at && ledger_at < licence_at && css[ledger_at, 120].include?("order: 1")
39
+ fail!("detail view can no longer hide the listing") unless hidden_at && hidden_at > rule_at
40
+ fail!("rail no longer dissolves so the licence can leave it") unless css.include?(".rascunhos-main .rascunhos-body>.writing-rail:not([hidden])") &&
41
+ css.include?("display: contents")
42
+
43
+ puts "PASS test-rascunhos-licence"
data/scripts/validate.sh CHANGED
@@ -60,6 +60,10 @@ TESTS=(
60
60
  scripts/test/test-hydrate-reserve.rb
61
61
  scripts/test/test-hero-summary.rb
62
62
  scripts/test/test-lighthouse-policy.rb
63
+ scripts/test/test-home-lists.rb
64
+ scripts/test/test-home-articles.rb
65
+ scripts/test/test-rascunhos-licence.rb
66
+ scripts/test/test-pwa-icons.rb
63
67
  scripts/test/test-css-delivery.rb
64
68
  scripts/test/test-discovery.rb
65
69
  )
data/studio/index.html CHANGED
@@ -6,7 +6,10 @@
6
6
  <meta name="robots" content="noindex" />
7
7
  <link rel="icon" href="/assets/icons/studio-logo.svg" type="image/svg+xml" sizes="any" />
8
8
  <link rel="icon" href="/assets/icons/studio-logo-32.png" type="image/png" sizes="32x32" />
9
- <link rel="apple-touch-icon" href="/assets/icons/studio-logo-32.png" />
9
+ <link rel="apple-touch-icon" href="/assets/icons/studio-apple-touch-icon.png" sizes="180x180" />
10
+ <link rel="icon" href="/assets/icons/studio-logo.ico" sizes="16x16 32x32" />
11
+ <link rel="manifest" href="/studio.webmanifest" />
12
+ <meta name="theme-color" content="#5e5d59" />
10
13
  <title>Studio</title>
11
14
  <link rel="preconnect" href="https://fonts.googleapis.com" />
12
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -6,7 +6,10 @@
6
6
  <meta name="robots" content="noindex" />
7
7
  <link rel="icon" href="/assets/icons/studio-logo.svg" type="image/svg+xml" sizes="any" />
8
8
  <link rel="icon" href="/assets/icons/studio-logo-32.png" type="image/png" sizes="32x32" />
9
- <link rel="apple-touch-icon" href="/assets/icons/studio-logo-32.png" />
9
+ <link rel="apple-touch-icon" href="/assets/icons/studio-apple-touch-icon.png" sizes="180x180" />
10
+ <link rel="icon" href="/assets/icons/studio-logo.ico" sizes="16x16 32x32" />
11
+ <link rel="manifest" href="/studio.webmanifest" />
12
+ <meta name="theme-color" content="#5e5d59" />
10
13
  <title>Studio</title>
11
14
  <link rel="preconnect" href="https://fonts.googleapis.com" />
12
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pandorga
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.7
4
+ version: 1.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -227,7 +227,23 @@ files:
227
227
  - assets/css/ledger/sources.css
228
228
  - assets/css/ledger/writing.css
229
229
  - assets/css/tailwind.css
230
+ - assets/icons/apple-touch-icon.png
231
+ - assets/icons/favicon-32.png
232
+ - assets/icons/favicon.ico
233
+ - assets/icons/favicon.svg
234
+ - assets/icons/icon-192.png
235
+ - assets/icons/icon-32.png
236
+ - assets/icons/icon-512.png
237
+ - assets/icons/icon-maskable-192.png
238
+ - assets/icons/icon-maskable-512.png
239
+ - assets/icons/studio-apple-touch-icon.png
240
+ - assets/icons/studio-icon-192.png
241
+ - assets/icons/studio-icon-512.png
242
+ - assets/icons/studio-logo-32.png
243
+ - assets/icons/studio-logo.ico
230
244
  - assets/icons/studio-logo.svg
245
+ - assets/icons/studio-maskable-192.png
246
+ - assets/icons/studio-maskable-512.png
231
247
  - assets/tailwind-play.js
232
248
  - examples/full/Gemfile
233
249
  - examples/full/_config.yml
@@ -300,12 +316,14 @@ files:
300
316
  - lib/pandorga/jekyll/plugins/lib/writing_entry.rb
301
317
  - lib/pandorga/jekyll/plugins/local_detail_rewrites.rb
302
318
  - lib/pandorga/jekyll/plugins/timeline.rb
319
+ - lib/pandorga/jekyll/plugins/web_manifest.rb
303
320
  - lib/pandorga/jekyll/plugins/writing_slug_limits.rb
304
321
  - lib/pandorga/object_store_env.rb
305
322
  - lib/pandorga/registry.rb
306
323
  - lib/pandorga/registry/schema.yml
307
324
  - lib/pandorga/version.rb
308
325
  - scripts/cdn-allowlist.json
326
+ - scripts/check_home_lists.mjs
309
327
  - scripts/content/backfill-content-keys.rb
310
328
  - scripts/content/band_art.rb
311
329
  - scripts/content/export-content-json.rb
@@ -326,6 +344,7 @@ files:
326
344
  - scripts/lighthouse_server.py
327
345
  - scripts/pdf/md-to-pdf.mjs
328
346
  - scripts/pdf/md-to-pdf.sh
347
+ - scripts/render-pwa-icons.py
329
348
  - scripts/tailwind/build.sh
330
349
  - scripts/tailwind/input.css
331
350
  - scripts/tailwind/tailwind.config.js
@@ -409,7 +428,9 @@ files:
409
428
  - scripts/test/test-hero-portraits.mjs
410
429
  - scripts/test/test-hero-portraits.rb
411
430
  - scripts/test/test-hero-summary.rb
431
+ - scripts/test/test-home-articles.rb
412
432
  - scripts/test/test-home-band-art.rb
433
+ - scripts/test/test-home-lists.rb
413
434
  - scripts/test/test-hydrate-reserve.rb
414
435
  - scripts/test/test-install-functions.rb
415
436
  - scripts/test/test-language-export.rb
@@ -422,8 +443,10 @@ files:
422
443
  - scripts/test/test-no-personal-data.rb
423
444
  - scripts/test/test-public-cv-export.rb
424
445
  - scripts/test/test-publish-site-root.rb
446
+ - scripts/test/test-pwa-icons.rb
425
447
  - scripts/test/test-r2-config-consistency.rb
426
448
  - scripts/test/test-r2-json-sync.rb
449
+ - scripts/test/test-rascunhos-licence.rb
427
450
  - scripts/test/test-registry-home.rb
428
451
  - scripts/test/test-runtime-js-syntax.rb
429
452
  - scripts/test/test-shell-a11y.rb