@superdoc-dev/fonts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/assets/AGPL-3.0.txt +228 -0
- package/assets/Apache-2.0.txt +204 -0
- package/assets/ArchivoBlack-Regular.woff2 +0 -0
- package/assets/BacasimeAntique-Regular.woff2 +0 -0
- package/assets/C059-BdIta.woff2 +0 -0
- package/assets/C059-Bold.woff2 +0 -0
- package/assets/C059-Italic.woff2 +0 -0
- package/assets/C059-Roman.woff2 +0 -0
- package/assets/Caladea-Bold.woff2 +0 -0
- package/assets/Caladea-BoldItalic.woff2 +0 -0
- package/assets/Caladea-Italic.woff2 +0 -0
- package/assets/Caladea-Regular.woff2 +0 -0
- package/assets/Caprasimo-Regular.woff2 +0 -0
- package/assets/Cardo-Bold.woff2 +0 -0
- package/assets/Cardo-Italic.woff2 +0 -0
- package/assets/Cardo-Regular.woff2 +0 -0
- package/assets/Carlito-Bold.woff2 +0 -0
- package/assets/Carlito-BoldItalic.woff2 +0 -0
- package/assets/Carlito-Italic.woff2 +0 -0
- package/assets/Carlito-Regular.woff2 +0 -0
- package/assets/ComicRelief-Bold.woff2 +0 -0
- package/assets/ComicRelief-Regular.woff2 +0 -0
- package/assets/GPL-2.0.txt +116 -0
- package/assets/GUST-Font-License-1.0.txt +28 -0
- package/assets/Gelasio-Bold.woff2 +0 -0
- package/assets/Gelasio-BoldItalic.woff2 +0 -0
- package/assets/Gelasio-Italic.woff2 +0 -0
- package/assets/Gelasio-Regular.woff2 +0 -0
- package/assets/InconsolataSemiExpanded-Bold.woff2 +0 -0
- package/assets/InconsolataSemiExpanded-Regular.woff2 +0 -0
- package/assets/LICENSES.md +309 -0
- package/assets/LiberationMono-Bold.woff2 +0 -0
- package/assets/LiberationMono-BoldItalic.woff2 +0 -0
- package/assets/LiberationMono-Italic.woff2 +0 -0
- package/assets/LiberationMono-Regular.woff2 +0 -0
- package/assets/LiberationSans-Bold.woff2 +0 -0
- package/assets/LiberationSans-BoldItalic.woff2 +0 -0
- package/assets/LiberationSans-Italic.woff2 +0 -0
- package/assets/LiberationSans-Regular.woff2 +0 -0
- package/assets/LiberationSansNarrow-Bold.woff2 +0 -0
- package/assets/LiberationSansNarrow-BoldItalic.woff2 +0 -0
- package/assets/LiberationSansNarrow-Italic.woff2 +0 -0
- package/assets/LiberationSansNarrow-Regular.woff2 +0 -0
- package/assets/LiberationSerif-Bold.woff2 +0 -0
- package/assets/LiberationSerif-BoldItalic.woff2 +0 -0
- package/assets/LiberationSerif-Italic.woff2 +0 -0
- package/assets/LiberationSerif-Regular.woff2 +0 -0
- package/assets/NotoSans-Bold.woff2 +0 -0
- package/assets/NotoSans-BoldItalic.woff2 +0 -0
- package/assets/NotoSans-Italic.woff2 +0 -0
- package/assets/NotoSans-Regular.woff2 +0 -0
- package/assets/NotoSansMono-Bold.woff2 +0 -0
- package/assets/NotoSansMono-Regular.woff2 +0 -0
- package/assets/OFL.txt +191 -0
- package/assets/OreganoItalic-Regular.woff2 +0 -0
- package/assets/PTSans-Bold.woff2 +0 -0
- package/assets/PTSans-BoldItalic.woff2 +0 -0
- package/assets/PTSans-Italic.woff2 +0 -0
- package/assets/PTSans-Regular.woff2 +0 -0
- package/assets/PTSansNarrow-Bold.woff2 +0 -0
- package/assets/PTSansNarrow-Regular.woff2 +0 -0
- package/assets/Selawik-Bold.woff2 +0 -0
- package/assets/Selawik-Regular.woff2 +0 -0
- package/assets/TeXGyreBonum-Bold.woff2 +0 -0
- package/assets/TeXGyreBonum-BoldItalic.woff2 +0 -0
- package/assets/TeXGyreBonum-Italic.woff2 +0 -0
- package/assets/TeXGyreBonum-Regular.woff2 +0 -0
- package/assets/URWGothic-Book.woff2 +0 -0
- package/assets/URWGothic-BookOblique.woff2 +0 -0
- package/assets/URWGothic-Demi.woff2 +0 -0
- package/assets/URWGothic-DemiOblique.woff2 +0 -0
- package/dist/asset-urls.d.ts +8 -0
- package/dist/asset-urls.js +76 -0
- package/dist/bundled-families.d.ts +9 -0
- package/dist/bundled-families.js +40 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +158 -0
- package/package.json +41 -0
- package/src/asset-urls.ts +76 -0
- package/src/bundled-families.ts +40 -0
- package/src/index.test.ts +72 -0
- package/src/index.ts +209 -0
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@superdoc-dev/fonts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Optional bundled metric-compatible font substitutes for SuperDoc, shipped as bundler-resolvable assets.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN assets/LICENSES.md",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"source": "./src/index.ts",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"assets"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"scripts": {
|
|
29
|
+
"sync": "node scripts/sync-assets.mjs",
|
|
30
|
+
"generate": "node scripts/generate-asset-urls.mjs && pnpm exec tsx scripts/generate-bundled-families.ts",
|
|
31
|
+
"check:families": "pnpm exec tsx scripts/check-bundled-families.ts",
|
|
32
|
+
"build": "node scripts/sync-assets.mjs && node scripts/generate-asset-urls.mjs && tsc --project tsconfig.json",
|
|
33
|
+
"prepare": "node scripts/sync-assets.mjs && node scripts/generate-asset-urls.mjs && tsc --project tsconfig.json",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "vitest"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "catalog:",
|
|
39
|
+
"typescript": "catalog:"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by scripts/generate-asset-urls.mjs - do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Each value is a static `new URL('../assets/<file>', import.meta.url)`, the cross-bundler
|
|
5
|
+
* pattern for "emit this asset and give me its final URL". Bundlers rewrite these at build
|
|
6
|
+
* time; under plain Node ESM they resolve to the file:// path inside the package.
|
|
7
|
+
*/
|
|
8
|
+
export const BUNDLED_FONT_ASSET_URLS: Readonly<Record<string, string>> = Object.freeze({
|
|
9
|
+
'ArchivoBlack-Regular.woff2': new URL('../assets/ArchivoBlack-Regular.woff2', import.meta.url).href,
|
|
10
|
+
'BacasimeAntique-Regular.woff2': new URL('../assets/BacasimeAntique-Regular.woff2', import.meta.url).href,
|
|
11
|
+
'C059-BdIta.woff2': new URL('../assets/C059-BdIta.woff2', import.meta.url).href,
|
|
12
|
+
'C059-Bold.woff2': new URL('../assets/C059-Bold.woff2', import.meta.url).href,
|
|
13
|
+
'C059-Italic.woff2': new URL('../assets/C059-Italic.woff2', import.meta.url).href,
|
|
14
|
+
'C059-Roman.woff2': new URL('../assets/C059-Roman.woff2', import.meta.url).href,
|
|
15
|
+
'Caladea-Bold.woff2': new URL('../assets/Caladea-Bold.woff2', import.meta.url).href,
|
|
16
|
+
'Caladea-BoldItalic.woff2': new URL('../assets/Caladea-BoldItalic.woff2', import.meta.url).href,
|
|
17
|
+
'Caladea-Italic.woff2': new URL('../assets/Caladea-Italic.woff2', import.meta.url).href,
|
|
18
|
+
'Caladea-Regular.woff2': new URL('../assets/Caladea-Regular.woff2', import.meta.url).href,
|
|
19
|
+
'Caprasimo-Regular.woff2': new URL('../assets/Caprasimo-Regular.woff2', import.meta.url).href,
|
|
20
|
+
'Cardo-Bold.woff2': new URL('../assets/Cardo-Bold.woff2', import.meta.url).href,
|
|
21
|
+
'Cardo-Italic.woff2': new URL('../assets/Cardo-Italic.woff2', import.meta.url).href,
|
|
22
|
+
'Cardo-Regular.woff2': new URL('../assets/Cardo-Regular.woff2', import.meta.url).href,
|
|
23
|
+
'Carlito-Bold.woff2': new URL('../assets/Carlito-Bold.woff2', import.meta.url).href,
|
|
24
|
+
'Carlito-BoldItalic.woff2': new URL('../assets/Carlito-BoldItalic.woff2', import.meta.url).href,
|
|
25
|
+
'Carlito-Italic.woff2': new URL('../assets/Carlito-Italic.woff2', import.meta.url).href,
|
|
26
|
+
'Carlito-Regular.woff2': new URL('../assets/Carlito-Regular.woff2', import.meta.url).href,
|
|
27
|
+
'ComicRelief-Bold.woff2': new URL('../assets/ComicRelief-Bold.woff2', import.meta.url).href,
|
|
28
|
+
'ComicRelief-Regular.woff2': new URL('../assets/ComicRelief-Regular.woff2', import.meta.url).href,
|
|
29
|
+
'Gelasio-Bold.woff2': new URL('../assets/Gelasio-Bold.woff2', import.meta.url).href,
|
|
30
|
+
'Gelasio-BoldItalic.woff2': new URL('../assets/Gelasio-BoldItalic.woff2', import.meta.url).href,
|
|
31
|
+
'Gelasio-Italic.woff2': new URL('../assets/Gelasio-Italic.woff2', import.meta.url).href,
|
|
32
|
+
'Gelasio-Regular.woff2': new URL('../assets/Gelasio-Regular.woff2', import.meta.url).href,
|
|
33
|
+
'InconsolataSemiExpanded-Bold.woff2': new URL('../assets/InconsolataSemiExpanded-Bold.woff2', import.meta.url).href,
|
|
34
|
+
'InconsolataSemiExpanded-Regular.woff2': new URL('../assets/InconsolataSemiExpanded-Regular.woff2', import.meta.url)
|
|
35
|
+
.href,
|
|
36
|
+
'LiberationMono-Bold.woff2': new URL('../assets/LiberationMono-Bold.woff2', import.meta.url).href,
|
|
37
|
+
'LiberationMono-BoldItalic.woff2': new URL('../assets/LiberationMono-BoldItalic.woff2', import.meta.url).href,
|
|
38
|
+
'LiberationMono-Italic.woff2': new URL('../assets/LiberationMono-Italic.woff2', import.meta.url).href,
|
|
39
|
+
'LiberationMono-Regular.woff2': new URL('../assets/LiberationMono-Regular.woff2', import.meta.url).href,
|
|
40
|
+
'LiberationSans-Bold.woff2': new URL('../assets/LiberationSans-Bold.woff2', import.meta.url).href,
|
|
41
|
+
'LiberationSans-BoldItalic.woff2': new URL('../assets/LiberationSans-BoldItalic.woff2', import.meta.url).href,
|
|
42
|
+
'LiberationSans-Italic.woff2': new URL('../assets/LiberationSans-Italic.woff2', import.meta.url).href,
|
|
43
|
+
'LiberationSans-Regular.woff2': new URL('../assets/LiberationSans-Regular.woff2', import.meta.url).href,
|
|
44
|
+
'LiberationSansNarrow-Bold.woff2': new URL('../assets/LiberationSansNarrow-Bold.woff2', import.meta.url).href,
|
|
45
|
+
'LiberationSansNarrow-BoldItalic.woff2': new URL('../assets/LiberationSansNarrow-BoldItalic.woff2', import.meta.url)
|
|
46
|
+
.href,
|
|
47
|
+
'LiberationSansNarrow-Italic.woff2': new URL('../assets/LiberationSansNarrow-Italic.woff2', import.meta.url).href,
|
|
48
|
+
'LiberationSansNarrow-Regular.woff2': new URL('../assets/LiberationSansNarrow-Regular.woff2', import.meta.url).href,
|
|
49
|
+
'LiberationSerif-Bold.woff2': new URL('../assets/LiberationSerif-Bold.woff2', import.meta.url).href,
|
|
50
|
+
'LiberationSerif-BoldItalic.woff2': new URL('../assets/LiberationSerif-BoldItalic.woff2', import.meta.url).href,
|
|
51
|
+
'LiberationSerif-Italic.woff2': new URL('../assets/LiberationSerif-Italic.woff2', import.meta.url).href,
|
|
52
|
+
'LiberationSerif-Regular.woff2': new URL('../assets/LiberationSerif-Regular.woff2', import.meta.url).href,
|
|
53
|
+
'NotoSans-Bold.woff2': new URL('../assets/NotoSans-Bold.woff2', import.meta.url).href,
|
|
54
|
+
'NotoSans-BoldItalic.woff2': new URL('../assets/NotoSans-BoldItalic.woff2', import.meta.url).href,
|
|
55
|
+
'NotoSans-Italic.woff2': new URL('../assets/NotoSans-Italic.woff2', import.meta.url).href,
|
|
56
|
+
'NotoSans-Regular.woff2': new URL('../assets/NotoSans-Regular.woff2', import.meta.url).href,
|
|
57
|
+
'NotoSansMono-Bold.woff2': new URL('../assets/NotoSansMono-Bold.woff2', import.meta.url).href,
|
|
58
|
+
'NotoSansMono-Regular.woff2': new URL('../assets/NotoSansMono-Regular.woff2', import.meta.url).href,
|
|
59
|
+
'OreganoItalic-Regular.woff2': new URL('../assets/OreganoItalic-Regular.woff2', import.meta.url).href,
|
|
60
|
+
'PTSans-Bold.woff2': new URL('../assets/PTSans-Bold.woff2', import.meta.url).href,
|
|
61
|
+
'PTSans-BoldItalic.woff2': new URL('../assets/PTSans-BoldItalic.woff2', import.meta.url).href,
|
|
62
|
+
'PTSans-Italic.woff2': new URL('../assets/PTSans-Italic.woff2', import.meta.url).href,
|
|
63
|
+
'PTSans-Regular.woff2': new URL('../assets/PTSans-Regular.woff2', import.meta.url).href,
|
|
64
|
+
'PTSansNarrow-Bold.woff2': new URL('../assets/PTSansNarrow-Bold.woff2', import.meta.url).href,
|
|
65
|
+
'PTSansNarrow-Regular.woff2': new URL('../assets/PTSansNarrow-Regular.woff2', import.meta.url).href,
|
|
66
|
+
'Selawik-Bold.woff2': new URL('../assets/Selawik-Bold.woff2', import.meta.url).href,
|
|
67
|
+
'Selawik-Regular.woff2': new URL('../assets/Selawik-Regular.woff2', import.meta.url).href,
|
|
68
|
+
'TeXGyreBonum-Bold.woff2': new URL('../assets/TeXGyreBonum-Bold.woff2', import.meta.url).href,
|
|
69
|
+
'TeXGyreBonum-BoldItalic.woff2': new URL('../assets/TeXGyreBonum-BoldItalic.woff2', import.meta.url).href,
|
|
70
|
+
'TeXGyreBonum-Italic.woff2': new URL('../assets/TeXGyreBonum-Italic.woff2', import.meta.url).href,
|
|
71
|
+
'TeXGyreBonum-Regular.woff2': new URL('../assets/TeXGyreBonum-Regular.woff2', import.meta.url).href,
|
|
72
|
+
'URWGothic-Book.woff2': new URL('../assets/URWGothic-Book.woff2', import.meta.url).href,
|
|
73
|
+
'URWGothic-BookOblique.woff2': new URL('../assets/URWGothic-BookOblique.woff2', import.meta.url).href,
|
|
74
|
+
'URWGothic-Demi.woff2': new URL('../assets/URWGothic-Demi.woff2', import.meta.url).href,
|
|
75
|
+
'URWGothic-DemiOblique.woff2': new URL('../assets/URWGothic-DemiOblique.woff2', import.meta.url).href,
|
|
76
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by scripts/generate-bundled-families.ts - do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* The Word family names a document can curate via createSuperDocFonts({ include / exclude }), mirrored
|
|
5
|
+
* from @superdoc/font-system's runtime curation set (every logical family the resolver substitutes to a
|
|
6
|
+
* bundled face, including category fallbacks like Verdana). A name outside this set is a typo or an
|
|
7
|
+
* unbundled font, so createSuperDocFonts rejects it instead of silently dropping the intended fonts.
|
|
8
|
+
*/
|
|
9
|
+
export const BUNDLED_FAMILY_NAMES: readonly string[] = Object.freeze([
|
|
10
|
+
'Arial',
|
|
11
|
+
'Arial Black',
|
|
12
|
+
'Arial MT',
|
|
13
|
+
'Arial Narrow',
|
|
14
|
+
'Baskerville Old Face',
|
|
15
|
+
'Bookman Old Style',
|
|
16
|
+
'Brush Script MT',
|
|
17
|
+
'Calibri',
|
|
18
|
+
'Calibri Light',
|
|
19
|
+
'Cambria',
|
|
20
|
+
'Century',
|
|
21
|
+
'Century Gothic',
|
|
22
|
+
'Century Schoolbook',
|
|
23
|
+
'Comic Sans MS',
|
|
24
|
+
'Consolas',
|
|
25
|
+
'Cooper Black',
|
|
26
|
+
'Courier',
|
|
27
|
+
'Courier New',
|
|
28
|
+
'Garamond',
|
|
29
|
+
'Georgia',
|
|
30
|
+
'Gill Sans MT Condensed',
|
|
31
|
+
'Helvetica',
|
|
32
|
+
'ITC Bookman',
|
|
33
|
+
'Lucida Console',
|
|
34
|
+
'Segoe UI',
|
|
35
|
+
'Tahoma',
|
|
36
|
+
'Times',
|
|
37
|
+
'Times New Roman',
|
|
38
|
+
'Trebuchet MS',
|
|
39
|
+
'Verdana',
|
|
40
|
+
]);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { BUNDLED_FONT_ASSET_URLS, createSuperDocFonts, resolveBundledFontAssetUrl, superdocFonts } from './index';
|
|
3
|
+
|
|
4
|
+
describe('superdocFonts / resolveBundledFontAssetUrl', () => {
|
|
5
|
+
it('superdocFonts is a ready-made resolveAssetUrl config', () => {
|
|
6
|
+
expect(typeof superdocFonts.resolveAssetUrl).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('resolves a known bundled face to its emitted URL', () => {
|
|
10
|
+
const file = Object.keys(BUNDLED_FONT_ASSET_URLS)[0];
|
|
11
|
+
expect(file).toBeTruthy();
|
|
12
|
+
expect(superdocFonts.resolveAssetUrl({ file })).toBe(BUNDLED_FONT_ASSET_URLS[file]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('throws on an unknown file so a version mismatch surfaces immediately', () => {
|
|
16
|
+
expect(() => resolveBundledFontAssetUrl({ file: 'NotAFont.woff2' })).toThrow(/no bundled asset/);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('createSuperDocFonts', () => {
|
|
21
|
+
it('with no options is the full pack: resolver only, no curation', () => {
|
|
22
|
+
const config = createSuperDocFonts();
|
|
23
|
+
expect(typeof config.resolveAssetUrl).toBe('function');
|
|
24
|
+
expect(config.bundled).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('include sets an allow-list', () => {
|
|
28
|
+
expect(createSuperDocFonts({ include: ['Calibri', 'Cambria'] }).bundled).toEqual({
|
|
29
|
+
include: ['Calibri', 'Cambria'],
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('exclude sets a block-list', () => {
|
|
34
|
+
expect(createSuperDocFonts({ exclude: ['Cooper Black'] }).bundled).toEqual({ exclude: ['Cooper Black'] });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('trims entries and drops empty lists', () => {
|
|
38
|
+
expect(createSuperDocFonts({ include: [' Calibri '] }).bundled).toEqual({ include: ['Calibri'] });
|
|
39
|
+
expect(createSuperDocFonts({ include: [] }).bundled).toBeUndefined();
|
|
40
|
+
expect(createSuperDocFonts({ exclude: [] }).bundled).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('throws when include and exclude are both given (mutually exclusive intents)', () => {
|
|
44
|
+
expect(() => createSuperDocFonts({ include: ['Calibri'], exclude: ['Cambria'] })).toThrow(/not both/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws on a non-array list or non-string entries', () => {
|
|
48
|
+
expect(() => createSuperDocFonts({ include: 'Calibri' as unknown as string[] })).toThrow(/must be an array/);
|
|
49
|
+
expect(() => createSuperDocFonts({ exclude: ['Calibri', 42 as unknown as string] })).toThrow(
|
|
50
|
+
/non-empty font name strings/,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('throws on a font name SuperDoc does not bundle, suggesting the closest match for a typo', () => {
|
|
55
|
+
expect(() => createSuperDocFonts({ include: ['Calbri'] })).toThrow(/does not bundle/);
|
|
56
|
+
expect(() => createSuperDocFonts({ include: ['Calbri'] })).toThrow(/did you mean "Calibri"\?/);
|
|
57
|
+
// A real Word font with no bundled clone is also rejected (curating it would be a no-op).
|
|
58
|
+
expect(() => createSuperDocFonts({ exclude: ['Aptos'] })).toThrow(/does not bundle/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('accepts every family the toolbar advertises, including category fallbacks like Verdana', () => {
|
|
62
|
+
// Regression: the curatable list must mirror the resolver/offerings, not the manifest's narrower
|
|
63
|
+
// metric-clone `replaces` (which omits Verdana). A font shown in the toolbar must be curatable.
|
|
64
|
+
expect(() => createSuperDocFonts({ exclude: ['Verdana'] })).not.toThrow();
|
|
65
|
+
expect(() => createSuperDocFonts({ include: ['Verdana', 'Calibri'] })).not.toThrow();
|
|
66
|
+
expect(createSuperDocFonts({ exclude: ['Verdana'] }).bundled).toEqual({ exclude: ['Verdana'] });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('accepts case- and quote-variant spellings of bundled families (matched normalized)', () => {
|
|
70
|
+
expect(() => createSuperDocFonts({ include: [' calibri ', '"Cambria"'] })).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@superdoc-dev/fonts` - the reviewed metric-compatible font substitutes SuperDoc renders for
|
|
3
|
+
* proprietary Word fonts (Carlito for Calibri, Liberation Serif for Times New Roman, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Optional: install it to make the bundled fallbacks load automatically in any bundler app, with
|
|
6
|
+
* no copy step and no `assetBaseUrl`. The asset URLs are written as
|
|
7
|
+
* `new URL('../assets/<file>', import.meta.url)`, which Vite, Webpack 5, Next, Nuxt, esbuild, and
|
|
8
|
+
* Parcel detect, emit, and rewrite to the final hashed path.
|
|
9
|
+
*
|
|
10
|
+
* @beta This package is in preview; its surface may change before 1.0.
|
|
11
|
+
*/
|
|
12
|
+
import { BUNDLED_FONT_ASSET_URLS } from './asset-urls';
|
|
13
|
+
import { BUNDLED_FAMILY_NAMES } from './bundled-families';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The full filename -> bundler-emitted URL map for every bundled face. Most consumers use
|
|
17
|
+
* {@link superdocFonts} or {@link resolveBundledFontAssetUrl} instead of reading this directly.
|
|
18
|
+
*
|
|
19
|
+
* @beta
|
|
20
|
+
*/
|
|
21
|
+
export { BUNDLED_FONT_ASSET_URLS };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal structural shape of SuperDoc's bundled-font asset context. Kept LOCAL so this
|
|
25
|
+
* package has no dependency on `@superdoc/font-system`; only `file` is read. Assignable to
|
|
26
|
+
* SuperDoc's `FontAssetUrlResolver` by structural typing.
|
|
27
|
+
*
|
|
28
|
+
* @beta
|
|
29
|
+
*/
|
|
30
|
+
export interface BundledFontAssetContext {
|
|
31
|
+
/** Asset filename, e.g. `Carlito-Regular.woff2`. */
|
|
32
|
+
file: string;
|
|
33
|
+
family?: string;
|
|
34
|
+
weight?: string;
|
|
35
|
+
style?: 'normal' | 'italic';
|
|
36
|
+
source?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a bundled substitute face filename to a bundler-emitted URL.
|
|
41
|
+
*
|
|
42
|
+
* Pass as SuperDoc's `fonts.resolveAssetUrl` so the reviewed fallback pack loads from THIS
|
|
43
|
+
* package's emitted assets, with no manual copy step and no `assetBaseUrl`:
|
|
44
|
+
*
|
|
45
|
+
* import { resolveBundledFontAssetUrl } from '@superdoc-dev/fonts';
|
|
46
|
+
* new SuperDoc({ selector: '#editor', document, fonts: { resolveAssetUrl: resolveBundledFontAssetUrl } });
|
|
47
|
+
*
|
|
48
|
+
* Throws on an unknown file, which signals a version mismatch between `@superdoc-dev/fonts` and
|
|
49
|
+
* the `superdoc` core manifest rather than silently degrading to the logical font name.
|
|
50
|
+
*
|
|
51
|
+
* @beta
|
|
52
|
+
*/
|
|
53
|
+
export function resolveBundledFontAssetUrl(context: BundledFontAssetContext): string {
|
|
54
|
+
const url = BUNDLED_FONT_ASSET_URLS[context.file];
|
|
55
|
+
if (!url) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`[@superdoc-dev/fonts] no bundled asset for "${context.file}". This pack ships ` +
|
|
58
|
+
`${Object.keys(BUNDLED_FONT_ASSET_URLS).length} faces; the file is unknown, so ` +
|
|
59
|
+
`@superdoc-dev/fonts and superdoc are likely version-mismatched. Align their versions.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return url;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Drop-in value for SuperDoc's `fonts` config: `new SuperDoc({ fonts: superdocFonts })`.
|
|
67
|
+
* Equivalent to `{ resolveAssetUrl: resolveBundledFontAssetUrl }`.
|
|
68
|
+
*
|
|
69
|
+
* @beta
|
|
70
|
+
*/
|
|
71
|
+
export const superdocFonts: { resolveAssetUrl: (context: BundledFontAssetContext) => string } = {
|
|
72
|
+
resolveAssetUrl: resolveBundledFontAssetUrl,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Choose which bundled families SuperDoc advertises and renders, by LOGICAL Word family name
|
|
77
|
+
* (`"Calibri"`, never the physical `"Carlito"`).
|
|
78
|
+
*
|
|
79
|
+
* @beta
|
|
80
|
+
*/
|
|
81
|
+
export interface SuperDocFontsOptions {
|
|
82
|
+
/** Allow-list: ONLY these logical families are active, e.g. `['Calibri', 'Cambria']`. */
|
|
83
|
+
include?: string[];
|
|
84
|
+
/** Block-list: every bundled family EXCEPT these, e.g. `['Cooper Black', 'Brush Script MT']`. */
|
|
85
|
+
exclude?: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The `fonts` config {@link createSuperDocFonts} returns: the bundled-asset resolver plus the
|
|
90
|
+
* curation. Structurally assignable to SuperDoc's `fonts` option.
|
|
91
|
+
*
|
|
92
|
+
* @beta
|
|
93
|
+
*/
|
|
94
|
+
export interface SuperDocFontsConfig {
|
|
95
|
+
resolveAssetUrl: (context: BundledFontAssetContext) => string;
|
|
96
|
+
bundled?: { include?: string[]; exclude?: string[] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build a curated `fonts` config: the bundled pack, narrowed to the families you choose.
|
|
101
|
+
*
|
|
102
|
+
* import { createSuperDocFonts } from '@superdoc-dev/fonts';
|
|
103
|
+
* new SuperDoc({
|
|
104
|
+
* selector: '#editor',
|
|
105
|
+
* document,
|
|
106
|
+
* fonts: createSuperDocFonts({ exclude: ['Cooper Black', 'Brush Script MT'] }),
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* Names are Word logical families. Pass neither `include` nor `exclude` for the full pack - that is
|
|
110
|
+
* exactly {@link superdocFonts}. Curation governs the BUNDLED pack only; your own licensed fonts stay
|
|
111
|
+
* separate (`fonts.families` / `fonts.map`).
|
|
112
|
+
*
|
|
113
|
+
* @beta
|
|
114
|
+
*/
|
|
115
|
+
export function createSuperDocFonts(options: SuperDocFontsOptions = {}): SuperDocFontsConfig {
|
|
116
|
+
const include = normalizeNameList(options.include, 'include');
|
|
117
|
+
const exclude = normalizeNameList(options.exclude, 'exclude');
|
|
118
|
+
// include and exclude are mutually exclusive intents; accepting both silently would hide which one
|
|
119
|
+
// wins. Reject at the API boundary (this runs in the consumer's setup code, so it fails fast and
|
|
120
|
+
// clearly) rather than picking one arbitrarily.
|
|
121
|
+
if (include && exclude) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'[@superdoc-dev/fonts] createSuperDocFonts: pass `include` OR `exclude`, not both. ' +
|
|
124
|
+
'`include` is an allow-list (only those families resolve and appear in the toolbar); ' +
|
|
125
|
+
'`exclude` keeps everything but the named families.',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
// Reject names SuperDoc does not bundle. This runs in the consumer's setup code, so it fails fast
|
|
129
|
+
// and clearly - especially for `include`, where a typo would otherwise silently hide the fonts the
|
|
130
|
+
// consumer meant to keep. (Raw `fonts.bundled` set directly stays lenient and only warns.)
|
|
131
|
+
if (include) assertKnownFamilies(include, 'include');
|
|
132
|
+
if (exclude) assertKnownFamilies(exclude, 'exclude');
|
|
133
|
+
const config: SuperDocFontsConfig = { resolveAssetUrl: resolveBundledFontAssetUrl };
|
|
134
|
+
if (include) config.bundled = { include };
|
|
135
|
+
else if (exclude) config.bundled = { exclude };
|
|
136
|
+
return config;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Validate and clean a curation list: an array of non-empty strings, or omitted. Throws on misuse. */
|
|
140
|
+
function normalizeNameList(value: string[] | undefined, field: 'include' | 'exclude'): string[] | undefined {
|
|
141
|
+
if (value == null) return undefined;
|
|
142
|
+
if (!Array.isArray(value)) {
|
|
143
|
+
throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` must be an array of Word font names.`);
|
|
144
|
+
}
|
|
145
|
+
const names = value.map((name) => (typeof name === 'string' ? name.trim() : '')).filter(Boolean);
|
|
146
|
+
if (names.length !== value.length) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` must contain only non-empty font name strings ` +
|
|
149
|
+
`(e.g. ["Calibri", "Cambria"]).`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return names.length ? names : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Normalize a family name for matching: trim, strip surrounding quotes, lowercase. */
|
|
156
|
+
function normalizeKey(name: string): string {
|
|
157
|
+
return name
|
|
158
|
+
.trim()
|
|
159
|
+
.replace(/^["']|["']$/g, '')
|
|
160
|
+
.toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const KNOWN_FAMILY_KEYS: ReadonlySet<string> = new Set(BUNDLED_FAMILY_NAMES.map(normalizeKey));
|
|
164
|
+
|
|
165
|
+
/** Bounded Levenshtein distance between two short strings, for a "did you mean" hint on a typo. */
|
|
166
|
+
function editDistance(a: string, b: string): number {
|
|
167
|
+
const m = a.length;
|
|
168
|
+
const n = b.length;
|
|
169
|
+
if (Math.abs(m - n) > 2) return 3;
|
|
170
|
+
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
171
|
+
for (let i = 1; i <= m; i += 1) {
|
|
172
|
+
const curr = [i];
|
|
173
|
+
for (let j = 1; j <= n; j += 1) {
|
|
174
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
175
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
176
|
+
}
|
|
177
|
+
prev = curr;
|
|
178
|
+
}
|
|
179
|
+
return prev[n];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The closest bundled family within edit distance 2, or null - so a typo gets a concrete suggestion. */
|
|
183
|
+
function closestKnownFamily(name: string): string | null {
|
|
184
|
+
const key = normalizeKey(name);
|
|
185
|
+
let best: string | null = null;
|
|
186
|
+
let bestDist = 3;
|
|
187
|
+
for (const family of BUNDLED_FAMILY_NAMES) {
|
|
188
|
+
const dist = editDistance(key, normalizeKey(family));
|
|
189
|
+
if (dist < bestDist) {
|
|
190
|
+
bestDist = dist;
|
|
191
|
+
best = family;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return best;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Throw on any curation name SuperDoc does not bundle, with a suggestion and the full valid list. */
|
|
198
|
+
function assertKnownFamilies(names: readonly string[], field: 'include' | 'exclude'): void {
|
|
199
|
+
const unknown = names.filter((name) => !KNOWN_FAMILY_KEYS.has(normalizeKey(name)));
|
|
200
|
+
if (unknown.length === 0) return;
|
|
201
|
+
const hints = unknown.map((name) => {
|
|
202
|
+
const suggestion = closestKnownFamily(name);
|
|
203
|
+
return suggestion ? `"${name}" (did you mean "${suggestion}"?)` : `"${name}"`;
|
|
204
|
+
});
|
|
205
|
+
throw new Error(
|
|
206
|
+
`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` names a font SuperDoc does not bundle: ${hints.join(', ')}. ` +
|
|
207
|
+
`Curate by Word family name. Bundled families: ${BUNDLED_FAMILY_NAMES.join(', ')}.`,
|
|
208
|
+
);
|
|
209
|
+
}
|