pandorga 1.14.0 → 2.0.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 +31 -0
- data/DESIGN.md +15 -30
- data/_includes/content-runtime/43-blocks.html +0 -3
- data/_includes/content-runtime/60-fragments.html +83 -149
- data/_includes/content-runtime/90-bootstrap.html +3 -0
- data/_includes/cv/cv-box.html +10 -4
- data/_includes/home/band-index.html +22 -7
- data/_includes/home/band-listing.html +7 -3
- data/_includes/page/timeline-section.html +8 -3
- data/_includes/theme/theme-vars-block.liquid +3 -0
- data/_layouts/articles.html +0 -2
- data/_layouts/bibliography.html +0 -2
- data/_layouts/blog.html +0 -5
- data/_layouts/cv.html +27 -8
- data/_layouts/home.html +2 -13
- data/_layouts/projects.html +17 -9
- data/_layouts/resources.html +0 -9
- data/_layouts/shell.html +1 -7
- data/assets/css/article.css +10 -2
- data/assets/css/base.css +68 -44
- data/assets/css/ledger/bibliography.css +0 -31
- data/assets/css/ledger/chrome.css +51 -2
- data/assets/css/ledger/cv.css +0 -40
- data/assets/css/ledger/detail.css +20 -6
- data/assets/css/ledger/home.css +261 -402
- data/assets/css/ledger/projects.css +17 -19
- data/assets/css/ledger/rascunhos.css +0 -27
- data/assets/css/ledger/sources.css +0 -57
- data/assets/css/ledger/writing.css +0 -62
- data/lib/pandorga/commands/new.rb +2 -2
- data/lib/pandorga/commands/thumbnails.rb +3 -2
- data/lib/pandorga/registry/schema.yml +0 -3
- data/lib/pandorga/thumbnail_variants.rb +29 -3
- data/lib/pandorga/version.rb +1 -1
- data/scripts/check_home_lists.mjs +33 -5
- data/scripts/content/hero_portraits.rb +59 -11
- data/scripts/test/test-css-structure.rb +0 -1
- data/scripts/test/test-hero-16x9.rb +379 -0
- data/scripts/test/test-hero-portraits.mjs +10 -20
- data/scripts/test/test-hero-portraits.rb +31 -25
- data/scripts/test/test-highlighter-pills.rb +12 -2
- data/scripts/test/test-home-lists.rb +5 -0
- data/scripts/test/test-new.rb +6 -6
- data/scripts/test/test-shell-a11y.rb +2 -1
- data/scripts/test/test-studio-section-texts.rb +625 -0
- data/scripts/test/test-theme-contrast.rb +13 -2
- data/scripts/test/test-v3-layout.rb +559 -0
- data/scripts/validate.sh +3 -0
- data/studio/assets/{index-BJyN-kB3.js → index-B9h1WUYY.js} +1 -1
- data/studio/index.html +1 -1
- data/studio/schema.yml +126 -2
- data/studio-app/src/lib/yaml-doc.js +6 -0
- data/templates/network/studio.yml +10 -0
- metadata +5 -2
|
@@ -161,23 +161,16 @@
|
|
|
161
161
|
overflow: hidden;
|
|
162
162
|
display: grid;
|
|
163
163
|
background: var(--color-surface-card);
|
|
164
|
-
border: 1px solid var(--border-
|
|
165
|
-
border-
|
|
164
|
+
border: 1px solid var(--border-ink);
|
|
165
|
+
border-top: 2px solid var(--color-primary);
|
|
166
166
|
margin-bottom: var(--space-3xl);
|
|
167
167
|
transition: border-color var(--motion-duration-fast) var(--motion-ease-default);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
.projects-page .projects-lead:hover {
|
|
171
171
|
border-color: var(--border-gentle-hover);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.projects-page .projects-lead-shard {
|
|
176
|
-
right: -5rem;
|
|
177
|
-
top: -5rem;
|
|
178
|
-
width: 18rem;
|
|
179
|
-
height: 18rem;
|
|
180
|
-
background: var(--color-primary);
|
|
172
|
+
/* Keep the primary top rule; the shorthand above would repaint it. */
|
|
173
|
+
border-top-color: var(--color-primary);
|
|
181
174
|
}
|
|
182
175
|
|
|
183
176
|
.projects-page .projects-lead-media {
|
|
@@ -420,14 +413,6 @@
|
|
|
420
413
|
margin-bottom: var(--space-2xl);
|
|
421
414
|
}
|
|
422
415
|
|
|
423
|
-
.projects-page .projects-hero-shard {
|
|
424
|
-
right: -4rem;
|
|
425
|
-
bottom: -6rem;
|
|
426
|
-
width: 16rem;
|
|
427
|
-
height: 16rem;
|
|
428
|
-
background: var(--color-secondary);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
416
|
.projects-page .projects-hero-body {
|
|
432
417
|
position: relative;
|
|
433
418
|
z-index: 1;
|
|
@@ -542,6 +527,19 @@
|
|
|
542
527
|
reason recorded in the layout: at tablet width the dossier is narrow enough
|
|
543
528
|
to break a repository slug across three lines mid-word. */
|
|
544
529
|
@media (max-width: 767px) {
|
|
530
|
+
.projects-page .projects-lead-body {
|
|
531
|
+
padding: var(--space-md);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.projects-page .projects-grid {
|
|
535
|
+
gap: var(--space-md);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.projects-page .ledger-system-card>.ledger-poster,
|
|
539
|
+
.projects-page .ledger-system-card>.projects-plate {
|
|
540
|
+
margin: calc(var(--space-md) * -1) calc(var(--space-md) * -1) 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
545
543
|
.projects-page .projects-hero-body {
|
|
546
544
|
padding: var(--space-lg);
|
|
547
545
|
}
|
|
@@ -25,33 +25,6 @@
|
|
|
25
25
|
margin-bottom: var(--space-2xl);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/* Sized to the masthead it accents. It used to be 24rem x 15rem, which fitted
|
|
29
|
-
while the filter deck was a strip under the title and made the masthead
|
|
30
|
-
260px tall; with the filter moved into the rail the masthead is 107px and a
|
|
31
|
-
15rem shard hung 50px into the body, cutting across the rail's keyword box.
|
|
32
|
-
An accent that reaches the content it is accenting is not an accent. */
|
|
33
|
-
.rascunhos-shard {
|
|
34
|
-
top: calc(-1 * var(--space-2xl));
|
|
35
|
-
right: calc(-1 * var(--space-xl));
|
|
36
|
-
width: 16rem;
|
|
37
|
-
height: 9rem;
|
|
38
|
-
/* Page accent (ochre via `.page-accent--ochre`), not the triad secondary. */
|
|
39
|
-
background: var(--color-primary);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/* Between the 640px shard cut-off and the desktop layout a 24rem shard is
|
|
43
|
-
wider than the masthead is tall: at 768px it covered the title and the
|
|
44
|
-
standfirst and read as a mis-clipped slab rather than a corner accent. The
|
|
45
|
-
writing lead and the CV dossier carry the same correction. */
|
|
46
|
-
@media (max-width: 1023px) {
|
|
47
|
-
.rascunhos-shard {
|
|
48
|
-
top: calc(-1 * var(--space-xl));
|
|
49
|
-
right: calc(-1 * var(--space-lg));
|
|
50
|
-
width: 12rem;
|
|
51
|
-
height: 8rem;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
28
|
.rascunhos-header {
|
|
56
29
|
position: relative;
|
|
57
30
|
z-index: 1;
|
|
@@ -7,32 +7,11 @@
|
|
|
7
7
|
`language`, and the publisher host from `link`. Every colour is a token, so
|
|
8
8
|
both palettes resolve from the same rules. */
|
|
9
9
|
|
|
10
|
-
/* ── Shards ──
|
|
11
|
-
The page had no depth treatment of any kind, and it is the wrong shape for
|
|
12
|
-
the ambient pool the home bands carry: a pool behind a grid of many small
|
|
13
|
-
cards only darkens one corner of it. Shards instead, the two canonical
|
|
14
|
-
polygons from base.css, one at each end of the run — top right above the
|
|
15
|
-
grid, bottom left below it, the diagonal pair the CV dossier uses inside
|
|
16
|
-
one panel, opened out across the page. In dark the source cards stay
|
|
17
|
-
opaque so the shard reads in the gutter, not through the poster.
|
|
18
|
-
|
|
19
|
-
Neither container takes `overflow: hidden`: the masthead holds the filter
|
|
20
|
-
popover, which would be clipped by it. The offsets are therefore sized to
|
|
21
|
-
stay inside the page's own gutter at every width the shards survive, the
|
|
22
|
-
way the rascunhos masthead does it. The shared utility already unmounts
|
|
23
|
-
both below 640px. */
|
|
24
|
-
/* The masthead outranks the grid because the filter popover opens out of it.
|
|
25
|
-
Lifting the header over its own shard puts the popover inside a stacking
|
|
26
|
-
context, and the grid below has to be lifted over the foot shard in the
|
|
27
|
-
same way — without this the cards would be painted over an open filter
|
|
28
|
-
panel. */
|
|
29
10
|
.sources-masthead {
|
|
30
11
|
position: relative;
|
|
31
12
|
z-index: 2;
|
|
32
13
|
}
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
|
|
36
15
|
.sources-header {
|
|
37
16
|
position: relative;
|
|
38
17
|
z-index: 1;
|
|
@@ -43,14 +22,6 @@
|
|
|
43
22
|
opacity: 0.85;
|
|
44
23
|
}
|
|
45
24
|
|
|
46
|
-
.sources-masthead-shard {
|
|
47
|
-
top: calc(-1 * var(--space-2xl));
|
|
48
|
-
right: calc(-1 * var(--space-xl));
|
|
49
|
-
width: 16rem;
|
|
50
|
-
height: 9rem;
|
|
51
|
-
background: var(--color-primary);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
25
|
.sources-body {
|
|
55
26
|
position: relative;
|
|
56
27
|
}
|
|
@@ -65,34 +36,6 @@
|
|
|
65
36
|
z-index: 1;
|
|
66
37
|
}
|
|
67
38
|
|
|
68
|
-
/* The foot of the page, beside the pagination. It reaches up behind the last
|
|
69
|
-
row of cards. In dark those cards are opaque again, so what shows is the
|
|
70
|
-
wedge in the gutter and the band across the pagination strip. */
|
|
71
|
-
.sources-foot-shard {
|
|
72
|
-
left: calc(-1 * var(--space-xl));
|
|
73
|
-
bottom: calc(-1 * var(--space-lg));
|
|
74
|
-
width: 14rem;
|
|
75
|
-
height: 8rem;
|
|
76
|
-
background: color-mix(in srgb, var(--color-primary) 70%, transparent);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/* Below the desktop layout the page gutter halves, so the shards have to come
|
|
80
|
-
back inside it or they widen the document. */
|
|
81
|
-
@media (max-width: 1023px) {
|
|
82
|
-
.sources-masthead-shard {
|
|
83
|
-
top: calc(-1 * var(--space-xl));
|
|
84
|
-
right: calc(-1 * var(--space-lg));
|
|
85
|
-
width: 12rem;
|
|
86
|
-
height: 8rem;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.sources-foot-shard {
|
|
90
|
-
left: calc(-1 * var(--space-lg));
|
|
91
|
-
width: 10rem;
|
|
92
|
-
height: 7rem;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
39
|
/* Pagination hides rather than unmounts, so the cards keep their identity
|
|
97
40
|
between pages and the filter only has to toggle a class. */
|
|
98
41
|
.resource-hidden {
|
|
@@ -59,40 +59,6 @@
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/* The mockup filled the right of this panel with a fabricated proof figure.
|
|
63
|
-
A shard bleeding off the corner holds that weight instead.
|
|
64
|
-
|
|
65
|
-
This used to be a single 19rem wedge at the top right, stood down whenever
|
|
66
|
-
the entry carried a thumbnail — and the panel picks the first entry that
|
|
67
|
-
*has* a thumbnail, so the condition was always true and the shard never
|
|
68
|
-
rendered on a built page. It is a pair now, placed around the plate rather
|
|
69
|
-
than instead of it.
|
|
70
|
-
|
|
71
|
-
The alpha sits bottom left, under the foot of the copy, the one corner the
|
|
72
|
-
plate never reaches at any width. The beta is a true corner accent at the
|
|
73
|
-
top right: it is offset far enough up that its visible part is the triangle
|
|
74
|
-
above the plate's top edge, and it tucks behind the plate as it descends
|
|
75
|
-
rather than fighting it. Neither corner repeats the home panel's pair, and
|
|
76
|
-
the hues are the only two this page has — `.page-accent--green` rebinds
|
|
77
|
-
primary onto secondary, so green and ochre is the whole palette here. */
|
|
78
|
-
.writing-index .writing-lead-shard {
|
|
79
|
-
left: -3.5rem;
|
|
80
|
-
bottom: -4rem;
|
|
81
|
-
width: 12rem;
|
|
82
|
-
height: 12rem;
|
|
83
|
-
background: var(--color-primary);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.writing-index .writing-lead-shard--beta {
|
|
87
|
-
left: auto;
|
|
88
|
-
right: -4rem;
|
|
89
|
-
bottom: auto;
|
|
90
|
-
top: -5rem;
|
|
91
|
-
width: 10rem;
|
|
92
|
-
height: 10rem;
|
|
93
|
-
background: var(--color-tertiary);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
62
|
.writing-index .writing-lead-inner {
|
|
97
63
|
position: relative;
|
|
98
64
|
z-index: 1;
|
|
@@ -739,34 +705,6 @@ html[data-theme="light"] .writing-index .writing-lead-media .ledger-poster {
|
|
|
739
705
|
}
|
|
740
706
|
}
|
|
741
707
|
|
|
742
|
-
/* Between the 640px shard cut-off and the desktop layout the panel is narrow
|
|
743
|
-
enough that a shard sized for the wide card reaches the middle of it: at
|
|
744
|
-
768px the 19rem one this started as ran straight through the lead headline.
|
|
745
|
-
Back to corner accents. The CV dossier and the rascunhos masthead carry the
|
|
746
|
-
same correction. Both corners are restated, not just the size, because the
|
|
747
|
-
base rule sets one of `top`/`bottom` and `left`/`right` to `auto` on the
|
|
748
|
-
beta and a size-only override would leave the pair half-placed: the beta
|
|
749
|
-
carries both class names, so the alpha rule matches it too, and an element
|
|
750
|
-
left with all four offsets set resolves to top and left, which drops the
|
|
751
|
-
pair into a single corner. */
|
|
752
|
-
@media (max-width: 1023px) {
|
|
753
|
-
.writing-index .writing-lead-shard {
|
|
754
|
-
left: -2.5rem;
|
|
755
|
-
bottom: -3rem;
|
|
756
|
-
width: 8.5rem;
|
|
757
|
-
height: 8.5rem;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
.writing-index .writing-lead-shard--beta {
|
|
761
|
-
left: auto;
|
|
762
|
-
bottom: auto;
|
|
763
|
-
right: -3rem;
|
|
764
|
-
top: -3.5rem;
|
|
765
|
-
width: 7.5rem;
|
|
766
|
-
height: 7.5rem;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
708
|
/* Pagination count face/size live on `.pagination-count` in base.css. */
|
|
771
709
|
|
|
772
710
|
/* global-link.css underlines any <a> hovered inside an <article>, which would
|
|
@@ -22,9 +22,9 @@ module Pandorga
|
|
|
22
22
|
|
|
23
23
|
source "https://rubygems.org"
|
|
24
24
|
|
|
25
|
-
gem "pandorga", "~>
|
|
25
|
+
gem "pandorga", "~> 2.0"
|
|
26
26
|
# Local checkout: gem "pandorga", path: "../pandorga"
|
|
27
|
-
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "
|
|
27
|
+
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v2.0.0"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
|
@@ -5,8 +5,9 @@ require_relative "../thumbnail_variants"
|
|
|
5
5
|
|
|
6
6
|
module Pandorga
|
|
7
7
|
module Commands
|
|
8
|
-
# Generate 16:9 center-cropped width variants for local project thumbnails
|
|
9
|
-
#
|
|
8
|
+
# Generate 16:9 center-cropped width variants for local project thumbnails
|
|
9
|
+
# and the home hero photographs.
|
|
10
|
+
# Refs: PLT-AC-44, PLT-AC-51
|
|
10
11
|
class Thumbnails
|
|
11
12
|
def run(argv)
|
|
12
13
|
site_root = Pathname.new(argv[0] || Dir.pwd).expand_path
|
|
@@ -49,9 +49,6 @@ home:
|
|
|
49
49
|
hero_art:
|
|
50
50
|
type: string
|
|
51
51
|
notes: Include path for hero decoration (default `page/hero-default.html`)
|
|
52
|
-
hero_landscape:
|
|
53
|
-
type: string
|
|
54
|
-
notes: Optional URL of a 16:9 image. At or below 767px it is used only when no `pages/home/portraits.json` entry has its own `landscape` URL. Home front matter `hero_landscape` overrides this key. Omitted, and with no entry landscape, the phone plate stays 3:4
|
|
55
52
|
|
|
56
53
|
fonts:
|
|
57
54
|
title: "`pandorga.fonts`"
|
|
@@ -12,8 +12,8 @@ require_relative "registry"
|
|
|
12
12
|
|
|
13
13
|
module Pandorga
|
|
14
14
|
# Generates 16:9 center-cropped width variants for local project thumbnails
|
|
15
|
-
# and computes their srcset strings.
|
|
16
|
-
# Refs: PLT-AC-44
|
|
15
|
+
# and the home hero photographs, and computes their srcset strings.
|
|
16
|
+
# Refs: PLT-AC-44, PLT-AC-51
|
|
17
17
|
module ThumbnailVariants
|
|
18
18
|
WIDTHS = [480, 960, 1440].freeze
|
|
19
19
|
IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp].freeze
|
|
@@ -25,6 +25,9 @@ module Pandorga
|
|
|
25
25
|
root = Pathname.new(site_root).expand_path
|
|
26
26
|
content_root = resolve_content_root(root)
|
|
27
27
|
sources = project_thumbnail_sources(root, content_root)
|
|
28
|
+
hero_sources(content_root).each do |src|
|
|
29
|
+
sources << src unless sources.any? { |known| known.expand_path == src.expand_path }
|
|
30
|
+
end
|
|
28
31
|
|
|
29
32
|
summary = { sources: sources.length, written: 0, skipped: 0, failed: 0 }
|
|
30
33
|
return summary if sources.empty?
|
|
@@ -55,7 +58,10 @@ module Pandorga
|
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
dest = variants_dir.join("#{stem}-#{width}.jpg")
|
|
58
|
-
|
|
61
|
+
# A newer variant is kept only when it is already this 16:9 size.
|
|
62
|
+
# Hero variants used to be 3:4 under the same names.
|
|
63
|
+
if dest.file? && dest.mtime >= src_mtime &&
|
|
64
|
+
identify_size(bins[:identify], dest) == [width, (width * 9) / 16]
|
|
59
65
|
summary[:skipped] += 1
|
|
60
66
|
next
|
|
61
67
|
end
|
|
@@ -159,6 +165,16 @@ module Pandorga
|
|
|
159
165
|
sources
|
|
160
166
|
end
|
|
161
167
|
|
|
168
|
+
# Every image directly in media/images/hero/ (the hero pool, PLT-AC-51).
|
|
169
|
+
def hero_sources(content_root)
|
|
170
|
+
dir = Pathname.new(content_root).join("media/images/hero")
|
|
171
|
+
return [] unless dir.directory?
|
|
172
|
+
|
|
173
|
+
dir.children.select do |path|
|
|
174
|
+
path.file? && IMAGE_EXTENSIONS.include?(path.extname.downcase)
|
|
175
|
+
end.sort_by { |path| path.basename.to_s }
|
|
176
|
+
end
|
|
177
|
+
|
|
162
178
|
def normalize_local_media_path(path)
|
|
163
179
|
value = path.to_s.strip
|
|
164
180
|
return nil if value.empty?
|
|
@@ -237,6 +253,16 @@ module Pandorga
|
|
|
237
253
|
nil
|
|
238
254
|
end
|
|
239
255
|
|
|
256
|
+
def identify_size(identify_cmd, src)
|
|
257
|
+
cmd = [*identify_cmd, "-ping", "-format", "%w %h", "#{src}[0]"]
|
|
258
|
+
out, _err, status = Open3.capture3(*cmd)
|
|
259
|
+
return nil unless status.success?
|
|
260
|
+
|
|
261
|
+
out.to_s.split.map(&:to_i)
|
|
262
|
+
rescue StandardError
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
|
|
240
266
|
def write_variant(convert_cmd, src, dest, width)
|
|
241
267
|
height = (width * 9) / 16
|
|
242
268
|
FileUtils.mkdir_p(dest.dirname)
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -172,7 +172,12 @@ function filled(result) {
|
|
|
172
172
|
return result.projects > 0 && result.articles > 0 && result.posts > 0;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
// On the runner Chrome sometimes never opens its DevTools port (the dbus
|
|
176
|
+
// noise in its stderr is unrelated). A second launch with a fresh profile
|
|
177
|
+
// and port gets past that; the list checks below do not change.
|
|
178
|
+
const LAUNCH_ATTEMPTS = 2;
|
|
179
|
+
|
|
180
|
+
async function launchChrome() {
|
|
176
181
|
const profile = await mkdtemp(join(tmpdir(), 'pandorga-home-'));
|
|
177
182
|
const port = await freePort();
|
|
178
183
|
const chrome = spawn(chromePath, [
|
|
@@ -187,10 +192,35 @@ async function main() {
|
|
|
187
192
|
`--user-data-dir=${profile}`,
|
|
188
193
|
'about:blank'
|
|
189
194
|
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
195
|
+
return { chrome, profile, port };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function stopChrome(chrome, profile) {
|
|
199
|
+
chrome.kill('SIGKILL');
|
|
200
|
+
await delay(100);
|
|
201
|
+
await rm(profile, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function main() {
|
|
205
|
+
let chrome;
|
|
206
|
+
let profile;
|
|
207
|
+
let version;
|
|
208
|
+
for (let launch = 1; launch <= LAUNCH_ATTEMPTS; launch += 1) {
|
|
209
|
+
const started = await launchChrome();
|
|
210
|
+
chrome = started.chrome;
|
|
211
|
+
profile = started.profile;
|
|
212
|
+
try {
|
|
213
|
+
version = await waitForDevtools(started.port, chrome, profile);
|
|
214
|
+
break;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
await stopChrome(chrome, profile);
|
|
217
|
+
if (launch === LAUNCH_ATTEMPTS) throw error;
|
|
218
|
+
console.error(`WARN: ${error.message.split('\n')[0].slice(0, 200)}; launching Chrome again`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
190
221
|
|
|
191
222
|
let failed = false;
|
|
192
223
|
try {
|
|
193
|
-
const version = await waitForDevtools(port, chrome, profile);
|
|
194
224
|
const ws = await openSocket(version.webSocketDebuggerUrl);
|
|
195
225
|
const browser = new Cdp(ws);
|
|
196
226
|
const created = await browser.send('Target.createTarget', { url: 'about:blank' });
|
|
@@ -223,9 +253,7 @@ async function main() {
|
|
|
223
253
|
}
|
|
224
254
|
ws.close();
|
|
225
255
|
} finally {
|
|
226
|
-
chrome
|
|
227
|
-
await delay(100);
|
|
228
|
-
await rm(profile, { recursive: true, force: true });
|
|
256
|
+
await stopChrome(chrome, profile);
|
|
229
257
|
}
|
|
230
258
|
|
|
231
259
|
if (failed) process.exit(1);
|
|
@@ -3,22 +3,20 @@
|
|
|
3
3
|
# The home hero pool is the folder. The content export writes the public
|
|
4
4
|
# paths to pages/home/portraits.json. The author uploads a photograph under
|
|
5
5
|
# content/media/images/hero/ and does not keep a second list.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# pandorga.home.hero_landscape is only the fallback when no entry has one.
|
|
6
|
+
# The plate is 16:9 at every width (PLT-AC-51). The export writes 16:9 width
|
|
7
|
+
# variants under variants/ with the project thumbnail generator
|
|
8
|
+
# (Pandorga::ThumbnailVariants), so they are not extra draws. An entry lists
|
|
9
|
+
# only the variants that are 16:9: the old 3:4 hero variants had the same
|
|
10
|
+
# names and must not reach srcset when ImageMagick could not replace them.
|
|
12
11
|
|
|
13
12
|
require "pathname"
|
|
14
13
|
|
|
15
14
|
module HeroPortraits
|
|
16
15
|
IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp .gif].freeze
|
|
17
16
|
WIDTHS = [480, 960, 1440].freeze
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
SIZES = "(max-width: 767px) 100vw, (max-width: 1023px) 18rem, 20rem"
|
|
17
|
+
# Below 1024px the plate spans the column (edge to edge on a phone); from
|
|
18
|
+
# 1024px it takes a little under half the row.
|
|
19
|
+
SIZES = "(max-width: 1023px) 100vw, 50vw"
|
|
22
20
|
|
|
23
21
|
module_function
|
|
24
22
|
|
|
@@ -29,7 +27,7 @@ module HeroPortraits
|
|
|
29
27
|
stem = file.basename.to_s.sub(/\.[^.]+\z/, "")
|
|
30
28
|
parts = WIDTHS.filter_map do |width|
|
|
31
29
|
variant = root.join("media/images/hero/variants/#{stem}-#{width}.jpg")
|
|
32
|
-
next unless variant.file?
|
|
30
|
+
next unless variant.file? && wide?(variant)
|
|
33
31
|
|
|
34
32
|
"/media/images/hero/variants/#{variant.basename} #{width}w"
|
|
35
33
|
end
|
|
@@ -47,4 +45,54 @@ module HeroPortraits
|
|
|
47
45
|
path.file? && IMAGE_EXTENSIONS.include?(path.extname.downcase)
|
|
48
46
|
end.sort_by { |path| path.basename.to_s }
|
|
49
47
|
end
|
|
48
|
+
|
|
49
|
+
# True when the file is 16:9 to within a pixel.
|
|
50
|
+
def wide?(path)
|
|
51
|
+
size = image_size(path)
|
|
52
|
+
return false unless size
|
|
53
|
+
|
|
54
|
+
width, height = size
|
|
55
|
+
width.positive? && (height - (width * 9 / 16.0)).abs <= 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# [width, height] from a JPEG or PNG header, sniffed by content, or nil.
|
|
59
|
+
def image_size(path)
|
|
60
|
+
File.open(path, "rb") do |io|
|
|
61
|
+
head = io.read(24).to_s
|
|
62
|
+
if head.start_with?("\x89PNG\r\n\x1A\n".b) && head.byteslice(12, 4) == "IHDR"
|
|
63
|
+
return head.byteslice(16, 8).unpack("NN")
|
|
64
|
+
end
|
|
65
|
+
return nil unless head.start_with?("\xFF\xD8".b)
|
|
66
|
+
|
|
67
|
+
jpeg_size(io)
|
|
68
|
+
end
|
|
69
|
+
rescue StandardError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def jpeg_size(io)
|
|
74
|
+
io.seek(2)
|
|
75
|
+
loop do
|
|
76
|
+
byte = io.read(1)
|
|
77
|
+
return nil unless byte
|
|
78
|
+
next unless byte.ord == 0xFF
|
|
79
|
+
|
|
80
|
+
marker = io.read(1)&.ord
|
|
81
|
+
return nil unless marker
|
|
82
|
+
next if marker == 0xFF || marker == 0x01 || (0xD0..0xD8).cover?(marker)
|
|
83
|
+
return nil if marker == 0xD9 || marker == 0xDA
|
|
84
|
+
|
|
85
|
+
length = io.read(2)&.unpack1("n")
|
|
86
|
+
return nil unless length && length >= 2
|
|
87
|
+
|
|
88
|
+
if (0xC0..0xCF).cover?(marker) && ![0xC4, 0xC8, 0xCC].include?(marker)
|
|
89
|
+
data = io.read(5)
|
|
90
|
+
return nil unless data && data.bytesize == 5
|
|
91
|
+
|
|
92
|
+
height, width = data.byteslice(1, 4).unpack("nn")
|
|
93
|
+
return [width, height]
|
|
94
|
+
end
|
|
95
|
+
io.seek(length - 2, IO::SEEK_CUR)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
50
98
|
end
|