pandorga 1.6.8 → 1.7.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 +21 -0
- data/_includes/content-runtime/40-listing.html +13 -5
- data/_includes/content-runtime/60-fragments.html +36 -10
- data/_includes/theme/font-loader.html +2 -54
- data/_includes/theme/theme-vars-block.liquid +10 -18
- data/_layouts/home.html +44 -15
- data/examples/full/_config.yml +9 -0
- data/lib/pandorga/fonts.rb +235 -0
- data/lib/pandorga/jekyll/plugins/fonts.rb +17 -0
- data/lib/pandorga/registry/schema.yml +21 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/check_home_lists.mjs +39 -11
- data/scripts/generate-configuration-doc.rb +1 -1
- data/scripts/test/test-examples-build.rb +9 -0
- data/scripts/test/test-fonts.rb +77 -0
- data/scripts/test/test-home-lists.rb +10 -1
- data/scripts/validate.sh +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b57cef517472b9cc15b3c3dcd4c13f119d495df666d167f9c741fff760b0e02b
|
|
4
|
+
data.tar.gz: '08c03b31d4b06e92df419ad20b091e63c31537fef06354a3af52abda07d5371a'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c60041b89ca661282fbfbc53052e55b46405a23eb914b3aa35ead456628b35c6834efe9f369e3cb301f311e4fd7f82932cf57c403e7afbfb5c36bdf525c0ed2c
|
|
7
|
+
data.tar.gz: fbf2fc58bd6578caba218953e8203a393dae30b2913e8d0ed6beb051ff0b795397807b697d1ec72bcdda4b20f222c11e8f7ef1654b472a84e090c0378bd448de
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.0] — 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- A site can name its typefaces under `pandorga.fonts`. `title`, `body`,
|
|
8
|
+
and `mono` take a Google Fonts family, or a map with `family` and
|
|
9
|
+
optional `weights` and `italic`. `label` and `subtitle` are optional.
|
|
10
|
+
Omitted roles keep Cinzel Decorative, Newsreader, and Courier Prime.
|
|
11
|
+
The text faces still load after the first paint.
|
|
12
|
+
|
|
13
|
+
## [1.6.9] — 2026-10-10
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Home list hydration no longer fills every band in one turn. The project
|
|
18
|
+
specimen measurement yields between projects, each band starts on its
|
|
19
|
+
own turn, and each home section introduction yields before it is
|
|
20
|
+
parsed. Dates reuse one formatter. The lists still render.
|
|
21
|
+
|
|
22
|
+
No `pandorga:` config keys were added or renamed.
|
|
23
|
+
|
|
3
24
|
## [1.6.8] — 2026-10-10
|
|
4
25
|
|
|
5
26
|
### Fixed
|
|
@@ -113,15 +113,23 @@
|
|
|
113
113
|
return textExcerpt((item && item.body_markdown) || '');
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
var dateFormatters = Object.create(null);
|
|
117
|
+
|
|
116
118
|
function formatDate(value, locale) {
|
|
117
119
|
if (!value) return '';
|
|
118
120
|
var parts = String(value).slice(0, 10).split('-').map(Number);
|
|
119
121
|
if (parts.length !== 3 || parts.some(isNaN)) return value;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
var key = locale || 'en';
|
|
123
|
+
var formatter = dateFormatters[key];
|
|
124
|
+
if (!formatter) {
|
|
125
|
+
formatter = new Intl.DateTimeFormat(key, {
|
|
126
|
+
day: '2-digit',
|
|
127
|
+
month: 'short',
|
|
128
|
+
year: 'numeric'
|
|
129
|
+
});
|
|
130
|
+
dateFormatters[key] = formatter;
|
|
131
|
+
}
|
|
132
|
+
return formatter.format(new Date(parts[0], parts[1] - 1, parts[2]));
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
function chronologySortKey(item) {
|
|
@@ -353,15 +353,31 @@
|
|
|
353
353
|
});
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
function yieldToMain() {
|
|
357
|
+
return new Promise(function (resolve) {
|
|
358
|
+
if (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function') {
|
|
359
|
+
scheduler.yield().then(resolve);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
window.setTimeout(resolve, 0);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
356
366
|
function hydrateHomeSections() {
|
|
357
367
|
var roots = Array.prototype.slice.call(
|
|
358
368
|
document.querySelectorAll('[data-content-home-section]')
|
|
359
369
|
);
|
|
360
370
|
if (!roots.length) return Promise.resolve();
|
|
361
371
|
return loadPageHeaders().then(function (sections) {
|
|
372
|
+
var chain = Promise.resolve();
|
|
362
373
|
roots.forEach(function (root) {
|
|
363
|
-
|
|
374
|
+
chain = chain.then(function () {
|
|
375
|
+
return yieldToMain().then(function () {
|
|
376
|
+
applyHomeSection(root, sections[root.getAttribute('data-content-home-section')]);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
364
379
|
});
|
|
380
|
+
return chain;
|
|
365
381
|
}).catch(function (error) {
|
|
366
382
|
console.error(error);
|
|
367
383
|
roots.forEach(function (root) { applyHomeSection(root, null); });
|
|
@@ -605,21 +621,31 @@
|
|
|
605
621
|
}
|
|
606
622
|
|
|
607
623
|
function hydratePageFragments() {
|
|
608
|
-
document.querySelectorAll('[data-content-page-header]')
|
|
609
|
-
|
|
624
|
+
var headers = Array.prototype.slice.call(document.querySelectorAll('[data-content-page-header]'));
|
|
625
|
+
var chain = Promise.resolve();
|
|
626
|
+
headers.forEach(function (el) {
|
|
627
|
+
chain = chain.then(function () {
|
|
628
|
+
return yieldToMain().then(function () { return hydratePageHeader(el); });
|
|
629
|
+
});
|
|
610
630
|
});
|
|
611
631
|
document.querySelectorAll('[data-content-fragment]').forEach(function (el) {
|
|
612
632
|
var path = el.getAttribute('data-content-fragment');
|
|
613
633
|
if (!path) return;
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
634
|
+
chain = chain.then(function () {
|
|
635
|
+
return yieldToMain().then(function () {
|
|
636
|
+
return mountMarkdownFragment(path, el, {
|
|
637
|
+
loadingText: el.getAttribute('data-content-loading') || 'Loading...',
|
|
638
|
+
errorText: el.getAttribute('data-content-error') || 'Content could not be loaded right now.'
|
|
639
|
+
});
|
|
640
|
+
});
|
|
617
641
|
});
|
|
618
642
|
});
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
643
|
+
chain.then(function () {
|
|
644
|
+
hydrateHomeSections();
|
|
645
|
+
hydrateBandArt();
|
|
646
|
+
hydrateContacts();
|
|
647
|
+
hydrateHeroPortrait();
|
|
648
|
+
});
|
|
623
649
|
}
|
|
624
650
|
|
|
625
651
|
function schedulePageFragmentHydration() {
|
|
@@ -1,62 +1,10 @@
|
|
|
1
|
-
{% include theme/theme-context.liquid %}
|
|
2
|
-
{% assign font_title = sf.title | default: "Lora" %}
|
|
3
|
-
{% assign font_subtitle = sf.subtitle | default: "Lato" %}
|
|
4
|
-
{% assign font_label = sf.label | default: "Ysabeau SC" %}
|
|
5
|
-
{% assign font_body = sf.body | default: "Newsreader" %}
|
|
6
|
-
{% assign font_mono = sf.mono %}
|
|
7
|
-
{% assign font_code = sf.code %}
|
|
8
|
-
{% assign font_cv_body = sf.cv_body %}
|
|
9
|
-
{% assign title_slug = font_title | replace: ' ', '+' %}
|
|
10
|
-
{% assign subtitle_slug = font_subtitle | replace: ' ', '+' %}
|
|
11
|
-
{% assign label_slug = font_label | replace: ' ', '+' %}
|
|
12
|
-
{% assign body_slug = font_body | replace: ' ', '+' %}
|
|
13
|
-
{% assign mono_slug = font_mono | replace: ' ', '+' %}
|
|
14
|
-
{% assign code_slug = font_code | replace: ' ', '+' %}
|
|
15
|
-
{% assign cv_body_slug = font_cv_body | replace: ' ', '+' %}
|
|
16
|
-
{% assign title_spec = sf.google_title_spec | default: "ital,wght@0,400;0,500;0,700;1,400;1,500" %}
|
|
17
|
-
{% assign subtitle_spec = sf.google_subtitle_spec | default: "ital,wght@0,300;0,400;0,500;0,700;1,400;1,500" %}
|
|
18
|
-
{% assign label_spec = sf.google_label_spec | default: "ital,wght@0,400;0,500;0,700;1,400;1,500" %}
|
|
19
|
-
{% assign body_spec = sf.google_body_spec | default: "ital,wght@0,300;0,400;0,500;0,700;1,400;1,500;1,700" %}
|
|
20
|
-
{% assign mono_spec = sf.google_mono_spec | default: "wght@400;500;700" %}
|
|
21
|
-
{% assign code_spec = sf.google_code_spec | default: "wght@300;400;500;600;700" %}
|
|
22
|
-
{% assign cv_body_spec = sf.google_cv_body_spec | default: "wght@300;400;500;600;700" %}
|
|
23
|
-
{% comment %}
|
|
24
|
-
Build the Google Fonts URL without repeating a family. Title is always
|
|
25
|
-
requested. Body / label / subtitle / mono / code append only when their
|
|
26
|
-
family name differs from ones already on the URL.
|
|
27
|
-
{% endcomment %}
|
|
28
|
-
{% assign fonts_href = "https://fonts.googleapis.com/css2?family=" | append: title_slug | append: ":" | append: title_spec %}
|
|
29
|
-
{% unless font_label == font_title %}
|
|
30
|
-
{% assign fonts_href = fonts_href | append: "&family=" | append: label_slug | append: ":" | append: label_spec %}
|
|
31
|
-
{% endunless %}
|
|
32
|
-
{% unless font_body == font_title or font_body == font_label %}
|
|
33
|
-
{% assign fonts_href = fonts_href | append: "&family=" | append: body_slug | append: ":" | append: body_spec %}
|
|
34
|
-
{% endunless %}
|
|
35
|
-
{% if font_mono and font_mono != '' %}
|
|
36
|
-
{% unless font_mono == font_title or font_mono == font_label or font_mono == font_body %}
|
|
37
|
-
{% assign fonts_href = fonts_href | append: "&family=" | append: mono_slug | append: ":" | append: mono_spec %}
|
|
38
|
-
{% endunless %}
|
|
39
|
-
{% endif %}
|
|
40
|
-
{% if font_code and font_code != '' %}
|
|
41
|
-
{% unless font_code == font_title or font_code == font_label or font_code == font_body or font_code == font_mono %}
|
|
42
|
-
{% assign fonts_href = fonts_href | append: "&family=" | append: code_slug | append: ":" | append: code_spec %}
|
|
43
|
-
{% endunless %}
|
|
44
|
-
{% endif %}
|
|
45
|
-
{% if font_subtitle != font_body and font_subtitle != font_label and font_subtitle != font_title %}
|
|
46
|
-
{% assign fonts_href = fonts_href | append: "&family=" | append: subtitle_slug | append: ":" | append: subtitle_spec %}
|
|
47
|
-
{% endif %}
|
|
48
|
-
{% if font_cv_body and font_cv_body != '' %}
|
|
49
|
-
{% unless font_cv_body == font_title or font_cv_body == font_label or font_cv_body == font_body or font_cv_body == font_subtitle %}
|
|
50
|
-
{% assign fonts_href = fonts_href | append: "&family=" | append: cv_body_slug | append: ":" | append: cv_body_spec %}
|
|
51
|
-
{% endunless %}
|
|
52
|
-
{% endif %}
|
|
53
|
-
{% assign fonts_href = fonts_href | append: "&display=swap" %}
|
|
54
1
|
{% comment %}
|
|
55
2
|
Not a render-blocking head link, and not the load event. A blocking
|
|
56
3
|
sheet holds first paint on the font host. The load event holds the hero
|
|
57
4
|
summary until images finish. The shell inserts this sheet after the
|
|
58
|
-
first paint. display=swap
|
|
5
|
+
first paint. The URL always ends in &display=swap.
|
|
59
6
|
{% endcomment %}
|
|
7
|
+
{% assign fonts_href = site | pandorga_font_stylesheet %}
|
|
60
8
|
<script>
|
|
61
9
|
window.__pandorgaFontsHref = {{ fonts_href | jsonify }};
|
|
62
10
|
</script>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{% comment %}
|
|
2
2
|
Emits CSS custom properties for one palette. Expects `theme_name` (e.g. light, dark).
|
|
3
|
-
Colour roles come from _data/themes/<name>.yml
|
|
3
|
+
Colour roles come from _data/themes/<name>.yml. Type stacks come from
|
|
4
|
+
pandorga.fonts, then shared.yml. Motion and geometry come from shared.yml.
|
|
4
5
|
{% endcomment %}
|
|
5
6
|
{% assign theme_profile = site.data.themes[theme_name] %}
|
|
6
7
|
{% if theme_profile == nil %}
|
|
@@ -17,34 +18,25 @@
|
|
|
17
18
|
{% assign color_primary = tc.primary | default: "#1c5a7a" %}
|
|
18
19
|
{% assign color_secondary = tc.secondary | default: "#287445" %}
|
|
19
20
|
{% assign color_tertiary = tc.tertiary | default: "#746f30" %}
|
|
20
|
-
{% assign font_title = sf.title | default: "Lora" %}
|
|
21
|
-
{% assign font_subtitle = sf.subtitle | default: "Lato" %}
|
|
22
|
-
{% assign font_label = sf.label | default: "Ysabeau SC" %}
|
|
23
|
-
{% assign font_body = sf.body | default: "Newsreader" %}
|
|
24
|
-
{% assign font_cv_body = sf.cv_body | default: "Newsreader" %}
|
|
25
21
|
{% assign font_cv_body_weight = sf.cv_body_weight | default: 400 %}
|
|
26
|
-
{% assign font_title_fallback = sf.title_fallback | default: "serif" %}
|
|
27
|
-
{% assign font_subtitle_fallback = sf.subtitle_fallback | default: "serif" %}
|
|
28
|
-
{% assign font_label_fallback = sf.label_fallback | default: "serif" %}
|
|
29
|
-
{% assign font_body_fallback = sf.body_fallback | default: "Georgia, serif" %}
|
|
30
22
|
{% assign bg_secondary = tn.background_secondary | default: tn.surface_container_low | default: "#f5f5f5" %}
|
|
31
23
|
{% assign surface_value = tn.surface | default: "#ffffff" %}
|
|
32
24
|
{% assign surface_variant_value = tn.surface_variant | default: "#e2e2e2" %}
|
|
33
25
|
{% if theme_name == "light" %}:root,{% endif %}
|
|
34
26
|
[data-theme="{{ theme_name }}"] {
|
|
35
|
-
--font-title:
|
|
36
|
-
--font-subtitle:
|
|
37
|
-
--font-label:
|
|
38
|
-
--font-body:
|
|
39
|
-
--font-cv-body:
|
|
27
|
+
--font-title: {{ site | pandorga_font_stack: "title" }};
|
|
28
|
+
--font-subtitle: {{ site | pandorga_font_stack: "subtitle" }};
|
|
29
|
+
--font-label: {{ site | pandorga_font_stack: "label" }};
|
|
30
|
+
--font-body: {{ site | pandorga_font_stack: "body" }};
|
|
31
|
+
--font-cv-body: {{ site | pandorga_font_stack: "cv_body" }};
|
|
40
32
|
--font-cv-body-weight: {{ font_cv_body_weight }};
|
|
41
|
-
--font-serif:
|
|
33
|
+
--font-serif: {{ site | pandorga_font_stack: "title" }};
|
|
42
34
|
/* Content titles share the body face at 600 — Cinzel Decorative is
|
|
43
35
|
reserved for page/display titles. */
|
|
44
36
|
--font-content-title: var(--font-body);
|
|
45
37
|
--font-sans: var(--font-body);
|
|
46
|
-
--font-mono: {
|
|
47
|
-
--font-code: {
|
|
38
|
+
--font-mono: {{ site | pandorga_font_stack: "mono" }};
|
|
39
|
+
--font-code: {{ site | pandorga_font_stack: "code" }};
|
|
48
40
|
|
|
49
41
|
/* Motion. Named for what they are for: state change, poster filter
|
|
50
42
|
warm-up, poster/plate scale. Shared across palettes. */
|
data/_layouts/home.html
CHANGED
|
@@ -121,7 +121,21 @@ layout: shell
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function byDateDesc(a, b) {
|
|
124
|
-
|
|
124
|
+
var left = String(b.date || '');
|
|
125
|
+
var right = String(a.date || '');
|
|
126
|
+
if (left > right) return 1;
|
|
127
|
+
if (left < right) return -1;
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function afterPaint() {
|
|
132
|
+
return new Promise(function (resolve) {
|
|
133
|
+
if (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function') {
|
|
134
|
+
scheduler.yield().then(resolve);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
window.setTimeout(resolve, 0);
|
|
138
|
+
});
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
function tagHrefFrom(element) {
|
|
@@ -175,8 +189,13 @@ layout: shell
|
|
|
175
189
|
function draftFolios(posts) {
|
|
176
190
|
var folios = Object.create(null);
|
|
177
191
|
posts.slice().sort(function (a, b) {
|
|
178
|
-
var byDate = String(a.date || '').
|
|
179
|
-
|
|
192
|
+
var byDate = String(a.date || '') < String(b.date || '') ? -1 : (String(a.date || '') > String(b.date || '') ? 1 : 0);
|
|
193
|
+
if (byDate !== 0) return byDate;
|
|
194
|
+
var left = String(a.slug || '');
|
|
195
|
+
var right = String(b.slug || '');
|
|
196
|
+
if (left < right) return -1;
|
|
197
|
+
if (left > right) return 1;
|
|
198
|
+
return 0;
|
|
180
199
|
}).forEach(function (entry, index) {
|
|
181
200
|
folios[entry.slug] = index + 1;
|
|
182
201
|
});
|
|
@@ -221,7 +240,8 @@ layout: shell
|
|
|
221
240
|
element.appendChild(fragment);
|
|
222
241
|
}
|
|
223
242
|
|
|
224
|
-
function fillBand(section, index) {
|
|
243
|
+
async function fillBand(section, index) {
|
|
244
|
+
await afterPaint();
|
|
225
245
|
var kind = section.getAttribute('data-home-kind');
|
|
226
246
|
var collection = section.getAttribute('data-home-collection');
|
|
227
247
|
var limit = parseInt(section.getAttribute('data-home-limit') || '3', 10) || 3;
|
|
@@ -316,7 +336,7 @@ layout: shell
|
|
|
316
336
|
fitted(best);
|
|
317
337
|
}
|
|
318
338
|
|
|
319
|
-
function paintSpecimen(entry) {
|
|
339
|
+
async function paintSpecimen(entry) {
|
|
320
340
|
specimenSlot.innerHTML = '';
|
|
321
341
|
specimenSlot.appendChild(runtime.renderProjectSpecimen(asItem(entry), {
|
|
322
342
|
href: entry.url || runtime.projectUrl(entry.slug),
|
|
@@ -324,10 +344,11 @@ layout: shell
|
|
|
324
344
|
folio: positionOf(entry.slug) + 1,
|
|
325
345
|
openLabel: 'Open project'
|
|
326
346
|
}));
|
|
347
|
+
await afterPaint();
|
|
327
348
|
ellipsizeSpecimenCopy(specimenSlot);
|
|
328
349
|
}
|
|
329
350
|
|
|
330
|
-
function measureTallestSpecimen() {
|
|
351
|
+
async function measureTallestSpecimen() {
|
|
331
352
|
var bench = specimenSlot.closest('.home-projects-bench');
|
|
332
353
|
if (!bench) return 0;
|
|
333
354
|
var width = specimenSlot.getBoundingClientRect().width;
|
|
@@ -348,7 +369,8 @@ layout: shell
|
|
|
348
369
|
bench.appendChild(probe);
|
|
349
370
|
|
|
350
371
|
var max = 0;
|
|
351
|
-
portfolio.
|
|
372
|
+
for (var i = 0; i < portfolio.length; i += 1) {
|
|
373
|
+
var entry = portfolio[i];
|
|
352
374
|
probe.innerHTML = '';
|
|
353
375
|
var card = runtime.renderProjectSpecimen(asItem(entry), {
|
|
354
376
|
href: entry.url || runtime.projectUrl(entry.slug),
|
|
@@ -360,15 +382,16 @@ layout: shell
|
|
|
360
382
|
card.style.minHeight = '0';
|
|
361
383
|
probe.appendChild(card);
|
|
362
384
|
max = Math.max(max, Math.ceil(card.getBoundingClientRect().height));
|
|
363
|
-
|
|
385
|
+
await afterPaint();
|
|
386
|
+
}
|
|
364
387
|
probe.remove();
|
|
365
388
|
return max;
|
|
366
389
|
}
|
|
367
390
|
|
|
368
|
-
function lockSpecimenHeight() {
|
|
391
|
+
async function lockSpecimenHeight() {
|
|
369
392
|
var bench = specimenSlot.closest('.home-projects-bench');
|
|
370
393
|
if (!bench) return;
|
|
371
|
-
var tallest = measureTallestSpecimen();
|
|
394
|
+
var tallest = await measureTallestSpecimen();
|
|
372
395
|
/* Floor at the CSS inventory minimum so six rows always clear
|
|
373
396
|
three stacked resource icons (see --home-inventory-min-height). */
|
|
374
397
|
var inventoryMin = 0;
|
|
@@ -425,8 +448,9 @@ layout: shell
|
|
|
425
448
|
});
|
|
426
449
|
}
|
|
427
450
|
|
|
428
|
-
lockSpecimenHeight();
|
|
429
|
-
paintSpecimen(entryBySlug(selectedSlug));
|
|
451
|
+
await lockSpecimenHeight();
|
|
452
|
+
await paintSpecimen(entryBySlug(selectedSlug));
|
|
453
|
+
await afterPaint();
|
|
430
454
|
paintInventory();
|
|
431
455
|
|
|
432
456
|
var resizeTimer = null;
|
|
@@ -437,8 +461,9 @@ layout: shell
|
|
|
437
461
|
var nextWidth = Math.round(specimenSlot.getBoundingClientRect().width);
|
|
438
462
|
if (nextWidth === lastMeasureWidth) return;
|
|
439
463
|
lastMeasureWidth = nextWidth;
|
|
440
|
-
lockSpecimenHeight()
|
|
441
|
-
|
|
464
|
+
lockSpecimenHeight().then(function () {
|
|
465
|
+
return paintSpecimen(entryBySlug(selectedSlug));
|
|
466
|
+
});
|
|
442
467
|
}, 120);
|
|
443
468
|
});
|
|
444
469
|
return;
|
|
@@ -484,7 +509,11 @@ layout: shell
|
|
|
484
509
|
|
|
485
510
|
runtime.loadJson('data/writing_index.json').then(function (entries) {
|
|
486
511
|
var index = Array.isArray(entries) ? entries : [];
|
|
487
|
-
|
|
512
|
+
var chain = Promise.resolve();
|
|
513
|
+
sections.forEach(function (section) {
|
|
514
|
+
chain = chain.then(function () { return fillBand(section, index); });
|
|
515
|
+
});
|
|
516
|
+
return chain;
|
|
488
517
|
}).catch(function (error) {
|
|
489
518
|
console.error(error);
|
|
490
519
|
sections.forEach(function (section) {
|
data/examples/full/_config.yml
CHANGED
|
@@ -18,6 +18,15 @@ pandorga:
|
|
|
18
18
|
base_url: ""
|
|
19
19
|
home:
|
|
20
20
|
hero_art: page/hero-default.html
|
|
21
|
+
fonts:
|
|
22
|
+
title: "Cinzel Decorative"
|
|
23
|
+
body:
|
|
24
|
+
family: "Newsreader"
|
|
25
|
+
weights: [300, 400, 500, 600, 700]
|
|
26
|
+
mono:
|
|
27
|
+
family: "Courier Prime"
|
|
28
|
+
weights: [400, 700]
|
|
29
|
+
italic: true
|
|
21
30
|
taxonomies:
|
|
22
31
|
articles:
|
|
23
32
|
tags: [Writing]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Spec: docs/features/fonts.md
|
|
4
|
+
# Refs: PLT-AC-30
|
|
5
|
+
module Pandorga
|
|
6
|
+
# Google Fonts families for one site. An omitted role keeps the
|
|
7
|
+
# Architectural Ledger face. A set role replaces that face only.
|
|
8
|
+
module Fonts
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
ROLES = %i[title label body mono code subtitle cv_body].freeze
|
|
12
|
+
# These follow another role when the theme used one family for both
|
|
13
|
+
# and the site did not name them. Changing the body then changes the
|
|
14
|
+
# tagline, instead of leaving Newsreader behind.
|
|
15
|
+
FOLLOW = { subtitle: :body, code: :mono, cv_body: :body }.freeze
|
|
16
|
+
MONO_FALLBACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"
|
|
17
|
+
FAMILY_NAME = /\A[\p{L}\p{N}][\p{L}\p{N} .+\-]*\z/
|
|
18
|
+
|
|
19
|
+
DEFAULTS = {
|
|
20
|
+
title: { family: "Cinzel Decorative", spec: "wght@400;700;900", fallback: "serif" },
|
|
21
|
+
subtitle: {
|
|
22
|
+
family: "Newsreader",
|
|
23
|
+
spec: "ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500",
|
|
24
|
+
fallback: "serif"
|
|
25
|
+
},
|
|
26
|
+
label: { family: "Marcellus SC", spec: "wght@400;700", fallback: "serif" },
|
|
27
|
+
body: { family: "Newsreader", spec: "wght@300;400;500;600;700", fallback: "Georgia, serif" },
|
|
28
|
+
mono: { family: "Courier Prime", spec: "ital,wght@0,400;0,700;1,400;1,700", fallback: MONO_FALLBACK },
|
|
29
|
+
code: { family: "Courier Prime", spec: "ital,wght@0,300;0,400;0,700;1,400;1,700", fallback: MONO_FALLBACK },
|
|
30
|
+
cv_body: { family: "Newsreader", spec: "wght@300;400;500;600;700", fallback: "Georgia, serif" }
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def resolve(site_fonts, theme_fonts = nil)
|
|
34
|
+
site = string_keys(site_fonts)
|
|
35
|
+
theme = string_keys(theme_fonts)
|
|
36
|
+
resolved = {}
|
|
37
|
+
ROLES.each do |role|
|
|
38
|
+
resolved[role] = resolve_role(role, site[role.to_s], theme)
|
|
39
|
+
end
|
|
40
|
+
FOLLOW.each do |role, source|
|
|
41
|
+
next if named?(site[role.to_s])
|
|
42
|
+
next unless same_family?(theme, role, source)
|
|
43
|
+
|
|
44
|
+
resolved[role] = resolved[role].merge(
|
|
45
|
+
family: resolved[source][:family],
|
|
46
|
+
spec: resolved[source][:spec]
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
resolved
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def from_site(site)
|
|
53
|
+
pandorga = lookup(site, "pandorga")
|
|
54
|
+
fonts = lookup(pandorga, "fonts")
|
|
55
|
+
data = lookup(site, "data")
|
|
56
|
+
themes = lookup(data, "themes")
|
|
57
|
+
shared = lookup(themes, "shared")
|
|
58
|
+
resolve(fonts, lookup(shared, "fonts"))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def stylesheet_href(resolved)
|
|
62
|
+
seen = []
|
|
63
|
+
parts = []
|
|
64
|
+
ROLES.each do |role|
|
|
65
|
+
face = resolved[role]
|
|
66
|
+
family = face[:family].to_s
|
|
67
|
+
next if family.empty? || seen.include?(family)
|
|
68
|
+
|
|
69
|
+
seen << family
|
|
70
|
+
parts << "#{family.tr(' ', '+')}:#{face[:spec]}"
|
|
71
|
+
end
|
|
72
|
+
return "" if parts.empty?
|
|
73
|
+
|
|
74
|
+
"https://fonts.googleapis.com/css2?family=#{parts.join('&family=')}&display=swap"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def css_stack(resolved, role)
|
|
78
|
+
face = resolved[role.to_sym]
|
|
79
|
+
return "" unless face
|
|
80
|
+
|
|
81
|
+
quoted = face[:family].to_s.empty? ? nil : "'#{escape_css(face[:family])}'"
|
|
82
|
+
[quoted, face[:fallback].to_s.strip].compact.reject(&:empty?).join(", ")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def resolve_role(role, override, theme)
|
|
86
|
+
base = DEFAULTS.fetch(role)
|
|
87
|
+
family = text(theme[role.to_s])
|
|
88
|
+
family = base[:family] if family.empty?
|
|
89
|
+
spec = text(theme["google_#{role}_spec"])
|
|
90
|
+
spec = base[:spec] if spec.empty?
|
|
91
|
+
fallback = fallback_for(role, theme, base)
|
|
92
|
+
|
|
93
|
+
parsed = parse_override(override)
|
|
94
|
+
return { family: family, spec: spec, fallback: fallback } unless parsed
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
family: parsed[:family],
|
|
98
|
+
spec: spec_for(parsed, family, spec, base[:family]),
|
|
99
|
+
fallback: fallback
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fallback_for(role, theme, base)
|
|
104
|
+
if role == :cv_body
|
|
105
|
+
body = text(theme["body_fallback"])
|
|
106
|
+
return body unless body.empty?
|
|
107
|
+
end
|
|
108
|
+
own = text(theme["#{role}_fallback"])
|
|
109
|
+
own.empty? ? base[:fallback] : own
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def spec_for(parsed, theme_family, theme_spec, default_family)
|
|
113
|
+
if parsed[:weights] || !parsed[:italic].nil?
|
|
114
|
+
weights = parsed[:weights] || [400, 700]
|
|
115
|
+
if parsed[:italic]
|
|
116
|
+
# Roman tuples first, then italic, so the axis list stays sorted.
|
|
117
|
+
tuples = weights.map { |weight| "0,#{weight}" } + weights.map { |weight| "1,#{weight}" }
|
|
118
|
+
"ital,wght@#{tuples.join(';')}"
|
|
119
|
+
else
|
|
120
|
+
"wght@#{weights.join(';')}"
|
|
121
|
+
end
|
|
122
|
+
elsif parsed[:family] == theme_family || parsed[:family] == default_family
|
|
123
|
+
theme_spec
|
|
124
|
+
else
|
|
125
|
+
"wght@400;700"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def parse_override(value)
|
|
130
|
+
case value
|
|
131
|
+
when String
|
|
132
|
+
family = clean_family(value)
|
|
133
|
+
family && { family: family, weights: nil, italic: nil }
|
|
134
|
+
when Hash
|
|
135
|
+
fields = string_keys(value)
|
|
136
|
+
family = clean_family(fields["family"] || fields["name"])
|
|
137
|
+
return nil unless family
|
|
138
|
+
|
|
139
|
+
{ family: family, weights: parse_weights(fields["weights"]), italic: italic_flag(fields) }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def italic_flag(fields)
|
|
144
|
+
return nil unless fields.key?("italic")
|
|
145
|
+
|
|
146
|
+
value = fields["italic"]
|
|
147
|
+
case value
|
|
148
|
+
when true, "true", "yes", 1, "1" then true
|
|
149
|
+
else false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def parse_weights(value)
|
|
154
|
+
return nil if value.nil?
|
|
155
|
+
|
|
156
|
+
list = case value
|
|
157
|
+
when Array then value
|
|
158
|
+
when Integer then [value]
|
|
159
|
+
when String then value.split(",")
|
|
160
|
+
else return nil
|
|
161
|
+
end
|
|
162
|
+
numbers = []
|
|
163
|
+
list.each do |item|
|
|
164
|
+
text = item.to_s.strip
|
|
165
|
+
next if text.empty?
|
|
166
|
+
return nil unless text.match?(/\A\d+\z/)
|
|
167
|
+
|
|
168
|
+
number = text.to_i
|
|
169
|
+
return nil unless number.between?(1, 1000)
|
|
170
|
+
|
|
171
|
+
numbers << number
|
|
172
|
+
end
|
|
173
|
+
return nil if numbers.empty?
|
|
174
|
+
|
|
175
|
+
numbers.uniq.sort
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def clean_family(value)
|
|
179
|
+
family = value.to_s.strip
|
|
180
|
+
return nil if family.empty? || !FAMILY_NAME.match?(family)
|
|
181
|
+
|
|
182
|
+
family
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def named?(value)
|
|
186
|
+
!parse_override(value).nil?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def same_family?(theme, role, source)
|
|
190
|
+
role_family = text(theme[role.to_s])
|
|
191
|
+
source_family = text(theme[source.to_s])
|
|
192
|
+
role_family = DEFAULTS.fetch(role)[:family] if role_family.empty?
|
|
193
|
+
source_family = DEFAULTS.fetch(source)[:family] if source_family.empty?
|
|
194
|
+
role_family == source_family
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def text(value)
|
|
198
|
+
value.to_s.strip
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def string_keys(value)
|
|
202
|
+
return {} unless value.is_a?(Hash)
|
|
203
|
+
|
|
204
|
+
value.each_with_object({}) { |(key, item), out| out[key.to_s] = item }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def lookup(object, key)
|
|
208
|
+
return nil if object.nil?
|
|
209
|
+
|
|
210
|
+
string_key = key.to_s
|
|
211
|
+
if object.is_a?(Hash)
|
|
212
|
+
return object[string_key] if object.key?(string_key)
|
|
213
|
+
return object[string_key.to_sym] if object.key?(string_key.to_sym)
|
|
214
|
+
|
|
215
|
+
return nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if object.respond_to?(:[])
|
|
219
|
+
begin
|
|
220
|
+
found = object[string_key]
|
|
221
|
+
return found unless found.nil?
|
|
222
|
+
rescue StandardError
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
return object.public_send(string_key) if object.respond_to?(string_key)
|
|
227
|
+
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def escape_css(family)
|
|
232
|
+
family.to_s.gsub("\\", "\\\\\\\\").gsub("'", "\\\\'").gsub(/[\r\n]/, "")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../fonts"
|
|
4
|
+
|
|
5
|
+
# Liquid filters for pandorga.fonts. The stylesheet URL and the CSS stacks
|
|
6
|
+
# come from one resolver so the head and the tokens cannot drift.
|
|
7
|
+
module PandorgaFontFilter
|
|
8
|
+
def pandorga_font_stylesheet(site)
|
|
9
|
+
Pandorga::Fonts.stylesheet_href(Pandorga::Fonts.from_site(site))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pandorga_font_stack(site, role)
|
|
13
|
+
Pandorga::Fonts.css_stack(Pandorga::Fonts.from_site(site), role)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Liquid::Template.register_filter(PandorgaFontFilter) if defined?(Liquid::Template)
|
|
@@ -50,6 +50,27 @@ home:
|
|
|
50
50
|
type: string
|
|
51
51
|
notes: Include path for hero decoration (default `page/hero-default.html`)
|
|
52
52
|
|
|
53
|
+
fonts:
|
|
54
|
+
title: "`pandorga.fonts`"
|
|
55
|
+
intro: |
|
|
56
|
+
Each value is a Google Fonts family name, or a map with `family` plus optional `weights` (integers) and `italic` (boolean). An omitted role keeps the Architectural Ledger face. `code` follows `mono`, and the tagline and CV body follow `body`, when the theme used one family for both. Text faces still load after the first paint, with `display=swap`. A `_data/themes/shared.yml` file remains the fallback under any role this map does not set.
|
|
57
|
+
keys:
|
|
58
|
+
title:
|
|
59
|
+
type: string or map
|
|
60
|
+
notes: Display face. Default family `Cinzel Decorative`, weights 400, 700, and 900
|
|
61
|
+
body:
|
|
62
|
+
type: string or map
|
|
63
|
+
notes: Reading face. Default family `Newsreader`, weights 300, 400, 500, 600, and 700
|
|
64
|
+
mono:
|
|
65
|
+
type: string or map
|
|
66
|
+
notes: Monospace face. Default family `Courier Prime`, roman and italic at 400 and 700
|
|
67
|
+
label:
|
|
68
|
+
type: string or map
|
|
69
|
+
notes: Optional chrome face. Default family `Marcellus SC`, weights 400 and 700
|
|
70
|
+
subtitle:
|
|
71
|
+
type: string or map
|
|
72
|
+
notes: Optional tagline face. Omitted, it follows `body` when the theme used the same family
|
|
73
|
+
|
|
53
74
|
taxonomies:
|
|
54
75
|
title: "`pandorga.taxonomies`"
|
|
55
76
|
prose: |
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Lighthouse run treats an empty home as a fast page.
|
|
5
5
|
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
7
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
8
8
|
import { createServer } from 'node:net';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
@@ -90,17 +90,44 @@ class Cdp {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
async function activePort(profile) {
|
|
94
|
+
try {
|
|
95
|
+
const text = await readFile(join(profile, 'DevToolsActivePort'), 'utf8');
|
|
96
|
+
const value = Number(text.split('\n')[0]);
|
|
97
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
98
|
+
} catch (_error) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function waitForDevtools(port, chrome, profile) {
|
|
104
|
+
let stderr = '';
|
|
105
|
+
if (chrome.stderr) {
|
|
106
|
+
chrome.stderr.on('data', (chunk) => {
|
|
107
|
+
stderr += chunk;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
111
|
+
if (chrome.exitCode !== null) {
|
|
112
|
+
throw new Error(`Chrome exited ${chrome.exitCode}: ${stderr.slice(-500)}`);
|
|
113
|
+
}
|
|
114
|
+
const opened = await activePort(profile);
|
|
115
|
+
const candidates = opened && opened !== port ? [port, opened] : [port];
|
|
116
|
+
for (const candidate of candidates) {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(`http://127.0.0.1:${candidate}/json/version`);
|
|
119
|
+
if (response.ok) return response.json();
|
|
120
|
+
} catch (_error) {
|
|
121
|
+
// Chrome is still starting.
|
|
122
|
+
}
|
|
100
123
|
}
|
|
101
124
|
await delay(100);
|
|
102
125
|
}
|
|
103
|
-
|
|
126
|
+
const opened = await activePort(profile);
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Chrome DevTools did not answer on ${port}` +
|
|
129
|
+
`${opened ? ` (DevToolsActivePort ${opened})` : ''}. ${stderr.slice(-500)}`
|
|
130
|
+
);
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
async function openSocket(url) {
|
|
@@ -155,14 +182,15 @@ async function main() {
|
|
|
155
182
|
'--disable-dev-shm-usage',
|
|
156
183
|
'--no-first-run',
|
|
157
184
|
'--remote-allow-origins=*',
|
|
185
|
+
'--remote-debugging-address=127.0.0.1',
|
|
158
186
|
`--remote-debugging-port=${port}`,
|
|
159
187
|
`--user-data-dir=${profile}`,
|
|
160
188
|
'about:blank'
|
|
161
|
-
], { stdio: 'ignore' });
|
|
189
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
162
190
|
|
|
163
191
|
let failed = false;
|
|
164
192
|
try {
|
|
165
|
-
const version = await waitForDevtools(port);
|
|
193
|
+
const version = await waitForDevtools(port, chrome, profile);
|
|
166
194
|
const ws = await openSocket(version.webSocketDebuggerUrl);
|
|
167
195
|
const browser = new Cdp(ws);
|
|
168
196
|
const created = await browser.send('Target.createTarget', { url: 'about:blank' });
|
|
@@ -66,7 +66,7 @@ def generate(schema)
|
|
|
66
66
|
out << "via `scripts/generate-configuration-doc.rb` (`PLT-AC-11`)."
|
|
67
67
|
out << ""
|
|
68
68
|
|
|
69
|
-
%w[identity content home].each do |section|
|
|
69
|
+
%w[identity content home fonts].each do |section|
|
|
70
70
|
data = schema.fetch(section)
|
|
71
71
|
out << "## #{data.fetch('title')}"
|
|
72
72
|
out << ""
|
|
@@ -90,6 +90,15 @@ end
|
|
|
90
90
|
warn "FAIL test-examples-build: #{name} home is missing JSON-LD"
|
|
91
91
|
exit 1
|
|
92
92
|
end
|
|
93
|
+
ledger_faces = "family=Cinzel+Decorative:wght@400;700;900"
|
|
94
|
+
unless html.include?(ledger_faces) && html.include?("&display=swap") && html.include?("__pandorgaFontsHref")
|
|
95
|
+
warn "FAIL test-examples-build: #{name} home dropped the ledger font request"
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
unless html.include?("--font-title: 'Cinzel Decorative', serif")
|
|
99
|
+
warn "FAIL test-examples-build: #{name} home dropped the title face"
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
93
102
|
end
|
|
94
103
|
|
|
95
104
|
puts "PASS test-examples-build"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# PLT-AC-30. A site names its Google Fonts families in pandorga.fonts.
|
|
5
|
+
# Omitted roles keep Cinzel Decorative, Newsreader, and Courier Prime.
|
|
6
|
+
|
|
7
|
+
require "pathname"
|
|
8
|
+
require "yaml"
|
|
9
|
+
|
|
10
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
11
|
+
|
|
12
|
+
def fail!(message)
|
|
13
|
+
warn "FAIL test-fonts: #{message}"
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require_relative "../../lib/pandorga/fonts"
|
|
18
|
+
|
|
19
|
+
DEFAULT_HREF = "https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700;900" \
|
|
20
|
+
"&family=Marcellus+SC:wght@400;700" \
|
|
21
|
+
"&family=Newsreader:wght@300;400;500;600;700" \
|
|
22
|
+
"&family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700" \
|
|
23
|
+
"&display=swap"
|
|
24
|
+
|
|
25
|
+
theme = YAML.safe_load(ROOT.join("_data/themes/shared.yml").read).fetch("fonts")
|
|
26
|
+
bare = Pandorga::Fonts.resolve(nil, nil)
|
|
27
|
+
from_theme = Pandorga::Fonts.resolve(nil, theme)
|
|
28
|
+
fail!("unset config dropped the ledger faces") unless Pandorga::Fonts.stylesheet_href(bare) == DEFAULT_HREF
|
|
29
|
+
fail!("theme tokens drifted from the ledger faces") unless Pandorga::Fonts.stylesheet_href(from_theme) == DEFAULT_HREF
|
|
30
|
+
|
|
31
|
+
fail!("title stack dropped Cinzel") unless Pandorga::Fonts.css_stack(from_theme, "title") == "'Cinzel Decorative', serif"
|
|
32
|
+
fail!("body stack dropped the serif fallback") unless Pandorga::Fonts.css_stack(from_theme, "body") == "'Newsreader', Georgia, serif"
|
|
33
|
+
fail!("mono stack dropped Courier Prime") unless Pandorga::Fonts.css_stack(from_theme, "mono").start_with?("'Courier Prime', ui-monospace")
|
|
34
|
+
|
|
35
|
+
custom = Pandorga::Fonts.resolve(
|
|
36
|
+
{
|
|
37
|
+
"title" => "Fraunces",
|
|
38
|
+
"body" => { "family" => "Source Serif 4", "weights" => [600, 400, 400] },
|
|
39
|
+
"mono" => { "family" => "IBM Plex Mono", "weights" => [400, 500], "italic" => true }
|
|
40
|
+
},
|
|
41
|
+
theme
|
|
42
|
+
)
|
|
43
|
+
custom_href = Pandorga::Fonts.stylesheet_href(custom)
|
|
44
|
+
fail!("custom title was not requested") unless custom_href.include?("family=Fraunces:wght@400;700")
|
|
45
|
+
fail!("custom title kept the Cinzel axis") if custom_href.include?("Cinzel")
|
|
46
|
+
fail!("body weights were not sorted") unless custom_href.include?("family=Source+Serif+4:wght@400;600")
|
|
47
|
+
fail!("mono italic axis is wrong") unless custom_href.include?("family=IBM+Plex+Mono:ital,wght@0,400;0,500;1,400;1,500")
|
|
48
|
+
fail!("unset label left Marcellus") unless custom_href.include?("family=Marcellus+SC:wght@400;700")
|
|
49
|
+
fail!("body change left the tagline on Newsreader") unless Pandorga::Fonts.css_stack(custom, "subtitle").start_with?("'Source Serif 4'")
|
|
50
|
+
fail!("code face did not follow mono") unless Pandorga::Fonts.css_stack(custom, "code").start_with?("'IBM Plex Mono'")
|
|
51
|
+
|
|
52
|
+
kept = Pandorga::Fonts.resolve({ "title" => "Cinzel Decorative" }, theme)
|
|
53
|
+
fail!("naming the default title changed the stylesheet") unless Pandorga::Fonts.stylesheet_href(kept) == DEFAULT_HREF
|
|
54
|
+
|
|
55
|
+
ignored = Pandorga::Fonts.resolve({ "title" => "Fraunces</style>" }, theme)
|
|
56
|
+
fail!("a broken family name was accepted") unless Pandorga::Fonts.stylesheet_href(ignored) == DEFAULT_HREF
|
|
57
|
+
|
|
58
|
+
example = YAML.safe_load(ROOT.join("examples/full/_config.yml").read)
|
|
59
|
+
example_fonts = example.dig("pandorga", "fonts")
|
|
60
|
+
fail!("full example has no pandorga.fonts") unless example_fonts.is_a?(Hash)
|
|
61
|
+
fail!("full example changed the measured faces") unless Pandorga::Fonts.stylesheet_href(Pandorga::Fonts.resolve(example_fonts, theme)) == DEFAULT_HREF
|
|
62
|
+
|
|
63
|
+
loader = ROOT.join("_includes/theme/font-loader.html").read
|
|
64
|
+
shell = ROOT.join("_layouts/shell.html").read
|
|
65
|
+
fail!("loader no longer hands the href to the shell") unless loader.include?("__pandorgaFontsHref")
|
|
66
|
+
fail!("loader preloads the text faces") if loader.include?('rel="preload"')
|
|
67
|
+
fail!("text faces wait for the load event") if loader.include?("addEventListener('load'")
|
|
68
|
+
preconnect_at = shell.index("fonts.googleapis.com")
|
|
69
|
+
script_at = shell.index("theme/critical-preload.html")
|
|
70
|
+
fail!("font host is not preconnected before the preload script") unless preconnect_at && script_at && preconnect_at < script_at
|
|
71
|
+
fail!("text faces are not requested after the first paint") unless shell.include?("requestAnimationFrame") && shell.include?("__pandorgaFontsHref")
|
|
72
|
+
|
|
73
|
+
spec = ROOT.join("docs/spec.md").read
|
|
74
|
+
fail!("spec is missing PLT-AC-30") unless spec.include?("PLT-AC-30")
|
|
75
|
+
fail!("configuration reference omits pandorga.fonts") unless ROOT.join("docs/configuration.md").read.include?("pandorga.fonts")
|
|
76
|
+
|
|
77
|
+
puts "PASS test-fonts"
|
|
@@ -43,7 +43,14 @@ end
|
|
|
43
43
|
|
|
44
44
|
home = ROOT.join("_layouts/home.html").read
|
|
45
45
|
fail!("home paint still runs before the runtime") unless home.scan("pandorga:runtime").length >= 2
|
|
46
|
-
fail!("home bands still
|
|
46
|
+
fail!("home bands still fill in one turn") unless home.include?("scheduler.yield") && home.include?("await afterPaint()")
|
|
47
|
+
fail!("specimen measurement does not yield") unless home.include?("await measureTallestSpecimen()")
|
|
48
|
+
fail!("home still fills every band inside one callback") if home.include?("sections.forEach(function (section) { fillBand(section, index); })")
|
|
49
|
+
fragments = ROOT.join("_includes/content-runtime/60-fragments.html").read
|
|
50
|
+
fail!("home section markdown still runs in one turn") unless fragments.include?("function yieldToMain()") &&
|
|
51
|
+
fragments.include?("return yieldToMain().then(function () {")
|
|
52
|
+
listing = ROOT.join("_includes/content-runtime/40-listing.html").read
|
|
53
|
+
fail!("date formatting builds a new formatter per card") unless listing.include?("dateFormatters")
|
|
47
54
|
|
|
48
55
|
marked = ROOT.join("_includes/content-runtime/00-config.html").read
|
|
49
56
|
fail!("markdown library no longer defers") unless marked.include?("<script defer ")
|
|
@@ -67,6 +74,8 @@ fail!("probe ignores the project inventory") unless probe.include?("#home-projec
|
|
|
67
74
|
fail!("probe ignores article cards") unless probe.include?("#home-articles .home-featured, #home-articles .ledger-card")
|
|
68
75
|
fail!("probe ignores the phone viewport") unless probe.include?("name: 'mobile'")
|
|
69
76
|
fail!("probe ignores the desktop viewport") unless probe.include?("name: 'desktop'")
|
|
77
|
+
fail!("probe does not pin DevTools to IPv4") unless probe.include?("--remote-debugging-address=127.0.0.1")
|
|
78
|
+
fail!("probe gives Chrome only a few seconds to open DevTools") unless probe.include?("attempt < 200")
|
|
70
79
|
|
|
71
80
|
_out, err, status = Open3.capture3("node", "--check", ROOT.join("scripts/check_home_lists.mjs").to_s)
|
|
72
81
|
fail!("home list probe is not valid JavaScript\n#{err}") unless status.success?
|
data/scripts/validate.sh
CHANGED
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.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Pandorga contributors
|
|
@@ -288,6 +288,7 @@ files:
|
|
|
288
288
|
- lib/pandorga/commands/serve.rb
|
|
289
289
|
- lib/pandorga/commands/studio_schema.rb
|
|
290
290
|
- lib/pandorga/discovery.rb
|
|
291
|
+
- lib/pandorga/fonts.rb
|
|
291
292
|
- lib/pandorga/hero_summary.rb
|
|
292
293
|
- lib/pandorga/jekyll.rb
|
|
293
294
|
- lib/pandorga/jekyll/generator.rb
|
|
@@ -303,6 +304,7 @@ files:
|
|
|
303
304
|
- lib/pandorga/jekyll/plugins/content_writing_index.rb
|
|
304
305
|
- lib/pandorga/jekyll/plugins/content_writing_paths.rb
|
|
305
306
|
- lib/pandorga/jekyll/plugins/discovery.rb
|
|
307
|
+
- lib/pandorga/jekyll/plugins/fonts.rb
|
|
306
308
|
- lib/pandorga/jekyll/plugins/force_utf8_encoding.rb
|
|
307
309
|
- lib/pandorga/jekyll/plugins/git_chronology.rb
|
|
308
310
|
- lib/pandorga/jekyll/plugins/git_metadata.rb
|
|
@@ -422,6 +424,7 @@ files:
|
|
|
422
424
|
- scripts/test/test-export-content-json-incremental.rb
|
|
423
425
|
- scripts/test/test-export.rb
|
|
424
426
|
- scripts/test/test-exported-fields-reach-the-view.rb
|
|
427
|
+
- scripts/test/test-fonts.rb
|
|
425
428
|
- scripts/test/test-form-control-theming.rb
|
|
426
429
|
- scripts/test/test-git-chronology.rb
|
|
427
430
|
- scripts/test/test-hero-name.rb
|