@superdoc-dev/fonts 0.1.0 → 0.1.1-next.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 +21 -0
- package/dist/bundled-families.d.ts +7 -1
- package/dist/bundled-files.d.ts +8 -0
- package/dist/bundled-files.js +74 -0
- package/dist/cdn-entry.d.ts +27 -0
- package/dist/cdn-entry.js +67 -0
- package/dist/curation.d.ts +65 -0
- package/dist/curation.js +102 -0
- package/dist/index.d.ts +11 -40
- package/dist/index.js +13 -90
- package/dist/superdoc-fonts.min.js +3 -0
- package/dist/superdoc-fonts.min.js.map +1 -0
- package/package.json +9 -8
- package/src/bundled-families.ts +9 -2
- package/src/bundled-files.ts +74 -0
- package/src/cdn-entry.test.ts +48 -0
- package/src/cdn-entry.ts +84 -0
- package/src/curation.ts +157 -0
- package/src/index.test.ts +16 -1
- package/src/index.ts +18 -131
package/README.md
CHANGED
|
@@ -40,6 +40,27 @@ That is the whole setup. No copying files into `public/`, no `assetBaseUrl`. The
|
|
|
40
40
|
written as `new URL('../assets/<file>', import.meta.url)`, which Vite, Webpack 5, Next, Nuxt,
|
|
41
41
|
esbuild, and Parcel all detect, emit, and rewrite to the final hashed path.
|
|
42
42
|
|
|
43
|
+
### CDN / `<script>` tag (no bundler)
|
|
44
|
+
|
|
45
|
+
For a plain HTML page, load the browser build and use the `SuperDocFonts` global. It exposes the
|
|
46
|
+
same API, and the faces resolve relative to the script, so there is no bundler and no path config:
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<script src="https://cdn.jsdelivr.net/npm/superdoc/dist/superdoc.min.js"></script>
|
|
50
|
+
<script src="https://cdn.jsdelivr.net/npm/@superdoc-dev/fonts/dist/superdoc-fonts.min.js"></script>
|
|
51
|
+
<script>
|
|
52
|
+
new SuperDoc({
|
|
53
|
+
selector: '#editor',
|
|
54
|
+
document: 'contract.docx',
|
|
55
|
+
fonts: SuperDocFonts.superdocFonts, // or SuperDocFonts.createSuperDocFonts({ exclude: [...] })
|
|
56
|
+
});
|
|
57
|
+
</script>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The `superdoc` CDN build ships no fonts and shows the baseline until you add this script. Self-hosting
|
|
61
|
+
works too: keep the package's `dist/` and `assets/` in their relative layout, since the faces resolve
|
|
62
|
+
to `../assets/` next to `superdoc-fonts.min.js`.
|
|
63
|
+
|
|
43
64
|
### Choosing which fonts
|
|
44
65
|
|
|
45
66
|
`superdocFonts` enables every reviewed family. To narrow the set, use `createSuperDocFonts` and name
|
|
@@ -6,4 +6,10 @@
|
|
|
6
6
|
* bundled face, including category fallbacks like Verdana). A name outside this set is a typo or an
|
|
7
7
|
* unbundled font, so createSuperDocFonts rejects it instead of silently dropping the intended fonts.
|
|
8
8
|
*/
|
|
9
|
-
export declare const BUNDLED_FAMILY_NAMES: readonly
|
|
9
|
+
export declare const BUNDLED_FAMILY_NAMES: readonly ["Arial", "Arial Black", "Arial MT", "Arial Narrow", "Baskerville Old Face", "Bookman Old Style", "Brush Script MT", "Calibri", "Calibri Light", "Cambria", "Century", "Century Gothic", "Century Schoolbook", "Comic Sans MS", "Consolas", "Cooper Black", "Courier", "Courier New", "Garamond", "Georgia", "Gill Sans MT Condensed", "Helvetica", "ITC Bookman", "Lucida Console", "Segoe UI", "Tahoma", "Times", "Times New Roman", "Trebuchet MS", "Verdana"];
|
|
10
|
+
/**
|
|
11
|
+
* Union of the Word family names the bundled pack can curate, for autocomplete on
|
|
12
|
+
* createSuperDocFonts({ include / exclude }). Those options also accept any string (case- and
|
|
13
|
+
* quote-insensitive, validated at runtime), so a list built from dynamic config still type-checks.
|
|
14
|
+
*/
|
|
15
|
+
export type BundledFontFamilyName = (typeof BUNDLED_FAMILY_NAMES)[number];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by scripts/generate-asset-urls.mjs - do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* The flat list of bundled .woff2 filenames, with NO `import.meta.url` (unlike asset-urls.ts).
|
|
5
|
+
* The browser/IIFE entry (cdn-entry.ts) imports this to validate face files and to resolve them
|
|
6
|
+
* relative to its own <script>, since import.meta.url is not available in an IIFE bundle.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BUNDLED_FONT_FILES: readonly string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by scripts/generate-asset-urls.mjs - do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* The flat list of bundled .woff2 filenames, with NO `import.meta.url` (unlike asset-urls.ts).
|
|
5
|
+
* The browser/IIFE entry (cdn-entry.ts) imports this to validate face files and to resolve them
|
|
6
|
+
* relative to its own <script>, since import.meta.url is not available in an IIFE bundle.
|
|
7
|
+
*/
|
|
8
|
+
export const BUNDLED_FONT_FILES = Object.freeze([
|
|
9
|
+
'ArchivoBlack-Regular.woff2',
|
|
10
|
+
'BacasimeAntique-Regular.woff2',
|
|
11
|
+
'C059-BdIta.woff2',
|
|
12
|
+
'C059-Bold.woff2',
|
|
13
|
+
'C059-Italic.woff2',
|
|
14
|
+
'C059-Roman.woff2',
|
|
15
|
+
'Caladea-Bold.woff2',
|
|
16
|
+
'Caladea-BoldItalic.woff2',
|
|
17
|
+
'Caladea-Italic.woff2',
|
|
18
|
+
'Caladea-Regular.woff2',
|
|
19
|
+
'Caprasimo-Regular.woff2',
|
|
20
|
+
'Cardo-Bold.woff2',
|
|
21
|
+
'Cardo-Italic.woff2',
|
|
22
|
+
'Cardo-Regular.woff2',
|
|
23
|
+
'Carlito-Bold.woff2',
|
|
24
|
+
'Carlito-BoldItalic.woff2',
|
|
25
|
+
'Carlito-Italic.woff2',
|
|
26
|
+
'Carlito-Regular.woff2',
|
|
27
|
+
'ComicRelief-Bold.woff2',
|
|
28
|
+
'ComicRelief-Regular.woff2',
|
|
29
|
+
'Gelasio-Bold.woff2',
|
|
30
|
+
'Gelasio-BoldItalic.woff2',
|
|
31
|
+
'Gelasio-Italic.woff2',
|
|
32
|
+
'Gelasio-Regular.woff2',
|
|
33
|
+
'InconsolataSemiExpanded-Bold.woff2',
|
|
34
|
+
'InconsolataSemiExpanded-Regular.woff2',
|
|
35
|
+
'LiberationMono-Bold.woff2',
|
|
36
|
+
'LiberationMono-BoldItalic.woff2',
|
|
37
|
+
'LiberationMono-Italic.woff2',
|
|
38
|
+
'LiberationMono-Regular.woff2',
|
|
39
|
+
'LiberationSans-Bold.woff2',
|
|
40
|
+
'LiberationSans-BoldItalic.woff2',
|
|
41
|
+
'LiberationSans-Italic.woff2',
|
|
42
|
+
'LiberationSans-Regular.woff2',
|
|
43
|
+
'LiberationSansNarrow-Bold.woff2',
|
|
44
|
+
'LiberationSansNarrow-BoldItalic.woff2',
|
|
45
|
+
'LiberationSansNarrow-Italic.woff2',
|
|
46
|
+
'LiberationSansNarrow-Regular.woff2',
|
|
47
|
+
'LiberationSerif-Bold.woff2',
|
|
48
|
+
'LiberationSerif-BoldItalic.woff2',
|
|
49
|
+
'LiberationSerif-Italic.woff2',
|
|
50
|
+
'LiberationSerif-Regular.woff2',
|
|
51
|
+
'NotoSans-Bold.woff2',
|
|
52
|
+
'NotoSans-BoldItalic.woff2',
|
|
53
|
+
'NotoSans-Italic.woff2',
|
|
54
|
+
'NotoSans-Regular.woff2',
|
|
55
|
+
'NotoSansMono-Bold.woff2',
|
|
56
|
+
'NotoSansMono-Regular.woff2',
|
|
57
|
+
'OreganoItalic-Regular.woff2',
|
|
58
|
+
'PTSans-Bold.woff2',
|
|
59
|
+
'PTSans-BoldItalic.woff2',
|
|
60
|
+
'PTSans-Italic.woff2',
|
|
61
|
+
'PTSans-Regular.woff2',
|
|
62
|
+
'PTSansNarrow-Bold.woff2',
|
|
63
|
+
'PTSansNarrow-Regular.woff2',
|
|
64
|
+
'Selawik-Bold.woff2',
|
|
65
|
+
'Selawik-Regular.woff2',
|
|
66
|
+
'TeXGyreBonum-Bold.woff2',
|
|
67
|
+
'TeXGyreBonum-BoldItalic.woff2',
|
|
68
|
+
'TeXGyreBonum-Italic.woff2',
|
|
69
|
+
'TeXGyreBonum-Regular.woff2',
|
|
70
|
+
'URWGothic-Book.woff2',
|
|
71
|
+
'URWGothic-BookOblique.woff2',
|
|
72
|
+
'URWGothic-Demi.woff2',
|
|
73
|
+
'URWGothic-DemiOblique.woff2',
|
|
74
|
+
]);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type BundledFontAssetContext, type SuperDocFontsConfig, type SuperDocFontsOptions } from './curation.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a bundled face filename to a URL relative to THIS script. Throws on an unknown file
|
|
4
|
+
* (a version mismatch between `@superdoc-dev/fonts` and `superdoc`) and on a missing script URL,
|
|
5
|
+
* matching the bundler entry's fail-loud-not-silent contract.
|
|
6
|
+
*
|
|
7
|
+
* @beta
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveBundledFontAssetUrl(context: BundledFontAssetContext): string;
|
|
10
|
+
/**
|
|
11
|
+
* Drop-in value for SuperDoc's `fonts` config: `new SuperDoc({ fonts: SuperDocFonts.superdocFonts })`.
|
|
12
|
+
*
|
|
13
|
+
* @beta
|
|
14
|
+
*/
|
|
15
|
+
export declare const superdocFonts: SuperDocFontsConfig;
|
|
16
|
+
/**
|
|
17
|
+
* Build a curated `fonts` config: the bundled pack narrowed to the families you choose. Same API
|
|
18
|
+
* and validation as the bundler entry's `createSuperDocFonts`; only the resolver differs.
|
|
19
|
+
*
|
|
20
|
+
* new SuperDoc({ fonts: SuperDocFonts.createSuperDocFonts({ exclude: ['Cooper Black'] }) });
|
|
21
|
+
*
|
|
22
|
+
* @beta
|
|
23
|
+
*/
|
|
24
|
+
export declare function createSuperDocFonts(options?: SuperDocFontsOptions): SuperDocFontsConfig;
|
|
25
|
+
export type { BundledFontAssetContext, SuperDocFontsConfig, SuperDocFontsOptions };
|
|
26
|
+
export { BUNDLED_FAMILY_NAMES } from './bundled-families.js';
|
|
27
|
+
export type { BundledFontFamilyName } from './bundled-families.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser / `<script>` (IIFE) entry for `@superdoc-dev/fonts`, built to
|
|
3
|
+
* `dist/superdoc-fonts.min.js` and exposed as the `SuperDocFonts` global.
|
|
4
|
+
*
|
|
5
|
+
* Unlike the bundler entry ({@link "./index"}), it resolves the `.woff2` relative to its OWN
|
|
6
|
+
* `<script>` (`document.currentScript`) instead of `import.meta.url`, so a plain CDN / self-host
|
|
7
|
+
* `<script>` tag works with no bundler:
|
|
8
|
+
*
|
|
9
|
+
* <script src="https://cdn.jsdelivr.net/npm/superdoc/dist/superdoc.min.js"></script>
|
|
10
|
+
* <script src="https://cdn.jsdelivr.net/npm/@superdoc-dev/fonts/dist/superdoc-fonts.min.js"></script>
|
|
11
|
+
* <script>
|
|
12
|
+
* new SuperDoc({ selector: '#editor', document: 'contract.docx', fonts: SuperDocFonts.superdocFonts });
|
|
13
|
+
* </script>
|
|
14
|
+
*
|
|
15
|
+
* The faces ship in the package's `assets/` dir and this bundle in `dist/`, so a face resolves to
|
|
16
|
+
* `../assets/<file>` relative to the script. On a public CDN (jsDelivr / unpkg) the whole package
|
|
17
|
+
* is served, so that resolves with no setup; self-hosting requires the `dist/` and `assets/` dirs
|
|
18
|
+
* to keep their relative layout.
|
|
19
|
+
*
|
|
20
|
+
* @beta
|
|
21
|
+
*/
|
|
22
|
+
import { BUNDLED_FONT_FILES } from './bundled-files.js';
|
|
23
|
+
import { resolveCuration, } from './curation.js';
|
|
24
|
+
// Capture the executing <script> src at load. `document.currentScript` is valid while this
|
|
25
|
+
// top-level IIFE runs and null later (e.g. in async callbacks), so it MUST be read here, at module
|
|
26
|
+
// eval, not inside the resolver. Duck-typed (no `instanceof HTMLScriptElement`) so the module also
|
|
27
|
+
// evaluates under a non-DOM test environment where that constructor is absent.
|
|
28
|
+
const currentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
29
|
+
const SCRIPT_SRC = currentScript?.src ?? '';
|
|
30
|
+
const KNOWN_FILES = new Set(BUNDLED_FONT_FILES);
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a bundled face filename to a URL relative to THIS script. Throws on an unknown file
|
|
33
|
+
* (a version mismatch between `@superdoc-dev/fonts` and `superdoc`) and on a missing script URL,
|
|
34
|
+
* matching the bundler entry's fail-loud-not-silent contract.
|
|
35
|
+
*
|
|
36
|
+
* @beta
|
|
37
|
+
*/
|
|
38
|
+
export function resolveBundledFontAssetUrl(context) {
|
|
39
|
+
if (!KNOWN_FILES.has(context.file)) {
|
|
40
|
+
throw new Error(`[@superdoc-dev/fonts] no bundled asset for "${context.file}". This pack ships ` +
|
|
41
|
+
`${BUNDLED_FONT_FILES.length} faces; the file is unknown, so @superdoc-dev/fonts and ` +
|
|
42
|
+
`superdoc are likely version-mismatched. Align their versions.`);
|
|
43
|
+
}
|
|
44
|
+
if (!SCRIPT_SRC) {
|
|
45
|
+
throw new Error('[@superdoc-dev/fonts] could not determine the script URL to resolve bundled fonts from. ' +
|
|
46
|
+
'Load superdoc-fonts.min.js with a normal <script src="..."> tag.');
|
|
47
|
+
}
|
|
48
|
+
return new URL(`../assets/${context.file}`, SCRIPT_SRC).href;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Drop-in value for SuperDoc's `fonts` config: `new SuperDoc({ fonts: SuperDocFonts.superdocFonts })`.
|
|
52
|
+
*
|
|
53
|
+
* @beta
|
|
54
|
+
*/
|
|
55
|
+
export const superdocFonts = { resolveAssetUrl: resolveBundledFontAssetUrl };
|
|
56
|
+
/**
|
|
57
|
+
* Build a curated `fonts` config: the bundled pack narrowed to the families you choose. Same API
|
|
58
|
+
* and validation as the bundler entry's `createSuperDocFonts`; only the resolver differs.
|
|
59
|
+
*
|
|
60
|
+
* new SuperDoc({ fonts: SuperDocFonts.createSuperDocFonts({ exclude: ['Cooper Black'] }) });
|
|
61
|
+
*
|
|
62
|
+
* @beta
|
|
63
|
+
*/
|
|
64
|
+
export function createSuperDocFonts(options = {}) {
|
|
65
|
+
return { resolveAssetUrl: resolveBundledFontAssetUrl, ...resolveCuration(options) };
|
|
66
|
+
}
|
|
67
|
+
export { BUNDLED_FAMILY_NAMES } from './bundled-families.js';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver-independent curation + validation shared by the two entries: the bundler entry
|
|
3
|
+
* (`index.ts`, asset URLs via `import.meta.url`) and the browser/IIFE entry (`cdn-entry.ts`, URLs
|
|
4
|
+
* relative to its own `<script>`). Each entry supplies only its own asset-URL resolver; the
|
|
5
|
+
* `include` / `exclude` validation lives here once so the two entries can never drift.
|
|
6
|
+
*
|
|
7
|
+
* @beta
|
|
8
|
+
*/
|
|
9
|
+
import { type BundledFontFamilyName } from './bundled-families.js';
|
|
10
|
+
/**
|
|
11
|
+
* Minimal structural shape of SuperDoc's bundled-font asset context. Kept LOCAL so this package
|
|
12
|
+
* has no dependency on `@superdoc/font-system`; only `file` is read. Assignable to SuperDoc's
|
|
13
|
+
* `FontAssetUrlResolver` by structural typing.
|
|
14
|
+
*
|
|
15
|
+
* @beta
|
|
16
|
+
*/
|
|
17
|
+
export interface BundledFontAssetContext {
|
|
18
|
+
/** Asset filename, e.g. `Carlito-Regular.woff2`. */
|
|
19
|
+
file: string;
|
|
20
|
+
family?: string;
|
|
21
|
+
weight?: string;
|
|
22
|
+
style?: 'normal' | 'italic';
|
|
23
|
+
source?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Per-face asset-URL resolver: maps a bundled face filename to a URL the browser can fetch. */
|
|
26
|
+
export type BundledFontAssetResolver = (context: BundledFontAssetContext) => string;
|
|
27
|
+
/**
|
|
28
|
+
* Choose which bundled families SuperDoc advertises and renders, by LOGICAL Word family name
|
|
29
|
+
* (`"Calibri"`, never the physical `"Carlito"`).
|
|
30
|
+
*
|
|
31
|
+
* @beta
|
|
32
|
+
*/
|
|
33
|
+
export interface SuperDocFontsOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Allow-list: ONLY these logical families are active, e.g. `['Calibri', 'Cambria']`. Autocompletes
|
|
36
|
+
* the bundled names, but also accepts any string (case- and quote-insensitive, validated at
|
|
37
|
+
* runtime), so a list built from dynamic config still type-checks.
|
|
38
|
+
*/
|
|
39
|
+
include?: readonly (BundledFontFamilyName | (string & {}))[];
|
|
40
|
+
/** Block-list: every bundled family EXCEPT these, e.g. `['Cooper Black', 'Brush Script MT']`. */
|
|
41
|
+
exclude?: readonly (BundledFontFamilyName | (string & {}))[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The `fonts` config the entries return: the bundled-asset resolver plus the curation.
|
|
45
|
+
* Structurally assignable to SuperDoc's `fonts` option.
|
|
46
|
+
*
|
|
47
|
+
* @beta
|
|
48
|
+
*/
|
|
49
|
+
export interface SuperDocFontsConfig {
|
|
50
|
+
resolveAssetUrl: BundledFontAssetResolver;
|
|
51
|
+
bundled?: {
|
|
52
|
+
include?: string[];
|
|
53
|
+
exclude?: string[];
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate the curation options and return the `{ bundled? }` slice (no resolver). Each entry
|
|
58
|
+
* spreads this into its own `{ resolveAssetUrl, ...resolveCuration(options) }`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveCuration(options?: SuperDocFontsOptions): {
|
|
61
|
+
bundled?: {
|
|
62
|
+
include?: string[];
|
|
63
|
+
exclude?: string[];
|
|
64
|
+
};
|
|
65
|
+
};
|
package/dist/curation.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver-independent curation + validation shared by the two entries: the bundler entry
|
|
3
|
+
* (`index.ts`, asset URLs via `import.meta.url`) and the browser/IIFE entry (`cdn-entry.ts`, URLs
|
|
4
|
+
* relative to its own `<script>`). Each entry supplies only its own asset-URL resolver; the
|
|
5
|
+
* `include` / `exclude` validation lives here once so the two entries can never drift.
|
|
6
|
+
*
|
|
7
|
+
* @beta
|
|
8
|
+
*/
|
|
9
|
+
import { BUNDLED_FAMILY_NAMES } from './bundled-families.js';
|
|
10
|
+
/**
|
|
11
|
+
* Validate the curation options and return the `{ bundled? }` slice (no resolver). Each entry
|
|
12
|
+
* spreads this into its own `{ resolveAssetUrl, ...resolveCuration(options) }`.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveCuration(options = {}) {
|
|
15
|
+
const include = normalizeNameList(options.include, 'include');
|
|
16
|
+
const exclude = normalizeNameList(options.exclude, 'exclude');
|
|
17
|
+
// include and exclude are mutually exclusive intents; accepting both silently would hide which one
|
|
18
|
+
// wins. Reject at the API boundary (this runs in the consumer's setup code, so it fails fast and
|
|
19
|
+
// clearly) rather than picking one arbitrarily.
|
|
20
|
+
if (include && exclude) {
|
|
21
|
+
throw new Error('[@superdoc-dev/fonts] createSuperDocFonts: pass `include` OR `exclude`, not both. ' +
|
|
22
|
+
'`include` is an allow-list (only those families resolve and appear in the toolbar); ' +
|
|
23
|
+
'`exclude` keeps everything but the named families.');
|
|
24
|
+
}
|
|
25
|
+
// Reject names SuperDoc does not bundle. This runs in the consumer's setup code, so it fails fast
|
|
26
|
+
// and clearly - especially for `include`, where a typo would otherwise silently hide the fonts the
|
|
27
|
+
// consumer meant to keep. (Raw `fonts.bundled` set directly stays lenient and only warns.)
|
|
28
|
+
if (include)
|
|
29
|
+
assertKnownFamilies(include, 'include');
|
|
30
|
+
if (exclude)
|
|
31
|
+
assertKnownFamilies(exclude, 'exclude');
|
|
32
|
+
if (include)
|
|
33
|
+
return { bundled: { include } };
|
|
34
|
+
if (exclude)
|
|
35
|
+
return { bundled: { exclude } };
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
/** Validate and clean a curation list: an array of non-empty strings, or omitted. Throws on misuse. */
|
|
39
|
+
function normalizeNameList(value, field) {
|
|
40
|
+
if (value == null)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (!Array.isArray(value)) {
|
|
43
|
+
throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` must be an array of Word font names.`);
|
|
44
|
+
}
|
|
45
|
+
const names = value.map((name) => (typeof name === 'string' ? name.trim() : '')).filter(Boolean);
|
|
46
|
+
if (names.length !== value.length) {
|
|
47
|
+
throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` must contain only non-empty font name strings ` +
|
|
48
|
+
`(e.g. ["Calibri", "Cambria"]).`);
|
|
49
|
+
}
|
|
50
|
+
return names.length ? names : undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Normalize a family name for matching: trim, strip surrounding quotes, lowercase. */
|
|
53
|
+
function normalizeKey(name) {
|
|
54
|
+
return name
|
|
55
|
+
.trim()
|
|
56
|
+
.replace(/^["']|["']$/g, '')
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
const KNOWN_FAMILY_KEYS = new Set(BUNDLED_FAMILY_NAMES.map(normalizeKey));
|
|
60
|
+
/** Bounded Levenshtein distance between two short strings, for a "did you mean" hint on a typo. */
|
|
61
|
+
function editDistance(a, b) {
|
|
62
|
+
const m = a.length;
|
|
63
|
+
const n = b.length;
|
|
64
|
+
if (Math.abs(m - n) > 2)
|
|
65
|
+
return 3;
|
|
66
|
+
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
67
|
+
for (let i = 1; i <= m; i += 1) {
|
|
68
|
+
const curr = [i];
|
|
69
|
+
for (let j = 1; j <= n; j += 1) {
|
|
70
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
71
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
72
|
+
}
|
|
73
|
+
prev = curr;
|
|
74
|
+
}
|
|
75
|
+
return prev[n];
|
|
76
|
+
}
|
|
77
|
+
/** The closest bundled family within edit distance 2, or null - so a typo gets a concrete suggestion. */
|
|
78
|
+
function closestKnownFamily(name) {
|
|
79
|
+
const key = normalizeKey(name);
|
|
80
|
+
let best = null;
|
|
81
|
+
let bestDist = 3;
|
|
82
|
+
for (const family of BUNDLED_FAMILY_NAMES) {
|
|
83
|
+
const dist = editDistance(key, normalizeKey(family));
|
|
84
|
+
if (dist < bestDist) {
|
|
85
|
+
bestDist = dist;
|
|
86
|
+
best = family;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return best;
|
|
90
|
+
}
|
|
91
|
+
/** Throw on any curation name SuperDoc does not bundle, with a suggestion and the full valid list. */
|
|
92
|
+
function assertKnownFamilies(names, field) {
|
|
93
|
+
const unknown = names.filter((name) => !KNOWN_FAMILY_KEYS.has(normalizeKey(name)));
|
|
94
|
+
if (unknown.length === 0)
|
|
95
|
+
return;
|
|
96
|
+
const hints = unknown.map((name) => {
|
|
97
|
+
const suggestion = closestKnownFamily(name);
|
|
98
|
+
return suggestion ? `"${name}" (did you mean "${suggestion}"?)` : `"${name}"`;
|
|
99
|
+
});
|
|
100
|
+
throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` names a font SuperDoc does not bundle: ${hints.join(', ')}. ` +
|
|
101
|
+
`Curate by Word family name. Bundled families: ${BUNDLED_FAMILY_NAMES.join(', ')}.`);
|
|
102
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
* `new URL('../assets/<file>', import.meta.url)`, which Vite, Webpack 5, Next, Nuxt, esbuild, and
|
|
8
8
|
* Parcel detect, emit, and rewrite to the final hashed path.
|
|
9
9
|
*
|
|
10
|
+
* For a plain `<script>` / CDN page (no bundler), load `dist/superdoc-fonts.min.js` instead and use
|
|
11
|
+
* the `SuperDocFonts` global - same API, faces resolved relative to the script. See `cdn-entry.ts`.
|
|
12
|
+
*
|
|
10
13
|
* @beta This package is in preview; its surface may change before 1.0.
|
|
11
14
|
*/
|
|
12
|
-
import { BUNDLED_FONT_ASSET_URLS } from './asset-urls';
|
|
15
|
+
import { BUNDLED_FONT_ASSET_URLS } from './asset-urls.js';
|
|
16
|
+
import { type BundledFontAssetContext, type SuperDocFontsConfig, type SuperDocFontsOptions } from './curation.js';
|
|
13
17
|
/**
|
|
14
18
|
* The full filename -> bundler-emitted URL map for every bundled face. Most consumers use
|
|
15
19
|
* {@link superdocFonts} or {@link resolveBundledFontAssetUrl} instead of reading this directly.
|
|
@@ -17,21 +21,15 @@ import { BUNDLED_FONT_ASSET_URLS } from './asset-urls';
|
|
|
17
21
|
* @beta
|
|
18
22
|
*/
|
|
19
23
|
export { BUNDLED_FONT_ASSET_URLS };
|
|
24
|
+
export type { BundledFontAssetContext, SuperDocFontsConfig, SuperDocFontsOptions };
|
|
20
25
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* SuperDoc's `FontAssetUrlResolver` by structural typing.
|
|
26
|
+
* The curatable Word family names (and their union type), for building `include` / `exclude` lists
|
|
27
|
+
* with autocomplete. Re-exported from the generated source.
|
|
24
28
|
*
|
|
25
29
|
* @beta
|
|
26
30
|
*/
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
file: string;
|
|
30
|
-
family?: string;
|
|
31
|
-
weight?: string;
|
|
32
|
-
style?: 'normal' | 'italic';
|
|
33
|
-
source?: string;
|
|
34
|
-
}
|
|
31
|
+
export { BUNDLED_FAMILY_NAMES } from './bundled-families.js';
|
|
32
|
+
export type { BundledFontFamilyName } from './bundled-families.js';
|
|
35
33
|
/**
|
|
36
34
|
* Resolve a bundled substitute face filename to a bundler-emitted URL.
|
|
37
35
|
*
|
|
@@ -53,34 +51,7 @@ export declare function resolveBundledFontAssetUrl(context: BundledFontAssetCont
|
|
|
53
51
|
*
|
|
54
52
|
* @beta
|
|
55
53
|
*/
|
|
56
|
-
export declare const superdocFonts:
|
|
57
|
-
resolveAssetUrl: (context: BundledFontAssetContext) => string;
|
|
58
|
-
};
|
|
59
|
-
/**
|
|
60
|
-
* Choose which bundled families SuperDoc advertises and renders, by LOGICAL Word family name
|
|
61
|
-
* (`"Calibri"`, never the physical `"Carlito"`).
|
|
62
|
-
*
|
|
63
|
-
* @beta
|
|
64
|
-
*/
|
|
65
|
-
export interface SuperDocFontsOptions {
|
|
66
|
-
/** Allow-list: ONLY these logical families are active, e.g. `['Calibri', 'Cambria']`. */
|
|
67
|
-
include?: string[];
|
|
68
|
-
/** Block-list: every bundled family EXCEPT these, e.g. `['Cooper Black', 'Brush Script MT']`. */
|
|
69
|
-
exclude?: string[];
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* The `fonts` config {@link createSuperDocFonts} returns: the bundled-asset resolver plus the
|
|
73
|
-
* curation. Structurally assignable to SuperDoc's `fonts` option.
|
|
74
|
-
*
|
|
75
|
-
* @beta
|
|
76
|
-
*/
|
|
77
|
-
export interface SuperDocFontsConfig {
|
|
78
|
-
resolveAssetUrl: (context: BundledFontAssetContext) => string;
|
|
79
|
-
bundled?: {
|
|
80
|
-
include?: string[];
|
|
81
|
-
exclude?: string[];
|
|
82
|
-
};
|
|
83
|
-
}
|
|
54
|
+
export declare const superdocFonts: SuperDocFontsConfig;
|
|
84
55
|
/**
|
|
85
56
|
* Build a curated `fonts` config: the bundled pack, narrowed to the families you choose.
|
|
86
57
|
*
|
package/dist/index.js
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* `new URL('../assets/<file>', import.meta.url)`, which Vite, Webpack 5, Next, Nuxt, esbuild, and
|
|
8
8
|
* Parcel detect, emit, and rewrite to the final hashed path.
|
|
9
9
|
*
|
|
10
|
+
* For a plain `<script>` / CDN page (no bundler), load `dist/superdoc-fonts.min.js` instead and use
|
|
11
|
+
* the `SuperDocFonts` global - same API, faces resolved relative to the script. See `cdn-entry.ts`.
|
|
12
|
+
*
|
|
10
13
|
* @beta This package is in preview; its surface may change before 1.0.
|
|
11
14
|
*/
|
|
12
|
-
import { BUNDLED_FONT_ASSET_URLS } from './asset-urls';
|
|
13
|
-
import {
|
|
15
|
+
import { BUNDLED_FONT_ASSET_URLS } from './asset-urls.js';
|
|
16
|
+
import { resolveCuration, } from './curation.js';
|
|
14
17
|
/**
|
|
15
18
|
* The full filename -> bundler-emitted URL map for every bundled face. Most consumers use
|
|
16
19
|
* {@link superdocFonts} or {@link resolveBundledFontAssetUrl} instead of reading this directly.
|
|
@@ -18,6 +21,13 @@ import { BUNDLED_FAMILY_NAMES } from './bundled-families';
|
|
|
18
21
|
* @beta
|
|
19
22
|
*/
|
|
20
23
|
export { BUNDLED_FONT_ASSET_URLS };
|
|
24
|
+
/**
|
|
25
|
+
* The curatable Word family names (and their union type), for building `include` / `exclude` lists
|
|
26
|
+
* with autocomplete. Re-exported from the generated source.
|
|
27
|
+
*
|
|
28
|
+
* @beta
|
|
29
|
+
*/
|
|
30
|
+
export { BUNDLED_FAMILY_NAMES } from './bundled-families.js';
|
|
21
31
|
/**
|
|
22
32
|
* Resolve a bundled substitute face filename to a bundler-emitted URL.
|
|
23
33
|
*
|
|
@@ -67,92 +77,5 @@ export const superdocFonts = {
|
|
|
67
77
|
* @beta
|
|
68
78
|
*/
|
|
69
79
|
export function createSuperDocFonts(options = {}) {
|
|
70
|
-
|
|
71
|
-
const exclude = normalizeNameList(options.exclude, 'exclude');
|
|
72
|
-
// include and exclude are mutually exclusive intents; accepting both silently would hide which one
|
|
73
|
-
// wins. Reject at the API boundary (this runs in the consumer's setup code, so it fails fast and
|
|
74
|
-
// clearly) rather than picking one arbitrarily.
|
|
75
|
-
if (include && exclude) {
|
|
76
|
-
throw new Error('[@superdoc-dev/fonts] createSuperDocFonts: pass `include` OR `exclude`, not both. ' +
|
|
77
|
-
'`include` is an allow-list (only those families resolve and appear in the toolbar); ' +
|
|
78
|
-
'`exclude` keeps everything but the named families.');
|
|
79
|
-
}
|
|
80
|
-
// Reject names SuperDoc does not bundle. This runs in the consumer's setup code, so it fails fast
|
|
81
|
-
// and clearly - especially for `include`, where a typo would otherwise silently hide the fonts the
|
|
82
|
-
// consumer meant to keep. (Raw `fonts.bundled` set directly stays lenient and only warns.)
|
|
83
|
-
if (include)
|
|
84
|
-
assertKnownFamilies(include, 'include');
|
|
85
|
-
if (exclude)
|
|
86
|
-
assertKnownFamilies(exclude, 'exclude');
|
|
87
|
-
const config = { resolveAssetUrl: resolveBundledFontAssetUrl };
|
|
88
|
-
if (include)
|
|
89
|
-
config.bundled = { include };
|
|
90
|
-
else if (exclude)
|
|
91
|
-
config.bundled = { exclude };
|
|
92
|
-
return config;
|
|
93
|
-
}
|
|
94
|
-
/** Validate and clean a curation list: an array of non-empty strings, or omitted. Throws on misuse. */
|
|
95
|
-
function normalizeNameList(value, field) {
|
|
96
|
-
if (value == null)
|
|
97
|
-
return undefined;
|
|
98
|
-
if (!Array.isArray(value)) {
|
|
99
|
-
throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` must be an array of Word font names.`);
|
|
100
|
-
}
|
|
101
|
-
const names = value.map((name) => (typeof name === 'string' ? name.trim() : '')).filter(Boolean);
|
|
102
|
-
if (names.length !== value.length) {
|
|
103
|
-
throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` must contain only non-empty font name strings ` +
|
|
104
|
-
`(e.g. ["Calibri", "Cambria"]).`);
|
|
105
|
-
}
|
|
106
|
-
return names.length ? names : undefined;
|
|
107
|
-
}
|
|
108
|
-
/** Normalize a family name for matching: trim, strip surrounding quotes, lowercase. */
|
|
109
|
-
function normalizeKey(name) {
|
|
110
|
-
return name
|
|
111
|
-
.trim()
|
|
112
|
-
.replace(/^["']|["']$/g, '')
|
|
113
|
-
.toLowerCase();
|
|
114
|
-
}
|
|
115
|
-
const KNOWN_FAMILY_KEYS = new Set(BUNDLED_FAMILY_NAMES.map(normalizeKey));
|
|
116
|
-
/** Bounded Levenshtein distance between two short strings, for a "did you mean" hint on a typo. */
|
|
117
|
-
function editDistance(a, b) {
|
|
118
|
-
const m = a.length;
|
|
119
|
-
const n = b.length;
|
|
120
|
-
if (Math.abs(m - n) > 2)
|
|
121
|
-
return 3;
|
|
122
|
-
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
123
|
-
for (let i = 1; i <= m; i += 1) {
|
|
124
|
-
const curr = [i];
|
|
125
|
-
for (let j = 1; j <= n; j += 1) {
|
|
126
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
127
|
-
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
128
|
-
}
|
|
129
|
-
prev = curr;
|
|
130
|
-
}
|
|
131
|
-
return prev[n];
|
|
132
|
-
}
|
|
133
|
-
/** The closest bundled family within edit distance 2, or null - so a typo gets a concrete suggestion. */
|
|
134
|
-
function closestKnownFamily(name) {
|
|
135
|
-
const key = normalizeKey(name);
|
|
136
|
-
let best = null;
|
|
137
|
-
let bestDist = 3;
|
|
138
|
-
for (const family of BUNDLED_FAMILY_NAMES) {
|
|
139
|
-
const dist = editDistance(key, normalizeKey(family));
|
|
140
|
-
if (dist < bestDist) {
|
|
141
|
-
bestDist = dist;
|
|
142
|
-
best = family;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return best;
|
|
146
|
-
}
|
|
147
|
-
/** Throw on any curation name SuperDoc does not bundle, with a suggestion and the full valid list. */
|
|
148
|
-
function assertKnownFamilies(names, field) {
|
|
149
|
-
const unknown = names.filter((name) => !KNOWN_FAMILY_KEYS.has(normalizeKey(name)));
|
|
150
|
-
if (unknown.length === 0)
|
|
151
|
-
return;
|
|
152
|
-
const hints = unknown.map((name) => {
|
|
153
|
-
const suggestion = closestKnownFamily(name);
|
|
154
|
-
return suggestion ? `"${name}" (did you mean "${suggestion}"?)` : `"${name}"`;
|
|
155
|
-
});
|
|
156
|
-
throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` names a font SuperDoc does not bundle: ${hints.join(', ')}. ` +
|
|
157
|
-
`Curate by Word family name. Bundled families: ${BUNDLED_FAMILY_NAMES.join(', ')}.`);
|
|
80
|
+
return { resolveAssetUrl: resolveBundledFontAssetUrl, ...resolveCuration(options) };
|
|
158
81
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var SuperDocFonts=(function(f){Object.defineProperty(f,Symbol.toStringTag,{value:"Module"});const d=Object.freeze(["ArchivoBlack-Regular.woff2","BacasimeAntique-Regular.woff2","C059-BdIta.woff2","C059-Bold.woff2","C059-Italic.woff2","C059-Roman.woff2","Caladea-Bold.woff2","Caladea-BoldItalic.woff2","Caladea-Italic.woff2","Caladea-Regular.woff2","Caprasimo-Regular.woff2","Cardo-Bold.woff2","Cardo-Italic.woff2","Cardo-Regular.woff2","Carlito-Bold.woff2","Carlito-BoldItalic.woff2","Carlito-Italic.woff2","Carlito-Regular.woff2","ComicRelief-Bold.woff2","ComicRelief-Regular.woff2","Gelasio-Bold.woff2","Gelasio-BoldItalic.woff2","Gelasio-Italic.woff2","Gelasio-Regular.woff2","InconsolataSemiExpanded-Bold.woff2","InconsolataSemiExpanded-Regular.woff2","LiberationMono-Bold.woff2","LiberationMono-BoldItalic.woff2","LiberationMono-Italic.woff2","LiberationMono-Regular.woff2","LiberationSans-Bold.woff2","LiberationSans-BoldItalic.woff2","LiberationSans-Italic.woff2","LiberationSans-Regular.woff2","LiberationSansNarrow-Bold.woff2","LiberationSansNarrow-BoldItalic.woff2","LiberationSansNarrow-Italic.woff2","LiberationSansNarrow-Regular.woff2","LiberationSerif-Bold.woff2","LiberationSerif-BoldItalic.woff2","LiberationSerif-Italic.woff2","LiberationSerif-Regular.woff2","NotoSans-Bold.woff2","NotoSans-BoldItalic.woff2","NotoSans-Italic.woff2","NotoSans-Regular.woff2","NotoSansMono-Bold.woff2","NotoSansMono-Regular.woff2","OreganoItalic-Regular.woff2","PTSans-Bold.woff2","PTSans-BoldItalic.woff2","PTSans-Italic.woff2","PTSans-Regular.woff2","PTSansNarrow-Bold.woff2","PTSansNarrow-Regular.woff2","Selawik-Bold.woff2","Selawik-Regular.woff2","TeXGyreBonum-Bold.woff2","TeXGyreBonum-BoldItalic.woff2","TeXGyreBonum-Italic.woff2","TeXGyreBonum-Regular.woff2","URWGothic-Book.woff2","URWGothic-BookOblique.woff2","URWGothic-Demi.woff2","URWGothic-DemiOblique.woff2"]),s=Object.freeze(["Arial","Arial Black","Arial MT","Arial Narrow","Baskerville Old Face","Bookman Old Style","Brush Script MT","Calibri","Calibri Light","Cambria","Century","Century Gothic","Century Schoolbook","Comic Sans MS","Consolas","Cooper Black","Courier","Courier New","Garamond","Georgia","Gill Sans MT Condensed","Helvetica","ITC Bookman","Lucida Console","Segoe UI","Tahoma","Times","Times New Roman","Trebuchet MS","Verdana"]);function B(o={}){const r=w(o.include,"include"),e=w(o.exclude,"exclude");if(r&&e)throw new Error("[@superdoc-dev/fonts] createSuperDocFonts: pass `include` OR `exclude`, not both. `include` is an allow-list (only those families resolve and appear in the toolbar); `exclude` keeps everything but the named families.");return r&&m(r,"include"),e&&m(e,"exclude"),r?{bundled:{include:r}}:e?{bundled:{exclude:e}}:{}}function w(o,r){if(o==null)return;if(!Array.isArray(o))throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${r}\` must be an array of Word font names.`);const e=o.map(a=>typeof a=="string"?a.trim():"").filter(Boolean);if(e.length!==o.length)throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${r}\` must contain only non-empty font name strings (e.g. ["Calibri", "Cambria"]).`);return e.length?e:void 0}function c(o){return o.trim().replace(/^["']|["']$/g,"").toLowerCase()}var g=new Set(s.map(c));function h(o,r){const e=o.length,a=r.length;if(Math.abs(e-a)>2)return 3;let n=Array.from({length:a+1},(t,l)=>l);for(let t=1;t<=e;t+=1){const l=[t];for(let i=1;i<=a;i+=1){const R=o[t-1]===r[i-1]?0:1;l[i]=Math.min(l[i-1]+1,n[i]+1,n[i-1]+R)}n=l}return n[a]}function b(o){const r=c(o);let e=null,a=3;for(const n of s){const t=h(r,c(n));t<a&&(a=t,e=n)}return e}function m(o,r){const e=o.filter(n=>!g.has(c(n)));if(e.length===0)return;const a=e.map(n=>{const t=b(n);return t?`"${n}" (did you mean "${t}"?)`:`"${n}"`});throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${r}\` names a font SuperDoc does not bundle: ${a.join(", ")}. Curate by Word family name. Bundled families: ${s.join(", ")}.`)}var S=(typeof document<"u"?document.currentScript:null)?.src??"",C=new Set(d);function u(o){if(!C.has(o.file))throw new Error(`[@superdoc-dev/fonts] no bundled asset for "${o.file}". This pack ships ${d.length} faces; the file is unknown, so @superdoc-dev/fonts and superdoc are likely version-mismatched. Align their versions.`);if(!S)throw new Error('[@superdoc-dev/fonts] could not determine the script URL to resolve bundled fonts from. Load superdoc-fonts.min.js with a normal <script src="..."> tag.');return new URL(`../assets/${o.file}`,S).href}const p={resolveAssetUrl:u};function I(o={}){return{resolveAssetUrl:u,...B(o)}}return f.BUNDLED_FAMILY_NAMES=s,f.createSuperDocFonts=I,f.resolveBundledFontAssetUrl=u,f.superdocFonts=p,f})({});
|
|
2
|
+
|
|
3
|
+
//# sourceMappingURL=superdoc-fonts.min.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"superdoc-fonts.min.js","names":["BUNDLED_FONT_FILES: readonly string[]","KNOWN_FAMILY_KEYS: ReadonlySet<string>","best: string | null","SCRIPT_SRC: string","KNOWN_FILES: ReadonlySet<string>","superdocFonts: SuperDocFontsConfig"],"sources":["../src/bundled-files.ts","../src/bundled-families.ts","../src/curation.ts","../src/cdn-entry.ts"],"sourcesContent":["/**\n * GENERATED by scripts/generate-asset-urls.mjs - do not edit by hand.\n *\n * The flat list of bundled .woff2 filenames, with NO `import.meta.url` (unlike asset-urls.ts).\n * The browser/IIFE entry (cdn-entry.ts) imports this to validate face files and to resolve them\n * relative to its own <script>, since import.meta.url is not available in an IIFE bundle.\n */\nexport const BUNDLED_FONT_FILES: readonly string[] = Object.freeze([\n 'ArchivoBlack-Regular.woff2',\n 'BacasimeAntique-Regular.woff2',\n 'C059-BdIta.woff2',\n 'C059-Bold.woff2',\n 'C059-Italic.woff2',\n 'C059-Roman.woff2',\n 'Caladea-Bold.woff2',\n 'Caladea-BoldItalic.woff2',\n 'Caladea-Italic.woff2',\n 'Caladea-Regular.woff2',\n 'Caprasimo-Regular.woff2',\n 'Cardo-Bold.woff2',\n 'Cardo-Italic.woff2',\n 'Cardo-Regular.woff2',\n 'Carlito-Bold.woff2',\n 'Carlito-BoldItalic.woff2',\n 'Carlito-Italic.woff2',\n 'Carlito-Regular.woff2',\n 'ComicRelief-Bold.woff2',\n 'ComicRelief-Regular.woff2',\n 'Gelasio-Bold.woff2',\n 'Gelasio-BoldItalic.woff2',\n 'Gelasio-Italic.woff2',\n 'Gelasio-Regular.woff2',\n 'InconsolataSemiExpanded-Bold.woff2',\n 'InconsolataSemiExpanded-Regular.woff2',\n 'LiberationMono-Bold.woff2',\n 'LiberationMono-BoldItalic.woff2',\n 'LiberationMono-Italic.woff2',\n 'LiberationMono-Regular.woff2',\n 'LiberationSans-Bold.woff2',\n 'LiberationSans-BoldItalic.woff2',\n 'LiberationSans-Italic.woff2',\n 'LiberationSans-Regular.woff2',\n 'LiberationSansNarrow-Bold.woff2',\n 'LiberationSansNarrow-BoldItalic.woff2',\n 'LiberationSansNarrow-Italic.woff2',\n 'LiberationSansNarrow-Regular.woff2',\n 'LiberationSerif-Bold.woff2',\n 'LiberationSerif-BoldItalic.woff2',\n 'LiberationSerif-Italic.woff2',\n 'LiberationSerif-Regular.woff2',\n 'NotoSans-Bold.woff2',\n 'NotoSans-BoldItalic.woff2',\n 'NotoSans-Italic.woff2',\n 'NotoSans-Regular.woff2',\n 'NotoSansMono-Bold.woff2',\n 'NotoSansMono-Regular.woff2',\n 'OreganoItalic-Regular.woff2',\n 'PTSans-Bold.woff2',\n 'PTSans-BoldItalic.woff2',\n 'PTSans-Italic.woff2',\n 'PTSans-Regular.woff2',\n 'PTSansNarrow-Bold.woff2',\n 'PTSansNarrow-Regular.woff2',\n 'Selawik-Bold.woff2',\n 'Selawik-Regular.woff2',\n 'TeXGyreBonum-Bold.woff2',\n 'TeXGyreBonum-BoldItalic.woff2',\n 'TeXGyreBonum-Italic.woff2',\n 'TeXGyreBonum-Regular.woff2',\n 'URWGothic-Book.woff2',\n 'URWGothic-BookOblique.woff2',\n 'URWGothic-Demi.woff2',\n 'URWGothic-DemiOblique.woff2',\n]);\n","/**\n * GENERATED by scripts/generate-bundled-families.ts - do not edit by hand.\n *\n * The Word family names a document can curate via createSuperDocFonts({ include / exclude }), mirrored\n * from @superdoc/font-system's runtime curation set (every logical family the resolver substitutes to a\n * bundled face, including category fallbacks like Verdana). A name outside this set is a typo or an\n * unbundled font, so createSuperDocFonts rejects it instead of silently dropping the intended fonts.\n */\nexport const BUNDLED_FAMILY_NAMES = Object.freeze([\n 'Arial',\n 'Arial Black',\n 'Arial MT',\n 'Arial Narrow',\n 'Baskerville Old Face',\n 'Bookman Old Style',\n 'Brush Script MT',\n 'Calibri',\n 'Calibri Light',\n 'Cambria',\n 'Century',\n 'Century Gothic',\n 'Century Schoolbook',\n 'Comic Sans MS',\n 'Consolas',\n 'Cooper Black',\n 'Courier',\n 'Courier New',\n 'Garamond',\n 'Georgia',\n 'Gill Sans MT Condensed',\n 'Helvetica',\n 'ITC Bookman',\n 'Lucida Console',\n 'Segoe UI',\n 'Tahoma',\n 'Times',\n 'Times New Roman',\n 'Trebuchet MS',\n 'Verdana',\n] as const);\n\n/**\n * Union of the Word family names the bundled pack can curate, for autocomplete on\n * createSuperDocFonts({ include / exclude }). Those options also accept any string (case- and\n * quote-insensitive, validated at runtime), so a list built from dynamic config still type-checks.\n */\nexport type BundledFontFamilyName = (typeof BUNDLED_FAMILY_NAMES)[number];\n","/**\n * Resolver-independent curation + validation shared by the two entries: the bundler entry\n * (`index.ts`, asset URLs via `import.meta.url`) and the browser/IIFE entry (`cdn-entry.ts`, URLs\n * relative to its own `<script>`). Each entry supplies only its own asset-URL resolver; the\n * `include` / `exclude` validation lives here once so the two entries can never drift.\n *\n * @beta\n */\nimport { BUNDLED_FAMILY_NAMES, type BundledFontFamilyName } from './bundled-families.js';\n\n/**\n * Minimal structural shape of SuperDoc's bundled-font asset context. Kept LOCAL so this package\n * has no dependency on `@superdoc/font-system`; only `file` is read. Assignable to SuperDoc's\n * `FontAssetUrlResolver` by structural typing.\n *\n * @beta\n */\nexport interface BundledFontAssetContext {\n /** Asset filename, e.g. `Carlito-Regular.woff2`. */\n file: string;\n family?: string;\n weight?: string;\n style?: 'normal' | 'italic';\n source?: string;\n}\n\n/** Per-face asset-URL resolver: maps a bundled face filename to a URL the browser can fetch. */\nexport type BundledFontAssetResolver = (context: BundledFontAssetContext) => string;\n\n/**\n * Choose which bundled families SuperDoc advertises and renders, by LOGICAL Word family name\n * (`\"Calibri\"`, never the physical `\"Carlito\"`).\n *\n * @beta\n */\nexport interface SuperDocFontsOptions {\n /**\n * Allow-list: ONLY these logical families are active, e.g. `['Calibri', 'Cambria']`. Autocompletes\n * the bundled names, but also accepts any string (case- and quote-insensitive, validated at\n * runtime), so a list built from dynamic config still type-checks.\n */\n include?: readonly (BundledFontFamilyName | (string & {}))[];\n /** Block-list: every bundled family EXCEPT these, e.g. `['Cooper Black', 'Brush Script MT']`. */\n exclude?: readonly (BundledFontFamilyName | (string & {}))[];\n}\n\n/**\n * The `fonts` config the entries return: the bundled-asset resolver plus the curation.\n * Structurally assignable to SuperDoc's `fonts` option.\n *\n * @beta\n */\nexport interface SuperDocFontsConfig {\n resolveAssetUrl: BundledFontAssetResolver;\n bundled?: { include?: string[]; exclude?: string[] };\n}\n\n/**\n * Validate the curation options and return the `{ bundled? }` slice (no resolver). Each entry\n * spreads this into its own `{ resolveAssetUrl, ...resolveCuration(options) }`.\n */\nexport function resolveCuration(options: SuperDocFontsOptions = {}): {\n bundled?: { include?: string[]; exclude?: string[] };\n} {\n const include = normalizeNameList(options.include, 'include');\n const exclude = normalizeNameList(options.exclude, 'exclude');\n // include and exclude are mutually exclusive intents; accepting both silently would hide which one\n // wins. Reject at the API boundary (this runs in the consumer's setup code, so it fails fast and\n // clearly) rather than picking one arbitrarily.\n if (include && exclude) {\n throw new Error(\n '[@superdoc-dev/fonts] createSuperDocFonts: pass `include` OR `exclude`, not both. ' +\n '`include` is an allow-list (only those families resolve and appear in the toolbar); ' +\n '`exclude` keeps everything but the named families.',\n );\n }\n // Reject names SuperDoc does not bundle. This runs in the consumer's setup code, so it fails fast\n // and clearly - especially for `include`, where a typo would otherwise silently hide the fonts the\n // consumer meant to keep. (Raw `fonts.bundled` set directly stays lenient and only warns.)\n if (include) assertKnownFamilies(include, 'include');\n if (exclude) assertKnownFamilies(exclude, 'exclude');\n if (include) return { bundled: { include } };\n if (exclude) return { bundled: { exclude } };\n return {};\n}\n\n/** Validate and clean a curation list: an array of non-empty strings, or omitted. Throws on misuse. */\nfunction normalizeNameList(value: readonly string[] | undefined, field: 'include' | 'exclude'): string[] | undefined {\n if (value == null) return undefined;\n if (!Array.isArray(value)) {\n throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \\`${field}\\` must be an array of Word font names.`);\n }\n const names = value.map((name) => (typeof name === 'string' ? name.trim() : '')).filter(Boolean);\n if (names.length !== value.length) {\n throw new Error(\n `[@superdoc-dev/fonts] createSuperDocFonts: \\`${field}\\` must contain only non-empty font name strings ` +\n `(e.g. [\"Calibri\", \"Cambria\"]).`,\n );\n }\n return names.length ? names : undefined;\n}\n\n/** Normalize a family name for matching: trim, strip surrounding quotes, lowercase. */\nfunction normalizeKey(name: string): string {\n return name\n .trim()\n .replace(/^[\"']|[\"']$/g, '')\n .toLowerCase();\n}\n\nconst KNOWN_FAMILY_KEYS: ReadonlySet<string> = new Set(BUNDLED_FAMILY_NAMES.map(normalizeKey));\n\n/** Bounded Levenshtein distance between two short strings, for a \"did you mean\" hint on a typo. */\nfunction editDistance(a: string, b: string): number {\n const m = a.length;\n const n = b.length;\n if (Math.abs(m - n) > 2) return 3;\n let prev = Array.from({ length: n + 1 }, (_, i) => i);\n for (let i = 1; i <= m; i += 1) {\n const curr = [i];\n for (let j = 1; j <= n; j += 1) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);\n }\n prev = curr;\n }\n return prev[n];\n}\n\n/** The closest bundled family within edit distance 2, or null - so a typo gets a concrete suggestion. */\nfunction closestKnownFamily(name: string): string | null {\n const key = normalizeKey(name);\n let best: string | null = null;\n let bestDist = 3;\n for (const family of BUNDLED_FAMILY_NAMES) {\n const dist = editDistance(key, normalizeKey(family));\n if (dist < bestDist) {\n bestDist = dist;\n best = family;\n }\n }\n return best;\n}\n\n/** Throw on any curation name SuperDoc does not bundle, with a suggestion and the full valid list. */\nfunction assertKnownFamilies(names: readonly string[], field: 'include' | 'exclude'): void {\n const unknown = names.filter((name) => !KNOWN_FAMILY_KEYS.has(normalizeKey(name)));\n if (unknown.length === 0) return;\n const hints = unknown.map((name) => {\n const suggestion = closestKnownFamily(name);\n return suggestion ? `\"${name}\" (did you mean \"${suggestion}\"?)` : `\"${name}\"`;\n });\n throw new Error(\n `[@superdoc-dev/fonts] createSuperDocFonts: \\`${field}\\` names a font SuperDoc does not bundle: ${hints.join(', ')}. ` +\n `Curate by Word family name. Bundled families: ${BUNDLED_FAMILY_NAMES.join(', ')}.`,\n );\n}\n","/**\n * Browser / `<script>` (IIFE) entry for `@superdoc-dev/fonts`, built to\n * `dist/superdoc-fonts.min.js` and exposed as the `SuperDocFonts` global.\n *\n * Unlike the bundler entry ({@link \"./index\"}), it resolves the `.woff2` relative to its OWN\n * `<script>` (`document.currentScript`) instead of `import.meta.url`, so a plain CDN / self-host\n * `<script>` tag works with no bundler:\n *\n * <script src=\"https://cdn.jsdelivr.net/npm/superdoc/dist/superdoc.min.js\"></script>\n * <script src=\"https://cdn.jsdelivr.net/npm/@superdoc-dev/fonts/dist/superdoc-fonts.min.js\"></script>\n * <script>\n * new SuperDoc({ selector: '#editor', document: 'contract.docx', fonts: SuperDocFonts.superdocFonts });\n * </script>\n *\n * The faces ship in the package's `assets/` dir and this bundle in `dist/`, so a face resolves to\n * `../assets/<file>` relative to the script. On a public CDN (jsDelivr / unpkg) the whole package\n * is served, so that resolves with no setup; self-hosting requires the `dist/` and `assets/` dirs\n * to keep their relative layout.\n *\n * @beta\n */\nimport { BUNDLED_FONT_FILES } from './bundled-files.js';\nimport {\n resolveCuration,\n type BundledFontAssetContext,\n type SuperDocFontsConfig,\n type SuperDocFontsOptions,\n} from './curation.js';\n\n// Capture the executing <script> src at load. `document.currentScript` is valid while this\n// top-level IIFE runs and null later (e.g. in async callbacks), so it MUST be read here, at module\n// eval, not inside the resolver. Duck-typed (no `instanceof HTMLScriptElement`) so the module also\n// evaluates under a non-DOM test environment where that constructor is absent.\nconst currentScript = typeof document !== 'undefined' ? (document.currentScript as { src?: string } | null) : null;\nconst SCRIPT_SRC: string = currentScript?.src ?? '';\n\nconst KNOWN_FILES: ReadonlySet<string> = new Set(BUNDLED_FONT_FILES);\n\n/**\n * Resolve a bundled face filename to a URL relative to THIS script. Throws on an unknown file\n * (a version mismatch between `@superdoc-dev/fonts` and `superdoc`) and on a missing script URL,\n * matching the bundler entry's fail-loud-not-silent contract.\n *\n * @beta\n */\nexport function resolveBundledFontAssetUrl(context: BundledFontAssetContext): string {\n if (!KNOWN_FILES.has(context.file)) {\n throw new Error(\n `[@superdoc-dev/fonts] no bundled asset for \"${context.file}\". This pack ships ` +\n `${BUNDLED_FONT_FILES.length} faces; the file is unknown, so @superdoc-dev/fonts and ` +\n `superdoc are likely version-mismatched. Align their versions.`,\n );\n }\n if (!SCRIPT_SRC) {\n throw new Error(\n '[@superdoc-dev/fonts] could not determine the script URL to resolve bundled fonts from. ' +\n 'Load superdoc-fonts.min.js with a normal <script src=\"...\"> tag.',\n );\n }\n return new URL(`../assets/${context.file}`, SCRIPT_SRC).href;\n}\n\n/**\n * Drop-in value for SuperDoc's `fonts` config: `new SuperDoc({ fonts: SuperDocFonts.superdocFonts })`.\n *\n * @beta\n */\nexport const superdocFonts: SuperDocFontsConfig = { resolveAssetUrl: resolveBundledFontAssetUrl };\n\n/**\n * Build a curated `fonts` config: the bundled pack narrowed to the families you choose. Same API\n * and validation as the bundler entry's `createSuperDocFonts`; only the resolver differs.\n *\n * new SuperDoc({ fonts: SuperDocFonts.createSuperDocFonts({ exclude: ['Cooper Black'] }) });\n *\n * @beta\n */\nexport function createSuperDocFonts(options: SuperDocFontsOptions = {}): SuperDocFontsConfig {\n return { resolveAssetUrl: resolveBundledFontAssetUrl, ...resolveCuration(options) };\n}\n\nexport type { BundledFontAssetContext, SuperDocFontsConfig, SuperDocFontsOptions };\nexport { BUNDLED_FAMILY_NAMES } from './bundled-families.js';\nexport type { BundledFontFamilyName } from './bundled-families.js';\n"],"mappings":"4FAOA,MAAaA,EAAwC,OAAO,OAAO,CACjE,6BACA,gCACA,mBACA,kBACA,oBACA,mBACA,qBACA,2BACA,uBACA,wBACA,0BACA,mBACA,qBACA,sBACA,qBACA,2BACA,uBACA,wBACA,yBACA,4BACA,qBACA,2BACA,uBACA,wBACA,qCACA,wCACA,4BACA,kCACA,8BACA,+BACA,4BACA,kCACA,8BACA,+BACA,kCACA,wCACA,oCACA,qCACA,6BACA,mCACA,+BACA,gCACA,sBACA,4BACA,wBACA,yBACA,0BACA,6BACA,8BACA,oBACA,0BACA,sBACA,uBACA,0BACA,6BACA,qBACA,wBACA,0BACA,gCACA,4BACA,6BACA,uBACA,8BACA,uBACA,8BACD,ECjEY,EAAuB,OAAO,OAAO,CAChD,QACA,cACA,WACA,eACA,uBACA,oBACA,kBACA,UACA,gBACA,UACA,UACA,iBACA,qBACA,gBACA,WACA,eACA,UACA,cACA,WACA,UACA,yBACA,YACA,cACA,iBACA,WACA,SACA,QACA,kBACA,eACA,UACD,ECsBD,SAAgB,EAAgB,EAAgC,CAAA,EAE9D,CACA,MAAM,EAAU,EAAkB,EAAQ,QAAS,SAAA,EAC7C,EAAU,EAAkB,EAAQ,QAAS,SAAA,EAInD,GAAI,GAAW,EACb,MAAM,IAAI,MACR,0NAAA,EAUJ,OAFI,GAAS,EAAoB,EAAS,SAAA,EACtC,GAAS,EAAoB,EAAS,SAAA,EACtC,EAAgB,CAAE,QAAS,CAAE,QAAA,CAAA,CAAS,EACtC,EAAgB,CAAE,QAAS,CAAE,QAAA,CAAA,CAAS,EACnC,CAAA,EAIT,SAAS,EAAkB,EAAsC,EAAoD,CACnH,GAAI,GAAS,KAAM,OACnB,GAAI,CAAC,MAAM,QAAQ,CAAA,EACjB,MAAM,IAAI,MAAM,gDAAgD,CAAA,yCAAM,EAExE,MAAM,EAAQ,EAAM,IAAK,GAAU,OAAO,GAAS,SAAW,EAAK,KAAA,EAAS,EAAA,EAAK,OAAO,OAAA,EACxF,GAAI,EAAM,SAAW,EAAM,OACzB,MAAM,IAAI,MACR,gDAAgD,CAAA,iFAAM,EAI1D,OAAO,EAAM,OAAS,EAAQ,OAIhC,SAAS,EAAa,EAAsB,CAC1C,OAAO,EACJ,KAAA,EACA,QAAQ,eAAgB,EAAA,EACxB,YAAA,EAGL,IAAMC,EAAyC,IAAI,IAAI,EAAqB,IAAI,CAAA,CAAa,EAG7F,SAAS,EAAa,EAAW,EAAmB,CAClD,MAAM,EAAI,EAAE,OACN,EAAI,EAAE,OACZ,GAAI,KAAK,IAAI,EAAI,CAAA,EAAK,EAAG,MAAO,GAChC,IAAI,EAAO,MAAM,KAAK,CAAE,OAAQ,EAAI,CAAA,EAAG,CAAG,EAAG,IAAM,CAAA,EACnD,QAAS,EAAI,EAAG,GAAK,EAAG,GAAK,EAAG,CAC9B,MAAM,EAAO,CAAC,CAAA,EACd,QAAS,EAAI,EAAG,GAAK,EAAG,GAAK,EAAG,CAC9B,MAAM,EAAO,EAAE,EAAI,CAAA,IAAO,EAAE,EAAI,CAAA,EAAK,EAAI,EACzC,EAAK,CAAA,EAAK,KAAK,IAAI,EAAK,EAAI,CAAA,EAAK,EAAG,EAAK,CAAA,EAAK,EAAG,EAAK,EAAI,CAAA,EAAK,CAAA,EAEjE,EAAO,EAET,OAAO,EAAK,CAAA,EAId,SAAS,EAAmB,EAA6B,CACvD,MAAM,EAAM,EAAa,CAAA,EACzB,IAAIC,EAAsB,KACtB,EAAW,EACf,UAAW,KAAU,EAAsB,CACzC,MAAM,EAAO,EAAa,EAAK,EAAa,CAAA,CAAO,EAC/C,EAAO,IACT,EAAW,EACX,EAAO,GAGX,OAAO,EAIT,SAAS,EAAoB,EAA0B,EAAoC,CACzF,MAAM,EAAU,EAAM,OAAQ,GAAS,CAAC,EAAkB,IAAI,EAAa,CAAA,CAAK,CAAC,EACjF,GAAI,EAAQ,SAAW,EAAG,OAC1B,MAAM,EAAQ,EAAQ,IAAK,GAAS,CAClC,MAAM,EAAa,EAAmB,CAAA,EACtC,OAAO,EAAa,IAAI,CAAA,oBAAwB,CAAA,MAAkB,IAAI,CAAA,MAExE,MAAM,IAAI,MACR,gDAAgD,CAAA,6CAAkD,EAAM,KAAK,IAAA,CAAK,mDAC/D,EAAqB,KAAK,IAAA,CAAK,GAAC,ECxHvF,IAAMC,GADgB,OAAO,SAAa,IAAe,SAAS,cAA4C,OACpE,KAAO,GAE3CC,EAAmC,IAAI,IAAI,CAAA,EASjD,SAAgB,EAA2B,EAA0C,CACnF,GAAI,CAAC,EAAY,IAAI,EAAQ,IAAA,EAC3B,MAAM,IAAI,MACR,+CAA+C,EAAQ,IAAA,sBAClD,EAAmB,MAAA,uHAAO,EAInC,GAAI,CAAC,EACH,MAAM,IAAI,MACR,0JAAA,EAIJ,OAAO,IAAI,IAAI,aAAa,EAAQ,IAAA,GAAQ,CAAA,EAAY,KAQ1D,MAAaC,EAAqC,CAAE,gBAAiB,CAAA,EAUrE,SAAgB,EAAoB,EAAgC,CAAA,EAAyB,CAC3F,MAAO,CAAE,gBAAiB,EAA4B,GAAG,EAAgB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superdoc-dev/fonts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1-next.2",
|
|
4
4
|
"description": "Optional bundled metric-compatible font substitutes for SuperDoc, shipped as bundler-resolvable assets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN assets/LICENSES.md",
|
|
@@ -25,17 +25,18 @@
|
|
|
25
25
|
"assets"
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": false,
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "22.19.2",
|
|
30
|
+
"typescript": "^5.9.2",
|
|
31
|
+
"vite": "^7.2.7"
|
|
32
|
+
},
|
|
28
33
|
"scripts": {
|
|
29
34
|
"sync": "node scripts/sync-assets.mjs",
|
|
30
35
|
"generate": "node scripts/generate-asset-urls.mjs && pnpm exec tsx scripts/generate-bundled-families.ts",
|
|
31
36
|
"check:families": "pnpm exec tsx scripts/check-bundled-families.ts",
|
|
32
|
-
"build": "
|
|
33
|
-
"
|
|
37
|
+
"build:cdn": "vite build",
|
|
38
|
+
"build": "node scripts/sync-assets.mjs && node scripts/generate-asset-urls.mjs && tsc --project tsconfig.json && vite build",
|
|
34
39
|
"typecheck": "tsc --noEmit",
|
|
35
40
|
"test": "vitest"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/node": "catalog:",
|
|
39
|
-
"typescript": "catalog:"
|
|
40
41
|
}
|
|
41
|
-
}
|
|
42
|
+
}
|
package/src/bundled-families.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* bundled face, including category fallbacks like Verdana). A name outside this set is a typo or an
|
|
7
7
|
* unbundled font, so createSuperDocFonts rejects it instead of silently dropping the intended fonts.
|
|
8
8
|
*/
|
|
9
|
-
export const BUNDLED_FAMILY_NAMES
|
|
9
|
+
export const BUNDLED_FAMILY_NAMES = Object.freeze([
|
|
10
10
|
'Arial',
|
|
11
11
|
'Arial Black',
|
|
12
12
|
'Arial MT',
|
|
@@ -37,4 +37,11 @@ export const BUNDLED_FAMILY_NAMES: readonly string[] = Object.freeze([
|
|
|
37
37
|
'Times New Roman',
|
|
38
38
|
'Trebuchet MS',
|
|
39
39
|
'Verdana',
|
|
40
|
-
]);
|
|
40
|
+
] as const);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Union of the Word family names the bundled pack can curate, for autocomplete on
|
|
44
|
+
* createSuperDocFonts({ include / exclude }). Those options also accept any string (case- and
|
|
45
|
+
* quote-insensitive, validated at runtime), so a list built from dynamic config still type-checks.
|
|
46
|
+
*/
|
|
47
|
+
export type BundledFontFamilyName = (typeof BUNDLED_FAMILY_NAMES)[number];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by scripts/generate-asset-urls.mjs - do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* The flat list of bundled .woff2 filenames, with NO `import.meta.url` (unlike asset-urls.ts).
|
|
5
|
+
* The browser/IIFE entry (cdn-entry.ts) imports this to validate face files and to resolve them
|
|
6
|
+
* relative to its own <script>, since import.meta.url is not available in an IIFE bundle.
|
|
7
|
+
*/
|
|
8
|
+
export const BUNDLED_FONT_FILES: readonly string[] = Object.freeze([
|
|
9
|
+
'ArchivoBlack-Regular.woff2',
|
|
10
|
+
'BacasimeAntique-Regular.woff2',
|
|
11
|
+
'C059-BdIta.woff2',
|
|
12
|
+
'C059-Bold.woff2',
|
|
13
|
+
'C059-Italic.woff2',
|
|
14
|
+
'C059-Roman.woff2',
|
|
15
|
+
'Caladea-Bold.woff2',
|
|
16
|
+
'Caladea-BoldItalic.woff2',
|
|
17
|
+
'Caladea-Italic.woff2',
|
|
18
|
+
'Caladea-Regular.woff2',
|
|
19
|
+
'Caprasimo-Regular.woff2',
|
|
20
|
+
'Cardo-Bold.woff2',
|
|
21
|
+
'Cardo-Italic.woff2',
|
|
22
|
+
'Cardo-Regular.woff2',
|
|
23
|
+
'Carlito-Bold.woff2',
|
|
24
|
+
'Carlito-BoldItalic.woff2',
|
|
25
|
+
'Carlito-Italic.woff2',
|
|
26
|
+
'Carlito-Regular.woff2',
|
|
27
|
+
'ComicRelief-Bold.woff2',
|
|
28
|
+
'ComicRelief-Regular.woff2',
|
|
29
|
+
'Gelasio-Bold.woff2',
|
|
30
|
+
'Gelasio-BoldItalic.woff2',
|
|
31
|
+
'Gelasio-Italic.woff2',
|
|
32
|
+
'Gelasio-Regular.woff2',
|
|
33
|
+
'InconsolataSemiExpanded-Bold.woff2',
|
|
34
|
+
'InconsolataSemiExpanded-Regular.woff2',
|
|
35
|
+
'LiberationMono-Bold.woff2',
|
|
36
|
+
'LiberationMono-BoldItalic.woff2',
|
|
37
|
+
'LiberationMono-Italic.woff2',
|
|
38
|
+
'LiberationMono-Regular.woff2',
|
|
39
|
+
'LiberationSans-Bold.woff2',
|
|
40
|
+
'LiberationSans-BoldItalic.woff2',
|
|
41
|
+
'LiberationSans-Italic.woff2',
|
|
42
|
+
'LiberationSans-Regular.woff2',
|
|
43
|
+
'LiberationSansNarrow-Bold.woff2',
|
|
44
|
+
'LiberationSansNarrow-BoldItalic.woff2',
|
|
45
|
+
'LiberationSansNarrow-Italic.woff2',
|
|
46
|
+
'LiberationSansNarrow-Regular.woff2',
|
|
47
|
+
'LiberationSerif-Bold.woff2',
|
|
48
|
+
'LiberationSerif-BoldItalic.woff2',
|
|
49
|
+
'LiberationSerif-Italic.woff2',
|
|
50
|
+
'LiberationSerif-Regular.woff2',
|
|
51
|
+
'NotoSans-Bold.woff2',
|
|
52
|
+
'NotoSans-BoldItalic.woff2',
|
|
53
|
+
'NotoSans-Italic.woff2',
|
|
54
|
+
'NotoSans-Regular.woff2',
|
|
55
|
+
'NotoSansMono-Bold.woff2',
|
|
56
|
+
'NotoSansMono-Regular.woff2',
|
|
57
|
+
'OreganoItalic-Regular.woff2',
|
|
58
|
+
'PTSans-Bold.woff2',
|
|
59
|
+
'PTSans-BoldItalic.woff2',
|
|
60
|
+
'PTSans-Italic.woff2',
|
|
61
|
+
'PTSans-Regular.woff2',
|
|
62
|
+
'PTSansNarrow-Bold.woff2',
|
|
63
|
+
'PTSansNarrow-Regular.woff2',
|
|
64
|
+
'Selawik-Bold.woff2',
|
|
65
|
+
'Selawik-Regular.woff2',
|
|
66
|
+
'TeXGyreBonum-Bold.woff2',
|
|
67
|
+
'TeXGyreBonum-BoldItalic.woff2',
|
|
68
|
+
'TeXGyreBonum-Italic.woff2',
|
|
69
|
+
'TeXGyreBonum-Regular.woff2',
|
|
70
|
+
'URWGothic-Book.woff2',
|
|
71
|
+
'URWGothic-BookOblique.woff2',
|
|
72
|
+
'URWGothic-Demi.woff2',
|
|
73
|
+
'URWGothic-DemiOblique.woff2',
|
|
74
|
+
]);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// cdn-entry resolves faces relative to its own <script> (document.currentScript), captured at module
|
|
4
|
+
// eval. The fonts test env is `node` (no document), so we stub a minimal document.currentScript and
|
|
5
|
+
// re-import the module per case to re-run that capture, mirroring what the browser sees.
|
|
6
|
+
const SCRIPT = 'https://cdn.jsdelivr.net/npm/@superdoc-dev/fonts@0.1.0/dist/superdoc-fonts.min.js';
|
|
7
|
+
const ASSET_BASE = 'https://cdn.jsdelivr.net/npm/@superdoc-dev/fonts@0.1.0/assets/';
|
|
8
|
+
|
|
9
|
+
describe('cdn-entry (browser/IIFE): script-relative resolver', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.stubGlobal('document', { currentScript: { src: SCRIPT } });
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.unstubAllGlobals();
|
|
17
|
+
vi.resetModules();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('resolves a known face to ../assets/<file> relative to the script', async () => {
|
|
21
|
+
const { resolveBundledFontAssetUrl, superdocFonts } = await import('./cdn-entry');
|
|
22
|
+
expect(resolveBundledFontAssetUrl({ file: 'Carlito-Regular.woff2' })).toBe(`${ASSET_BASE}Carlito-Regular.woff2`);
|
|
23
|
+
// superdocFonts wires the same resolver.
|
|
24
|
+
expect(superdocFonts.resolveAssetUrl({ file: 'Carlito-Regular.woff2' })).toBe(`${ASSET_BASE}Carlito-Regular.woff2`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('throws on an unknown face so a version mismatch surfaces', async () => {
|
|
28
|
+
const { resolveBundledFontAssetUrl } = await import('./cdn-entry');
|
|
29
|
+
expect(() => resolveBundledFontAssetUrl({ file: 'NotAFont.woff2' })).toThrow(/no bundled asset/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('createSuperDocFonts shares the bundler entry validation', async () => {
|
|
33
|
+
const { createSuperDocFonts } = await import('./cdn-entry');
|
|
34
|
+
expect(createSuperDocFonts({ exclude: ['Cooper Black'] }).bundled).toEqual({ exclude: ['Cooper Black'] });
|
|
35
|
+
expect(createSuperDocFonts().bundled).toBeUndefined();
|
|
36
|
+
expect(() => createSuperDocFonts({ include: ['Calbri'] })).toThrow(/did you mean "Calibri"\?/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('throws a clear error when no script URL can be determined', async () => {
|
|
40
|
+
vi.stubGlobal('document', { currentScript: null });
|
|
41
|
+
vi.resetModules();
|
|
42
|
+
const { resolveBundledFontAssetUrl } = await import('./cdn-entry');
|
|
43
|
+
// Known file, so it passes the file check and reaches the missing-script-URL guard.
|
|
44
|
+
expect(() => resolveBundledFontAssetUrl({ file: 'Carlito-Regular.woff2' })).toThrow(
|
|
45
|
+
/could not determine the script URL/,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/cdn-entry.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser / `<script>` (IIFE) entry for `@superdoc-dev/fonts`, built to
|
|
3
|
+
* `dist/superdoc-fonts.min.js` and exposed as the `SuperDocFonts` global.
|
|
4
|
+
*
|
|
5
|
+
* Unlike the bundler entry ({@link "./index"}), it resolves the `.woff2` relative to its OWN
|
|
6
|
+
* `<script>` (`document.currentScript`) instead of `import.meta.url`, so a plain CDN / self-host
|
|
7
|
+
* `<script>` tag works with no bundler:
|
|
8
|
+
*
|
|
9
|
+
* <script src="https://cdn.jsdelivr.net/npm/superdoc/dist/superdoc.min.js"></script>
|
|
10
|
+
* <script src="https://cdn.jsdelivr.net/npm/@superdoc-dev/fonts/dist/superdoc-fonts.min.js"></script>
|
|
11
|
+
* <script>
|
|
12
|
+
* new SuperDoc({ selector: '#editor', document: 'contract.docx', fonts: SuperDocFonts.superdocFonts });
|
|
13
|
+
* </script>
|
|
14
|
+
*
|
|
15
|
+
* The faces ship in the package's `assets/` dir and this bundle in `dist/`, so a face resolves to
|
|
16
|
+
* `../assets/<file>` relative to the script. On a public CDN (jsDelivr / unpkg) the whole package
|
|
17
|
+
* is served, so that resolves with no setup; self-hosting requires the `dist/` and `assets/` dirs
|
|
18
|
+
* to keep their relative layout.
|
|
19
|
+
*
|
|
20
|
+
* @beta
|
|
21
|
+
*/
|
|
22
|
+
import { BUNDLED_FONT_FILES } from './bundled-files.js';
|
|
23
|
+
import {
|
|
24
|
+
resolveCuration,
|
|
25
|
+
type BundledFontAssetContext,
|
|
26
|
+
type SuperDocFontsConfig,
|
|
27
|
+
type SuperDocFontsOptions,
|
|
28
|
+
} from './curation.js';
|
|
29
|
+
|
|
30
|
+
// Capture the executing <script> src at load. `document.currentScript` is valid while this
|
|
31
|
+
// top-level IIFE runs and null later (e.g. in async callbacks), so it MUST be read here, at module
|
|
32
|
+
// eval, not inside the resolver. Duck-typed (no `instanceof HTMLScriptElement`) so the module also
|
|
33
|
+
// evaluates under a non-DOM test environment where that constructor is absent.
|
|
34
|
+
const currentScript = typeof document !== 'undefined' ? (document.currentScript as { src?: string } | null) : null;
|
|
35
|
+
const SCRIPT_SRC: string = currentScript?.src ?? '';
|
|
36
|
+
|
|
37
|
+
const KNOWN_FILES: ReadonlySet<string> = new Set(BUNDLED_FONT_FILES);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a bundled face filename to a URL relative to THIS script. Throws on an unknown file
|
|
41
|
+
* (a version mismatch between `@superdoc-dev/fonts` and `superdoc`) and on a missing script URL,
|
|
42
|
+
* matching the bundler entry's fail-loud-not-silent contract.
|
|
43
|
+
*
|
|
44
|
+
* @beta
|
|
45
|
+
*/
|
|
46
|
+
export function resolveBundledFontAssetUrl(context: BundledFontAssetContext): string {
|
|
47
|
+
if (!KNOWN_FILES.has(context.file)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`[@superdoc-dev/fonts] no bundled asset for "${context.file}". This pack ships ` +
|
|
50
|
+
`${BUNDLED_FONT_FILES.length} faces; the file is unknown, so @superdoc-dev/fonts and ` +
|
|
51
|
+
`superdoc are likely version-mismatched. Align their versions.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (!SCRIPT_SRC) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'[@superdoc-dev/fonts] could not determine the script URL to resolve bundled fonts from. ' +
|
|
57
|
+
'Load superdoc-fonts.min.js with a normal <script src="..."> tag.',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return new URL(`../assets/${context.file}`, SCRIPT_SRC).href;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Drop-in value for SuperDoc's `fonts` config: `new SuperDoc({ fonts: SuperDocFonts.superdocFonts })`.
|
|
65
|
+
*
|
|
66
|
+
* @beta
|
|
67
|
+
*/
|
|
68
|
+
export const superdocFonts: SuperDocFontsConfig = { resolveAssetUrl: resolveBundledFontAssetUrl };
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a curated `fonts` config: the bundled pack narrowed to the families you choose. Same API
|
|
72
|
+
* and validation as the bundler entry's `createSuperDocFonts`; only the resolver differs.
|
|
73
|
+
*
|
|
74
|
+
* new SuperDoc({ fonts: SuperDocFonts.createSuperDocFonts({ exclude: ['Cooper Black'] }) });
|
|
75
|
+
*
|
|
76
|
+
* @beta
|
|
77
|
+
*/
|
|
78
|
+
export function createSuperDocFonts(options: SuperDocFontsOptions = {}): SuperDocFontsConfig {
|
|
79
|
+
return { resolveAssetUrl: resolveBundledFontAssetUrl, ...resolveCuration(options) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type { BundledFontAssetContext, SuperDocFontsConfig, SuperDocFontsOptions };
|
|
83
|
+
export { BUNDLED_FAMILY_NAMES } from './bundled-families.js';
|
|
84
|
+
export type { BundledFontFamilyName } from './bundled-families.js';
|
package/src/curation.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver-independent curation + validation shared by the two entries: the bundler entry
|
|
3
|
+
* (`index.ts`, asset URLs via `import.meta.url`) and the browser/IIFE entry (`cdn-entry.ts`, URLs
|
|
4
|
+
* relative to its own `<script>`). Each entry supplies only its own asset-URL resolver; the
|
|
5
|
+
* `include` / `exclude` validation lives here once so the two entries can never drift.
|
|
6
|
+
*
|
|
7
|
+
* @beta
|
|
8
|
+
*/
|
|
9
|
+
import { BUNDLED_FAMILY_NAMES, type BundledFontFamilyName } from './bundled-families.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Minimal structural shape of SuperDoc's bundled-font asset context. Kept LOCAL so this package
|
|
13
|
+
* has no dependency on `@superdoc/font-system`; only `file` is read. Assignable to SuperDoc's
|
|
14
|
+
* `FontAssetUrlResolver` by structural typing.
|
|
15
|
+
*
|
|
16
|
+
* @beta
|
|
17
|
+
*/
|
|
18
|
+
export interface BundledFontAssetContext {
|
|
19
|
+
/** Asset filename, e.g. `Carlito-Regular.woff2`. */
|
|
20
|
+
file: string;
|
|
21
|
+
family?: string;
|
|
22
|
+
weight?: string;
|
|
23
|
+
style?: 'normal' | 'italic';
|
|
24
|
+
source?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Per-face asset-URL resolver: maps a bundled face filename to a URL the browser can fetch. */
|
|
28
|
+
export type BundledFontAssetResolver = (context: BundledFontAssetContext) => string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Choose which bundled families SuperDoc advertises and renders, by LOGICAL Word family name
|
|
32
|
+
* (`"Calibri"`, never the physical `"Carlito"`).
|
|
33
|
+
*
|
|
34
|
+
* @beta
|
|
35
|
+
*/
|
|
36
|
+
export interface SuperDocFontsOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Allow-list: ONLY these logical families are active, e.g. `['Calibri', 'Cambria']`. Autocompletes
|
|
39
|
+
* the bundled names, but also accepts any string (case- and quote-insensitive, validated at
|
|
40
|
+
* runtime), so a list built from dynamic config still type-checks.
|
|
41
|
+
*/
|
|
42
|
+
include?: readonly (BundledFontFamilyName | (string & {}))[];
|
|
43
|
+
/** Block-list: every bundled family EXCEPT these, e.g. `['Cooper Black', 'Brush Script MT']`. */
|
|
44
|
+
exclude?: readonly (BundledFontFamilyName | (string & {}))[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The `fonts` config the entries return: the bundled-asset resolver plus the curation.
|
|
49
|
+
* Structurally assignable to SuperDoc's `fonts` option.
|
|
50
|
+
*
|
|
51
|
+
* @beta
|
|
52
|
+
*/
|
|
53
|
+
export interface SuperDocFontsConfig {
|
|
54
|
+
resolveAssetUrl: BundledFontAssetResolver;
|
|
55
|
+
bundled?: { include?: string[]; exclude?: string[] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate the curation options and return the `{ bundled? }` slice (no resolver). Each entry
|
|
60
|
+
* spreads this into its own `{ resolveAssetUrl, ...resolveCuration(options) }`.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveCuration(options: SuperDocFontsOptions = {}): {
|
|
63
|
+
bundled?: { include?: string[]; exclude?: string[] };
|
|
64
|
+
} {
|
|
65
|
+
const include = normalizeNameList(options.include, 'include');
|
|
66
|
+
const exclude = normalizeNameList(options.exclude, 'exclude');
|
|
67
|
+
// include and exclude are mutually exclusive intents; accepting both silently would hide which one
|
|
68
|
+
// wins. Reject at the API boundary (this runs in the consumer's setup code, so it fails fast and
|
|
69
|
+
// clearly) rather than picking one arbitrarily.
|
|
70
|
+
if (include && exclude) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'[@superdoc-dev/fonts] createSuperDocFonts: pass `include` OR `exclude`, not both. ' +
|
|
73
|
+
'`include` is an allow-list (only those families resolve and appear in the toolbar); ' +
|
|
74
|
+
'`exclude` keeps everything but the named families.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
// Reject names SuperDoc does not bundle. This runs in the consumer's setup code, so it fails fast
|
|
78
|
+
// and clearly - especially for `include`, where a typo would otherwise silently hide the fonts the
|
|
79
|
+
// consumer meant to keep. (Raw `fonts.bundled` set directly stays lenient and only warns.)
|
|
80
|
+
if (include) assertKnownFamilies(include, 'include');
|
|
81
|
+
if (exclude) assertKnownFamilies(exclude, 'exclude');
|
|
82
|
+
if (include) return { bundled: { include } };
|
|
83
|
+
if (exclude) return { bundled: { exclude } };
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Validate and clean a curation list: an array of non-empty strings, or omitted. Throws on misuse. */
|
|
88
|
+
function normalizeNameList(value: readonly string[] | undefined, field: 'include' | 'exclude'): string[] | undefined {
|
|
89
|
+
if (value == null) return undefined;
|
|
90
|
+
if (!Array.isArray(value)) {
|
|
91
|
+
throw new Error(`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` must be an array of Word font names.`);
|
|
92
|
+
}
|
|
93
|
+
const names = value.map((name) => (typeof name === 'string' ? name.trim() : '')).filter(Boolean);
|
|
94
|
+
if (names.length !== value.length) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` must contain only non-empty font name strings ` +
|
|
97
|
+
`(e.g. ["Calibri", "Cambria"]).`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return names.length ? names : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Normalize a family name for matching: trim, strip surrounding quotes, lowercase. */
|
|
104
|
+
function normalizeKey(name: string): string {
|
|
105
|
+
return name
|
|
106
|
+
.trim()
|
|
107
|
+
.replace(/^["']|["']$/g, '')
|
|
108
|
+
.toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const KNOWN_FAMILY_KEYS: ReadonlySet<string> = new Set(BUNDLED_FAMILY_NAMES.map(normalizeKey));
|
|
112
|
+
|
|
113
|
+
/** Bounded Levenshtein distance between two short strings, for a "did you mean" hint on a typo. */
|
|
114
|
+
function editDistance(a: string, b: string): number {
|
|
115
|
+
const m = a.length;
|
|
116
|
+
const n = b.length;
|
|
117
|
+
if (Math.abs(m - n) > 2) return 3;
|
|
118
|
+
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
119
|
+
for (let i = 1; i <= m; i += 1) {
|
|
120
|
+
const curr = [i];
|
|
121
|
+
for (let j = 1; j <= n; j += 1) {
|
|
122
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
123
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
124
|
+
}
|
|
125
|
+
prev = curr;
|
|
126
|
+
}
|
|
127
|
+
return prev[n];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The closest bundled family within edit distance 2, or null - so a typo gets a concrete suggestion. */
|
|
131
|
+
function closestKnownFamily(name: string): string | null {
|
|
132
|
+
const key = normalizeKey(name);
|
|
133
|
+
let best: string | null = null;
|
|
134
|
+
let bestDist = 3;
|
|
135
|
+
for (const family of BUNDLED_FAMILY_NAMES) {
|
|
136
|
+
const dist = editDistance(key, normalizeKey(family));
|
|
137
|
+
if (dist < bestDist) {
|
|
138
|
+
bestDist = dist;
|
|
139
|
+
best = family;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return best;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Throw on any curation name SuperDoc does not bundle, with a suggestion and the full valid list. */
|
|
146
|
+
function assertKnownFamilies(names: readonly string[], field: 'include' | 'exclude'): void {
|
|
147
|
+
const unknown = names.filter((name) => !KNOWN_FAMILY_KEYS.has(normalizeKey(name)));
|
|
148
|
+
if (unknown.length === 0) return;
|
|
149
|
+
const hints = unknown.map((name) => {
|
|
150
|
+
const suggestion = closestKnownFamily(name);
|
|
151
|
+
return suggestion ? `"${name}" (did you mean "${suggestion}"?)` : `"${name}"`;
|
|
152
|
+
});
|
|
153
|
+
throw new Error(
|
|
154
|
+
`[@superdoc-dev/fonts] createSuperDocFonts: \`${field}\` names a font SuperDoc does not bundle: ${hints.join(', ')}. ` +
|
|
155
|
+
`Curate by Word family name. Bundled families: ${BUNDLED_FAMILY_NAMES.join(', ')}.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BUNDLED_FAMILY_NAMES,
|
|
4
|
+
BUNDLED_FONT_ASSET_URLS,
|
|
5
|
+
createSuperDocFonts,
|
|
6
|
+
resolveBundledFontAssetUrl,
|
|
7
|
+
superdocFonts,
|
|
8
|
+
} from './index';
|
|
3
9
|
|
|
4
10
|
describe('superdocFonts / resolveBundledFontAssetUrl', () => {
|
|
5
11
|
it('superdocFonts is a ready-made resolveAssetUrl config', () => {
|
|
@@ -70,3 +76,12 @@ describe('createSuperDocFonts', () => {
|
|
|
70
76
|
expect(() => createSuperDocFonts({ include: [' calibri ', '"Cambria"'] })).not.toThrow();
|
|
71
77
|
});
|
|
72
78
|
});
|
|
79
|
+
|
|
80
|
+
describe('BUNDLED_FAMILY_NAMES', () => {
|
|
81
|
+
it('is exported as a non-empty list of curatable Word family names', () => {
|
|
82
|
+
expect(BUNDLED_FAMILY_NAMES.length).toBeGreaterThan(0);
|
|
83
|
+
expect(BUNDLED_FAMILY_NAMES).toContain('Calibri');
|
|
84
|
+
// Every advertised name is accepted by the curation API (the list and the validator agree).
|
|
85
|
+
expect(() => createSuperDocFonts({ include: [...BUNDLED_FAMILY_NAMES] })).not.toThrow();
|
|
86
|
+
});
|
|
87
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
* `new URL('../assets/<file>', import.meta.url)`, which Vite, Webpack 5, Next, Nuxt, esbuild, and
|
|
8
8
|
* Parcel detect, emit, and rewrite to the final hashed path.
|
|
9
9
|
*
|
|
10
|
+
* For a plain `<script>` / CDN page (no bundler), load `dist/superdoc-fonts.min.js` instead and use
|
|
11
|
+
* the `SuperDocFonts` global - same API, faces resolved relative to the script. See `cdn-entry.ts`.
|
|
12
|
+
*
|
|
10
13
|
* @beta This package is in preview; its surface may change before 1.0.
|
|
11
14
|
*/
|
|
12
|
-
import { BUNDLED_FONT_ASSET_URLS } from './asset-urls';
|
|
13
|
-
import {
|
|
15
|
+
import { BUNDLED_FONT_ASSET_URLS } from './asset-urls.js';
|
|
16
|
+
import {
|
|
17
|
+
resolveCuration,
|
|
18
|
+
type BundledFontAssetContext,
|
|
19
|
+
type SuperDocFontsConfig,
|
|
20
|
+
type SuperDocFontsOptions,
|
|
21
|
+
} from './curation.js';
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
24
|
* The full filename -> bundler-emitted URL map for every bundled face. Most consumers use
|
|
@@ -20,21 +28,16 @@ import { BUNDLED_FAMILY_NAMES } from './bundled-families';
|
|
|
20
28
|
*/
|
|
21
29
|
export { BUNDLED_FONT_ASSET_URLS };
|
|
22
30
|
|
|
31
|
+
export type { BundledFontAssetContext, SuperDocFontsConfig, SuperDocFontsOptions };
|
|
32
|
+
|
|
23
33
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* SuperDoc's `FontAssetUrlResolver` by structural typing.
|
|
34
|
+
* The curatable Word family names (and their union type), for building `include` / `exclude` lists
|
|
35
|
+
* with autocomplete. Re-exported from the generated source.
|
|
27
36
|
*
|
|
28
37
|
* @beta
|
|
29
38
|
*/
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
file: string;
|
|
33
|
-
family?: string;
|
|
34
|
-
weight?: string;
|
|
35
|
-
style?: 'normal' | 'italic';
|
|
36
|
-
source?: string;
|
|
37
|
-
}
|
|
39
|
+
export { BUNDLED_FAMILY_NAMES } from './bundled-families.js';
|
|
40
|
+
export type { BundledFontFamilyName } from './bundled-families.js';
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* Resolve a bundled substitute face filename to a bundler-emitted URL.
|
|
@@ -68,34 +71,10 @@ export function resolveBundledFontAssetUrl(context: BundledFontAssetContext): st
|
|
|
68
71
|
*
|
|
69
72
|
* @beta
|
|
70
73
|
*/
|
|
71
|
-
export const superdocFonts:
|
|
74
|
+
export const superdocFonts: SuperDocFontsConfig = {
|
|
72
75
|
resolveAssetUrl: resolveBundledFontAssetUrl,
|
|
73
76
|
};
|
|
74
77
|
|
|
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
78
|
/**
|
|
100
79
|
* Build a curated `fonts` config: the bundled pack, narrowed to the families you choose.
|
|
101
80
|
*
|
|
@@ -113,97 +92,5 @@ export interface SuperDocFontsConfig {
|
|
|
113
92
|
* @beta
|
|
114
93
|
*/
|
|
115
94
|
export function createSuperDocFonts(options: SuperDocFontsOptions = {}): SuperDocFontsConfig {
|
|
116
|
-
|
|
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
|
-
);
|
|
95
|
+
return { resolveAssetUrl: resolveBundledFontAssetUrl, ...resolveCuration(options) };
|
|
209
96
|
}
|