pandorga 1.12.0 → 1.14.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 +17 -0
- data/_includes/content-runtime/43-blocks.html +26 -4
- data/_includes/content-runtime/60-fragments.html +1 -0
- data/_includes/page/timeline-section.html +228 -29
- data/_layouts/projects.html +7 -1
- data/_layouts/shell.html +1 -1
- data/assets/css/base.css +174 -116
- data/assets/css/ledger/bibliography.css +15 -10
- data/assets/css/ledger/chrome.css +3 -12
- data/assets/css/ledger/cv.css +21 -0
- data/assets/css/ledger/detail.css +29 -15
- data/assets/css/ledger/home.css +6 -2
- data/assets/css/ledger/sources.css +27 -12
- data/assets/css/ledger/writing.css +10 -15
- data/lib/pandorga/cli.rb +4 -1
- data/lib/pandorga/commands/new.rb +1 -1
- data/lib/pandorga/commands/thumbnails.rb +19 -0
- 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/test-highlighter-pills.rb +306 -0
- data/scripts/test/test-poster-srcset.mjs +159 -0
- data/scripts/test/test-shell-a11y.rb +38 -0
- data/scripts/test/test-theme-contrast.rb +91 -0
- data/scripts/test/test-thumbnail-variants.rb +380 -0
- data/scripts/test/test-timeline-pan.mjs +147 -0
- data/scripts/test/test-timeline-pan.rb +116 -0
- data/scripts/validate.sh +13 -0
- metadata +8 -1
|
@@ -171,26 +171,34 @@
|
|
|
171
171
|
display: contents;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/* One geometry for all four.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
filled chip everywhere else on the site; here it outlines like its three
|
|
178
|
-
neighbors, and its hover still fills — which is how a reader can tell that
|
|
179
|
-
this one, alone, is a link. */
|
|
174
|
+
/* One geometry for all four. Facets lose their border and get a facet-strength
|
|
175
|
+
mark in their own ink. The tag chip is the only link and deepens its mark
|
|
176
|
+
on hover. */
|
|
180
177
|
.source-facets .source-facet,
|
|
181
178
|
.source-facets .ledger-chip,
|
|
182
179
|
.source-facets .lang-pill {
|
|
183
180
|
display: inline-flex;
|
|
184
181
|
align-items: center;
|
|
185
182
|
height: var(--source-facet-height);
|
|
186
|
-
padding: 0 var(--space-sm);
|
|
187
|
-
border:
|
|
183
|
+
padding: 0 calc(var(--space-sm) + 1.5px);
|
|
184
|
+
border: 0;
|
|
188
185
|
border-radius: var(--radius-card);
|
|
189
|
-
background: transparent;
|
|
190
186
|
white-space: nowrap;
|
|
191
187
|
font-size: var(--type-label);
|
|
192
188
|
}
|
|
193
189
|
|
|
190
|
+
.source-facets .ledger-chip {
|
|
191
|
+
--mark-1: var(--mark-facet-a);
|
|
192
|
+
--mark-2: var(--mark-facet-b);
|
|
193
|
+
--mark-3: var(--mark-facet-c);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.source-facets a.ledger-chip:hover {
|
|
197
|
+
--mark-1: var(--mark-hover-a);
|
|
198
|
+
--mark-2: var(--mark-hover-b);
|
|
199
|
+
--mark-3: var(--mark-hover-c);
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
/* The two chips this page owns. The shared pill and chip bring their own
|
|
195
203
|
type; these match it rather than inventing a second label voice. */
|
|
196
204
|
.source-facet {
|
|
@@ -199,12 +207,19 @@
|
|
|
199
207
|
font-weight: var(--weight-label);
|
|
200
208
|
letter-spacing: 0.03em;
|
|
201
209
|
line-height: 1;
|
|
202
|
-
|
|
210
|
+
--facet-ink: var(--color-on-surface-variant);
|
|
211
|
+
color: color-mix(in srgb, var(--facet-ink) var(--facet-ink-mix), var(--color-on-surface));
|
|
212
|
+
--mark-ink: currentColor;
|
|
213
|
+
--mark-1: var(--mark-facet-a);
|
|
214
|
+
--mark-2: var(--mark-facet-b);
|
|
215
|
+
--mark-3: var(--mark-facet-c);
|
|
203
216
|
}
|
|
204
217
|
|
|
205
|
-
/* flag-icons sizes from the em, so the flag's own font-size is its height.
|
|
218
|
+
/* flag-icons sizes from the em, so the flag's own font-size is its height.
|
|
219
|
+
The country chip gets no mark because the flag is its own shape. */
|
|
206
220
|
.source-facet--country {
|
|
207
221
|
padding-inline: 0.35rem;
|
|
222
|
+
background: none;
|
|
208
223
|
}
|
|
209
224
|
|
|
210
225
|
.source-facet--country .fi {
|
|
@@ -323,5 +338,5 @@ html[data-theme="light"] .source-card {
|
|
|
323
338
|
/* The medium chip and the card edge read the same custom property, so they
|
|
324
339
|
cannot drift apart: one colour, stated once, shown twice. */
|
|
325
340
|
.source-facet--medium {
|
|
326
|
-
|
|
341
|
+
--facet-ink: var(--source-accent);
|
|
327
342
|
}
|
|
@@ -207,16 +207,20 @@
|
|
|
207
207
|
border-top: 1px solid var(--border-gentle-divider);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/* Solid mark: opaque under the label, fades only in the trailing padding;
|
|
211
|
+
hover fills solid. */
|
|
210
212
|
.writing-index .writing-lead-foot .writing-lead-cta {
|
|
211
213
|
margin-left: auto;
|
|
212
214
|
padding: 0.35rem var(--space-md);
|
|
213
|
-
background: var(--color-primary);
|
|
215
|
+
background: linear-gradient(104deg, transparent 0.9%, var(--color-primary) 2.4%, var(--color-primary) calc(100% - 0.8rem), color-mix(in srgb, var(--color-primary) 40%, transparent) 100%);
|
|
216
|
+
-webkit-box-decoration-break: clone;
|
|
217
|
+
box-decoration-break: clone;
|
|
214
218
|
color: var(--color-on-primary);
|
|
215
219
|
font-weight: var(--weight-label);
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
.writing-index .writing-lead-cta:hover {
|
|
219
|
-
background:
|
|
223
|
+
background: var(--color-primary);
|
|
220
224
|
color: var(--color-on-primary);
|
|
221
225
|
text-decoration: none;
|
|
222
226
|
}
|
|
@@ -613,11 +617,10 @@ html[data-theme="light"] .writing-index .writing-lead-media .ledger-poster {
|
|
|
613
617
|
}
|
|
614
618
|
|
|
615
619
|
.writing-rail .listing-filter-chip {
|
|
620
|
+
align-self: flex-start;
|
|
616
621
|
justify-content: flex-start;
|
|
617
|
-
padding: 0.4rem 0.5rem;
|
|
618
|
-
background: none;
|
|
622
|
+
padding: 0.4rem 0.5rem 0.4rem calc(0.5rem + 2px);
|
|
619
623
|
border: 0;
|
|
620
|
-
border-left: 2px solid transparent;
|
|
621
624
|
color: var(--color-on-surface-variant);
|
|
622
625
|
font-family: var(--font-label);
|
|
623
626
|
font-size: var(--type-label-lg);
|
|
@@ -628,20 +631,12 @@ html[data-theme="light"] .writing-index .writing-lead-media .ledger-poster {
|
|
|
628
631
|
}
|
|
629
632
|
|
|
630
633
|
.writing-rail .listing-filter-chip:hover {
|
|
631
|
-
background: var(--color-surface-high);
|
|
632
|
-
border-left-color: var(--color-outline-variant);
|
|
633
634
|
color: var(--color-on-surface);
|
|
634
635
|
}
|
|
635
636
|
|
|
636
|
-
/*
|
|
637
|
-
on the left edge. Cabin (label role) can take real weight; colour still does
|
|
638
|
-
the selected cue so the rail stays quiet. `--color-primary` follows
|
|
639
|
-
`.page-accent--*` (green on Articles, ochre on Rascunhos), so the same rail
|
|
640
|
-
markup does not stay locked to secondary. */
|
|
637
|
+
/* Active filter chip uses the shared highlighter mark without a left border rule or tinted fill. */
|
|
641
638
|
.writing-rail .listing-filter-chip.is-active {
|
|
642
|
-
|
|
643
|
-
border-left-color: var(--color-primary);
|
|
644
|
-
color: var(--color-primary);
|
|
639
|
+
color: var(--color-on-surface);
|
|
645
640
|
}
|
|
646
641
|
|
|
647
642
|
/* ── Licence ──
|
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
|
|
@@ -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.14.0"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
|
@@ -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
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "set"
|
|
8
|
+
require "time"
|
|
9
|
+
require "yaml"
|
|
10
|
+
|
|
11
|
+
require_relative "registry"
|
|
12
|
+
|
|
13
|
+
module Pandorga
|
|
14
|
+
# Generates 16:9 center-cropped width variants for local project thumbnails
|
|
15
|
+
# and computes their srcset strings.
|
|
16
|
+
# Refs: PLT-AC-44
|
|
17
|
+
module ThumbnailVariants
|
|
18
|
+
WIDTHS = [480, 960, 1440].freeze
|
|
19
|
+
IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp].freeze
|
|
20
|
+
QUALITY = 82
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def generate(site_root)
|
|
25
|
+
root = Pathname.new(site_root).expand_path
|
|
26
|
+
content_root = resolve_content_root(root)
|
|
27
|
+
sources = project_thumbnail_sources(root, content_root)
|
|
28
|
+
|
|
29
|
+
summary = { sources: sources.length, written: 0, skipped: 0, failed: 0 }
|
|
30
|
+
return summary if sources.empty?
|
|
31
|
+
|
|
32
|
+
bins = resolve_imagemagick
|
|
33
|
+
unless bins
|
|
34
|
+
warn "warning: ImageMagick (magick or convert) not found; skipping project thumbnail variants"
|
|
35
|
+
return summary
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sources.each do |src|
|
|
39
|
+
src_width = identify_width(bins[:identify], src)
|
|
40
|
+
unless src_width && src_width.positive?
|
|
41
|
+
# An unreadable image keeps its original and must not stop the export.
|
|
42
|
+
warn "warning: cannot read #{src}; skipping its thumbnail variants"
|
|
43
|
+
summary[:failed] += 1
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
stem = src.basename(src.extname).to_s
|
|
48
|
+
variants_dir = src.dirname.join("variants")
|
|
49
|
+
src_mtime = src.mtime
|
|
50
|
+
|
|
51
|
+
WIDTHS.each do |width|
|
|
52
|
+
if width > src_width
|
|
53
|
+
summary[:skipped] += 1
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
dest = variants_dir.join("#{stem}-#{width}.jpg")
|
|
58
|
+
if dest.file? && dest.mtime >= src_mtime
|
|
59
|
+
summary[:skipped] += 1
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
write_variant(bins[:convert], src, dest, width)
|
|
65
|
+
summary[:written] += 1
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
warn "warning: #{e.message.lines.first.to_s.strip}; keeping the original only"
|
|
68
|
+
summary[:failed] += 1
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
summary
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def srcset_for(site_root, thumbnail_path)
|
|
77
|
+
media_rel = normalize_local_media_path(thumbnail_path)
|
|
78
|
+
return nil unless media_rel
|
|
79
|
+
|
|
80
|
+
ext = File.extname(media_rel).downcase
|
|
81
|
+
return nil unless IMAGE_EXTENSIONS.include?(ext)
|
|
82
|
+
|
|
83
|
+
root = Pathname.new(site_root).expand_path
|
|
84
|
+
content_root = resolve_content_root(root)
|
|
85
|
+
src = content_root.join(media_rel.delete_prefix("/"))
|
|
86
|
+
return nil unless src.file?
|
|
87
|
+
|
|
88
|
+
stem = src.basename(src.extname).to_s
|
|
89
|
+
variants_dir = src.dirname.join("variants")
|
|
90
|
+
return nil unless variants_dir.directory?
|
|
91
|
+
|
|
92
|
+
url_dir = File.dirname(media_rel)
|
|
93
|
+
parts = WIDTHS.filter_map do |width|
|
|
94
|
+
variant = variants_dir.join("#{stem}-#{width}.jpg")
|
|
95
|
+
next unless variant.file?
|
|
96
|
+
|
|
97
|
+
"#{url_dir}/variants/#{stem}-#{width}.jpg #{width}w"
|
|
98
|
+
end
|
|
99
|
+
return nil if parts.empty?
|
|
100
|
+
|
|
101
|
+
parts.join(", ")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def resolve_content_root(root)
|
|
105
|
+
candidate = root.join("content")
|
|
106
|
+
return candidate if candidate.directory?
|
|
107
|
+
return root if root.join("collections").directory? || root.join("media").directory?
|
|
108
|
+
|
|
109
|
+
candidate
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def project_collections(root)
|
|
113
|
+
config_path = root.join("_config.yml")
|
|
114
|
+
config = if config_path.file?
|
|
115
|
+
YAML.safe_load(config_path.read, permitted_classes: [Date, Time], aliases: true) || {}
|
|
116
|
+
else
|
|
117
|
+
{}
|
|
118
|
+
end
|
|
119
|
+
pages = PandorgaRegistry.pages(config)
|
|
120
|
+
names = pages.filter_map do |page|
|
|
121
|
+
next unless page.is_a?(Hash) && page["template"].to_s == "portfolio"
|
|
122
|
+
|
|
123
|
+
col = page["collection"].to_s.strip
|
|
124
|
+
col = page.dig("collections", "projects").to_s.strip if col.empty? && page["collections"].is_a?(Hash)
|
|
125
|
+
col unless col.empty?
|
|
126
|
+
end.uniq
|
|
127
|
+
names.empty? ? ["projects"] : names
|
|
128
|
+
rescue StandardError
|
|
129
|
+
["projects"]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def project_thumbnail_sources(root, content_root)
|
|
133
|
+
seen = Set.new
|
|
134
|
+
sources = []
|
|
135
|
+
|
|
136
|
+
project_collections(root).each do |collection|
|
|
137
|
+
dir = content_root.join("collections", collection)
|
|
138
|
+
next unless dir.directory?
|
|
139
|
+
|
|
140
|
+
dir.glob("**/*.{md,markdown}").sort.each do |md_path|
|
|
141
|
+
fm = read_front_matter(md_path)
|
|
142
|
+
next if fm["published"] == false && ENV["CONTENT_EXPORT_INCLUDE_UNPUBLISHED"] != "1"
|
|
143
|
+
|
|
144
|
+
media_rel = normalize_local_media_path(fm["thumbnail"])
|
|
145
|
+
next unless media_rel
|
|
146
|
+
next unless IMAGE_EXTENSIONS.include?(File.extname(media_rel).downcase)
|
|
147
|
+
|
|
148
|
+
src = content_root.join(media_rel.delete_prefix("/"))
|
|
149
|
+
next unless src.file?
|
|
150
|
+
|
|
151
|
+
key = src.expand_path.to_s
|
|
152
|
+
next if seen.include?(key)
|
|
153
|
+
|
|
154
|
+
seen << key
|
|
155
|
+
sources << src
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sources
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def normalize_local_media_path(path)
|
|
163
|
+
value = path.to_s.strip
|
|
164
|
+
return nil if value.empty?
|
|
165
|
+
return nil if value.match?(%r{\Ahttps?://}i)
|
|
166
|
+
|
|
167
|
+
if value.start_with?("/content/media/")
|
|
168
|
+
"/media/#{value.delete_prefix("/content/media/")}"
|
|
169
|
+
elsif value.start_with?("content/media/")
|
|
170
|
+
"/media/#{value.delete_prefix("content/media/")}"
|
|
171
|
+
elsif value.start_with?("/media/")
|
|
172
|
+
value
|
|
173
|
+
elsif value.start_with?("media/")
|
|
174
|
+
"/#{value}"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def read_front_matter(path)
|
|
179
|
+
text = path.read.sub(/\A\xEF\xBB\xBF/, "")
|
|
180
|
+
return {} unless text.start_with?("---\n") || text.start_with?("---\r\n")
|
|
181
|
+
|
|
182
|
+
parts = text.split(/^---\s*$\r?\n?/, 3)
|
|
183
|
+
return {} unless parts.length == 3
|
|
184
|
+
|
|
185
|
+
YAML.safe_load(parts[1], permitted_classes: [Date, Time], aliases: true) || {}
|
|
186
|
+
rescue StandardError
|
|
187
|
+
{}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def resolve_imagemagick
|
|
191
|
+
override = ENV["PANDORGA_IMAGEMAGICK"].to_s.strip
|
|
192
|
+
unless override.empty?
|
|
193
|
+
return nil unless executable_available?(override)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
convert: [override],
|
|
197
|
+
identify: File.basename(override) == "magick" ? [override, "identify"] : [ resolve_companion_identify(override) ]
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if executable_available?("magick")
|
|
202
|
+
{ convert: ["magick"], identify: ["magick", "identify"] }
|
|
203
|
+
elsif executable_available?("convert") && executable_available?("identify")
|
|
204
|
+
{ convert: ["convert"], identify: ["identify"] }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def resolve_companion_identify(convert_bin)
|
|
209
|
+
sibling = File.join(File.dirname(convert_bin), "identify")
|
|
210
|
+
return sibling if File.executable?(sibling) && !File.directory?(sibling)
|
|
211
|
+
|
|
212
|
+
"identify"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def executable_available?(bin)
|
|
216
|
+
return false if bin.to_s.empty?
|
|
217
|
+
|
|
218
|
+
if bin.include?("/")
|
|
219
|
+
File.executable?(bin) && !File.directory?(bin)
|
|
220
|
+
else
|
|
221
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
|
|
222
|
+
next false if dir.empty?
|
|
223
|
+
|
|
224
|
+
candidate = File.join(dir, bin)
|
|
225
|
+
File.executable?(candidate) && !File.directory?(candidate)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def identify_width(identify_cmd, src)
|
|
231
|
+
cmd = [*identify_cmd, "-ping", "-format", "%w", "#{src}[0]"]
|
|
232
|
+
out, _err, status = Open3.capture3(*cmd)
|
|
233
|
+
return nil unless status.success?
|
|
234
|
+
|
|
235
|
+
out.to_s.strip.to_i
|
|
236
|
+
rescue StandardError
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def write_variant(convert_cmd, src, dest, width)
|
|
241
|
+
height = (width * 9) / 16
|
|
242
|
+
FileUtils.mkdir_p(dest.dirname)
|
|
243
|
+
tmp = dest.dirname.join(".#{dest.basename}.tmp-#{Process.pid}")
|
|
244
|
+
cmd = [
|
|
245
|
+
*convert_cmd,
|
|
246
|
+
"#{src}[0]",
|
|
247
|
+
"-auto-orient",
|
|
248
|
+
"-resize", "#{width}x#{height}^",
|
|
249
|
+
"-gravity", "center",
|
|
250
|
+
"-extent", "#{width}x#{height}",
|
|
251
|
+
"-strip",
|
|
252
|
+
"-interlace", "JPEG",
|
|
253
|
+
"-sampling-factor", "4:2:0",
|
|
254
|
+
"-quality", QUALITY.to_s,
|
|
255
|
+
"jpeg:#{tmp}"
|
|
256
|
+
]
|
|
257
|
+
out, err, status = Open3.capture3(*cmd)
|
|
258
|
+
unless status.success? && tmp.file?
|
|
259
|
+
FileUtils.rm_f(tmp)
|
|
260
|
+
raise "ImageMagick failed for #{src} (#{width}w): #{out}\n#{err}"
|
|
261
|
+
end
|
|
262
|
+
File.rename(tmp.to_s, dest.to_s)
|
|
263
|
+
ensure
|
|
264
|
+
FileUtils.rm_f(tmp) if tmp && tmp.exist?
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
data/lib/pandorga/version.rb
CHANGED
data/lib/pandorga.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "../lib/content_license"
|
|
|
21
21
|
require_relative "../lib/writing_index_contract"
|
|
22
22
|
require_relative "band_art"
|
|
23
23
|
require_relative "hero_portraits"
|
|
24
|
+
require_relative "../../lib/pandorga/thumbnail_variants"
|
|
24
25
|
|
|
25
26
|
REPO_ROOT = Pathname.new(ENV.fetch("PANDORGA_SITE_ROOT", Dir.pwd)).expand_path
|
|
26
27
|
CONTENT_ROOT = Pathname.new(ARGV[0] || "content").expand_path
|
|
@@ -121,6 +122,16 @@ def rewrite_media_references(text)
|
|
|
121
122
|
rewrite_media_attribute_paths(rewrite_markdown_media_paths(text))
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
def rewrite_srcset_media_urls(srcset)
|
|
126
|
+
return nil unless srcset.is_a?(String) && !srcset.strip.empty?
|
|
127
|
+
|
|
128
|
+
srcset.split(",").map do |candidate|
|
|
129
|
+
url, descriptor = candidate.strip.split(/\s+/, 2)
|
|
130
|
+
resolved = resolve_media_url(url)
|
|
131
|
+
descriptor ? "#{resolved} #{descriptor}" : resolved
|
|
132
|
+
end.join(", ")
|
|
133
|
+
end
|
|
134
|
+
|
|
124
135
|
def rewrite_front_matter_media!(front_matter)
|
|
125
136
|
return front_matter unless front_matter.is_a?(Hash)
|
|
126
137
|
|
|
@@ -360,6 +371,11 @@ def export_markdown_tree(source_dir, output_dir, type)
|
|
|
360
371
|
end
|
|
361
372
|
|
|
362
373
|
apply_git_chronology!(front_matter, repo_relative_path, type)
|
|
374
|
+
if type == "project"
|
|
375
|
+
raw_srcset = Pandorga::ThumbnailVariants.srcset_for(REPO_ROOT, front_matter["thumbnail"])
|
|
376
|
+
rewritten_srcset = rewrite_srcset_media_urls(raw_srcset)
|
|
377
|
+
front_matter["thumbnail_srcset"] = rewritten_srcset if rewritten_srcset
|
|
378
|
+
end
|
|
363
379
|
rewrite_front_matter_media!(front_matter)
|
|
364
380
|
|
|
365
381
|
if ContentKeys::KEYED_TYPES.include?(type)
|
|
@@ -594,7 +610,7 @@ def writing_index_entry_from_export(payload, content_root, collection)
|
|
|
594
610
|
end
|
|
595
611
|
|
|
596
612
|
legacy = File.basename(source_path, File.extname(source_path))
|
|
597
|
-
{
|
|
613
|
+
entry = {
|
|
598
614
|
"slug" => slug,
|
|
599
615
|
"key" => key,
|
|
600
616
|
"title" => title,
|
|
@@ -630,6 +646,9 @@ def writing_index_entry_from_export(payload, content_root, collection)
|
|
|
630
646
|
"path_segment" => slug
|
|
631
647
|
)
|
|
632
648
|
}
|
|
649
|
+
exported_srcset = payload.dig("front_matter", "thumbnail_srcset").to_s
|
|
650
|
+
entry["thumbnail_srcset"] = exported_srcset unless exported_srcset.empty?
|
|
651
|
+
entry
|
|
633
652
|
end
|
|
634
653
|
|
|
635
654
|
def export_derived_data(content_root, output_root)
|
|
@@ -710,6 +729,8 @@ def export_collection_trees
|
|
|
710
729
|
trees
|
|
711
730
|
end
|
|
712
731
|
|
|
732
|
+
Pandorga::ThumbnailVariants.generate(REPO_ROOT)
|
|
733
|
+
|
|
713
734
|
files = export_collection_trees
|
|
714
735
|
files["page_fragments"] = export_page_fragments(CONTENT_ROOT.join("pages"), OUTPUT_ROOT.join("pages"))
|
|
715
736
|
files["page_headers"] = export_page_headers(CONTENT_ROOT, OUTPUT_ROOT) << export_hero_portraits(CONTENT_ROOT, OUTPUT_ROOT)
|