@abreen/tada 1.16.1 → 1.16.2
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.
- package/README.md +15 -0
- package/assets/material-symbols/celebration_24px.svg +1 -1
- package/assets/material-symbols/close_24px.svg +1 -1
- package/assets/material-symbols/contrast_20px.svg +1 -0
- package/assets/material-symbols/contrast_rtl_off_20px.svg +1 -0
- package/assets/material-symbols/info_20px.svg +1 -0
- package/assets/material-symbols/info_40px.svg +1 -0
- package/assets/material-symbols/menu_24px.svg +1 -1
- package/assets/material-symbols/open_in_new_20px.svg +1 -0
- package/assets/material-symbols/search_20px.svg +1 -0
- package/assets/material-symbols/smart_display_24px.svg +1 -0
- package/assets/material-symbols/south_east_20px.svg +1 -0
- package/assets/material-symbols/warning_20px.svg +1 -0
- package/assets/material-symbols/warning_40px.svg +1 -0
- package/build/bundle.ts +23 -4
- package/build/custom-fonts.ts +300 -0
- package/build/generate-fonts.ts +29 -3
- package/build/material-symbols.ts +58 -20
- package/build/pipeline.ts +1 -1
- package/build/types.d.ts +27 -0
- package/build/utils/render.ts +13 -5
- package/build/watch/build-full.ts +1 -1
- package/build/watch/build-incremental.ts +1 -1
- package/build/watch/validation.ts +18 -7
- package/package.json +1 -1
- package/schema/site.schema.json +67 -0
- package/src/_alerts.scss +17 -12
- package/src/_layout.scss +2 -2
- package/src/anchor/index.ts +2 -35
- package/src/anchor/style.scss +4 -28
- package/src/appearance-picker/index.ts +18 -5
- package/src/appearance-picker/style.scss +0 -13
- package/src/header/_base.scss +10 -6
- package/src/material-symbols.scss +16 -3
- package/src/navigate/runtime.ts +13 -0
- package/src/question/style.scss +1 -2
- package/src/search/style.scss +5 -3
- package/src/style.scss +11 -5
- package/src/toc/style.scss +14 -10
- package/templates/_theme.scss +14 -4
- package/templates/_top.html +23 -11
- package/templates/code.html +1 -1
- package/assets/material-symbols/contrast_24px.svg +0 -1
- package/assets/material-symbols/contrast_rtl_off_24px.svg +0 -1
- package/assets/material-symbols/info_24px.svg +0 -1
- package/assets/material-symbols/north_east_24px.svg +0 -1
- package/assets/material-symbols/search_24px.svg +0 -1
- package/assets/material-symbols/south_east_24px.svg +0 -1
- package/assets/material-symbols/tag_24px.svg +0 -1
- package/assets/material-symbols/warning_24px.svg +0 -1
package/README.md
CHANGED
|
@@ -221,6 +221,20 @@ themeColor: hsl(351 70% 40%)
|
|
|
221
221
|
tintAmount: 0
|
|
222
222
|
defaultFont: serif
|
|
223
223
|
defaultContrast: high
|
|
224
|
+
fontOverrides:
|
|
225
|
+
serif:
|
|
226
|
+
regular: fonts/body-regular.woff2
|
|
227
|
+
tuning:
|
|
228
|
+
scale: 1.125
|
|
229
|
+
lineHeight: 1.5
|
|
230
|
+
headingScale: 0.9
|
|
231
|
+
headingWeight: 400
|
|
232
|
+
serifMono:
|
|
233
|
+
regular: fonts/mono-regular.woff2
|
|
234
|
+
features: [ss02]
|
|
235
|
+
tuning:
|
|
236
|
+
scale: 0.94
|
|
237
|
+
lineHeight: 1.5
|
|
224
238
|
features:
|
|
225
239
|
search: true
|
|
226
240
|
favicon: true
|
|
@@ -249,6 +263,7 @@ vars:
|
|
|
249
263
|
| `tintAmount` | *Optional*, percentage (0-100) of tint to apply (default `100`) |
|
|
250
264
|
| `defaultFont` | *Optional*, initial font pairing: `sans` or `serif` (default `sans`) |
|
|
251
265
|
| `defaultContrast` | *Optional*, initial neutral-palette contrast: `standard` or `high` (default `standard`) |
|
|
266
|
+
| `fontOverrides` | *Optional*, custom serif and serif-monospace WOFF2 faces from `public/`; each configured family requires `regular` and may include styled faces, OpenType `features`, and font-specific `tuning` |
|
|
252
267
|
| `faviconSymbol` | *Optional*, the text to use instead of `symbol` in the favicon |
|
|
253
268
|
| `features.search` | Enable search UI and Pagefind index generation |
|
|
254
269
|
| `features.favicon` | Enable automatically generated favicons |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m70.91-65.41 210.05-587.74 377.93 377.93L70.91-65.41Zm150.18-149.94L492.8-312 317.74-487.07l-96.65 271.72Zm370.24-239.06L545.74-500l219.93-219.93q33.44-33.44 80.71-33.44 47.27 0 80.71 33.44l24 24-45.83 45.58-24-24q-14-14-35-14t-35 14L591.33-454.41ZM423.91-621.59l-45.58-45.82 22.56-22.57q14-14 14-34t-14-34l-26-26 45.59-45.59 26 26q33.43 33.44 33.43 79.71 0 46.27-33.43 79.71l-22.57 22.56ZM507.74-538l-45.83-45.59 141.13-141.13q14-14 14-35t-14-35l-64-64 45.83-45.58 64 64q33.43 33.43 33.43 80.58t-33.43 80.59L507.74-538Zm167.17 167.41-45.58-45.82 62.56-62.57q33.44-33.43 80.71-33.43 47.27 0 80.7 33.43l64 64-45.82 45.83-64-64q-14-14-35-14t-35 14l-62.57 62.56ZM221.09-215.35Z"/></svg>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M256-192.35 192.35-256l224-224-224-224L256-767.65l224 224 224-224L767.65-704l-224 224 224 224L704-192.35l-224-224-224 224Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="M327.89-118.47q-71.43-30.6-125.13-84.29-53.69-53.7-84.29-125.13-30.6-71.44-30.6-152.61 0-81.17 30.6-152.11 30.6-70.93 84.29-124.63 53.7-53.69 125.13-84.29 71.44-30.6 152.61-30.6 81.17 0 152.11 30.6 70.93 30.6 124.63 84.29 53.69 53.7 84.29 124.63 30.6 70.94 30.6 152.11 0 81.17-30.6 152.61-30.6 71.43-84.29 125.13-53.7 53.69-124.63 84.29-70.94 30.6-152.11 30.6-81.17 0-152.61-30.6Zm190.98-54.88q115.09-14.95 192.67-100.9Q789.13-360.2 789.13-480q0-119.09-77.59-205.27-77.58-86.19-192.67-101.14v613.06Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="m771.83-84.98-70.81-70.56q-49.95 32.71-105.75 50.19-55.8 17.48-115.24 17.48-80.94 0-152.37-30.62-71.44-30.62-125-84.17-53.55-53.56-84.17-125-30.62-71.43-30.62-152.37 0-59.44 17.48-115.24t50.19-105.75L88.8-768l53.4-53.39 683.02 683.02-53.39 53.39ZM480-170.87q43.04 0 83.45-12.52 40.4-12.52 77.68-33.04L480-377.57v206.7Zm327.09-92.41-61.37-60.13q20.29-36.49 31.85-75.73 11.56-39.25 11.56-80.86 0-129.04-90.16-219.09-90.16-90.04-218.97-90.04v198.76L263.28-807.09q48.16-31.54 103.34-48.29Q421.8-872.13 480-872.13q81.91 0 152.84 30.62 70.94 30.62 124.5 84.17 53.55 53.56 84.17 124.5 30.62 70.93 30.62 152.84 0 58.2-16.75 113.38-16.75 55.18-48.29 103.34Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="M440.41-284.41h79.18V-528h-79.18v243.59Zm68.44-322.47q11.93-11.67 11.93-28.91 0-17.24-11.66-29.12-11.67-11.87-28.91-11.87-17.24 0-29.12 11.66-11.87 11.67-11.87 28.91 0 17.24 11.72 29.12 11.73 11.87 28.85 11.87t29.06-11.66ZM480.28-87.87q-81.19 0-152.62-30.62-71.44-30.62-125-84.17-53.55-53.56-84.17-124.95Q87.87-399 87.87-480.46q0-81.45 30.62-152.38 30.62-70.94 84.17-124.5 53.56-53.55 124.95-84.17 71.39-30.62 152.85-30.62 81.45 0 152.38 30.62 70.94 30.62 124.5 84.17 53.55 53.56 84.17 124.72 30.62 71.16 30.62 152.34 0 81.19-30.62 152.62-30.62 71.44-84.17 125-53.56 53.55-124.72 84.17-71.16 30.62-152.34 30.62Zm-.28-83q129.04 0 219.09-90.04 90.04-90.05 90.04-219.09 0-129.04-90.04-219.09-90.05-90.04-219.09-90.04-129.04 0-219.09 90.04-90.04 90.05-90.04 219.09 0 129.04 90.04 219.09 90.05 90.04 219.09 90.04ZM480-480Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="40" viewBox="0 -960 960 960" width="40"><path d="M445.72-277.05h72.56V-520h-72.56v242.95Zm62.24-326.08q11.36-11.11 11.36-27.54 0-17.17-11.3-28.58-11.3-11.4-28.01-11.4-16.88 0-28.11 11.4-11.22 11.41-11.22 28.5 0 16.53 11.37 27.63 11.36 11.11 27.95 11.11t27.96-11.12ZM480.2-73.3q-84.44 0-158.48-31.96-74.03-31.96-129.27-87.19-55.23-55.24-87.19-129.3Q73.3-395.82 73.3-480.31q0-84.5 31.96-158.58 31.96-74.09 87.17-129t129.28-86.94q74.08-32.03 158.59-32.03t158.61 32.02q74.11 32.02 129 86.91 54.9 54.88 86.92 129.08 32.03 74.2 32.03 158.67 0 84.46-32.03 158.5-32.03 74.03-86.94 129.12t-129.08 87.17Q564.64-73.3 480.2-73.3Zm.13-75.76q138.05 0 234.33-96.51 96.28-96.52 96.28-234.76 0-138.05-96.16-234.33-96.15-96.28-234.86-96.28-137.79 0-234.33 96.16-96.53 96.15-96.53 234.86 0 137.79 96.51 234.33 96.52 96.53 234.76 96.53ZM480-480Z"/></svg>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M111.87-228.28v-91h736.26v91H111.87Zm0-206.22v-91h736.26v91H111.87Zm0-206.22v-91h736.26v91H111.87Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="M218.87-135.87q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-522.26q0-34.48 24.26-58.74 24.26-24.26 58.74-24.26H480v83H218.87v522.26h522.26V-480h83v261.13q0 34.48-24.26 58.74-24.26 24.26-58.74 24.26H218.87ZM394.41-336 336-394.41l346.72-346.72H576v-83h248.13V-576h-83v-106.72L394.41-336Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="M764.52-134.91 523.85-375.59q-29.76 21.05-65.44 33.07-35.67 12.02-76.29 12.02-102.19 0-173.86-71.67-71.67-71.68-71.67-173.83t71.67-173.83q71.67-71.67 173.83-71.67 102.15 0 173.82 71.67 71.68 71.68 71.68 173.86 0 40.86-12.02 76.3-12.03 35.43-33.07 64.95l240.91 241.15-58.89 58.66ZM382.09-413.5q67.84 0 115.17-47.33 47.33-47.32 47.33-115.17t-47.33-115.17q-47.33-47.33-115.17-47.33-67.85 0-115.18 47.33-47.32 47.32-47.32 115.17t47.32 115.17q47.33 47.33 115.18 47.33Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M379.28-296.65 664.3-480 379.28-663.35v366.7ZM162.87-151.87q-37.78 0-64.39-26.61t-26.61-64.39v-474.26q0-37.78 26.61-64.39t64.39-26.61h634.26q37.78 0 64.39 26.61t26.61 64.39v474.26q0 37.78-26.61 64.39t-64.39 26.61H162.87Zm0-91h634.26v-474.26H162.87v474.26Zm0 0v-474.26 474.26Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="M380.41-208.59v-83h229.35L183.87-717.48l58.65-58.65 425.89 425.89v-229.35h83v371h-371Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="M41.07-139.93 480-872.13l438.93 732.2H41.07Zm139.91-79.42h598.04L480-717.3 180.98-219.35Zm325.24-56.14q10.98-10.76 10.98-26.21 0-15.45-10.77-26.31t-26.22-10.86q-15.45 0-26.43 10.65t-10.98 26.1q0 15.45 10.77 26.42 10.77 10.98 26.22 10.98t26.43-10.77ZM444-384h72v-189.37h-72V-384Zm36-84.33Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="40" viewBox="0 -960 960 960" width="40"><path d="M34.18-116.57 480-886.86l445.82 770.29H34.18Zm126.17-72.97h639.3L480-741.09 160.35-189.54Zm346.79-59.03q9.9-10.02 9.9-24.47 0-14.46-10.02-24.24-10.01-9.78-24.47-9.78-14.46 0-24.35 9.9-9.9 9.89-9.9 24.35t10.01 24.35q10.02 9.9 24.47 9.9 14.46 0 24.36-10.01ZM449.33-352H516v-213.85h-66.67V-352ZM480-465.31Z"/></svg>
|
package/build/bundle.ts
CHANGED
|
@@ -19,6 +19,13 @@ import type { SiteVariables } from './types';
|
|
|
19
19
|
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
20
20
|
import pkg from '../package.json' with { type: 'json' };
|
|
21
21
|
import { renderMaterialSymbolVariables } from './material-symbols';
|
|
22
|
+
import {
|
|
23
|
+
getSerifFontStack,
|
|
24
|
+
getSerifMonoFontStack,
|
|
25
|
+
renderCustomFontFaceScss,
|
|
26
|
+
renderCustomFontTuningScss,
|
|
27
|
+
renderFontFeatureSettings,
|
|
28
|
+
} from './custom-fonts';
|
|
22
29
|
|
|
23
30
|
interface BunBuildPlugin {
|
|
24
31
|
name: string;
|
|
@@ -62,6 +69,10 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
62
69
|
const bgTraceLineActive = `hsl(${traceLineActiveHue}deg 100% 86%)`;
|
|
63
70
|
const bgTraceLineActiveDark = `hsl(${traceLineActiveHue}deg 90% 18%)`;
|
|
64
71
|
const materialSymbolVariables = renderMaterialSymbolVariables();
|
|
72
|
+
const customFontFaces = renderCustomFontFaceScss(siteVariables.fontOverrides);
|
|
73
|
+
const customFontTuning = renderCustomFontTuningScss(
|
|
74
|
+
siteVariables.fontOverrides,
|
|
75
|
+
);
|
|
65
76
|
|
|
66
77
|
const renderedTemplate = _.template(template)({
|
|
67
78
|
...theme,
|
|
@@ -73,11 +84,19 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
73
84
|
linkColorHoverDark,
|
|
74
85
|
bgTraceLineActive,
|
|
75
86
|
bgTraceLineActiveDark,
|
|
87
|
+
serifFontStack: getSerifFontStack(siteVariables.fontOverrides),
|
|
88
|
+
serifMonoFontStack: getSerifMonoFontStack(siteVariables.fontOverrides),
|
|
89
|
+
serifFontFeatureSettings: renderFontFeatureSettings(
|
|
90
|
+
siteVariables.fontOverrides?.serif,
|
|
91
|
+
),
|
|
92
|
+
serifMonoFontFeatureSettings: renderFontFeatureSettings(
|
|
93
|
+
siteVariables.fontOverrides?.serifMono,
|
|
94
|
+
),
|
|
76
95
|
});
|
|
77
|
-
const rendered = renderedTemplate
|
|
78
|
-
'/*
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
const rendered = renderedTemplate
|
|
97
|
+
.replace('/* TADA_CUSTOM_FONT_FACES */', customFontFaces)
|
|
98
|
+
.replace('/* TADA_CUSTOM_FONT_TUNING */', customFontTuning)
|
|
99
|
+
.replace('/* TADA_MATERIAL_SYMBOL_VARIABLES */', materialSymbolVariables);
|
|
81
100
|
|
|
82
101
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
|
|
83
102
|
const configDir = path.join(tmpDir, 'config');
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as fontkit from 'fontkit';
|
|
4
|
+
import type { FontFamilyOverride, FontOverrides } from './types';
|
|
5
|
+
|
|
6
|
+
export const CUSTOM_FONT_FACE_DEFINITIONS = Object.freeze({
|
|
7
|
+
regular: Object.freeze({ fontStyle: 'normal', fontWeight: 400 }),
|
|
8
|
+
italic: Object.freeze({ fontStyle: 'italic', fontWeight: 400 }),
|
|
9
|
+
bold: Object.freeze({ fontStyle: 'normal', fontWeight: 700 }),
|
|
10
|
+
boldItalic: Object.freeze({ fontStyle: 'italic', fontWeight: 700 }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const CUSTOM_FONT_FAMILY_DEFINITIONS = Object.freeze({
|
|
14
|
+
serif: Object.freeze({ configKey: 'serif', cssFamily: 'Tada Custom Serif' }),
|
|
15
|
+
serifMono: Object.freeze({
|
|
16
|
+
configKey: 'serifMono',
|
|
17
|
+
cssFamily: 'Tada Custom Serif Mono',
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type CustomFontFamilyKey = keyof typeof CUSTOM_FONT_FAMILY_DEFINITIONS;
|
|
22
|
+
export type CustomFontFaceKey = keyof typeof CUSTOM_FONT_FACE_DEFINITIONS;
|
|
23
|
+
|
|
24
|
+
const FACE_KEYS = Object.freeze(
|
|
25
|
+
Object.keys(CUSTOM_FONT_FACE_DEFINITIONS) as CustomFontFaceKey[],
|
|
26
|
+
);
|
|
27
|
+
const FAMILY_KEYS = Object.freeze(
|
|
28
|
+
Object.keys(CUSTOM_FONT_FAMILY_DEFINITIONS) as CustomFontFamilyKey[],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
interface ParsedFont {
|
|
32
|
+
availableFeatures: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CustomFontValidationOptions {
|
|
36
|
+
fontOverrides: FontOverrides | undefined;
|
|
37
|
+
publicDir: string;
|
|
38
|
+
publicFiles: ReadonlySet<string>;
|
|
39
|
+
readFile?: (filePath: string) => Buffer;
|
|
40
|
+
parseFont?: (contents: Buffer) => ParsedFont;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isValidPublicWoff2Path(filePath: string): boolean {
|
|
44
|
+
if (
|
|
45
|
+
filePath.length === 0 ||
|
|
46
|
+
filePath.startsWith('/') ||
|
|
47
|
+
/^[A-Za-z]:/.test(filePath) ||
|
|
48
|
+
filePath.includes('\\') ||
|
|
49
|
+
filePath.includes('?') ||
|
|
50
|
+
filePath.includes('#') ||
|
|
51
|
+
!filePath.endsWith('.woff2')
|
|
52
|
+
) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const segments = filePath.split('/');
|
|
57
|
+
return segments.every(
|
|
58
|
+
segment => segment.length > 0 && segment !== '.' && segment !== '..',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolvePublicFontPath(
|
|
63
|
+
publicDir: string,
|
|
64
|
+
filePath: string,
|
|
65
|
+
): string {
|
|
66
|
+
return path.resolve(publicDir, ...filePath.split('/'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function encodePublicAssetPath(filePath: string): string {
|
|
70
|
+
return filePath
|
|
71
|
+
.split('/')
|
|
72
|
+
.map(segment => encodeURIComponent(segment).replaceAll("'", '%27'))
|
|
73
|
+
.join('/');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function renderFontFeatureSettings(
|
|
77
|
+
family: FontFamilyOverride | undefined,
|
|
78
|
+
): string {
|
|
79
|
+
return family?.features?.length
|
|
80
|
+
? family.features.map(feature => `'${feature}'`).join(', ')
|
|
81
|
+
: 'normal';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderCustomFontFaceScss(
|
|
85
|
+
fontOverrides: FontOverrides | undefined,
|
|
86
|
+
): string {
|
|
87
|
+
const rules: string[] = [];
|
|
88
|
+
for (const familyKey of FAMILY_KEYS) {
|
|
89
|
+
const family = fontOverrides?.[familyKey];
|
|
90
|
+
if (!family) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const cssFamily = CUSTOM_FONT_FAMILY_DEFINITIONS[familyKey].cssFamily;
|
|
94
|
+
for (const faceKey of FACE_KEYS) {
|
|
95
|
+
const filePath = family[faceKey];
|
|
96
|
+
if (!filePath) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const face = CUSTOM_FONT_FACE_DEFINITIONS[faceKey];
|
|
100
|
+
rules.push(` @font-face {
|
|
101
|
+
font-family: '${cssFamily}';
|
|
102
|
+
font-style: ${face.fontStyle};
|
|
103
|
+
font-weight: ${face.fontWeight};
|
|
104
|
+
src: url('${encodePublicAssetPath(filePath)}') format('woff2');
|
|
105
|
+
font-display: swap;
|
|
106
|
+
}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return rules.join('\n\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const HEADING_SIZES = Object.freeze({
|
|
113
|
+
h1: 2.5,
|
|
114
|
+
h2: 1.5,
|
|
115
|
+
h3: 1.17,
|
|
116
|
+
h4: 1,
|
|
117
|
+
h5: 0.83,
|
|
118
|
+
h6: 0.67,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function formatCssNumber(value: number): string {
|
|
122
|
+
return Number(value.toFixed(4)).toString();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function renderCustomFontTuningScss(
|
|
126
|
+
fontOverrides: FontOverrides | undefined,
|
|
127
|
+
): string {
|
|
128
|
+
const rules: string[] = [];
|
|
129
|
+
const serifTuning = fontOverrides?.serif?.tuning;
|
|
130
|
+
const monoTuning = fontOverrides?.serifMono?.tuning;
|
|
131
|
+
const rootSelector = ":root[data-font-preference='serif']";
|
|
132
|
+
|
|
133
|
+
if (serifTuning?.scale !== undefined) {
|
|
134
|
+
rules.push(
|
|
135
|
+
` ${rootSelector} {\n --font-size: ${formatCssNumber(serifTuning.scale)}rem;\n }`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (serifTuning?.lineHeight !== undefined) {
|
|
140
|
+
rules.push(
|
|
141
|
+
` ${rootSelector} main.body {\n line-height: ${formatCssNumber(serifTuning.lineHeight)};\n }`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
serifTuning?.scale !== undefined ||
|
|
147
|
+
serifTuning?.lineHeight !== undefined
|
|
148
|
+
) {
|
|
149
|
+
rules.push(
|
|
150
|
+
` ${rootSelector} main.body > :is(footer, .appearance-pickers, .slides-header, .file-header) {\n font-size: var(--font-size);\n line-height: var(--line-height);\n }`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
serifTuning?.scale !== undefined ||
|
|
156
|
+
serifTuning?.headingScale !== undefined
|
|
157
|
+
) {
|
|
158
|
+
const headingScale = serifTuning.headingScale ?? 1;
|
|
159
|
+
for (const [heading, size] of Object.entries(HEADING_SIZES)) {
|
|
160
|
+
rules.push(
|
|
161
|
+
` ${rootSelector} .main-content ${heading}:not(.file-title) { font-size: ${formatCssNumber(size * headingScale)}rem; }`,
|
|
162
|
+
);
|
|
163
|
+
rules.push(
|
|
164
|
+
` ${rootSelector} body.is-presenting .main-content .slide ${heading}:not(.file-title) { font-size: ${formatCssNumber(size * headingScale)}em; }`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (serifTuning?.headingWeight !== undefined) {
|
|
170
|
+
rules.push(
|
|
171
|
+
` ${rootSelector} .main-content :is(h1, h2, h3, h4, h5, h6):not(.file-title) {\n font-weight: ${serifTuning.headingWeight};\n }`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (monoTuning?.scale !== undefined || monoTuning?.lineHeight !== undefined) {
|
|
176
|
+
const declarations: string[] = [];
|
|
177
|
+
if (monoTuning.scale !== undefined) {
|
|
178
|
+
declarations.push(
|
|
179
|
+
`--mono-font-size: ${formatCssNumber(monoTuning.scale)}em;`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (monoTuning.lineHeight !== undefined) {
|
|
183
|
+
declarations.push(
|
|
184
|
+
`--mono-line-height: ${formatCssNumber(monoTuning.lineHeight)};`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
rules.push(` ${rootSelector} {\n ${declarations.join('\n ')}\n }`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return rules.join('\n\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getSerifFontStack(
|
|
194
|
+
fontOverrides: FontOverrides | undefined,
|
|
195
|
+
): string {
|
|
196
|
+
const custom = fontOverrides?.serif
|
|
197
|
+
? `'${CUSTOM_FONT_FAMILY_DEFINITIONS.serif.cssFamily}', `
|
|
198
|
+
: '';
|
|
199
|
+
return `${custom}'Source Serif 4', 'Times New Roman', 'Times', serif`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function getSerifMonoFontStack(
|
|
203
|
+
fontOverrides: FontOverrides | undefined,
|
|
204
|
+
): string {
|
|
205
|
+
const custom = fontOverrides?.serifMono
|
|
206
|
+
? `'${CUSTOM_FONT_FAMILY_DEFINITIONS.serifMono.cssFamily}', `
|
|
207
|
+
: '';
|
|
208
|
+
return `${custom}'Libertinus Mono', 'Courier New', 'Courier', monospace`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function defaultParseFont(contents: Buffer): ParsedFont {
|
|
212
|
+
const font = fontkit.create(contents) as fontkit.Font;
|
|
213
|
+
return { availableFeatures: font.availableFeatures };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function validateFamily(
|
|
217
|
+
familyKey: CustomFontFamilyKey,
|
|
218
|
+
family: FontFamilyOverride,
|
|
219
|
+
options: Required<
|
|
220
|
+
Pick<CustomFontValidationOptions, 'readFile' | 'parseFont'>
|
|
221
|
+
> &
|
|
222
|
+
Pick<CustomFontValidationOptions, 'publicDir' | 'publicFiles'>,
|
|
223
|
+
): string[] {
|
|
224
|
+
const diagnostics: string[] = [];
|
|
225
|
+
const parsedFaces: Partial<Record<CustomFontFaceKey, ParsedFont>> = {};
|
|
226
|
+
|
|
227
|
+
for (const faceKey of FACE_KEYS) {
|
|
228
|
+
const filePath = family[faceKey];
|
|
229
|
+
if (!filePath) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const configPath = `fontOverrides.${familyKey}.${faceKey}`;
|
|
233
|
+
if (!isValidPublicWoff2Path(filePath)) {
|
|
234
|
+
diagnostics.push(
|
|
235
|
+
`${configPath} must be a public-relative POSIX path ending in .woff2`,
|
|
236
|
+
);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const absolutePath = resolvePublicFontPath(options.publicDir, filePath);
|
|
241
|
+
if (!options.publicFiles.has(absolutePath)) {
|
|
242
|
+
diagnostics.push(`${configPath} "${filePath}" does not exist in public/`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let contents: Buffer;
|
|
247
|
+
try {
|
|
248
|
+
contents = options.readFile(absolutePath);
|
|
249
|
+
} catch {
|
|
250
|
+
diagnostics.push(`${configPath} "${filePath}" could not be read`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (contents.subarray(0, 4).toString('ascii') !== 'wOF2') {
|
|
254
|
+
diagnostics.push(`${configPath} "${filePath}" is not a WOFF2 font`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
parsedFaces[faceKey] = options.parseFont(contents);
|
|
259
|
+
} catch {
|
|
260
|
+
diagnostics.push(`${configPath} "${filePath}" is not a valid WOFF2 font`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const feature of family.features ?? []) {
|
|
265
|
+
for (const faceKey of FACE_KEYS) {
|
|
266
|
+
const parsed = parsedFaces[faceKey];
|
|
267
|
+
if (parsed && !parsed.availableFeatures.includes(feature)) {
|
|
268
|
+
diagnostics.push(
|
|
269
|
+
`fontOverrides.${familyKey}.features "${feature}" is not supported by fontOverrides.${familyKey}.${faceKey}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return diagnostics;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function validateCustomFontOverrides(
|
|
279
|
+
options: CustomFontValidationOptions,
|
|
280
|
+
): string[] {
|
|
281
|
+
const readFile = options.readFile ?? fs.readFileSync;
|
|
282
|
+
const parseFont = options.parseFont ?? defaultParseFont;
|
|
283
|
+
const diagnostics: string[] = [];
|
|
284
|
+
|
|
285
|
+
for (const familyKey of FAMILY_KEYS) {
|
|
286
|
+
const family = options.fontOverrides?.[familyKey];
|
|
287
|
+
if (family) {
|
|
288
|
+
diagnostics.push(
|
|
289
|
+
...validateFamily(familyKey, family, {
|
|
290
|
+
publicDir: options.publicDir,
|
|
291
|
+
publicFiles: options.publicFiles,
|
|
292
|
+
readFile,
|
|
293
|
+
parseFont,
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return diagnostics;
|
|
300
|
+
}
|
package/build/generate-fonts.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getPackageDir } from './utils/paths';
|
|
4
4
|
import { makeLogger } from './log';
|
|
5
|
+
import type { SiteVariables } from './types';
|
|
5
6
|
|
|
6
7
|
const log = makeLogger(import.meta.url);
|
|
7
8
|
const FONTS_DIR = path.join(getPackageDir(), 'fonts');
|
|
@@ -17,10 +18,35 @@ export const DEFAULT_FONT_PRELOAD_FILES = {
|
|
|
17
18
|
],
|
|
18
19
|
} as const;
|
|
19
20
|
|
|
21
|
+
export interface DefaultFontPreloadFile {
|
|
22
|
+
filePath: string;
|
|
23
|
+
source: 'package' | 'public';
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export function getDefaultFontPreloadFiles(
|
|
21
|
-
|
|
22
|
-
): readonly
|
|
23
|
-
|
|
27
|
+
siteVariables: Pick<SiteVariables, 'defaultFont' | 'fontOverrides'>,
|
|
28
|
+
): readonly DefaultFontPreloadFile[] {
|
|
29
|
+
if ((siteVariables.defaultFont ?? 'sans') === 'sans') {
|
|
30
|
+
return DEFAULT_FONT_PRELOAD_FILES.sans.map(filePath => ({
|
|
31
|
+
filePath,
|
|
32
|
+
source: 'package',
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
siteVariables.fontOverrides?.serif?.regular
|
|
38
|
+
? {
|
|
39
|
+
filePath: siteVariables.fontOverrides.serif.regular,
|
|
40
|
+
source: 'public',
|
|
41
|
+
}
|
|
42
|
+
: { filePath: DEFAULT_FONT_PRELOAD_FILES.serif[0], source: 'package' },
|
|
43
|
+
siteVariables.fontOverrides?.serifMono?.regular
|
|
44
|
+
? {
|
|
45
|
+
filePath: siteVariables.fontOverrides.serifMono.regular,
|
|
46
|
+
source: 'public',
|
|
47
|
+
}
|
|
48
|
+
: { filePath: DEFAULT_FONT_PRELOAD_FILES.serif[1], source: 'package' },
|
|
49
|
+
];
|
|
24
50
|
}
|
|
25
51
|
|
|
26
52
|
export function copyFonts(distDir: string): void {
|
|
@@ -5,43 +5,78 @@ import { getPackageDir } from './utils/paths';
|
|
|
5
5
|
export const MATERIAL_SYMBOL_CONFIG = Object.freeze({
|
|
6
6
|
family: 'Material Symbols Outlined',
|
|
7
7
|
fill: 0,
|
|
8
|
-
weight:
|
|
8
|
+
weight: 500,
|
|
9
9
|
grade: 0,
|
|
10
|
-
|
|
10
|
+
opticalSizes: Object.freeze([20, 24, 40] as const),
|
|
11
11
|
viewBox: '0 -960 960 960',
|
|
12
|
-
source:
|
|
13
|
-
'https://github.com/google/material-design-icons/tree/master/symbols/web',
|
|
12
|
+
source: 'https://fonts.google.com/icons',
|
|
14
13
|
} as const);
|
|
15
14
|
|
|
16
15
|
export const MATERIAL_SYMBOLS = Object.freeze({
|
|
17
|
-
tada: Object.freeze({
|
|
16
|
+
tada: Object.freeze({
|
|
17
|
+
cssVariable: '--icon-tada',
|
|
18
|
+
symbol: 'celebration',
|
|
19
|
+
opticalSize: 24,
|
|
20
|
+
}),
|
|
18
21
|
contrastStandard: Object.freeze({
|
|
19
22
|
cssVariable: '--icon-contrast-standard',
|
|
20
23
|
symbol: 'contrast_rtl_off',
|
|
24
|
+
opticalSize: 20,
|
|
21
25
|
}),
|
|
22
26
|
contrastHigh: Object.freeze({
|
|
23
27
|
cssVariable: '--icon-contrast-high',
|
|
24
28
|
symbol: 'contrast',
|
|
29
|
+
opticalSize: 20,
|
|
30
|
+
}),
|
|
31
|
+
info: Object.freeze({
|
|
32
|
+
cssVariable: '--icon-info',
|
|
33
|
+
symbol: 'info',
|
|
34
|
+
opticalSize: 40,
|
|
35
|
+
}),
|
|
36
|
+
infoCompact: Object.freeze({
|
|
37
|
+
cssVariable: '--icon-info-compact',
|
|
38
|
+
symbol: 'info',
|
|
39
|
+
opticalSize: 20,
|
|
40
|
+
}),
|
|
41
|
+
warning: Object.freeze({
|
|
42
|
+
cssVariable: '--icon-warning',
|
|
43
|
+
symbol: 'warning',
|
|
44
|
+
opticalSize: 40,
|
|
45
|
+
}),
|
|
46
|
+
warningCompact: Object.freeze({
|
|
47
|
+
cssVariable: '--icon-warning-compact',
|
|
48
|
+
symbol: 'warning',
|
|
49
|
+
opticalSize: 20,
|
|
50
|
+
}),
|
|
51
|
+
parent: Object.freeze({
|
|
52
|
+
cssVariable: '--icon-parent',
|
|
53
|
+
symbol: 'south_east',
|
|
54
|
+
opticalSize: 20,
|
|
55
|
+
}),
|
|
56
|
+
search: Object.freeze({
|
|
57
|
+
cssVariable: '--icon-search',
|
|
58
|
+
symbol: 'search',
|
|
59
|
+
opticalSize: 20,
|
|
25
60
|
}),
|
|
26
|
-
info: Object.freeze({ cssVariable: '--icon-info', symbol: 'info' }),
|
|
27
|
-
warning: Object.freeze({ cssVariable: '--icon-warning', symbol: 'warning' }),
|
|
28
|
-
parent: Object.freeze({ cssVariable: '--icon-parent', symbol: 'south_east' }),
|
|
29
|
-
search: Object.freeze({ cssVariable: '--icon-search', symbol: 'search' }),
|
|
30
61
|
externalLink: Object.freeze({
|
|
31
62
|
cssVariable: '--icon-external-link',
|
|
32
|
-
symbol: '
|
|
63
|
+
symbol: 'open_in_new',
|
|
64
|
+
opticalSize: 20,
|
|
33
65
|
}),
|
|
34
|
-
|
|
35
|
-
cssVariable: '--icon-heading-
|
|
36
|
-
symbol: '
|
|
66
|
+
headingPresent: Object.freeze({
|
|
67
|
+
cssVariable: '--icon-heading-present',
|
|
68
|
+
symbol: 'smart_display',
|
|
69
|
+
opticalSize: 24,
|
|
37
70
|
}),
|
|
38
71
|
headerMenu: Object.freeze({
|
|
39
72
|
cssVariable: '--icon-header-menu',
|
|
40
73
|
symbol: 'menu',
|
|
74
|
+
opticalSize: 24,
|
|
41
75
|
}),
|
|
42
76
|
headerClose: Object.freeze({
|
|
43
77
|
cssVariable: '--icon-header-close',
|
|
44
78
|
symbol: 'close',
|
|
79
|
+
opticalSize: 24,
|
|
45
80
|
}),
|
|
46
81
|
} as const);
|
|
47
82
|
|
|
@@ -56,11 +91,14 @@ function getAssetPath(packageDir: string, symbol: MaterialSymbol): string {
|
|
|
56
91
|
return path.join(
|
|
57
92
|
packageDir,
|
|
58
93
|
'assets/material-symbols',
|
|
59
|
-
`${symbol.symbol}_${
|
|
94
|
+
`${symbol.symbol}_${symbol.opticalSize}px.svg`,
|
|
60
95
|
);
|
|
61
96
|
}
|
|
62
97
|
|
|
63
|
-
function isConfiguredSvg(
|
|
98
|
+
function isConfiguredSvg(
|
|
99
|
+
svg: string,
|
|
100
|
+
opticalSize: MaterialSymbol['opticalSize'],
|
|
101
|
+
): boolean {
|
|
64
102
|
const svgTag = svg.match(/<svg\b[^>]*>/)?.[0];
|
|
65
103
|
if (!svgTag) {
|
|
66
104
|
return false;
|
|
@@ -70,8 +108,8 @@ function isConfiguredSvg(svg: string): boolean {
|
|
|
70
108
|
new RegExp(`\\b${name}=["']${value}["']`).test(svgTag);
|
|
71
109
|
|
|
72
110
|
return (
|
|
73
|
-
hasAttribute('width', String(
|
|
74
|
-
hasAttribute('height', String(
|
|
111
|
+
hasAttribute('width', String(opticalSize)) &&
|
|
112
|
+
hasAttribute('height', String(opticalSize)) &&
|
|
75
113
|
hasAttribute('viewBox', MATERIAL_SYMBOL_CONFIG.viewBox)
|
|
76
114
|
);
|
|
77
115
|
}
|
|
@@ -94,14 +132,14 @@ export function renderMaterialSymbolVariables({
|
|
|
94
132
|
);
|
|
95
133
|
}
|
|
96
134
|
|
|
97
|
-
if (!isConfiguredSvg(svg)) {
|
|
135
|
+
if (!isConfiguredSvg(svg, symbol.opticalSize)) {
|
|
98
136
|
throw new Error(
|
|
99
|
-
`Material Symbol "${symbol.symbol}" does not match the configured ${
|
|
137
|
+
`Material Symbol "${symbol.symbol}" does not match the configured ${symbol.opticalSize}px SVG format: ${assetPath}`,
|
|
100
138
|
);
|
|
101
139
|
}
|
|
102
140
|
|
|
103
141
|
const dataUri = `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
|
|
104
|
-
return ` ${symbol.cssVariable}: url("${dataUri}");`;
|
|
142
|
+
return ` ${symbol.cssVariable}: url("${dataUri}");\n ${symbol.cssVariable}-size: ${symbol.opticalSize}px;`;
|
|
105
143
|
})
|
|
106
144
|
.join('\n');
|
|
107
145
|
}
|
package/build/pipeline.ts
CHANGED
|
@@ -45,7 +45,7 @@ export async function runPipeline(
|
|
|
45
45
|
let published = false;
|
|
46
46
|
|
|
47
47
|
try {
|
|
48
|
-
const configDiagnostics = validateConfig(scan);
|
|
48
|
+
const configDiagnostics = validateConfig(scan, siteVariables);
|
|
49
49
|
if (configDiagnostics.length > 0) {
|
|
50
50
|
throw new Error(configDiagnostics[0].message);
|
|
51
51
|
}
|
package/build/types.d.ts
CHANGED
|
@@ -3,6 +3,32 @@ import type { TadaProjectScan } from './source-model';
|
|
|
3
3
|
|
|
4
4
|
export type PlainTextLanguage = 'text' | 'txt' | 'plain';
|
|
5
5
|
|
|
6
|
+
export interface SerifFontTuning {
|
|
7
|
+
scale?: number;
|
|
8
|
+
lineHeight?: number;
|
|
9
|
+
headingScale?: number;
|
|
10
|
+
headingWeight?: 400 | 700;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SerifMonoFontTuning {
|
|
14
|
+
scale?: number;
|
|
15
|
+
lineHeight?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FontFamilyOverride<Tuning = unknown> {
|
|
19
|
+
regular: string;
|
|
20
|
+
italic?: string;
|
|
21
|
+
bold?: string;
|
|
22
|
+
boldItalic?: string;
|
|
23
|
+
features?: string[];
|
|
24
|
+
tuning?: Tuning;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FontOverrides {
|
|
28
|
+
serif?: FontFamilyOverride<SerifFontTuning>;
|
|
29
|
+
serifMono?: FontFamilyOverride<SerifMonoFontTuning>;
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
/** Site configuration loaded from site.dev.yaml/yml/json or site.prod.yaml/yml/json */
|
|
7
33
|
export interface SiteVariables {
|
|
8
34
|
base: string;
|
|
@@ -18,6 +44,7 @@ export interface SiteVariables {
|
|
|
18
44
|
defaultTimeZone: string;
|
|
19
45
|
defaultFont?: 'sans' | 'serif';
|
|
20
46
|
defaultContrast?: 'standard' | 'high';
|
|
47
|
+
fontOverrides?: FontOverrides;
|
|
21
48
|
features: FeatureConfig;
|
|
22
49
|
extensionToShikiLanguage?: Record<
|
|
23
50
|
string,
|