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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 110f9fdf3636db9ef496093f969e15ef4cd3322ee143c5efadb210fb5e4b98b9
4
- data.tar.gz: 6c5625b1cbdac652c0120d2b25e49a722c59941862cf91da7f44eff6f1ada004
3
+ metadata.gz: b57cef517472b9cc15b3c3dcd4c13f119d495df666d167f9c741fff760b0e02b
4
+ data.tar.gz: '08c03b31d4b06e92df419ad20b091e63c31537fef06354a3af52abda07d5371a'
5
5
  SHA512:
6
- metadata.gz: 22f67174268b9edd80046d916e6d8394d4ce1df62e8802be7dc4eab8771479825c1ba8b3aa2f668ebef53bc95f116f840ddd300428362b938a19b9955d0914d8
7
- data.tar.gz: 85ce7605aae925b73d384a9ddbc0269b5b31bb58652b05b60617c5b784f8cfc2c68dc92cc3824ed07bf016f305aee36c5fba17a51c891ecce06cd58a0c3f94d0
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 stays.
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; fonts / motion / geometry from shared.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: '{{ font_title }}', {{ font_title_fallback }};
36
- --font-subtitle: '{{ font_subtitle }}', {{ font_subtitle_fallback }};
37
- --font-label: '{{ font_label }}', {{ font_label_fallback }};
38
- --font-body: '{{ font_body }}', {{ font_body_fallback }};
39
- --font-cv-body: '{{ font_cv_body }}', {{ font_body_fallback }};
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: '{{ font_title }}', {{ font_title_fallback }};
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: {% if sf.mono and sf.mono != '' %}'{{ sf.mono }}', {% endif %}ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
47
- --font-code: {% if sf.code and sf.code != '' %}'{{ sf.code }}', {% elsif sf.mono and sf.mono != '' %}'{{ sf.mono }}', {% endif %}ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
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. */
@@ -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: |
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.6.9"
4
+ VERSION = "1.7.0"
5
5
  end
@@ -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
@@ -64,6 +64,7 @@ TESTS=(
64
64
  scripts/test/test-home-articles.rb
65
65
  scripts/test/test-rascunhos-licence.rb
66
66
  scripts/test/test-pwa-icons.rb
67
+ scripts/test/test-fonts.rb
67
68
  scripts/test/test-css-delivery.rb
68
69
  scripts/test/test-discovery.rb
69
70
  )
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.6.9
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