pandorga 1.6.9 → 1.7.1
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 +37 -0
- data/DESIGN.md +9 -9
- data/_includes/content-runtime/30-includes.html +1 -1
- data/_includes/content-runtime/43-blocks.html +1 -1
- data/_includes/content-runtime/50-detail.html +5 -5
- data/_includes/content-runtime/60-fragments.html +1 -1
- data/_includes/page/network-graph.html +2 -2
- data/_includes/page/site-footer.html +2 -2
- data/_includes/theme/font-loader.html +2 -54
- data/_includes/theme/theme-vars-block.liquid +10 -18
- data/_layouts/articles.html +1 -1
- data/_layouts/projects.html +2 -2
- data/_layouts/resources.html +1 -1
- data/_layouts/shell.html +1 -1
- data/_layouts/text.html +4 -4
- data/_plugins/lib/writing_entry.rb +1 -1
- data/assets/css/article.css +3 -3
- data/assets/css/base.css +0 -79
- data/assets/css/global-link.css +0 -1
- data/assets/css/ledger/chrome.css +1 -1
- data/assets/css/ledger/cv.css +1 -1
- data/assets/css/ledger/detail.css +1 -1
- data/assets/css/ledger/footer.css +1 -1
- data/assets/css/ledger/home.css +13 -42
- data/assets/css/ledger/projects.css +2 -2
- data/assets/css/ledger/sources.css +3 -3
- data/assets/css/ledger/writing.css +1 -1
- 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/jekyll/plugins/lib/writing_entry.rb +1 -1
- data/lib/pandorga/registry/schema.yml +21 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/content/export-content-json.rb +1 -1
- data/scripts/generate-configuration-doc.rb +1 -1
- data/scripts/test/render-parity/load-runtime.mjs +1 -1
- data/scripts/test/test-css-structure.rb +20 -0
- data/scripts/test/test-discovery.rb +5 -0
- data/scripts/test/test-examples-build.rb +9 -0
- data/scripts/test/test-fonts.rb +91 -0
- data/scripts/test/test-listing-filter-contract.rb +1 -1
- data/scripts/test/test-shell-a11y.rb +21 -0
- data/scripts/validate.sh +1 -0
- metadata +4 -1
data/assets/css/ledger/home.css
CHANGED
|
@@ -49,18 +49,6 @@
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/* Structural edge: the short rule the mockup runs along the top-left of the
|
|
53
|
-
featured band, marking it as the page's first statement after the hero. */
|
|
54
|
-
.home-band--ruled::before {
|
|
55
|
-
content: "";
|
|
56
|
-
position: absolute;
|
|
57
|
-
top: 0;
|
|
58
|
-
left: 0;
|
|
59
|
-
width: 6rem;
|
|
60
|
-
height: 3px;
|
|
61
|
-
background: var(--color-primary);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
52
|
/* ── Hero ── */
|
|
65
53
|
/* The band was 3rem above the name and 4.5rem below, which read as the block
|
|
66
54
|
sitting low in its own space. Equal above and below, at 60% of the larger
|
|
@@ -117,7 +105,7 @@
|
|
|
117
105
|
}
|
|
118
106
|
}
|
|
119
107
|
|
|
120
|
-
/* Ambient pool
|
|
108
|
+
/* Ambient pool behind the hero.
|
|
121
109
|
*
|
|
122
110
|
* Same weight as the earlier primary-container bloom, but keyed to neutral
|
|
123
111
|
* ink/outline so it stays grey paper depth instead of a blue spotlight. */
|
|
@@ -418,7 +406,7 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
418
406
|
box-shadow: none;
|
|
419
407
|
}
|
|
420
408
|
|
|
421
|
-
/* Silk facet
|
|
409
|
+
/* Silk facet behind the portrait — restored; the Borders pass had hidden it. */
|
|
422
410
|
.home-hero-shard {
|
|
423
411
|
top: -2.25rem;
|
|
424
412
|
right: -2rem;
|
|
@@ -576,8 +564,7 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
576
564
|
was the piece the band leads with, and a reader scanning down met four
|
|
577
565
|
equal entries. It now takes the card rung of the surface ladder, is fenced
|
|
578
566
|
by the same hairline every other panel on the site carries, and opens on a
|
|
579
|
-
rule in the primary
|
|
580
|
-
`.home-band--ruled` — so the eye stops at the top of it. The image inside is
|
|
567
|
+
rule in the primary, so the eye stops at the top of it. The image inside is
|
|
581
568
|
inset by the panel's own padding rather than running to the band edge, which
|
|
582
569
|
is the other half of the promotion: the lead contains its picture, the cards
|
|
583
570
|
below are contained by theirs.
|
|
@@ -646,7 +633,7 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
646
633
|
cubist half of the identity.
|
|
647
634
|
|
|
648
635
|
The pair is placed around the poster rather than across the card, because
|
|
649
|
-
the poster is opaque and a shard
|
|
636
|
+
the poster is opaque and a shard behind it is a shard nobody sees. The
|
|
650
637
|
alpha takes the top-left, over the index column, where there is nothing but
|
|
651
638
|
chrome and air. The beta takes the bottom-right and comes out from *under*
|
|
652
639
|
the poster into the strip below it, which is the same move the index band
|
|
@@ -1050,8 +1037,8 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
1050
1037
|
box-sizing: border-box;
|
|
1051
1038
|
}
|
|
1052
1039
|
|
|
1053
|
-
/*
|
|
1054
|
-
|
|
1040
|
+
/* A primary top rule, at a weight that beats the light-paper hairline
|
|
1041
|
+
reset on `.ledger-system-card`. */
|
|
1055
1042
|
:root .home-projects-bench .home-project-specimen,
|
|
1056
1043
|
:root .home-projects-bench .home-project-specimen:hover {
|
|
1057
1044
|
border-top: 2px solid var(--color-primary);
|
|
@@ -1568,11 +1555,11 @@ button.home-project-inventory-row {
|
|
|
1568
1555
|
The figure is hidden until the markup lands, so a plate that never arrives
|
|
1569
1556
|
leaves the panel with its title and its link rather than an empty frame. */
|
|
1570
1557
|
|
|
1571
|
-
/* Ambient pool
|
|
1558
|
+
/* Ambient pool behind the pair. Dark palette only, for the reason set out
|
|
1572
1559
|
above the hero's.
|
|
1573
1560
|
*
|
|
1574
|
-
* The two panels sat side by side with a visible seam and nothing
|
|
1575
|
-
* them. One pool centred on that seam,
|
|
1561
|
+
* The two panels sat side by side with a visible seam and nothing behind
|
|
1562
|
+
* them. One pool centred on that seam, behind both, lights the pair as a
|
|
1576
1563
|
* single object rather than giving each panel an opening of its own. Held at
|
|
1577
1564
|
* 5% against the hero's 7%: at equal strength the page reads as having two
|
|
1578
1565
|
* openings, and only one of them is where it starts.
|
|
@@ -1580,7 +1567,7 @@ button.home-project-inventory-row {
|
|
|
1580
1567
|
* Centred is right here and nowhere else — the seam is the thing being lit,
|
|
1581
1568
|
* so the offset is vertical instead, which is what keeps the pool partly
|
|
1582
1569
|
* outside the band it backs. `.home-band` already carries `position:
|
|
1583
|
-
* relative`; `isolation: isolate` is what stops `z-index: -1` escaping
|
|
1570
|
+
* relative`; `isolation: isolate` is what stops `z-index: -1` escaping behind
|
|
1584
1571
|
* the page background. */
|
|
1585
1572
|
.home-band--index {
|
|
1586
1573
|
isolation: isolate;
|
|
@@ -1642,7 +1629,7 @@ button.home-project-inventory-row {
|
|
|
1642
1629
|
the set, rotating hue the way every other shard on the site does.
|
|
1643
1630
|
*
|
|
1644
1631
|
* They sit in the foot of the panel, under the title and the link: the plate
|
|
1645
|
-
* above carries an opaque background, so a shard
|
|
1632
|
+
* above carries an opaque background, so a shard behind it would be a shard
|
|
1646
1633
|
* nobody sees. `overflow: hidden` on the panel above keeps the negative
|
|
1647
1634
|
* offsets out of the document scroll width, and the shared utility already
|
|
1648
1635
|
* unmounts both below 640px. */
|
|
@@ -1663,21 +1650,12 @@ button.home-project-inventory-row {
|
|
|
1663
1650
|
background: var(--color-primary);
|
|
1664
1651
|
}
|
|
1665
1652
|
|
|
1666
|
-
.home-index-shard--gamma {
|
|
1667
|
-
right: -3rem;
|
|
1668
|
-
bottom: -3.5rem;
|
|
1669
|
-
width: 11rem;
|
|
1670
|
-
height: 11rem;
|
|
1671
|
-
background: var(--color-tertiary);
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
1653
|
/* Between the shard cut-off and the layout each panel is the full
|
|
1675
1654
|
band wide on small screens, so a shard sized for half of it stops being a corner accent. */
|
|
1676
1655
|
@media (max-width: 899px) {
|
|
1677
1656
|
|
|
1678
1657
|
.home-index-shard,
|
|
1679
|
-
.home-index-shard--beta
|
|
1680
|
-
.home-index-shard--gamma {
|
|
1658
|
+
.home-index-shard--beta {
|
|
1681
1659
|
width: 8rem;
|
|
1682
1660
|
height: 8rem;
|
|
1683
1661
|
bottom: -2.5rem;
|
|
@@ -1688,7 +1666,7 @@ button.home-project-inventory-row {
|
|
|
1688
1666
|
padding-top: var(--space-xl);
|
|
1689
1667
|
}
|
|
1690
1668
|
|
|
1691
|
-
/* The title and the link have to sit clear of the shard
|
|
1669
|
+
/* The title and the link have to sit clear of the shard behind them. The
|
|
1692
1670
|
plate is excluded because it is the one child that must stay full-bleed,
|
|
1693
1671
|
and the shard because it is the thing being sat clear of. */
|
|
1694
1672
|
.home-index-panel> :not(.home-index-plate):not(.shard-accent) {
|
|
@@ -1697,13 +1675,6 @@ button.home-project-inventory-row {
|
|
|
1697
1675
|
margin-inline: var(--space-lg);
|
|
1698
1676
|
}
|
|
1699
1677
|
|
|
1700
|
-
.home-index-note {
|
|
1701
|
-
margin: 0;
|
|
1702
|
-
font-size: var(--type-body);
|
|
1703
|
-
line-height: 1.5;
|
|
1704
|
-
flex: 1 1 auto;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
1678
|
.home-index-title {
|
|
1708
1679
|
margin: 0;
|
|
1709
1680
|
font-size: var(--type-card);
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
position: relative;
|
|
184
184
|
display: block;
|
|
185
185
|
text-decoration: none;
|
|
186
|
-
/* Empty blueprint as texture
|
|
186
|
+
/* Empty blueprint as texture behind the plate — not a second view mode. */
|
|
187
187
|
background-color: var(--color-panel-contrast);
|
|
188
188
|
background-image:
|
|
189
189
|
linear-gradient(color-mix(in srgb, var(--color-outline) 10%, transparent) 1px, transparent 1px),
|
|
@@ -350,7 +350,7 @@
|
|
|
350
350
|
|
|
351
351
|
/* ══ Repositories ══
|
|
352
352
|
The mockup closed on a source-code ledger. Its licence stamp, spec
|
|
353
|
-
download and commit hashes have nothing
|
|
353
|
+
download and commit hashes have nothing behind them; the repository rows
|
|
354
354
|
do — `github` is set on five of the seven projects. */
|
|
355
355
|
.projects-page .projects-repos {
|
|
356
356
|
margin-top: var(--space-3xl);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
/* ── Shards ──
|
|
11
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
|
|
12
|
+
the ambient pool the home bands carry: a pool behind a grid of many small
|
|
13
13
|
cards only darkens one corner of it. Shards instead, the two canonical
|
|
14
14
|
polygons from base.css, one at each end of the run — top right above the
|
|
15
15
|
grid, bottom left below it, the diagonal pair the CV dossier uses inside
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/* A card is not positioned, so without this the shard — which is — would be
|
|
59
|
-
painted over the bottom row rather than
|
|
59
|
+
painted over the bottom row rather than behind it. The rule the whole
|
|
60
60
|
family answers to is that a backdrop never touches the object in front of
|
|
61
61
|
it. */
|
|
62
62
|
.sources-body>.listing-card-grid,
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
z-index: 1;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/* The foot of the page, beside the pagination. It reaches up
|
|
68
|
+
/* The foot of the page, beside the pagination. It reaches up behind the last
|
|
69
69
|
row of cards. In dark those cards are opaque again, so what shows is the
|
|
70
70
|
wedge in the gutter and the band across the pagination strip. */
|
|
71
71
|
.sources-foot-shard {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
The alpha sits bottom left, under the foot of the copy, the one corner the
|
|
71
71
|
plate never reaches at any width. The beta is a true corner accent at the
|
|
72
72
|
top right: it is offset far enough up that its visible part is the triangle
|
|
73
|
-
above the plate's top edge, and it tucks
|
|
73
|
+
above the plate's top edge, and it tucks behind the plate as it descends
|
|
74
74
|
rather than fighting it. Neither corner repeats the home panel's pair, and
|
|
75
75
|
the hues are the only two this page has — `.page-accent--green` rebinds
|
|
76
76
|
primary onto secondary, so green and ochre is the whole palette here. */
|
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)
|
|
@@ -15,7 +15,7 @@ require_relative "text_excerpt"
|
|
|
15
15
|
# (standalone, for scripts/content/export-content-json.rb). They differ only in how
|
|
16
16
|
# they derive the repo-relative path used for Git chronology, so each passes
|
|
17
17
|
# its own. Everything downstream lives here, because a field added or a bug
|
|
18
|
-
# fixed in one copy used to leave the other silently
|
|
18
|
+
# fixed in one copy used to leave the other silently behind.
|
|
19
19
|
module WritingEntry
|
|
20
20
|
XCITE_RE = /\{%-?\s*include\s+xcite\.html\s+key=["']([^"']+)["'][^%]*-?%\}/
|
|
21
21
|
|
|
@@ -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
|
@@ -269,7 +269,7 @@ def prune_stale_output_files!
|
|
|
269
269
|
end
|
|
270
270
|
|
|
271
271
|
stale.each(&:delete)
|
|
272
|
-
# Remove empty directories left
|
|
272
|
+
# Remove empty directories left behind by unpublish/rename.
|
|
273
273
|
OUTPUT_ROOT.find.sort.reverse_each do |path|
|
|
274
274
|
next unless path.directory?
|
|
275
275
|
next if path == OUTPUT_ROOT
|
|
@@ -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 << ""
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// and a Jekyll build never executes them, which is how a block that renders
|
|
6
6
|
// an element with no backing field ships green. This loader concatenates the
|
|
7
7
|
// partials in the exact order _includes/page/content-runtime.html includes
|
|
8
|
-
// them, evaluates the result
|
|
8
|
+
// them, evaluates the result behind a deliberately thin DOM shim, and hands
|
|
9
9
|
// back the same object 90-bootstrap.html returns to the browser.
|
|
10
10
|
//
|
|
11
11
|
// The shim is small on purpose: createElement, className, setAttribute,
|
|
@@ -72,6 +72,26 @@ sheets.each do |path|
|
|
|
72
72
|
if source.include?("home-profile-summary")
|
|
73
73
|
errors << "#{relative}: .home-profile-summary is unused"
|
|
74
74
|
end
|
|
75
|
+
|
|
76
|
+
# Selectors that matched no template, runtime string, or current content.
|
|
77
|
+
# A comment may still name one; a ruleset may not.
|
|
78
|
+
retired = %w[
|
|
79
|
+
page-title
|
|
80
|
+
ledger-marker--primary
|
|
81
|
+
ledger-card--edge-primary
|
|
82
|
+
ledger-card--edge-secondary
|
|
83
|
+
ledger-card--edge-tertiary
|
|
84
|
+
ledger-section-rule
|
|
85
|
+
home-band--ruled
|
|
86
|
+
home-index-shard--gamma
|
|
87
|
+
home-index-note
|
|
88
|
+
post-content
|
|
89
|
+
]
|
|
90
|
+
retired.each do |name|
|
|
91
|
+
next unless cleaned.match?(/[.#]#{Regexp.escape(name)}\b/)
|
|
92
|
+
|
|
93
|
+
errors << "#{relative}: .#{name} is unused"
|
|
94
|
+
end
|
|
75
95
|
end
|
|
76
96
|
|
|
77
97
|
fail!("CSS structure problems:\n - #{errors.join("\n - ")}") unless errors.empty?
|
|
@@ -41,6 +41,11 @@ fail!("portrait srcset is not read") unless runtime.include?("record.srcset = sr
|
|
|
41
41
|
fail!("portrait sources are not applied") unless runtime.include?("applyHeroPortraitSources")
|
|
42
42
|
shell = ROOT.join("_layouts/shell.html").read
|
|
43
43
|
fail!("shell missing JSON-LD") unless shell.include?("theme/json-ld.html")
|
|
44
|
+
og_images = shell.scan(/property="og:image"/)
|
|
45
|
+
fail!("shell emits #{og_images.size} og:image properties; one page, one image") unless og_images.size == 1
|
|
46
|
+
image_meta = shell[/<meta name="image"[^>]*>/]
|
|
47
|
+
fail!("shell missing name=image") unless image_meta
|
|
48
|
+
fail!("name=image also sets property=og:image") if image_meta.include?("property=")
|
|
44
49
|
fail!("discovery generator is not loaded from the plugin dir") unless ROOT.join("lib/pandorga/jekyll/plugins/discovery.rb").file?
|
|
45
50
|
|
|
46
51
|
puts "PASS test-discovery"
|
|
@@ -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"
|