@superdoc-dev/fonts 0.1.0 → 0.1.1

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 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 string[];
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
+ };
@@ -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
- * Minimal structural shape of SuperDoc's bundled-font asset context. Kept LOCAL so this
22
- * package has no dependency on `@superdoc/font-system`; only `file` is read. Assignable to
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 interface BundledFontAssetContext {
28
- /** Asset filename, e.g. `Carlito-Regular.woff2`. */
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 { BUNDLED_FAMILY_NAMES } from './bundled-families';
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
- const include = normalizeNameList(options.include, 'include');
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.0",
3
+ "version": "0.1.1",
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": "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",
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
+ }
@@ -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: readonly string[] = Object.freeze([
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
+ });
@@ -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';
@@ -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 { BUNDLED_FONT_ASSET_URLS, createSuperDocFonts, resolveBundledFontAssetUrl, superdocFonts } from './index';
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 { BUNDLED_FAMILY_NAMES } from './bundled-families';
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
- * 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.
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 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
- }
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: { resolveAssetUrl: (context: BundledFontAssetContext) => string } = {
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
- 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
- );
95
+ return { resolveAssetUrl: resolveBundledFontAssetUrl, ...resolveCuration(options) };
209
96
  }