pandorga 1.6.9 → 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 +10 -0
- data/_includes/theme/font-loader.html +2 -54
- data/_includes/theme/theme-vars-block.liquid +10 -18
- 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/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/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,15 @@
|
|
|
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
|
+
|
|
3
13
|
## [1.6.9] — 2026-10-10
|
|
4
14
|
|
|
5
15
|
### Fixed
|
|
@@ -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/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
|
@@ -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"
|
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
|