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 +4 -4
- data/docs/.vitepress/config.ts +49 -8
- data/docs/.vitepress/theme/style.css +575 -99
- data/docs/package.json +1 -1
- data/docs/public/robots.txt +4 -0
- data/docs/scripts/post-build.mjs +81 -0
- data/fontist.gemspec +1 -1
- data/lib/fontist/collection_file.rb +4 -2
- data/lib/fontist/errors.rb +6 -0
- data/lib/fontist/font_file.rb +2 -19
- data/lib/fontist/manifest.rb +1 -2
- data/lib/fontist/system_index.rb +19 -12
- data/lib/fontist/utils/downloader.rb +55 -17
- data/lib/fontist/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5fa7d3f053af64da4931ed4d3808f703b5d984850d974d562fa5152b3f9f2ef
|
|
4
|
+
data.tar.gz: 2de87f3478a68c3486d426ccd327a99d7c8e5fcc9bb096f24497abb7790e7432
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec8d3ddcd93d2bc7167ee7d08165934adba5a7feb71a54a828a81fa244f969359f3205a0964feb04c85f8b56437a70d9b0b084dcdd11f1c399ea86b135881a13
|
|
7
|
+
data.tar.gz: f3926005478cf49b46d160570c2265aeccc31e6726f6cd52caa32b0bf557ba6e72915897d451e3f907909dc68988f20f17545e3279890cf16dcd994d46927276
|
data/docs/.vitepress/config.ts
CHANGED
|
@@ -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
|
-
["
|
|
61
|
+
["link", { rel: "preconnect", href: "https://fonts.googleapis.com" }],
|
|
62
|
+
["link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }],
|
|
42
63
|
[
|
|
43
|
-
"
|
|
64
|
+
"link",
|
|
44
65
|
{
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
/*
|
|
12
|
-
--
|
|
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(--
|
|
15
|
-
--vp-c-brand-soft: rgba(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
--vp-c-text-
|
|
19
|
-
--vp-c-
|
|
20
|
-
--vp-c-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
--vp-c-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(--
|
|
59
|
+
--vp-button-brand-border: var(--spec-rose);
|
|
37
60
|
--vp-button-brand-text: #ffffff;
|
|
38
|
-
--vp-button-brand-bg: var(--
|
|
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
|
-
|
|
50
|
-
--vp-
|
|
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
|
|
74
|
+
/* ── Dark mode specimen ───────────────────────────────────────── */
|
|
54
75
|
html.dark {
|
|
55
|
-
--
|
|
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:
|
|
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
|
-
|
|
61
|
-
--vp-
|
|
62
|
-
--vp-
|
|
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: #
|
|
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
|
-
/*
|
|
91
|
-
|
|
92
|
-
color: var(--
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
/*
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
/*
|
|
113
|
-
.
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
/*
|
|
130
|
-
.
|
|
131
|
-
background-color: var(--
|
|
132
|
-
border-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
/*
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
/*
|
|
145
|
-
|
|
146
|
-
background
|
|
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
|
-
/*
|
|
150
|
-
.
|
|
151
|
-
|
|
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
|
@@ -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.
|
|
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::
|
|
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.
|
|
56
|
+
"Font collection could not be loaded: #{e.message}"
|
|
55
57
|
end
|
|
56
58
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
57
59
|
|
data/lib/fontist/errors.rb
CHANGED
|
@@ -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
|
data/lib/fontist/font_file.rb
CHANGED
|
@@ -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
|
-
|
|
156
|
-
|
|
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
|
data/lib/fontist/manifest.rb
CHANGED
|
@@ -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
|
data/lib/fontist/system_index.rb
CHANGED
|
@@ -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
|
-
|
|
635
|
-
|
|
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
|
|
654
|
-
when
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
data/lib/fontist/version.rb
CHANGED
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|