fontist 3.0.7 → 3.0.9

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: 473f97386abfc5612b2386ef09c7166f57577346e1fa1576f10b89cde776a82a
4
- data.tar.gz: 496fe338410ab4a2fca8d03f51de0030ed831e365e3dd310f9a26f92bb441d16
3
+ metadata.gz: d5fa7d3f053af64da4931ed4d3808f703b5d984850d974d562fa5152b3f9f2ef
4
+ data.tar.gz: 2de87f3478a68c3486d426ccd327a99d7c8e5fcc9bb096f24497abb7790e7432
5
5
  SHA512:
6
- metadata.gz: 3df014b751d95d4e5c9b1ca536e51b8209a0edc85d35fa5e5f277016ef8b1a4a605b2592057afd74b466537446cd3594ea294f3c820530b393c078ce785e1e86
7
- data.tar.gz: cedd3d33a01cd1ceba826848c09ffb40dda37080bd5a1a4aa4db68dfd95a25869b9080ea4b8b1ba82eb01ebfae45602e35439fe6861730cba4ce5f16a9b8913d
6
+ metadata.gz: ec8d3ddcd93d2bc7167ee7d08165934adba5a7feb71a54a828a81fa244f969359f3205a0964feb04c85f8b56437a70d9b0b084dcdd11f1c399ea86b135881a13
7
+ data.tar.gz: f3926005478cf49b46d160570c2265aeccc31e6726f6cd52caa32b0bf557ba6e72915897d451e3f907909dc68988f20f17545e3279890cf16dcd994d46927276
@@ -1,6 +1,21 @@
1
- import { defineConfig } from "vitepress";
1
+ import { defineConfig, type HeadConfig } from "vitepress";
2
2
  import { fileURLToPath, URL } from "node:url";
3
3
 
4
+ const SITE_ORIGIN = "https://www.fontist.org";
5
+ const BASE_PATH = "/fontist/";
6
+ const DEFAULT_DESCRIPTION =
7
+ "Fontist brings cross-platform font management to the command line for Windows, Linux, and macOS. Free and open source.";
8
+ const DEFAULT_OG_IMAGE = `${SITE_ORIGIN}/og-image.png`;
9
+
10
+ // Map a page's source path to its canonical public URL (origin + base + clean path).
11
+ function pathToUrl(relativePath: string): string {
12
+ const bare = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
13
+ if (bare === "index") return `${SITE_ORIGIN}${BASE_PATH}`;
14
+ if (bare.endsWith("/index"))
15
+ return `${SITE_ORIGIN}${BASE_PATH}${bare.slice(0, -6)}/`;
16
+ return `${SITE_ORIGIN}${BASE_PATH}${bare}`;
17
+ }
18
+
4
19
  // https://vitepress.dev/reference/site-config
5
20
  export default defineConfig({
6
21
  // Register custom components
@@ -22,6 +37,11 @@ export default defineConfig({
22
37
 
23
38
  lastUpdated: true,
24
39
 
40
+ // https://vitepress.dev/reference/site-config#sitemap
41
+ sitemap: {
42
+ hostname: SITE_ORIGIN,
43
+ },
44
+
25
45
  // Base path for deployment (e.g., /fontist/ for fontist.org/fontist/)
26
46
  base: process.env.BASE_PATH || "/fontist/",
27
47
 
@@ -38,19 +58,40 @@ export default defineConfig({
38
58
  ],
39
59
  ["link", { rel: "manifest", href: "/site.webmanifest" }],
40
60
  ["meta", { property: "og:type", content: "website" }],
41
- ["meta", { property: "og:title", content: "Fontist" }],
61
+ ["link", { rel: "preconnect", href: "https://fonts.googleapis.com" }],
62
+ ["link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }],
42
63
  [
43
- "meta",
64
+ "link",
44
65
  {
45
- property: "og:description",
46
- content:
47
- "Fontist brings cross-platform font management to the command line for Windows, Linux, and macOS.",
66
+ rel: "stylesheet",
67
+ href: "https://fonts.googleapis.com/css2?family=Spectral:ital,opsz,wght@0,7..72,200..800;1,7..72,200..800&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap",
48
68
  },
49
69
  ],
50
- ["meta", { property: "og:image", content: "/logo-full.svg" }],
51
- ["meta", { name: "twitter:card", content: "summary_large_image" }],
52
70
  ],
53
71
 
72
+ // Per-page og:* and twitter:* tags are derived from each page's frontmatter.
73
+ // https://vitepress.dev/reference/site-config#transformhead
74
+ transformHead(ctx) {
75
+ const { pageData } = ctx;
76
+ const url = pathToUrl(pageData.relativePath);
77
+ const title = pageData.title || "Fontist";
78
+ const description = pageData.description || DEFAULT_DESCRIPTION;
79
+ const image = pageData.frontmatter.image || DEFAULT_OG_IMAGE;
80
+
81
+ const tags: HeadConfig[] = [
82
+ ["meta", { property: "og:title", content: title }],
83
+ ["meta", { property: "og:description", content: description }],
84
+ ["meta", { property: "og:url", content: url }],
85
+ ["meta", { property: "og:image", content: image }],
86
+ ["meta", { name: "twitter:card", content: "summary_large_image" }],
87
+ ["meta", { name: "twitter:title", content: title }],
88
+ ["meta", { name: "twitter:description", content: description }],
89
+ ["meta", { name: "twitter:image", content: image }],
90
+ ];
91
+
92
+ return tags;
93
+ },
94
+
54
95
  // https://vitepress.dev/reference/default-theme-config
55
96
  themeConfig: {
56
97
  logo: "/logo-full.svg",
@@ -1,5 +1,12 @@
1
+ /* ================================================================
2
+ Fontist — Type Specimen design system (site-wide)
3
+ One palette + type system applied across every surface:
4
+ body, VPNav, VPFooter, and .vp-doc (all content pages).
5
+ ================================================================ */
6
+
7
+ /* ── Brand + specimen palette ─────────────────────────────────── */
1
8
  :root {
2
- /* Brand Colors from Logo */
9
+ /* Brand colors from the logo */
3
10
  --fontist-rose: #bf4e6a;
4
11
  --fontist-rose-light: #d4718a;
5
12
  --fontist-dark: #4d4b54;
@@ -8,145 +15,614 @@
8
15
  --fontist-beige: #bebbac;
9
16
  --fontist-pale: #dddac8;
10
17
 
11
- /* VitePress Theme Overrides - Light Mode */
12
- --vp-c-brand-1: var(--fontist-rose);
18
+ /* Specimen tokens LIGHT (default) */
19
+ --spec-paper: #f1ece1;
20
+ --spec-paper-deep: #e7e0d1;
21
+ --spec-ink: #1c1a18;
22
+ --spec-ink-soft: #4a4744;
23
+ --spec-mute: #75716c;
24
+ --spec-rose: #b8475f;
25
+ --spec-rose-soft: #d4718a;
26
+ --spec-rule: rgba(28, 26, 24, 0.16);
27
+ --spec-rule-strong: rgba(28, 26, 24, 0.5);
28
+ --spec-term-bg: #1c1a18;
29
+ --spec-term-ink: #ecdfd0;
30
+
31
+ /* VitePress brand wiring */
32
+ --vp-c-brand-1: var(--spec-rose);
13
33
  --vp-c-brand-2: #a3435a;
14
- --vp-c-brand-3: var(--fontist-dark);
15
- --vp-c-brand-soft: rgba(191, 78, 106, 0.14);
16
-
17
- /* Text Colors */
18
- --vp-c-text-1: var(--fontist-dark);
19
- --vp-c-text-2: var(--fontist-gray);
20
- --vp-c-text-3: #8a8888;
21
-
22
- /* Background Colors */
23
- --vp-c-bg-soft: #f8f7f4;
24
- --vp-c-bg-alt: #f2f1ed;
25
- --vp-c-bg: #ffffff;
26
-
27
- /* Hero */
28
- --vp-home-hero-name-color: var(--fontist-rose);
29
- --vp-home-hero-name-background: linear-gradient(
30
- 120deg,
31
- var(--fontist-rose) 30%,
32
- #d4718a
33
- );
34
+ --vp-c-brand-3: var(--spec-ink);
35
+ --vp-c-brand-soft: rgba(184, 71, 95, 0.12);
36
+ --vp-c-text-1: var(--spec-ink);
37
+ --vp-c-text-2: var(--spec-ink-soft);
38
+ --vp-c-text-3: var(--spec-mute);
39
+ --vp-c-bg: var(--spec-paper);
40
+ --vp-c-bg-soft: var(--spec-paper-deep);
41
+ --vp-c-bg-alt: var(--spec-paper-deep);
42
+ --vp-c-divider: var(--spec-rule);
43
+ --vp-c-gutter: var(--spec-rule);
44
+
45
+ /* Nav bg must be opaque (paper) so scrolled content doesn't leak through */
46
+ --vp-nav-bg-color: var(--spec-paper);
47
+ --vp-sidebar-bg-color: var(--spec-paper);
48
+
49
+ /* Type */
50
+ --spec-font-display: "Spectral", Georgia, serif;
51
+ --spec-font-body: "IBM Plex Sans", -apple-system, system-ui, sans-serif;
52
+ --spec-font-mono: "IBM Plex Mono", ui-monospace, "JetBrains Mono", monospace;
53
+
54
+ /* Override VitePress's default Inter with our specimen fonts everywhere */
55
+ --vp-font-family-base: var(--spec-font-body);
56
+ --vp-font-family-mono: var(--spec-font-mono);
34
57
 
35
58
  /* Buttons */
36
- --vp-button-brand-border: var(--fontist-rose);
59
+ --vp-button-brand-border: var(--spec-rose);
37
60
  --vp-button-brand-text: #ffffff;
38
- --vp-button-brand-bg: var(--fontist-rose);
61
+ --vp-button-brand-bg: var(--spec-rose);
39
62
  --vp-button-brand-hover-border: #a3435a;
40
63
  --vp-button-brand-hover-text: #ffffff;
41
64
  --vp-button-brand-hover-bg: #a3435a;
42
- --vp-button-brand-active-border: #8a3849;
43
- --vp-button-brand-active-text: #ffffff;
44
- --vp-button-brand-active-bg: #8a3849;
45
-
46
- /* Sidebar */
47
- --vp-sidebar-bg-color: var(--vp-c-bg-soft);
48
65
 
49
- /* Nav */
50
- --vp-nav-bg-color: var(--vp-c-bg);
66
+ --vp-button-alt-border: var(--spec-rule-strong);
67
+ --vp-button-alt-text: var(--spec-ink);
68
+ --vp-button-alt-bg: transparent;
69
+ --vp-button-alt-hover-border: var(--spec-rose);
70
+ --vp-button-alt-hover-text: var(--spec-rose);
71
+ --vp-button-alt-hover-bg: transparent;
51
72
  }
52
73
 
53
- /* Dark Mode */
74
+ /* ── Dark mode specimen ───────────────────────────────────────── */
54
75
  html.dark {
55
- --vp-c-brand-1: var(--fontist-rose-light);
76
+ --spec-paper: #161513;
77
+ --spec-paper-deep: #211f1c;
78
+ --spec-ink: #ecdfd0;
79
+ --spec-ink-soft: #b8ada0;
80
+ --spec-mute: #8a857f;
81
+ --spec-rose: #d4718a;
82
+ --spec-rose-soft: #e08a9e;
83
+ --spec-rule: rgba(236, 223, 208, 0.14);
84
+ --spec-rule-strong: rgba(236, 223, 208, 0.42);
85
+ --spec-term-bg: #0d0c0c;
86
+ --spec-term-ink: #ecdfd0;
87
+
88
+ --vp-c-brand-1: var(--spec-rose);
56
89
  --vp-c-brand-2: var(--fontist-rose);
57
- --vp-c-brand-3: #e1dfd2;
90
+ --vp-c-brand-3: var(--spec-ink);
58
91
  --vp-c-brand-soft: rgba(212, 113, 138, 0.14);
92
+ --vp-c-text-1: var(--spec-ink);
93
+ --vp-c-text-2: var(--spec-ink-soft);
94
+ --vp-c-text-3: var(--spec-mute);
95
+ --vp-c-bg: var(--spec-paper);
96
+ --vp-c-bg-soft: var(--spec-paper-deep);
97
+ --vp-c-bg-alt: var(--spec-paper-deep);
98
+ --vp-c-divider: var(--spec-rule);
99
+ --vp-c-gutter: var(--spec-rule);
59
100
 
60
- /* Text Colors - Dark Mode */
61
- --vp-c-text-1: #e1dfd2;
62
- --vp-c-text-2: #bebbac;
63
- --vp-c-text-3: #8a8888;
64
-
65
- /* Background Colors - Dark Mode */
66
- --vp-c-bg: #1a1918;
67
- --vp-c-bg-soft: #222120;
68
- --vp-c-bg-alt: #2a2826;
69
- --vp-c-bg-elv: #2a2826;
70
-
71
- /* Hero - Dark Mode */
72
- --vp-home-hero-name-background: linear-gradient(
73
- 120deg,
74
- var(--fontist-rose-light) 30%,
75
- #e08a9e
76
- );
77
-
78
- /* Buttons - Dark Mode */
79
- --vp-button-brand-border: var(--fontist-rose-light);
80
- --vp-button-brand-text: #1a1918;
81
- --vp-button-brand-bg: var(--fontist-rose-light);
101
+ --vp-button-brand-border: var(--spec-rose);
102
+ --vp-button-brand-text: #161513;
103
+ --vp-button-brand-bg: var(--spec-rose);
82
104
  --vp-button-brand-hover-border: var(--fontist-rose);
83
- --vp-button-brand-hover-text: #1a1918;
105
+ --vp-button-brand-hover-text: #161513;
84
106
  --vp-button-brand-hover-bg: var(--fontist-rose);
85
- --vp-button-brand-active-border: #a3435a;
86
- --vp-button-brand-active-text: #1a1918;
87
- --vp-button-brand-active-bg: #a3435a;
88
107
  }
89
108
 
90
- /* Feature cards on home page - Light */
91
- .VPFeature .title {
92
- color: var(--fontist-dark);
109
+ /* ── Site-wide body: paper + grain + base type ────────────────── */
110
+ body {
111
+ background-color: var(--spec-paper);
112
+ font-family: var(--spec-font-body);
113
+ color: var(--spec-ink);
93
114
  }
94
-
95
- .VPFeature .details {
96
- color: var(--fontist-gray);
115
+ /* paper grain overlay (every page) */
116
+ body::before {
117
+ content: "";
118
+ position: fixed;
119
+ inset: 0;
120
+ z-index: 200;
121
+ pointer-events: none;
122
+ opacity: 0.04;
123
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.9 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
124
+ mix-blend-mode: multiply;
125
+ }
126
+ html.dark body::before {
127
+ opacity: 0.06;
128
+ mix-blend-mode: screen;
97
129
  }
98
130
 
99
- .VPFeature:hover {
100
- border-color: var(--fontist-rose);
131
+ /* ── Top navigation (VPNav) — specimen restyle ─────────────────── */
132
+ /* Use box-shadow for the rule, not border-bottom — VPNavBar is height:64px
133
+ with box-sizing:border-box, so border-bottom is inside the 64px and gets
134
+ overlapped by .content (also 64px). box-shadow renders outside the box. */
135
+ .VPNavBar {
136
+ background-color: var(--spec-paper) !important;
137
+ border-bottom: none !important;
138
+ box-shadow: 0 1px 0 var(--spec-rule) !important;
139
+ }
140
+ .VPNavBar.has-sidebar .content,
141
+ .VPNavBar .content {
142
+ background-color: transparent !important;
143
+ }
144
+ .VPNavBar .content .curtain {
145
+ display: none !important;
101
146
  }
102
147
 
103
- /* Feature cards - Dark */
104
- html.dark .VPFeature .title {
105
- color: var(--fontist-cream);
148
+ /* Nav links + dropdown groups: mono, tracked */
149
+ .VPNavBarMenuLink,
150
+ .VPNavBarMenuGroup {
151
+ font-family: var(--spec-font-mono) !important;
152
+ font-size: 12px !important;
153
+ letter-spacing: 0.14em !important;
154
+ text-transform: uppercase !important;
155
+ color: var(--spec-ink-soft) !important;
156
+ font-weight: 500 !important;
157
+ transition: color 0.2s ease;
158
+ }
159
+ .VPNavBarMenuLink:hover,
160
+ .VPNavBarMenuGroup:hover {
161
+ color: var(--spec-rose) !important;
162
+ }
163
+ .VPNavBarMenuLink .text,
164
+ .VPNavBarMenuGroup .text {
165
+ font-family: inherit !important;
166
+ }
167
+ /* Dropdown button — VitePress styles .button directly, so inheritance isn't enough */
168
+ .VPNavBarMenuGroup .button,
169
+ .VPNavBarExtra .button {
170
+ font-family: var(--spec-font-mono) !important;
171
+ font-size: 12px !important;
172
+ letter-spacing: 0.14em !important;
173
+ text-transform: uppercase !important;
174
+ font-weight: 500 !important;
175
+ color: var(--spec-ink-soft) !important;
176
+ line-height: 64px !important;
177
+ transition: color 0.2s ease;
178
+ }
179
+ .VPNavBarMenuGroup:hover .button,
180
+ .VPNavBarExtra:hover .button {
181
+ color: var(--spec-rose) !important;
182
+ }
183
+ /* VitePress renders the visible text in an inner .text span with its own
184
+ font-size (14px) and color — must target it directly, not via .button */
185
+ .VPNavBarMenuGroup .button .text,
186
+ .VPNavBarExtra .button .text {
187
+ font-size: 12px !important;
188
+ color: var(--spec-ink-soft) !important;
189
+ font-weight: 500 !important;
190
+ }
191
+ .VPNavBarMenuGroup:hover .button .text,
192
+ .VPNavBarExtra:hover .button .text {
193
+ color: var(--spec-rose) !important;
106
194
  }
107
195
 
108
- html.dark .VPFeature .details {
109
- color: var(--fontist-beige);
196
+ /* Flyout menu panel (shared by Integrations dropdown + ellipsis extra menu) */
197
+ .VPMenu {
198
+ background: var(--spec-paper) !important;
199
+ border: 1px solid var(--spec-rule) !important;
200
+ border-radius: 0 !important;
201
+ box-shadow: none !important;
202
+ }
203
+ .VPMenu .group-title {
204
+ font-family: var(--spec-font-mono) !important;
205
+ font-size: 11px !important;
206
+ letter-spacing: 0.14em !important;
207
+ text-transform: uppercase !important;
208
+ color: var(--spec-mute) !important;
209
+ }
210
+ /* Menu links — must out-specify VitePress's .VPLink.link (0-2-0),
211
+ so we chain .VPMenu parent to reach 0-2-1. Actual DOM:
212
+ .VPMenu > .items > .VPMenuLink > a.VPLink.link > span */
213
+ .VPMenu .VPMenuLink a,
214
+ .VPMenu .VPMenuLink a span {
215
+ font-family: var(--spec-font-mono) !important;
216
+ font-size: 12px !important;
217
+ letter-spacing: 0.14em !important;
218
+ text-transform: uppercase !important;
219
+ font-weight: 500 !important;
220
+ color: var(--spec-ink-soft) !important;
221
+ transition: color 0.2s ease, background 0.2s ease;
222
+ }
223
+ .VPMenu .VPMenuLink a:hover {
224
+ color: var(--spec-rose) !important;
225
+ background: var(--spec-paper-deep) !important;
226
+ }
227
+ .VPMenu .VPMenuLink a:hover span {
228
+ color: var(--spec-rose) !important;
229
+ }
230
+ /* Ellipsis menu: Appearance label + item labels — .VPMenu parent
231
+ boosts specificity above VitePress's scoped component styles */
232
+ .VPMenu .item .label,
233
+ .VPMenu .group .item {
234
+ font-family: var(--spec-font-mono) !important;
235
+ font-size: 12px !important;
236
+ letter-spacing: 0.14em !important;
237
+ text-transform: uppercase !important;
238
+ font-weight: 500 !important;
239
+ color: var(--spec-ink-soft) !important;
110
240
  }
111
241
 
112
- /* Links */
113
- .vp-doc a {
114
- color: var(--fontist-rose);
242
+ /* Logo / title */
243
+ .VPNavBar .VPNavBarTitle .title {
244
+ font-family: var(--spec-font-display) !important;
245
+ font-weight: 400 !important;
246
+ font-size: 18px !important;
247
+ letter-spacing: 0.02em !important;
248
+ color: var(--spec-ink) !important;
249
+ }
250
+ .VPNav .VPImage {
251
+ height: 32px;
115
252
  }
116
253
 
117
- .vp-doc a:hover {
118
- color: #a3435a;
254
+ /* Social links + appearance toggle + search: quieter */
255
+ .VPNavBar .VPNavBarSocialLinks .social-link,
256
+ .VPNavBar .VPSocialLink,
257
+ .VPNavBarAppearance,
258
+ .VPNavBarExtra {
259
+ color: var(--spec-ink-soft) !important;
260
+ }
261
+ .VPNavBar .VPNavBarSearch {
262
+ --vp-nav-height: 56px;
263
+ }
264
+ .VPNavBarSearch .DocSearch-Button {
265
+ background: transparent !important;
266
+ border: 1px solid var(--spec-rule) !important;
267
+ font-family: var(--spec-font-mono) !important;
268
+ font-size: 12px !important;
269
+ letter-spacing: 0.06em !important;
270
+ }
271
+ .VPNavBarSearch .DocSearch-Button:hover {
272
+ border-color: var(--spec-rose) !important;
119
273
  }
120
274
 
121
- html.dark .vp-doc a {
122
- color: var(--fontist-rose-light);
275
+ /* ── Footer — specimen colophon ───────────────────────────────── */
276
+ .VPFooter {
277
+ border-top: 1px solid var(--spec-rule) !important;
278
+ background-color: transparent !important;
279
+ padding: 28px 24px 40px !important;
280
+ }
281
+ .VPFooter .copyright,
282
+ .VPFooter .message {
283
+ font-family: var(--spec-font-mono) !important;
284
+ font-size: 11px !important;
285
+ letter-spacing: 0.1em !important;
286
+ color: var(--spec-mute) !important;
287
+ }
288
+ .VPFooter .message a {
289
+ color: var(--spec-ink-soft) !important;
290
+ border-bottom: 1px solid var(--spec-rule-strong);
291
+ text-decoration: none;
292
+ }
293
+ .VPFooter .message a:hover {
294
+ color: var(--spec-rose) !important;
123
295
  }
124
296
 
125
- html.dark .vp-doc a:hover {
126
- color: var(--fontist-rose);
297
+ /* ── .vp-doc typography — every content page (about, blog, docs) ─ */
298
+ .vp-doc {
299
+ font-family: var(--spec-font-body);
300
+ color: var(--spec-ink);
301
+ }
302
+ .vp-doc h1,
303
+ .vp-doc h2,
304
+ .vp-doc h3,
305
+ .vp-doc h4,
306
+ .vp-doc h5,
307
+ .vp-doc h6 {
308
+ font-family: var(--spec-font-display);
309
+ color: var(--spec-ink);
310
+ letter-spacing: -0.018em;
311
+ font-weight: 380;
312
+ }
313
+ .vp-doc h1 {
314
+ font-size: clamp(40px, 6vw, 64px);
315
+ font-weight: 360;
316
+ font-variation-settings: "opsz" 120;
317
+ line-height: 1.02;
318
+ letter-spacing: -0.025em;
319
+ margin-top: 0;
320
+ }
321
+ .vp-doc h2 {
322
+ font-size: clamp(28px, 3.4vw, 40px);
323
+ font-variation-settings: "opsz" 60;
324
+ border-top: 1px solid var(--spec-rule);
325
+ padding-top: 1.6em;
326
+ margin-top: 2.2em;
327
+ }
328
+ .vp-doc h3 {
329
+ font-size: clamp(20px, 2vw, 24px);
330
+ font-variation-settings: "opsz" 36;
331
+ }
332
+ .vp-doc p,
333
+ .vp-doc li {
334
+ font-size: 16.5px;
335
+ line-height: 1.65;
336
+ color: var(--spec-ink-soft);
337
+ }
338
+ .vp-doc p {
339
+ margin: 1.1em 0;
340
+ }
341
+ .vp-doc a {
342
+ color: var(--spec-rose);
343
+ text-decoration: none;
344
+ border-bottom: 1px solid var(--spec-rule);
345
+ transition: color 0.2s, border-color 0.2s;
346
+ }
347
+ .vp-doc a:hover {
348
+ color: var(--spec-rose);
349
+ border-color: var(--spec-rose);
350
+ }
351
+ .vp-doc strong {
352
+ color: var(--spec-ink);
353
+ font-weight: 600;
354
+ }
355
+ .vp-doc blockquote {
356
+ font-family: var(--spec-font-display);
357
+ font-style: italic;
358
+ font-weight: 380;
359
+ font-size: 1.25em;
360
+ line-height: 1.5;
361
+ color: var(--spec-ink);
362
+ border-left: 3px solid var(--spec-rose);
363
+ padding-left: 1.5em;
364
+ margin: 2em 0;
365
+ }
366
+ .vp-doc hr {
367
+ border: none;
368
+ border-top: 1px solid var(--spec-rule-strong);
369
+ width: 48px;
370
+ margin: 3em auto;
371
+ }
372
+ /* Drop cap — rose Spectral italic on the first paragraph of content pages */
373
+ .vp-doc > p:first-of-type::first-letter {
374
+ font-family: var(--spec-font-display);
375
+ font-style: italic;
376
+ font-weight: 460;
377
+ font-size: 3.4em;
378
+ float: left;
379
+ line-height: 0.8;
380
+ padding: 0.04em 0.1em 0 0;
381
+ color: var(--spec-rose);
382
+ font-variation-settings: "opsz" 72, "wght" 520;
383
+ }
384
+ /* Code blocks — light: warm paper-deep (matches Shiki light tokens);
385
+ dark: terminal ink (matches Shiki dark tokens) */
386
+ .vp-doc [class*="language-"] {
387
+ background-color: var(--spec-paper-deep) !important;
388
+ border: 1px solid var(--spec-rule);
389
+ font-family: var(--spec-font-mono);
390
+ font-size: 13.5px;
391
+ }
392
+ html.dark .vp-doc [class*="language-"] {
393
+ background-color: var(--spec-term-bg) !important;
394
+ }
395
+ .vp-doc :not(pre) > code {
396
+ font-family: var(--spec-font-mono);
397
+ font-size: 0.9em;
398
+ background: var(--spec-paper-deep);
399
+ border: 1px solid var(--spec-rule);
400
+ padding: 0.1em 0.4em;
401
+ border-radius: 2px;
402
+ color: var(--spec-ink);
403
+ }
404
+ /* Tables */
405
+ .vp-doc table {
406
+ font-size: 14px;
407
+ border-collapse: collapse;
408
+ }
409
+ .vp-doc table th,
410
+ .vp-doc table td {
411
+ border: 1px solid var(--spec-rule);
412
+ padding: 8px 12px;
413
+ }
414
+ .vp-doc table th {
415
+ font-family: var(--spec-font-mono);
416
+ font-size: 11px;
417
+ letter-spacing: 0.1em;
418
+ text-transform: uppercase;
419
+ color: var(--spec-mute);
420
+ background: var(--spec-paper-deep);
127
421
  }
128
422
 
129
- /* Footer */
130
- .VPFooter {
131
- background-color: var(--vp-c-bg-soft);
132
- border-top: 1px solid var(--fontist-pale);
423
+ /* ── Sidebar (if used) ────────────────────────────────────────── */
424
+ .VPSidebar {
425
+ background-color: var(--spec-paper) !important;
426
+ border-right: 1px solid var(--spec-rule) !important;
427
+ font-family: var(--spec-font-mono);
428
+ }
429
+ .VPSidebar .VPSidebarItem .text {
430
+ font-size: 13px;
133
431
  }
134
432
 
135
- html.dark .VPFooter {
136
- border-top-color: #3a3836;
433
+ /* ── 404 page — specimen ───────────────────────────────────────── */
434
+ .NotFound {
435
+ text-align: center;
436
+ padding: clamp(3rem, 8vw, 6rem) 1.5rem clamp(4rem, 10vw, 8rem);
437
+ font-family: var(--spec-font-body);
438
+ }
439
+ .NotFound .code {
440
+ font-family: var(--spec-font-display);
441
+ font-style: italic;
442
+ font-weight: 300;
443
+ font-variation-settings: "opsz" 144;
444
+ font-size: clamp(120px, 24vw, 280px);
445
+ line-height: 1;
446
+ color: var(--spec-rose);
447
+ opacity: 0.9;
448
+ margin: 0 0 0.5rem;
449
+ }
450
+ .NotFound .title {
451
+ font-family: var(--spec-font-mono);
452
+ font-size: 12px;
453
+ letter-spacing: 0.22em;
454
+ text-transform: uppercase;
455
+ color: var(--spec-ink-soft);
456
+ margin: 0;
457
+ }
458
+ .NotFound .divider {
459
+ width: 60px;
460
+ border-top: 1px solid var(--spec-rule-strong);
461
+ margin: 2rem auto;
462
+ }
463
+ .NotFound .quote {
464
+ font-family: var(--spec-font-display);
465
+ font-style: italic;
466
+ font-weight: 380;
467
+ font-size: clamp(18px, 2.2vw, 26px);
468
+ line-height: 1.5;
469
+ color: var(--spec-ink);
470
+ border: none;
471
+ max-width: 32ch;
472
+ margin: 0 auto 2rem;
473
+ padding: 0;
474
+ }
475
+ .NotFound .action .link {
476
+ font-family: var(--spec-font-mono);
477
+ font-size: 13px;
478
+ letter-spacing: 0.12em;
479
+ text-transform: uppercase;
480
+ color: var(--spec-paper);
481
+ background: var(--spec-ink);
482
+ padding: 12px 22px;
483
+ text-decoration: none;
484
+ display: inline-block;
485
+ transition: background 0.25s ease;
486
+ }
487
+ .NotFound .action .link:hover {
488
+ background: var(--spec-rose);
137
489
  }
138
490
 
139
- /* Code blocks - Light */
140
- .vp-doc [class*="language-"] {
141
- background-color: var(--fontist-dark);
491
+ /* ================================================================
492
+ VitePress home layout — VPHero + VPFeatures specimen restyle
493
+ Used by the fontist/docs and fontisan/docs homepages.
494
+ ================================================================ */
495
+
496
+ /* ── VPHero ───────────────────────────────────────────────────── */
497
+ .VPHero .container { gap: 48px; }
498
+ .VPHero .heading {
499
+ display: flex;
500
+ flex-direction: column;
501
+ margin-bottom: 0.5rem;
502
+ }
503
+ .VPHero .name {
504
+ font-family: var(--spec-font-display) !important;
505
+ font-weight: 360 !important;
506
+ font-variation-settings: "opsz" 72;
507
+ font-size: clamp(40px, 6vw, 72px) !important;
508
+ line-height: 1 !important;
509
+ letter-spacing: -0.02em !important;
510
+ background: none !important;
511
+ -webkit-background-clip: unset !important;
512
+ -webkit-text-fill-color: var(--spec-ink) !important;
513
+ color: var(--spec-ink) !important;
514
+ }
515
+ .VPHero .text {
516
+ font-family: var(--spec-font-display) !important;
517
+ font-weight: 340 !important;
518
+ font-size: clamp(28px, 4vw, 44px) !important;
519
+ line-height: 1.05 !important;
520
+ letter-spacing: -0.02em !important;
521
+ color: var(--spec-rose) !important;
522
+ margin-top: 0.1em !important;
523
+ display: block !important;
524
+ }
525
+ .VPHero .tagline {
526
+ font-family: var(--spec-font-body) !important;
527
+ font-size: 16px !important;
528
+ line-height: 1.6 !important;
529
+ color: var(--spec-ink-soft) !important;
530
+ max-width: 480px !important;
531
+ }
532
+ .VPHero .actions { gap: 12px; }
533
+ .VPHero .VPButton {
534
+ font-family: var(--spec-font-mono) !important;
535
+ font-size: 12px !important;
536
+ letter-spacing: 0.12em !important;
537
+ text-transform: uppercase !important;
538
+ border-radius: 0 !important;
539
+ }
540
+ .VPHero .VPButton.brand {
541
+ border: 1px solid var(--spec-ink) !important;
542
+ background: var(--spec-ink) !important;
543
+ color: var(--spec-paper) !important;
544
+ }
545
+ .VPHero .VPButton.brand:hover {
546
+ background: var(--spec-rose) !important;
547
+ border-color: var(--spec-rose) !important;
548
+ }
549
+ .VPHero .VPButton.alt {
550
+ border: 1px solid var(--spec-rule-strong) !important;
551
+ background: transparent !important;
552
+ color: var(--spec-ink) !important;
553
+ }
554
+ .VPHero .VPButton.alt:hover {
555
+ border-color: var(--spec-rose) !important;
556
+ color: var(--spec-rose) !important;
557
+ background: transparent !important;
142
558
  }
143
559
 
144
- /* Code blocks - Dark */
145
- html.dark .vp-doc [class*="language-"] {
146
- background-color: #0d0c0c;
560
+ /* ── HeroCodeBlock specimen terminal plate ──────────────────── */
561
+ .hero-code-block {
562
+ background: var(--spec-term-bg) !important;
563
+ border: 1px solid var(--spec-rule) !important;
564
+ border-radius: 0 !important;
565
+ box-shadow: 12px 12px 0 -1px var(--spec-paper-deep), 12px 12px 0 var(--spec-rule) !important;
566
+ max-width: 520px !important;
567
+ }
568
+ .code-header {
569
+ background: rgba(0, 0, 0, 0.2) !important;
570
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
147
571
  }
572
+ .code-title {
573
+ font-family: var(--spec-font-mono) !important;
574
+ font-size: 11px !important;
575
+ letter-spacing: 0.12em !important;
576
+ text-transform: uppercase !important;
577
+ color: var(--spec-rose-soft) !important;
578
+ }
579
+ .code-content {
580
+ font-family: var(--spec-font-mono) !important;
581
+ font-size: 13px !important;
582
+ line-height: 1.7 !important;
583
+ color: var(--spec-term-ink) !important;
584
+ }
585
+ .code-content .prompt { color: var(--spec-rose-soft) !important; font-weight: 500; }
586
+ .code-content .cmd { color: #93c5fd !important; }
587
+ .code-content .success { color: #8fb98a !important; }
588
+ .code-content .comment { color: rgba(236, 223, 208, 0.45) !important; }
589
+ .code-content .line { padding: 1px 0; }
148
590
 
149
- /* Logo sizing */
150
- .VPNav .VPImage {
151
- height: 39px;
591
+ /* ── VPFeatures — only style cards, DON'T override grid layout ─── */
592
+ /* VitePress renders responsive columns on .item.grid-N (child of .items).
593
+ Overriding .items as a grid fights VitePress's own engine. Only style .box. */
594
+ .VPFeatures .container { padding-top: 2rem; }
595
+
596
+ .VPFeature {
597
+ background: transparent !important;
598
+ border: none !important;
599
+ border-radius: 0 !important;
600
+ box-shadow: none !important;
601
+ }
602
+ .VPFeature .box {
603
+ background: var(--spec-paper) !important;
604
+ border: 1px solid var(--spec-rule) !important;
605
+ border-radius: 0 !important;
606
+ box-shadow: none !important;
607
+ height: 100% !important;
608
+ transition: border-color 0.2s ease, background 0.2s ease !important;
609
+ }
610
+ .VPFeature .box:hover {
611
+ background: var(--spec-paper-deep) !important;
612
+ border-color: var(--spec-rose) !important;
152
613
  }
614
+ .VPFeature .title {
615
+ font-family: var(--spec-font-display) !important;
616
+ font-weight: 400 !important;
617
+ font-size: 1.1rem !important;
618
+ color: var(--spec-ink) !important;
619
+ }
620
+ .VPFeature .details {
621
+ font-family: var(--spec-font-body) !important;
622
+ font-size: 14px !important;
623
+ line-height: 1.55 !important;
624
+ color: var(--spec-ink-soft) !important;
625
+ }
626
+ html.dark .VPFeature .box { background: var(--spec-paper) !important; }
627
+ html.dark .VPFeature .box:hover { background: var(--spec-paper-deep) !important; }
628
+
data/docs/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "scripts": {
3
3
  "dev": "vitepress dev",
4
- "build": "vitepress build",
4
+ "build": "vitepress build && node scripts/post-build.mjs",
5
5
  "preview": "vitepress preview",
6
6
  "format": "prettier -w ."
7
7
  },
@@ -0,0 +1,4 @@
1
+ User-agent: *
2
+ Allow: /
3
+
4
+ Sitemap: https://www.fontist.org/fontist/sitemap.xml
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ // Post-build fixes for GitHub Pages subpath deployment.
3
+ //
4
+ // 1. Dirify: convert VitePress file-style output (foo.html) to directory-style
5
+ // (foo/index.html) so GitHub Pages resolves BOTH /foo and /foo/. Without
6
+ // this, /foo/ 404s because foo.html is a file, not a directory.
7
+ //
8
+ // 2. Sitemap base: insert the deployment base path into sitemap.xml URLs.
9
+ // VitePress omits `base` from sitemap route paths (and ignores the path
10
+ // portion of `sitemap.hostname`), so the generated URLs point at the origin
11
+ // root instead of the subsite. We rewrite each <loc> to include the base.
12
+ //
13
+ // Idempotent. Kept at the dist root: index.html (site root), 404.html (GHP 404 page).
14
+ import {
15
+ existsSync,
16
+ mkdirSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ renameSync,
20
+ statSync,
21
+ writeFileSync,
22
+ } from "node:fs";
23
+ import { join } from "node:path";
24
+
25
+ const DIST = ".vitepress/dist";
26
+ const HTML = ".html";
27
+ const KEEP_AT_ROOT = new Set(["index.html", "404.html"]);
28
+ // Subsite identity — must match config.ts base and the real deployment URL.
29
+ const ORIGIN = "https://www.fontist.org";
30
+ const BASE = "fontist"; // path segment under ORIGIN (no slashes)
31
+
32
+ let moved = 0;
33
+ let skipped = 0;
34
+
35
+ function dirify(dir) {
36
+ for (const entry of readdirSync(dir)) {
37
+ const full = join(dir, entry);
38
+ if (statSync(full).isDirectory()) {
39
+ dirify(full);
40
+ continue;
41
+ }
42
+ if (!entry.endsWith(HTML)) continue;
43
+ if (entry === "index.html") continue; // already directory-style
44
+ if (dir === DIST && KEEP_AT_ROOT.has(entry)) {
45
+ skipped++; // root index.html / 404.html stay put
46
+ continue;
47
+ }
48
+ const name = entry.slice(0, -HTML.length);
49
+ const targetDir = join(dir, name);
50
+ const targetIndex = join(targetDir, "index.html");
51
+ if (existsSync(targetIndex)) {
52
+ console.warn(`[post-build] skip ${full.replace(DIST + "/", "")}: target exists`);
53
+ skipped++;
54
+ continue;
55
+ }
56
+ mkdirSync(targetDir, { recursive: true });
57
+ renameSync(full, targetIndex);
58
+ moved++;
59
+ }
60
+ }
61
+
62
+ function fixSitemapBase() {
63
+ const sitemap = `${DIST}/sitemap.xml`;
64
+ if (!existsSync(sitemap)) return false;
65
+ let xml = readFileSync(sitemap, "utf8");
66
+ // Insert BASE after the origin unless the path already starts with BASE
67
+ // (VitePress emits the homepage route as "/<base>"; other routes omit it).
68
+ const originPattern = new RegExp(
69
+ `(<loc>${ORIGIN.replaceAll("/", "\\/")}\\/)(?!${BASE}(\\/|$))`,
70
+ "g",
71
+ );
72
+ xml = xml.replace(originPattern, `$1${BASE}/`);
73
+ writeFileSync(sitemap, xml);
74
+ return true;
75
+ }
76
+
77
+ dirify(DIST);
78
+ const sitemapFixed = fixSitemapBase();
79
+ console.log(
80
+ `[post-build] dirified ${moved} route(s), kept ${skipped} at root; sitemap ${sitemapFixed ? "rewritten with /" + BASE + "/" : "absent (skipped)"}`,
81
+ );
data/fontist.gemspec CHANGED
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
31
31
 
32
32
  spec.add_dependency "down", "~> 5.0"
33
33
  spec.add_dependency "excavate", "~> 1.0", ">= 1.0.3"
34
- spec.add_dependency "fontisan", "~> 0.2", ">= 0.2.16"
34
+ spec.add_dependency "fontisan", "~> 0.2", ">= 0.2.17"
35
35
  spec.add_dependency "fuzzy_match", "~> 2.1"
36
36
  spec.add_dependency "git", "> 1.0"
37
37
  spec.add_dependency "json", "~> 2.0"
@@ -43,15 +43,17 @@ module Fontist
43
43
  "#{e.category}: #{e.message}"
44
44
  end.join("; ")
45
45
  # rubocop:disable Layout/LineLength
46
- raise Errors::FontFileError,
46
+ raise Errors::FontIndexabilityValidationError,
47
47
  "Font collection failed indexability validation (first font): #{error_messages}"
48
48
  # rubocop:enable Layout/LineLength
49
49
  end
50
50
 
51
51
  collection
52
+ rescue Errors::FontIndexabilityValidationError
53
+ raise
52
54
  rescue StandardError => e
53
55
  raise Errors::FontFileError,
54
- "Font collection could not be loaded: #{e.inspect}."
56
+ "Font collection could not be loaded: #{e.message}"
55
57
  end
56
58
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
57
59
 
@@ -20,6 +20,12 @@ module Fontist
20
20
 
21
21
  class FontFileError < GeneralError; end
22
22
 
23
+ # Raised when a font is structurally readable but fails the indexability
24
+ # profile (missing required tables, incomplete name records, etc.).
25
+ # Distinct from FontFileError so callers can tell "we don't recognise this
26
+ # file" apart from "this file is a font but unusable as an index entry".
27
+ class FontIndexabilityValidationError < FontFileError; end
28
+
23
29
  class FontExtractError < GeneralError; end
24
30
 
25
31
  class FontistVersionError < GeneralError; end
@@ -150,26 +150,9 @@ module Fontist
150
150
  end
151
151
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
152
152
 
153
- # rubocop:disable Metrics/MethodLength
154
153
  def detect_font_format(path)
155
- # Open file and check SFNT version to determine actual format
156
- File.open(path, "rb") do |io|
157
- signature = io.read(4)
158
- io.rewind
159
-
160
- case signature
161
- when "\x00\x01\x00\x00", "true"
162
- "ttf"
163
- when "OTTO"
164
- "otf"
165
- when "wOFF"
166
- "woff"
167
- when "wOF2"
168
- "woff2"
169
- else
170
- "unknown"
171
- end
172
- end
154
+ format = Fontisan::FontLoader.detect_format(path)
155
+ format ? format.to_s : "unknown"
173
156
  rescue StandardError
174
157
  "unknown"
175
158
  end
@@ -215,10 +215,9 @@ location: nil)
215
215
  installed_any = true
216
216
  end
217
217
  end
218
- # Only reset fontist index (not system index) if we actually installed fonts
219
218
  if installed_any
220
- # Reset only the fontist font cache, not system fonts
221
219
  Fontist::SystemFont.reset_fontist_font_paths_cache
220
+ Fontist::SystemFont.reset_find_styles_cache
222
221
  end
223
222
  to_response
224
223
  end
@@ -1,4 +1,5 @@
1
1
  require "paint"
2
+ require "fontisan"
2
3
 
3
4
  module Fontist
4
5
  # Statistics tracking for index building (thread-safe)
@@ -630,15 +631,12 @@ spinner_index = nil)
630
631
  return if excluded?(path)
631
632
 
632
633
  gather_fonts(path)
634
+ rescue Errors::FontIndexabilityValidationError => e
635
+ stats&.record_validation_failure
636
+ print_validation_error(e, path)
633
637
  rescue Errors::FontFileError => e
634
- # Check if this is a validation failure
635
- if e.message.include?("indexability validation")
636
- stats&.record_validation_failure
637
- print_validation_error(e, path)
638
- else
639
- stats&.record_error
640
- print_recognition_error(e, path)
641
- end
638
+ stats&.record_error
639
+ print_recognition_error(e, path)
642
640
  end
643
641
 
644
642
  def excluded?(path)
@@ -649,15 +647,24 @@ spinner_index = nil)
649
647
  @excluded_fonts ||= YAML.load_file(Fontist.excluded_fonts_path)
650
648
  end
651
649
 
650
+ # Dispatch a file to the file-font or collection parser based on its actual
651
+ # magic bytes, not its extension. Some vendors ship files with misleading
652
+ # extensions (notably macOS ships a single OpenType-CFF font as
653
+ # `SauberScript.ttc` inside its private FontServices framework). Trusting
654
+ # the extension causes such files to be routed through the collection
655
+ # parser, which correctly rejects them, producing spurious "not recognized"
656
+ # warnings.
652
657
  def gather_fonts(path)
653
- case File.extname(path).gsub(/^\./, "").downcase
654
- when "ttf", "otf", "woff", "woff2"
655
- detect_file_font(path)
656
- when "ttc", "otc"
658
+ case Fontisan::FontLoader.detect_format(path)
659
+ when :ttc, :otc
657
660
  detect_collection_fonts(path)
661
+ when :ttf, :otf, :woff, :woff2
662
+ detect_file_font(path)
658
663
  else
659
664
  print_recognition_error(Errors::UnknownFontTypeError.new(path), path)
660
665
  end
666
+ rescue Errno::ENOENT, Errno::EACCES, IOError => e
667
+ print_recognition_error(Errors::FontFileError.new(e.message), path)
661
668
  end
662
669
 
663
670
  def print_validation_error(exception, path)
@@ -25,27 +25,61 @@ module Fontist
25
25
  end
26
26
 
27
27
  def download
28
- file = @cache.fetch(url) do
29
- download_file
30
- end
31
-
32
- check_tampered(file)
33
-
28
+ cached = use_cached_file?
29
+ file = fetch_file
30
+ verify_checksum!(file)
34
31
  file
32
+ rescue Fontist::Errors::TamperedFileError => e
33
+ delete_cached_file(file)
34
+ raise invalid_resource_error(e) unless cached
35
+
36
+ retry_download_after_cache_mismatch
35
37
  end
36
38
 
37
39
  private
38
40
 
39
41
  attr_reader :file, :sha, :file_size
40
42
 
41
- def check_tampered(file)
43
+ def retry_download_after_cache_mismatch
44
+ file = fetch_file
45
+ verify_checksum!(file)
46
+ file
47
+ rescue Fontist::Errors::TamperedFileError => e
48
+ delete_cached_file(file)
49
+ raise invalid_resource_error(e)
50
+ end
51
+
52
+ def fetch_file
53
+ @cache.fetch(url) { download_file }
54
+ end
55
+
56
+ def use_cached_file?
57
+ Fontist.use_cache? && !!@cache.already_fetched?([url])
58
+ end
59
+
60
+ def delete_cached_file(file)
61
+ cached_path = file&.path
62
+ file&.close unless file&.closed?
63
+ @cache.delete(url)
64
+
65
+ FileUtils.rm_rf(Pathname.new(cached_path).dirname) if cached_path
66
+ end
67
+
68
+ def verify_checksum!(file)
69
+ return if sha.empty?
70
+
42
71
  file_checksum = Digest::SHA256.file(file).to_s
43
- if !sha.empty? && !sha.include?(file_checksum)
44
- Fontist.ui.error(
45
- "SHA256 checksum mismatch for #{url}: #{file_checksum}, " \
46
- "should be #{sha.join(', or ')}.",
47
- )
48
- end
72
+ return if sha.include?(file_checksum)
73
+
74
+ raise Fontist::Errors::TamperedFileError,
75
+ "SHA256 checksum mismatch for #{url}: #{file_checksum}, " \
76
+ "should be #{sha.join(', or ')}."
77
+ end
78
+
79
+ def invalid_resource_error(error)
80
+ Fontist::Errors::InvalidResourceError.new(
81
+ "Invalid resource: #{@file}. Error: #{error.message}.",
82
+ )
49
83
  end
50
84
 
51
85
  def byte_to_megabyte
@@ -62,15 +96,19 @@ module Fontist
62
96
  print_download_start if @verbose
63
97
  do_download_file
64
98
  rescue Down::Error => e
65
- if @tries < max_retries
66
- sleep(backoff_time(@tries))
67
- retry
68
- end
99
+ retry if retry_download?
69
100
 
70
101
  raise Fontist::Errors::InvalidResourceError,
71
102
  "Invalid URL: #{@file}. Error: #{e.inspect}."
72
103
  end
73
104
 
105
+ def retry_download?
106
+ return false unless @tries < max_retries
107
+
108
+ sleep(backoff_time(@tries))
109
+ true
110
+ end
111
+
74
112
  def max_retries
75
113
  @max_retries ||= 3
76
114
  end
@@ -1,3 +1,3 @@
1
1
  module Fontist
2
- VERSION = "3.0.7".freeze
2
+ VERSION = "3.0.9".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fontist
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.7
4
+ version: 3.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-20 00:00:00.000000000 Z
11
+ date: 2026-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down
@@ -53,7 +53,7 @@ dependencies:
53
53
  version: '0.2'
54
54
  - - ">="
55
55
  - !ruby/object:Gem::Version
56
- version: 0.2.16
56
+ version: 0.2.17
57
57
  type: :runtime
58
58
  prerelease: false
59
59
  version_requirements: !ruby/object:Gem::Requirement
@@ -63,7 +63,7 @@ dependencies:
63
63
  version: '0.2'
64
64
  - - ">="
65
65
  - !ruby/object:Gem::Version
66
- version: 0.2.16
66
+ version: 0.2.17
67
67
  - !ruby/object:Gem::Dependency
68
68
  name: fuzzy_match
69
69
  requirement: !ruby/object:Gem::Requirement
@@ -343,9 +343,11 @@ files:
343
343
  - docs/public/favicon.svg
344
344
  - docs/public/logo-full.svg
345
345
  - docs/public/logo.svg
346
+ - docs/public/robots.txt
346
347
  - docs/public/site.webmanifest
347
348
  - docs/public/web-app-manifest-192x192.png
348
349
  - docs/public/web-app-manifest-512x512.png
350
+ - docs/scripts/post-build.mjs
349
351
  - exe/fontist
350
352
  - fontist.gemspec
351
353
  - formula_filename_index.yml