@json-to-office/shared 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  formatErrorSummary,
3
3
  transformValueErrors
4
- } from "./chunk-ZKD5BAMU.js";
4
+ } from "./chunk-LLBCT7WL.js";
5
5
  import {
6
6
  isValidSemver,
7
7
  latestVersion
@@ -198,4 +198,4 @@ export {
198
198
  isValidationSuccess,
199
199
  getValidationSummary
200
200
  };
201
- //# sourceMappingURL=chunk-GPNPMKVZ.js.map
201
+ //# sourceMappingURL=chunk-A4CY2MAM.js.map
@@ -379,4 +379,4 @@ export {
379
379
  groupErrorsByPath,
380
380
  createJsonParseError
381
381
  };
382
- //# sourceMappingURL=chunk-ZKD5BAMU.js.map
382
+ //# sourceMappingURL=chunk-LLBCT7WL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/validation/unified/schema-utils.ts","../src/validation/unified/error-formatter-config.ts","../src/validation/unified/error-transformer.ts"],"sourcesContent":["import type { TSchema, TUnion, TObject, TLiteral } from '@sinclair/typebox';\n\nconst componentNamesCache = new Map<unknown, string[]>();\n\nexport function isUnionSchema(schema: TSchema): schema is TUnion {\n return 'anyOf' in schema && Array.isArray(schema.anyOf);\n}\n\nexport function isObjectSchema(schema: TSchema): schema is TObject {\n return schema.type === 'object' && 'properties' in schema;\n}\n\nexport function isLiteralSchema(schema: TSchema): schema is TLiteral {\n return 'const' in schema;\n}\n\nexport function getObjectSchemaPropertyNames(schema: TSchema): string[] {\n if (isObjectSchema(schema)) {\n return Object.keys(schema.properties);\n }\n return [];\n}\n\nexport function getLiteralValue(schema: TSchema): unknown {\n if (isLiteralSchema(schema)) {\n return schema.const;\n }\n return undefined;\n}\n\nexport function extractStandardComponentNames(schema: TSchema): string[] {\n if (componentNamesCache.has(schema)) {\n return componentNamesCache.get(schema)!;\n }\n\n const names: string[] = [];\n\n if ('anyOf' in schema && Array.isArray(schema.anyOf)) {\n for (const componentSchema of schema.anyOf) {\n if (isObjectSchema(componentSchema)) {\n const nameProperty = componentSchema.properties?.name;\n if (nameProperty && isLiteralSchema(nameProperty)) {\n const nameValue = getLiteralValue(nameProperty);\n if (typeof nameValue === 'string') {\n names.push(nameValue);\n }\n }\n }\n }\n } else if (isObjectSchema(schema)) {\n const nameProperty = schema.properties?.name ?? schema.properties?.type;\n if (nameProperty && isLiteralSchema(nameProperty)) {\n const nameValue = getLiteralValue(nameProperty);\n if (typeof nameValue === 'string') {\n names.push(nameValue);\n }\n }\n }\n\n componentNamesCache.set(schema, names);\n return names;\n}\n\nexport function clearComponentNamesCache(): void {\n componentNamesCache.clear();\n}\n\nexport function getSchemaMetadata(schema: TSchema): {\n type?: string;\n properties?: string[];\n literal?: unknown;\n} {\n const metadata: {\n type?: string;\n properties?: string[];\n literal?: unknown;\n } = {};\n\n if ('type' in schema) {\n metadata.type = String(schema.type);\n }\n\n if (isObjectSchema(schema)) {\n metadata.properties = getObjectSchemaPropertyNames(schema);\n }\n\n if (isLiteralSchema(schema)) {\n metadata.literal = getLiteralValue(schema);\n }\n\n return metadata;\n}\n","export interface ErrorFormatterConfig {\n includeEmojis?: boolean;\n verbosity?: 'minimal' | 'normal' | 'detailed';\n includeSuggestions?: boolean;\n includePath?: boolean;\n maxMessageLength?: number;\n includeDocLinks?: boolean;\n colorSupport?: boolean;\n}\n\nfunction isCI(): boolean {\n if (typeof process === 'undefined' || !process.env) return false;\n return !!(\n process.env.CI ||\n process.env.GITHUB_ACTIONS ||\n process.env.GITLAB_CI\n );\n}\n\nfunction hasColorSupport(): boolean {\n if (typeof process === 'undefined') return false;\n if (process.env?.NO_COLOR) return false;\n if (process.env?.FORCE_COLOR) return true;\n if (process.stdout?.isTTY) return true;\n return !isCI();\n}\n\nexport const DEFAULT_ERROR_CONFIG: Required<ErrorFormatterConfig> = {\n includeEmojis: !isCI(),\n verbosity: 'normal',\n includeSuggestions: true,\n includePath: true,\n maxMessageLength: 0,\n includeDocLinks: false,\n colorSupport: hasColorSupport(),\n};\n\nexport function createErrorConfig(\n config?: ErrorFormatterConfig\n): Required<ErrorFormatterConfig> {\n return { ...DEFAULT_ERROR_CONFIG, ...config };\n}\n\nexport const ERROR_EMOJIS = {\n ERROR: '\\u274c',\n WARNING: '\\u26a0\\ufe0f',\n FIX: '\\ud83d\\udd27',\n INFO: '\\u2139\\ufe0f',\n SUCCESS: '\\u2705',\n};\n\nexport function formatErrorMessage(\n message: string,\n config: Required<ErrorFormatterConfig>\n): string {\n if (config.maxMessageLength > 0 && message.length > config.maxMessageLength) {\n return message.substring(0, config.maxMessageLength) + '...';\n }\n return message;\n}\n","import type { ValueError } from '@sinclair/typebox/value';\nimport type { ValidationError } from './types';\nimport {\n isObjectSchema,\n getObjectSchemaPropertyNames,\n getLiteralValue,\n} from './schema-utils';\nimport {\n ErrorFormatterConfig,\n createErrorConfig,\n formatErrorMessage,\n} from './error-formatter-config';\n\nexport type { TransformedError } from './types';\n\nfunction generateEnhancedMessage(\n error: ValueError,\n _config: Required<ErrorFormatterConfig>\n): string {\n const typeStr = String(error.type || '');\n const path = error.path || 'root';\n\n if (typeStr === '62' || typeStr === 'union') {\n return generateUnionErrorMessage(error);\n }\n\n if (error.message?.includes('additionalProperties')) {\n return generateAdditionalPropertiesMessage(error);\n }\n\n if (error.message?.includes('Required property')) {\n return generateRequiredPropertyMessage(error);\n }\n\n if (\n typeStr === 'string' ||\n typeStr === 'number' ||\n typeStr === 'boolean' ||\n typeStr === 'array' ||\n typeStr === 'object'\n ) {\n return generateTypeMismatchMessage(error);\n }\n\n if (typeStr === 'literal') {\n return generateLiteralErrorMessage(error);\n }\n\n if (typeStr === 'pattern' || typeStr === 'RegExp') {\n return generatePatternErrorMessage(error);\n }\n\n return `At ${path}: ${error.message}`;\n}\n\nfunction generateUnionErrorMessage(error: ValueError): string {\n const path = error.path || 'root';\n const value = error.value;\n\n if (path === 'root' || path === '/' || path === '/jsonDefinition') {\n if (value && typeof value === 'object') {\n if ('name' in value) {\n return `Invalid component configuration for '${(value as any).name}'. Check that all required fields are present.`;\n }\n if ('children' in value && Array.isArray((value as any).children)) {\n return \"Document is missing required 'name' field.\";\n }\n }\n return 'Invalid document structure. Check required fields.';\n }\n\n if (path.includes('/children/')) {\n if (value && typeof value === 'object' && 'name' in value) {\n const componentType = (value as any).name;\n return `Invalid component configuration for type '${componentType}'. Check that all required fields are present and correctly formatted.`;\n }\n return 'Invalid component structure. Each component must have a \"name\" field and valid configuration.';\n }\n\n return `Value at ${path} doesn't match any of the expected formats. Check the structure and required fields.`;\n}\n\nfunction generateAdditionalPropertiesMessage(error: ValueError): string {\n const path = error.path || 'root';\n const value = error.value;\n\n if (typeof value === 'object' && value !== null) {\n const schema = error.schema;\n if (schema && isObjectSchema(schema)) {\n const knownProps = getObjectSchemaPropertyNames(schema);\n const actualProps = Object.keys(value);\n const unknownProps = actualProps.filter((p) => !knownProps.includes(p));\n\n if (unknownProps.length > 0) {\n return (\n `Unknown properties at ${path}: ${unknownProps.join(', ')}. ` +\n `Allowed properties are: ${knownProps.join(', ')}`\n );\n }\n }\n }\n\n return `Additional properties not allowed at ${path}. Check for typos or unsupported fields.`;\n}\n\nfunction generateRequiredPropertyMessage(error: ValueError): string {\n const path = error.path || 'root';\n const match = error.message?.match(/Required property '([^']+)'/);\n\n if (match) {\n const propName = match[1];\n return `Missing required field '${propName}' at ${path}. This field is mandatory.`;\n }\n\n return `Missing required property at ${path}. Check that all mandatory fields are present.`;\n}\n\nfunction generateTypeMismatchMessage(error: ValueError): string {\n const path = error.path || 'root';\n const expectedType = String(error.type);\n const actualType = Array.isArray(error.value) ? 'array' : typeof error.value;\n\n if (path.includes('alignment')) {\n return `Invalid alignment value at ${path}. Expected one of: left, center, right, justify`;\n }\n\n if (path.includes('color')) {\n return `Invalid color value at ${path}. Use hex format (#RRGGBB), rgb(r,g,b), or a named color`;\n }\n\n if (path.includes('fontSize') || path.includes('size')) {\n return `Invalid size value at ${path}. Expected a number (in points)`;\n }\n\n if (\n path.includes('margin') ||\n path.includes('padding') ||\n path.includes('spacing')\n ) {\n return `Invalid spacing value at ${path}. Expected a number or spacing object with top/bottom/left/right`;\n }\n\n return `Type mismatch at ${path}: Expected ${expectedType} but got ${actualType}`;\n}\n\nfunction generateLiteralErrorMessage(error: ValueError): string {\n const path = error.path || 'root';\n const expected = error.schema\n ? JSON.stringify(getLiteralValue(error.schema))\n : 'specific value';\n const actual = JSON.stringify(error.value);\n\n return `Invalid value at ${path}: Expected exactly ${expected} but got ${actual}`;\n}\n\nfunction generatePatternErrorMessage(error: ValueError): string {\n const path = error.path || 'root';\n\n if (path.includes('email')) {\n return `Invalid email format at ${path}. Use format: user@example.com`;\n }\n if (path.includes('url') || path.includes('link')) {\n return `Invalid URL format at ${path}. Use format: https://example.com`;\n }\n if (path.includes('date')) {\n return `Invalid date format at ${path}. Use ISO format: YYYY-MM-DD`;\n }\n\n return `Value at ${path} doesn't match the required pattern`;\n}\n\nexport function transformValueError(\n error: ValueError,\n jsonString?: string,\n config?: ErrorFormatterConfig\n): ValidationError {\n const formatterConfig = createErrorConfig(config);\n\n const enhancedMessage = generateEnhancedMessage(error, formatterConfig);\n\n const baseError: ValidationError = {\n path: error.path || 'root',\n message: formatErrorMessage(\n enhancedMessage || error.message,\n formatterConfig\n ),\n code: String(error.type || 'validation_error'),\n value: error.value,\n };\n\n if (formatterConfig.includeSuggestions) {\n const suggestion = getSuggestion(error, formatterConfig);\n if (suggestion) {\n baseError.suggestion = formatErrorMessage(suggestion, formatterConfig);\n }\n }\n\n if (jsonString && error.path) {\n const position = calculatePosition(jsonString, error.path);\n if (position) {\n baseError.line = position.line;\n baseError.column = position.column;\n }\n }\n\n return baseError;\n}\n\nexport function transformValueErrors(\n errors: ValueError[],\n options?: {\n jsonString?: string;\n maxErrors?: number;\n }\n): ValidationError[] {\n const maxErrors = options?.maxErrors ?? Number.MAX_SAFE_INTEGER;\n const result: ValidationError[] = [];\n const seenPaths = new Set<string>();\n\n for (const error of errors) {\n if (result.length >= maxErrors) break;\n\n const errorKey = `${error.path}:${error.type}`;\n\n if (!seenPaths.has(errorKey)) {\n seenPaths.add(errorKey);\n result.push(transformValueError(error, options?.jsonString, undefined));\n }\n }\n\n return result;\n}\n\nexport function calculatePosition(\n jsonString: string,\n path: string\n): { line: number; column: number } | null {\n try {\n const pathParts = path.split('/').filter(Boolean);\n if (pathParts.length === 0) {\n return { line: 1, column: 1 };\n }\n\n const lastPart = pathParts[pathParts.length - 1];\n const searchPattern = `\"${lastPart}\"`;\n const index = jsonString.indexOf(searchPattern);\n\n if (index === -1) {\n return { line: 1, column: 1 };\n }\n\n const beforeError = jsonString.substring(0, index);\n const lines = beforeError.split('\\n');\n const line = lines.length;\n const column = lines[lines.length - 1].length + 1;\n\n return { line, column };\n } catch {\n return null;\n }\n}\n\nfunction getSuggestion(\n error: ValueError,\n _config: Required<ErrorFormatterConfig>\n): string | undefined {\n const { type, path } = error;\n const typeStr = String(type);\n\n if (typeStr === '62' || typeStr === 'union') {\n if (path === 'root' || path === '/') {\n return 'Ensure the document has proper structure with a root \"name\" field';\n }\n if (path?.includes('/children/')) {\n return 'Check that the component has a valid \"name\" and all required fields';\n }\n return 'Review the structure and ensure all required fields are present with correct types';\n }\n\n if (error.message?.includes('additionalProperties')) {\n return 'Remove any unknown or unsupported fields.';\n }\n\n if (error.message?.includes('Required property')) {\n return 'Add the missing required field to fix this error';\n }\n\n if (typeStr === 'string') {\n if (path?.includes('color')) {\n return 'Use a valid color format (hex: #RRGGBB, rgb: rgb(r,g,b), or named color)';\n }\n return 'Provide a text string value';\n }\n\n if (typeStr === 'number') {\n return 'Provide a numeric value';\n }\n\n if (typeStr === 'boolean') {\n return 'Use true or false (without quotes)';\n }\n\n if (typeStr === 'array') {\n return 'Provide an array of values using square brackets []';\n }\n\n if (typeStr === 'object') {\n return 'Provide an object with key-value pairs using curly braces {}';\n }\n\n if (typeStr === 'literal') {\n const expected = error.schema\n ? JSON.stringify(getLiteralValue(error.schema))\n : 'specific value';\n return `Use exactly this value: ${expected}`;\n }\n\n return undefined;\n}\n\nexport function formatErrorSummary(errors: ValidationError[]): string {\n if (errors.length === 0) return 'No errors';\n\n if (errors.length === 1) {\n return errors[0].message;\n }\n\n const summary = errors\n .slice(0, 3)\n .map((e) => `${e.path}: ${e.message}`)\n .join(', ');\n\n if (errors.length > 3) {\n return `${summary} and ${errors.length - 3} more...`;\n }\n\n return summary;\n}\n\nexport function groupErrorsByPath(\n errors: ValidationError[]\n): Map<string, ValidationError[]> {\n const grouped = new Map<string, ValidationError[]>();\n\n for (const error of errors) {\n const path = error.path || 'root';\n const group = grouped.get(path) || [];\n group.push(error);\n grouped.set(path, group);\n }\n\n return grouped;\n}\n\nexport function createJsonParseError(\n error: Error,\n jsonString: string\n): ValidationError {\n const match = error.message.match(/position (\\d+)/);\n const position = match ? parseInt(match[1], 10) : 0;\n\n let line = 1;\n let column = 1;\n\n if (position > 0) {\n const lines = jsonString.substring(0, position).split('\\n');\n line = lines.length;\n column = lines[lines.length - 1].length + 1;\n }\n\n return {\n path: 'root',\n message: `JSON Parse Error: ${error.message}`,\n code: 'json_parse_error',\n line,\n column,\n suggestion: 'Check for missing commas, quotes, or brackets',\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,243 @@
1
+ // src/fonts/sources/ttf-name.ts
2
+ var FAMILY_NAME_IDS = /* @__PURE__ */ new Set([
3
+ 1,
4
+ // Font Family
5
+ 4,
6
+ // Full Font Name
7
+ 6,
8
+ // PostScript Name
9
+ 16
10
+ // Typographic/Preferred Family
11
+ ]);
12
+ var MAGIC_HEAD_CHECKSUM = 2981146554;
13
+ function sfntChecksum(buf) {
14
+ let sum = 0;
15
+ const end = buf.length;
16
+ const aligned = end - end % 4;
17
+ for (let i = 0; i < aligned; i += 4) {
18
+ sum = sum + buf.readUInt32BE(i) >>> 0;
19
+ }
20
+ if (aligned < end) {
21
+ let chunk = 0;
22
+ const remaining = end - aligned;
23
+ if (remaining >= 1) chunk |= buf[aligned] << 24;
24
+ if (remaining >= 2) chunk |= buf[aligned + 1] << 16;
25
+ if (remaining >= 3) chunk |= buf[aligned + 2] << 8;
26
+ sum = sum + chunk >>> 0;
27
+ }
28
+ return sum >>> 0;
29
+ }
30
+ function encodeString(record, value) {
31
+ if (record.platformID === 1) {
32
+ return Buffer.from(value, "ascii");
33
+ }
34
+ const out = Buffer.alloc(value.length * 2);
35
+ for (let i = 0; i < value.length; i += 1) {
36
+ out.writeUInt16BE(value.charCodeAt(i), i * 2);
37
+ }
38
+ return out;
39
+ }
40
+ function buildNameTable(records) {
41
+ const count = records.length;
42
+ const headerSize = 6 + count * 12;
43
+ let heapSize = 0;
44
+ for (const r of records) heapSize += r.bytes.length;
45
+ const raw = Buffer.alloc(headerSize + heapSize);
46
+ raw.writeUInt16BE(0, 0);
47
+ raw.writeUInt16BE(count, 2);
48
+ raw.writeUInt16BE(headerSize, 4);
49
+ let heapCursor = 0;
50
+ for (let i = 0; i < count; i += 1) {
51
+ const r = records[i];
52
+ const ro = 6 + i * 12;
53
+ raw.writeUInt16BE(r.platformID, ro);
54
+ raw.writeUInt16BE(r.encodingID, ro + 2);
55
+ raw.writeUInt16BE(r.languageID, ro + 4);
56
+ raw.writeUInt16BE(r.nameID, ro + 6);
57
+ raw.writeUInt16BE(r.bytes.length, ro + 8);
58
+ raw.writeUInt16BE(heapCursor, ro + 10);
59
+ r.bytes.copy(raw, headerSize + heapCursor);
60
+ heapCursor += r.bytes.length;
61
+ }
62
+ return raw;
63
+ }
64
+ function rewriteNameTable(input, decide) {
65
+ if (input.length < 12) return input;
66
+ const version = input.readUInt32BE(0);
67
+ const isSfnt = version === 65536 || version === 1330926671 || version === 1953658213 || version === 1954115633;
68
+ if (!isSfnt) return input;
69
+ const numTables = input.readUInt16BE(4);
70
+ if (numTables === 0 || input.length < 12 + numTables * 16) return input;
71
+ const tables = [];
72
+ for (let i = 0; i < numTables; i += 1) {
73
+ const eo = 12 + i * 16;
74
+ const tag = input.toString("ascii", eo, eo + 4);
75
+ const checksum = input.readUInt32BE(eo + 4);
76
+ const offset = input.readUInt32BE(eo + 8);
77
+ const length = input.readUInt32BE(eo + 12);
78
+ if (offset + length > input.length) return input;
79
+ tables.push({
80
+ tag,
81
+ checksum,
82
+ offset: 0,
83
+ originalOffset: offset,
84
+ data: input.slice(offset, offset + length)
85
+ });
86
+ }
87
+ const nameIdx = tables.findIndex((t) => t.tag === "name");
88
+ if (nameIdx === -1) return input;
89
+ const nameBuf = tables[nameIdx].data;
90
+ if (nameBuf.length < 6) return input;
91
+ if (nameBuf.readUInt16BE(0) !== 0) return input;
92
+ const recordCount = nameBuf.readUInt16BE(2);
93
+ const stringOffset = nameBuf.readUInt16BE(4);
94
+ if (nameBuf.length < 6 + recordCount * 12) return input;
95
+ const records = [];
96
+ for (let i = 0; i < recordCount; i += 1) {
97
+ const ro = 6 + i * 12;
98
+ const platformID = nameBuf.readUInt16BE(ro);
99
+ const encodingID = nameBuf.readUInt16BE(ro + 2);
100
+ const languageID = nameBuf.readUInt16BE(ro + 4);
101
+ const nameID = nameBuf.readUInt16BE(ro + 6);
102
+ const length = nameBuf.readUInt16BE(ro + 8);
103
+ const offset = nameBuf.readUInt16BE(ro + 10);
104
+ const bytes = nameBuf.slice(
105
+ stringOffset + offset,
106
+ stringOffset + offset + length
107
+ );
108
+ records.push({ platformID, encodingID, languageID, nameID, bytes });
109
+ }
110
+ const survivors = [];
111
+ for (const r of records) {
112
+ const verdict = decide(r);
113
+ if (verdict.action === "drop") continue;
114
+ if (verdict.action === "set") {
115
+ r.bytes = encodeString(r, verdict.value);
116
+ }
117
+ survivors.push(r);
118
+ }
119
+ records.length = 0;
120
+ records.push(...survivors);
121
+ const rebuiltName = buildNameTable(records);
122
+ tables[nameIdx] = {
123
+ ...tables[nameIdx],
124
+ data: rebuiltName,
125
+ checksum: sfntChecksum(rebuiltName)
126
+ };
127
+ tables.sort((a, b) => a.originalOffset - b.originalOffset);
128
+ let cursor = 12 + tables.length * 16;
129
+ for (const t of tables) {
130
+ cursor = cursor + 3 & ~3;
131
+ t.offset = cursor;
132
+ cursor += t.data.length;
133
+ }
134
+ const totalSize = cursor + 3 & ~3;
135
+ const out = Buffer.alloc(totalSize);
136
+ input.copy(out, 0, 0, 12);
137
+ out.writeUInt16BE(tables.length, 4);
138
+ const dirTables = [...tables].sort(
139
+ (a, b) => a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0
140
+ );
141
+ for (let i = 0; i < dirTables.length; i += 1) {
142
+ const t = dirTables[i];
143
+ const eo = 12 + i * 16;
144
+ out.write(t.tag, eo, 4, "ascii");
145
+ out.writeUInt32BE(t.checksum, eo + 4);
146
+ out.writeUInt32BE(t.offset, eo + 8);
147
+ out.writeUInt32BE(t.data.length, eo + 12);
148
+ }
149
+ for (const t of tables) {
150
+ t.data.copy(out, t.offset);
151
+ }
152
+ const headTable = tables.find((t) => t.tag === "head");
153
+ if (headTable && headTable.data.length >= 12) {
154
+ out.writeUInt32BE(0, headTable.offset + 8);
155
+ const fontSum = sfntChecksum(out);
156
+ const adjustment = MAGIC_HEAD_CHECKSUM - fontSum >>> 0;
157
+ out.writeUInt32BE(adjustment, headTable.offset + 8);
158
+ }
159
+ return out;
160
+ }
161
+ function rewriteFontFamilyName(input, newFamily) {
162
+ const psForbidden = /[[\](){}<>/%]/g;
163
+ const isAscii = /^[\x00-\x7f]*$/.test(newFamily);
164
+ const psName = newFamily.replace(/\s+/g, "").replace(psForbidden, "").replace(/[^\x21-\x7e]/g, "");
165
+ return rewriteNameTable(input, (r) => {
166
+ if (!FAMILY_NAME_IDS.has(r.nameID)) return { action: "keep" };
167
+ if (r.platformID === 1 && !isAscii) return { action: "drop" };
168
+ return { action: "set", value: r.nameID === 6 ? psName : newFamily };
169
+ });
170
+ }
171
+ var STANDARD_SUBFAMILY = {
172
+ 100: "Thin",
173
+ 200: "ExtraLight",
174
+ 300: "Light",
175
+ 400: "Regular",
176
+ 500: "Medium",
177
+ 600: "SemiBold",
178
+ 700: "Bold",
179
+ 800: "ExtraBold",
180
+ 900: "Black"
181
+ };
182
+ function standardSubfamilyNames(weight, italic) {
183
+ const base = STANDARD_SUBFAMILY[weight];
184
+ if (!base) return null;
185
+ return {
186
+ typographic: italic ? `${base} Italic` : base,
187
+ legacy: weight >= 600 ? italic ? "Bold Italic" : "Bold" : italic ? "Italic" : "Regular"
188
+ };
189
+ }
190
+ function rewriteFontSubfamilyNames(input, weight, italic) {
191
+ const std = standardSubfamilyNames(weight, italic);
192
+ if (!std) return input;
193
+ return rewriteNameTable(input, (r) => {
194
+ if (r.nameID === 2) return { action: "set", value: std.legacy };
195
+ if (r.nameID === 17) return { action: "set", value: std.typographic };
196
+ return { action: "keep" };
197
+ });
198
+ }
199
+
200
+ // src/fonts/sources/url-allowlist.ts
201
+ var FONT_URL_ALLOWLIST = [
202
+ "fonts.gstatic.com",
203
+ "fonts.googleapis.com",
204
+ "cdn.jsdelivr.net"
205
+ ];
206
+ function isAllowedFontUrl(url) {
207
+ let parsed;
208
+ try {
209
+ parsed = new URL(url);
210
+ } catch {
211
+ return false;
212
+ }
213
+ if (parsed.protocol !== "https:") return false;
214
+ return FONT_URL_ALLOWLIST.includes(parsed.hostname.toLowerCase());
215
+ }
216
+
217
+ // src/fonts/sources/format.ts
218
+ function detectFontFormat(buf) {
219
+ if (buf.length < 4) return "unknown";
220
+ const b0 = buf[0], b1 = buf[1], b2 = buf[2], b3 = buf[3];
221
+ if (b0 === 0 && b1 === 1 && b2 === 0 && b3 === 0 || b0 === 116 && b1 === 114 && b2 === 117 && b3 === 101 || b0 === 116 && b1 === 121 && b2 === 112 && b3 === 49) {
222
+ return "ttf";
223
+ }
224
+ if (b0 === 79 && b1 === 84 && b2 === 84 && b3 === 79) return "otf";
225
+ if (b0 === 119 && b1 === 79 && b2 === 70 && b3 === 70) return "woff";
226
+ if (b0 === 119 && b1 === 79 && b2 === 70 && b3 === 50) return "woff2";
227
+ if (buf.length >= 36 && buf[34] === 76 && buf[35] === 80) return "eot";
228
+ if (b0 === 128 && b1 === 1) return "pfb";
229
+ if (buf.length >= 14 && buf.slice(0, 14).toString("ascii") === "%!PS-AdobeFont") {
230
+ return "pfb";
231
+ }
232
+ return "unknown";
233
+ }
234
+
235
+ export {
236
+ rewriteFontFamilyName,
237
+ standardSubfamilyNames,
238
+ rewriteFontSubfamilyNames,
239
+ FONT_URL_ALLOWLIST,
240
+ isAllowedFontUrl,
241
+ detectFontFormat
242
+ };
243
+ //# sourceMappingURL=chunk-MCUIFSUN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fonts/sources/ttf-name.ts","../src/fonts/sources/url-allowlist.ts","../src/fonts/sources/format.ts"],"sourcesContent":["/**\n * Name-table rewriting for TTF/OTF sfnt fonts. Two public transforms share\n * the rebuild machinery:\n *\n * - `rewriteFontFamilyName` — rewrite `nameID` 1 / 4 / 6 / 16 to a\n * synthetic family name. Used by the preview-side font stagers so that\n * running-text references like `\"Inter Light\"` resolve to the correct\n * face when the stager registers it with Core Text / fontconfig / GDI\n * (all of which index by the font's internal `name` table rather than\n * the filename).\n *\n * - `rewriteFontSubfamilyNames` — rewrite `nameID` 2 / 17 to the standard\n * subfamily strings for a (weight, italic) pair. Used by the variable-\n * font instancer: harfbuzz preserves the source font's name records\n * verbatim, so an instanced Bold would otherwise keep the variable\n * font's default-instance \"Regular\" subfamily and trip\n * `validateFontMetadata`.\n *\n * Each transform rebuilds the whole font: new `name` table bytes, new\n * table directory with shifted offsets, recomputed per-table checksums,\n * and the magic `head.checkSumAdjustment` recomputed against the whole\n * output buffer. Nothing else is touched.\n *\n * OTF (CFF-flavoured) and TTF (glyf-flavoured) share the sfnt outer\n * structure, so the same code handles both.\n */\n\nconst FAMILY_NAME_IDS = new Set<number>([\n 1, // Font Family\n 4, // Full Font Name\n 6, // PostScript Name\n 16, // Typographic/Preferred Family\n]);\n\nconst MAGIC_HEAD_CHECKSUM = 0xb1b0afba;\n\ninterface NameRecord {\n platformID: number;\n encodingID: number;\n languageID: number;\n nameID: number;\n bytes: Buffer;\n}\n\ninterface TableEntry {\n tag: string;\n checksum: number;\n /** Assigned when rebuilding. */\n offset: number;\n data: Buffer;\n originalOffset: number;\n}\n\n/**\n * Compute the 32-bit big-endian uint sum of `buf`, treating it as a\n * stream of uint32s zero-padded to a 4-byte boundary. Used for per-table\n * checksums and the whole-font `head.checkSumAdjustment`.\n */\nfunction sfntChecksum(buf: Buffer): number {\n let sum = 0;\n const end = buf.length;\n const aligned = end - (end % 4);\n for (let i = 0; i < aligned; i += 4) {\n sum = (sum + buf.readUInt32BE(i)) >>> 0;\n }\n if (aligned < end) {\n let chunk = 0;\n const remaining = end - aligned;\n if (remaining >= 1) chunk |= buf[aligned] << 24;\n if (remaining >= 2) chunk |= buf[aligned + 1] << 16;\n if (remaining >= 3) chunk |= buf[aligned + 2] << 8;\n sum = (sum + chunk) >>> 0;\n }\n return sum >>> 0;\n}\n\nfunction encodeString(record: NameRecord, value: string): Buffer {\n // Platform 3 (Microsoft) and 0 (Unicode) use UTF-16 BE. Platform 1\n // (Macintosh) uses a legacy Roman encoding we approximate with ASCII —\n // non-ASCII family names are rare in this code path.\n if (record.platformID === 1) {\n return Buffer.from(value, 'ascii');\n }\n const out = Buffer.alloc(value.length * 2);\n for (let i = 0; i < value.length; i += 1) {\n out.writeUInt16BE(value.charCodeAt(i), i * 2);\n }\n return out;\n}\n\nfunction buildNameTable(records: NameRecord[]): Buffer {\n const count = records.length;\n const headerSize = 6 + count * 12;\n let heapSize = 0;\n for (const r of records) heapSize += r.bytes.length;\n const raw = Buffer.alloc(headerSize + heapSize);\n raw.writeUInt16BE(0, 0); // format 0\n raw.writeUInt16BE(count, 2);\n raw.writeUInt16BE(headerSize, 4); // stringOffset\n let heapCursor = 0;\n for (let i = 0; i < count; i += 1) {\n const r = records[i];\n const ro = 6 + i * 12;\n raw.writeUInt16BE(r.platformID, ro);\n raw.writeUInt16BE(r.encodingID, ro + 2);\n raw.writeUInt16BE(r.languageID, ro + 4);\n raw.writeUInt16BE(r.nameID, ro + 6);\n raw.writeUInt16BE(r.bytes.length, ro + 8);\n raw.writeUInt16BE(heapCursor, ro + 10);\n r.bytes.copy(raw, headerSize + heapCursor);\n heapCursor += r.bytes.length;\n }\n return raw;\n}\n\n/** Per-record decision for `rewriteNameTable`. */\ntype NameRewrite =\n | { action: 'keep' }\n | { action: 'drop' }\n | { action: 'set'; value: string };\n\n/**\n * Return a copy of `input` whose name records have been mapped through\n * `decide`. Returns the original buffer unchanged if the font has no\n * `name` table or the sfnt header is invalid.\n */\nfunction rewriteNameTable(\n input: Buffer,\n decide: (record: NameRecord) => NameRewrite\n): Buffer {\n if (input.length < 12) return input;\n const version = input.readUInt32BE(0);\n // Accept sfnt (0x00010000), OTTO (OpenType CFF), true, typ1.\n const isSfnt =\n version === 0x00010000 ||\n version === 0x4f54544f /* OTTO */ ||\n version === 0x74727565 /* true */ ||\n version === 0x74797031; /* typ1 */\n if (!isSfnt) return input;\n\n const numTables = input.readUInt16BE(4);\n if (numTables === 0 || input.length < 12 + numTables * 16) return input;\n\n // Read every table's directory entry, slurp its data.\n const tables: TableEntry[] = [];\n for (let i = 0; i < numTables; i += 1) {\n const eo = 12 + i * 16;\n const tag = input.toString('ascii', eo, eo + 4);\n const checksum = input.readUInt32BE(eo + 4);\n const offset = input.readUInt32BE(eo + 8);\n const length = input.readUInt32BE(eo + 12);\n if (offset + length > input.length) return input;\n tables.push({\n tag,\n checksum,\n offset: 0,\n originalOffset: offset,\n data: input.slice(offset, offset + length),\n });\n }\n\n const nameIdx = tables.findIndex((t) => t.tag === 'name');\n if (nameIdx === -1) return input;\n\n // Parse existing name records so we preserve all non-family entries.\n const nameBuf = tables[nameIdx].data;\n if (nameBuf.length < 6) return input;\n // `buildNameTable` emits format 0, which has no language-tag section. A\n // format-1 table keeps its tags after the name records, and any record with\n // languageID >= 0x8000 is an index into them — rewriting it as format 0\n // would strip the tags and leave those records pointing at nothing. Leaving\n // the font unstamped is the lesser loss, so hand it back untouched.\n // Preserving the tag section (and re-homing its string offsets) is the\n // follow-up if a real font ever needs the rewrite.\n if (nameBuf.readUInt16BE(0) !== 0) return input;\n const recordCount = nameBuf.readUInt16BE(2);\n const stringOffset = nameBuf.readUInt16BE(4);\n if (nameBuf.length < 6 + recordCount * 12) return input;\n\n const records: NameRecord[] = [];\n for (let i = 0; i < recordCount; i += 1) {\n const ro = 6 + i * 12;\n const platformID = nameBuf.readUInt16BE(ro);\n const encodingID = nameBuf.readUInt16BE(ro + 2);\n const languageID = nameBuf.readUInt16BE(ro + 4);\n const nameID = nameBuf.readUInt16BE(ro + 6);\n const length = nameBuf.readUInt16BE(ro + 8);\n const offset = nameBuf.readUInt16BE(ro + 10);\n const bytes = nameBuf.slice(\n stringOffset + offset,\n stringOffset + offset + length\n );\n records.push({ platformID, encodingID, languageID, nameID, bytes });\n }\n\n const survivors: NameRecord[] = [];\n for (const r of records) {\n const verdict = decide(r);\n if (verdict.action === 'drop') continue;\n if (verdict.action === 'set') {\n r.bytes = encodeString(r, verdict.value);\n }\n survivors.push(r);\n }\n records.length = 0;\n records.push(...survivors);\n\n const rebuiltName = buildNameTable(records);\n tables[nameIdx] = {\n ...tables[nameIdx],\n data: rebuiltName,\n checksum: sfntChecksum(rebuiltName),\n };\n\n // Preserve original physical order so tables whose offsets follow each\n // other stay contiguous (some consumers skim by offset rather than\n // directory). Offsets get reassigned either way — this is purely\n // aesthetic. Sort is stable.\n tables.sort((a, b) => a.originalOffset - b.originalOffset);\n\n // Assign new offsets, 4-byte aligned.\n let cursor = 12 + tables.length * 16;\n for (const t of tables) {\n cursor = (cursor + 3) & ~3;\n t.offset = cursor;\n cursor += t.data.length;\n }\n const totalSize = (cursor + 3) & ~3;\n\n const out = Buffer.alloc(totalSize);\n // Header — copy sfnt version, entrySelector, etc. verbatim; we preserve\n // numTables since we're not adding/removing entries.\n input.copy(out, 0, 0, 12);\n out.writeUInt16BE(tables.length, 4);\n\n // Directory entries go in alphabetical tag order per the sfnt spec.\n const dirTables = [...tables].sort((a, b) =>\n a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0\n );\n for (let i = 0; i < dirTables.length; i += 1) {\n const t = dirTables[i];\n const eo = 12 + i * 16;\n out.write(t.tag, eo, 4, 'ascii');\n out.writeUInt32BE(t.checksum, eo + 4);\n out.writeUInt32BE(t.offset, eo + 8);\n out.writeUInt32BE(t.data.length, eo + 12);\n }\n\n // Write each table's bytes at its new offset. `out` is zero-filled, so\n // the 0–3 byte alignment padding after each table is already correct.\n for (const t of tables) {\n t.data.copy(out, t.offset);\n }\n\n // Recompute head.checkSumAdjustment. The algorithm: zero the field,\n // sum the whole font, then set the field to MAGIC - sum.\n const headTable = tables.find((t) => t.tag === 'head');\n if (headTable && headTable.data.length >= 12) {\n out.writeUInt32BE(0, headTable.offset + 8);\n const fontSum = sfntChecksum(out);\n const adjustment = (MAGIC_HEAD_CHECKSUM - fontSum) >>> 0;\n out.writeUInt32BE(adjustment, headTable.offset + 8);\n }\n\n return out;\n}\n\n/**\n * Return a copy of `input` whose name table has `nameID` 1/4/6/16 rewritten\n * to `newFamily`. Returns the original buffer unchanged if the font has no\n * `name` table or the sfnt header is invalid.\n */\nexport function rewriteFontFamilyName(\n input: Buffer,\n newFamily: string\n): Buffer {\n // PostScript names (nameID 6) are restricted to printable ASCII 33-126\n // minus `[](){}<>/%` per the OpenType spec — fold spaces out and strip\n // any forbidden chars so Word doesn't silently reject the font.\n const psForbidden = /[[\\](){}<>/%]/g;\n // eslint-disable-next-line no-control-regex\n const isAscii = /^[\\x00-\\x7f]*$/.test(newFamily);\n const psName = newFamily\n .replace(/\\s+/g, '')\n .replace(psForbidden, '')\n // eslint-disable-next-line no-control-regex\n .replace(/[^\\x21-\\x7e]/g, '');\n return rewriteNameTable(input, (r) => {\n if (!FAMILY_NAME_IDS.has(r.nameID)) return { action: 'keep' };\n // Platform 1 (Macintosh Roman) only round-trips ASCII. For non-ASCII\n // family names (e.g. CJK), `Buffer.from(value, 'ascii')` silently\n // drops the high bytes and produces garbled Roman-encoded strings\n // that Core Text may still index. Drop those records instead —\n // platforms 0 (Unicode) and 3 (Microsoft) carry the UTF-16 form and\n // are what modern consumers prefer anyway.\n if (r.platformID === 1 && !isAscii) return { action: 'drop' };\n return { action: 'set', value: r.nameID === 6 ? psName : newFamily };\n });\n}\n\nconst STANDARD_SUBFAMILY: Record<number, string> = {\n 100: 'Thin',\n 200: 'ExtraLight',\n 300: 'Light',\n 400: 'Regular',\n 500: 'Medium',\n 600: 'SemiBold',\n 700: 'Bold',\n 800: 'ExtraBold',\n 900: 'Black',\n};\n\n/**\n * Standard OpenType subfamily strings for a (weight, italic) pair, or null\n * for a non-standard weight. `typographic` is the nameID 17 form (full\n * weight vocabulary); `legacy` is the nameID 2 form, restricted to the\n * four-style RIBBI model (Regular/Italic/Bold/Bold Italic) that GDI-era\n * consumers expect. Single source of truth shared with\n * `validateFontMetadata` so writer and checker cannot diverge.\n */\nexport function standardSubfamilyNames(\n weight: number,\n italic: boolean\n): { typographic: string; legacy: string } | null {\n const base = STANDARD_SUBFAMILY[weight];\n if (!base) return null;\n return {\n typographic: italic ? `${base} Italic` : base,\n legacy:\n weight >= 600\n ? italic\n ? 'Bold Italic'\n : 'Bold'\n : italic\n ? 'Italic'\n : 'Regular',\n };\n}\n\n/**\n * Return a copy of `input` whose existing `nameID` 2/17 records carry the\n * standard subfamily strings for (weight, italic). Missing records are not\n * added. Returns the original buffer unchanged for non-standard weights,\n * or if the font has no `name` table or the sfnt header is invalid.\n */\nexport function rewriteFontSubfamilyNames(\n input: Buffer,\n weight: number,\n italic: boolean\n): Buffer {\n const std = standardSubfamilyNames(weight, italic);\n if (!std) return input;\n return rewriteNameTable(input, (r) => {\n if (r.nameID === 2) return { action: 'set', value: std.legacy };\n if (r.nameID === 17) return { action: 'set', value: std.typographic };\n return { action: 'keep' };\n });\n}\n","/**\n * Hostname allowlist for font fetchers.\n *\n * `url-fetcher` and `variable-fetcher` can be handed arbitrary URLs via\n * `FontRegistryEntry.sources`, which may originate from document JSON. Without\n * a guard, a malicious doc could point fetchers at internal hosts (SSRF), the\n * filesystem (`file://`), or the IMDS endpoint. Limit downloads to the hosts\n * our catalog + UPSTREAM_OVERRIDES actually target.\n *\n * Keep the list small and HTTPS-only. Expansions should be deliberate code\n * reviews, not config-driven — the cost of a new domain is the code change.\n */\n\nexport const FONT_URL_ALLOWLIST: readonly string[] = [\n 'fonts.gstatic.com',\n 'fonts.googleapis.com',\n 'cdn.jsdelivr.net',\n];\n\nexport function isAllowedFontUrl(url: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return false;\n }\n if (parsed.protocol !== 'https:') return false;\n return FONT_URL_ALLOWLIST.includes(parsed.hostname.toLowerCase());\n}\n","/**\n * Font format detection from magic bytes.\n * Source: OpenType spec + WOFF1/WOFF2 W3C specs.\n */\n\nimport type { ResolvedFontSource } from '../types';\n\nexport function detectFontFormat(buf: Buffer): ResolvedFontSource['format'] {\n if (buf.length < 4) return 'unknown';\n\n const b0 = buf[0],\n b1 = buf[1],\n b2 = buf[2],\n b3 = buf[3];\n\n // TTF: 0x00010000 (SFNT) or 'true' (0x74727565) or 'typ1' (0x74797031)\n if (\n (b0 === 0x00 && b1 === 0x01 && b2 === 0x00 && b3 === 0x00) ||\n (b0 === 0x74 && b1 === 0x72 && b2 === 0x75 && b3 === 0x65) ||\n (b0 === 0x74 && b1 === 0x79 && b2 === 0x70 && b3 === 0x31)\n ) {\n return 'ttf';\n }\n // OTF: 'OTTO'\n if (b0 === 0x4f && b1 === 0x54 && b2 === 0x54 && b3 === 0x4f) return 'otf';\n // WOFF: 'wOFF'\n if (b0 === 0x77 && b1 === 0x4f && b2 === 0x46 && b3 === 0x46) return 'woff';\n // WOFF2: 'wOF2'\n if (b0 === 0x77 && b1 === 0x4f && b2 === 0x46 && b3 === 0x32) return 'woff2';\n // EOT: version bytes at offset 8-11 — rougher signature\n if (buf.length >= 36 && buf[34] === 0x4c && buf[35] === 0x50) return 'eot';\n // PostScript Type 1 (.pfb) — binary container marker byte 0x80 followed by\n // segment type 0x01 (ASCII). Also match the text-form ASCII header\n // \"%!PS-AdobeFont\". Note: .pfm (metric files) have no reliable magic and\n // stay in 'unknown' — same treatment (rejection at the loader).\n if (b0 === 0x80 && b1 === 0x01) return 'pfb';\n if (\n buf.length >= 14 &&\n buf.slice(0, 14).toString('ascii') === '%!PS-AdobeFont'\n ) {\n return 'pfb';\n }\n\n return 'unknown';\n}\n\n/**\n * Formats we detect but cannot legally embed in an OOXML document:\n * WOFF/WOFF2 are web-only containers; PostScript (.pfb) is explicitly\n * disallowed by Microsoft's embedding guidance.\n */\nexport const UNEMBEDDABLE_FORMATS = new Set<ResolvedFontSource['format']>([\n 'woff',\n 'woff2',\n 'pfb',\n]);\n"],"mappings":"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","names":[]}
@@ -31,10 +31,11 @@ declare class FontDiskCache {
31
31
  }
32
32
 
33
33
  /**
34
- * Variable-font instancer. Fetches a variable TTF once (disk-cached), then
35
- * pins its `wght` axis (plus any additional axes) to produce a clean static
36
- * TTF per requested weight. Uses harfbuzz via `subset-font` pure JS + WASM,
37
- * no native toolchain.
34
+ * Variable-font instancer. Fetches a variable font once (disk-cached; TTF,
35
+ * OTF, or WOFF/WOFF2 fontverter converts compressed containers before
36
+ * instancing), then pins its `wght` axis (plus any additional axes) to
37
+ * produce a clean static TTF per requested weight. Uses harfbuzz via
38
+ * `subset-font` — pure JS + WASM, no native toolchain.
38
39
  *
39
40
  * Why this exists. Google Fonts serves pre-instanced static TTFs for many
40
41
  * families, but the instancing step is lossy: Inter Thin (100) and
@@ -44,9 +45,9 @@ declare class FontDiskCache {
44
45
  * `wght` axis at exactly 100 vs 200 produces properly distinct instances.
45
46
  *
46
47
  * Cache strategy:
47
- * 1. Raw variable TTF cached at key `varsrc|<url>` — one download per URL
48
+ * 1. Raw variable font cached at key `varsrc|<url>` — one download per URL
48
49
  * per process (+ optional disk layer).
49
- * 2. Instanced static TTF cached at `variable|<url>|<weight>|<italic>` —
50
+ * 2. Instanced static TTF cached at `variable2|<url>|<weight>|<italic>` —
50
51
  * avoids re-running harfbuzz for weights we've already produced.
51
52
  *
52
53
  * Full-glyph retention. subset-font's `text` parameter drives which
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  detectFontFormat,
3
- isAllowedFontUrl
4
- } from "../chunk-FDSJYZ5W.js";
3
+ isAllowedFontUrl,
4
+ rewriteFontSubfamilyNames
5
+ } from "../chunk-MCUIFSUN.js";
5
6
 
6
7
  // src/fonts/sources/file-loader.ts
7
8
  import { readFile } from "fs/promises";
@@ -76,7 +77,7 @@ function rawCacheKey(url) {
76
77
  }
77
78
  function instanceCacheKey(url, weight, italic, axes) {
78
79
  const axisPart = axes ? "|" + Object.entries(axes).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",") : "";
79
- return `variable|${url}|${weight}|${italic ? "i" : "r"}${axisPart}`;
80
+ return `variable2|${url}|${weight}|${italic ? "i" : "r"}${axisPart}`;
80
81
  }
81
82
  var cachedBmpCharset = null;
82
83
  function bmpCharset() {
@@ -123,7 +124,7 @@ async function fetchVariableSource(opts) {
123
124
  if (buf.length < 1024)
124
125
  return { error: `response too small (${buf.length}B)` };
125
126
  const format = detectFontFormat(buf);
126
- if (format !== "ttf" && format !== "otf") {
127
+ if (format !== "ttf" && format !== "otf" && format !== "woff" && format !== "woff2") {
127
128
  return { error: `unexpected font format: ${format}` };
128
129
  }
129
130
  opts.memoryCache?.set(key, buf);
@@ -219,6 +220,7 @@ async function fetchVariableFontSource(opts) {
219
220
  ]
220
221
  };
221
222
  }
223
+ instanced = rewriteFontSubfamilyNames(instanced, opts.weight, opts.italic);
222
224
  opts.memoryCache?.set(key, instanced);
223
225
  await opts.diskCache?.set(key, instanced);
224
226
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/fonts/sources/file-loader.ts","../../src/fonts/cache/disk-cache.ts","../../src/fonts/sources/variable-fetcher.ts","../../src/fonts/rasterize-faces.ts"],"sourcesContent":["/**\n * Load a .ttf/.otf file from disk.\n * Node-only — called from the render pipeline.\n */\n\nimport { readFile } from 'fs/promises';\nimport { isAbsolute, resolve as resolvePath } from 'path';\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\n\nexport interface FileSourceInput {\n path: string;\n weight?: number;\n italic?: boolean;\n baseDir?: string;\n}\n\n/** Read a font file and wrap as a ResolvedFontSource. */\nexport async function loadFileFontSource(\n input: FileSourceInput\n): Promise<ResolvedFontSource> {\n const fullPath = isAbsolute(input.path)\n ? input.path\n : resolvePath(input.baseDir ?? process.cwd(), input.path);\n const data = await readFile(fullPath);\n const format = detectFontFormat(data);\n if (format === 'unknown') {\n throw new Error(\n `Font file at \"${fullPath}\" is not a recognized font file (expected TTF/OTF/WOFF/WOFF2)`\n );\n }\n // No format rejection here: bytes flow to the LibreOffice preview\n // stager, which handles WOFF/WOFF2 natively via fontconfig on\n // Linux/macOS. Office output never embeds these bytes — substitute/\n // custom modes rely on recipient-side fonts.\n return {\n data,\n weight: input.weight ?? 400,\n italic: input.italic ?? false,\n format,\n };\n}\n","/**\n * On-disk cache for fetched Google Fonts TTFs.\n * Optional — only active when a cacheDir is provided. Node-only.\n */\n\nimport { createHash } from 'crypto';\nimport { mkdir, readFile, writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nexport class FontDiskCache {\n private readonly dir: string;\n // In-flight promise dedupes the first-write mkdir across concurrent set()\n // calls. Without it, two simultaneous cold-cache writes could both see\n // `ensured=false`, both issue mkdir, and both flip the flag afterwards —\n // harmless today (recursive mkdir is idempotent) but the pattern is\n // right and leaves room to add per-directory locks if we ever need to.\n private ensurePromise: Promise<void> | null = null;\n\n constructor(dir: string) {\n this.dir = dir;\n }\n\n private ensureDir(): Promise<void> {\n if (!this.ensurePromise) {\n this.ensurePromise = mkdir(this.dir, { recursive: true }).then(\n () => undefined\n );\n }\n return this.ensurePromise;\n }\n\n private pathFor(key: string): string {\n const hash = createHash('sha256').update(key).digest('hex').slice(0, 24);\n return join(this.dir, `${hash}.bin`);\n }\n\n async get(key: string): Promise<Buffer | undefined> {\n try {\n return await readFile(this.pathFor(key));\n } catch {\n return undefined;\n }\n }\n\n async set(key: string, value: Buffer): Promise<void> {\n await this.ensureDir();\n await writeFile(this.pathFor(key), value);\n }\n}\n","/**\n * Variable-font instancer. Fetches a variable TTF once (disk-cached), then\n * pins its `wght` axis (plus any additional axes) to produce a clean static\n * TTF per requested weight. Uses harfbuzz via `subset-font` — pure JS + WASM,\n * no native toolchain.\n *\n * Why this exists. Google Fonts serves pre-instanced static TTFs for many\n * families, but the instancing step is lossy: Inter Thin (100) and\n * ExtraLight (200) both ship with `OS/2.usWeightClass=250` and near-\n * identical glyph outlines (xAvgCharWidth differs by 1.8%, glyf table\n * differs by 83 bytes out of 135 KB). Pinning the upstream variable TTF's\n * `wght` axis at exactly 100 vs 200 produces properly distinct instances.\n *\n * Cache strategy:\n * 1. Raw variable TTF cached at key `varsrc|<url>` — one download per URL\n * per process (+ optional disk layer).\n * 2. Instanced static TTF cached at `variable|<url>|<weight>|<italic>` —\n * avoids re-running harfbuzz for weights we've already produced.\n *\n * Full-glyph retention. subset-font's `text` parameter drives which\n * codepoints' glyphs survive. We pass every BMP codepoint so the output\n * is effectively a full-glyph static (not a subset) for any Latin /\n * Cyrillic / Greek / Vietnamese-covering family — which includes every\n * entry in our POPULAR_GOOGLE_FONTS catalog. Supplementary-plane glyphs\n * (emoji) would be dropped, but those aren't in the variable families we\n * target. `preserveNameIds` keeps the human-readable name records our\n * downstream normalization expects.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\nimport { isAllowedFontUrl } from './url-allowlist';\n\n// `subset-font` carries a harfbuzz WASM payload and is Node-only. Lazy-load\n// so a browser bundler that chases the generic `sources/` tree doesn't pull\n// it in. Cached across calls so the WASM heap is created once per process.\nlet subsetFontPromise: Promise<typeof import('subset-font').default> | null =\n null;\nfunction loadSubsetFont(): Promise<typeof import('subset-font').default> {\n if (!subsetFontPromise) {\n subsetFontPromise = import('subset-font').then((m) => m.default);\n }\n return subsetFontPromise;\n}\n\nexport interface VariableFetchOptions {\n url: string;\n weight: number;\n italic: boolean;\n /** Extra axis pins merged on top of the derived `wght` pin (e.g. `ital`,\n * `opsz`, `slnt`). Rare — the `weight`/`italic` pair is usually enough. */\n axes?: Record<string, number>;\n /** Family label used in error messages and diagnostics. */\n familyLabel?: string;\n fetchTimeoutMs?: number;\n fetcher?: typeof fetch;\n memoryCache?: {\n get(key: string): Buffer | undefined;\n set(key: string, value: Buffer): void;\n };\n diskCache?: {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n };\n}\n\nfunction rawCacheKey(url: string): string {\n return `varsrc|${url}`;\n}\n\nfunction instanceCacheKey(\n url: string,\n weight: number,\n italic: boolean,\n axes?: Record<string, number>\n): string {\n // Axes go into the key deterministically so different axis pins don't\n // collide. Sorted so `{a:1,b:2}` and `{b:2,a:1}` hash the same.\n const axisPart = axes\n ? '|' +\n Object.entries(axes)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}=${v}`)\n .join(',')\n : '';\n return `variable|${url}|${weight}|${italic ? 'i' : 'r'}${axisPart}`;\n}\n\n/**\n * String covering every assigned BMP codepoint (0x20-0xFFFF minus surrogate\n * range). Built lazily on first use — ~127 KiB of UTF-16 memory (0xFFFF\n * codepoints × 2 bytes per UTF-16 code unit, minus the surrogate range)\n * held for the lifetime of the process, which is negligible next to the\n * WASM heap harfbuzz already carries.\n */\nlet cachedBmpCharset: string | null = null;\nfunction bmpCharset(): string {\n if (cachedBmpCharset) return cachedBmpCharset;\n let s = '';\n for (let cp = 0x20; cp <= 0xffff; cp++) {\n // Surrogate range is structurally invalid as standalone codepoints —\n // harfbuzz rejects them. Skip.\n if (cp >= 0xd800 && cp <= 0xdfff) continue;\n s += String.fromCodePoint(cp);\n }\n cachedBmpCharset = s;\n return s;\n}\n\ntype FetchResult = { buf: Buffer } | { error: string };\n\nasync function fetchVariableSource(\n opts: VariableFetchOptions\n): Promise<FetchResult> {\n if (!isAllowedFontUrl(opts.url)) {\n return { error: 'host not in allowlist or non-HTTPS' };\n }\n const key = rawCacheKey(opts.url);\n const mem = opts.memoryCache?.get(key);\n if (mem) return { buf: mem };\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return { buf: disk };\n }\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 10000);\n try {\n const f = opts.fetcher ?? fetch;\n // redirect: 'manual' so the allowlist can't be bypassed via Location.\n let res = await f(opts.url, { signal: ctrl.signal, redirect: 'manual' });\n let hops = 0;\n while (res.status >= 300 && res.status < 400 && res.status !== 304) {\n const next = res.headers.get('location');\n if (!next) return { error: `${res.status} with no Location` };\n const resolved = new URL(next, opts.url).toString();\n if (!isAllowedFontUrl(resolved)) {\n return { error: `redirect to disallowed host: ${resolved}` };\n }\n if (++hops > 3) return { error: 'too many redirects' };\n res = await f(resolved, { signal: ctrl.signal, redirect: 'manual' });\n }\n if (!res.ok) return { error: `HTTP ${res.status} ${res.statusText}` };\n const ab = await res.arrayBuffer();\n const buf = Buffer.from(ab);\n // Sanity-check: reject sub-1KB or non-TTF responses up front. The\n // instancer would fail loudly on garbage, but a clear \"wrong URL\"\n // signal here shortens the debug cycle.\n if (buf.length < 1024)\n return { error: `response too small (${buf.length}B)` };\n const format = detectFontFormat(buf);\n if (format !== 'ttf' && format !== 'otf') {\n return { error: `unexpected font format: ${format}` };\n }\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n return { buf };\n } catch (err) {\n return { error: (err as Error).message };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function fetchVariableFontSource(\n opts: VariableFetchOptions\n): Promise<{ source?: ResolvedFontSource; warnings?: string[] }> {\n const key = instanceCacheKey(opts.url, opts.weight, opts.italic, opts.axes);\n const mem = opts.memoryCache?.get(key);\n if (mem) {\n return {\n source: {\n data: mem,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(mem),\n },\n warnings: [],\n };\n }\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return {\n source: {\n data: disk,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(disk),\n },\n warnings: [],\n };\n }\n\n const fetched = await fetchVariableSource(opts);\n if ('error' in fetched) {\n return {\n warnings: [\n `Variable font fetch \"${opts.url}\" for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${fetched.error}; falling back to host defaults.`,\n ],\n };\n }\n const raw = fetched.buf;\n\n // Harfbuzz refuses to emit WOFF2 for subset-font's default SFNT target,\n // but we need plain SFNT anyway — Office embeds TTFs, not compressed\n // formats. Pin the weight (and any extra axes) and preserve the name\n // records that our downstream `normalizeNameTable` depends on.\n //\n // Note: italic is encoded by URL (separate italic master), not by axis pin.\n // The `ital` axis exists on some fonts but not others (Inter ships a\n // separate InterVariable-Italic.ttf instead). Callers that want to force\n // an axis pin can pass `axes: { ital: 1 }` explicitly.\n const variationAxes: Record<string, number> = {\n wght: opts.weight,\n ...(opts.axes ?? {}),\n };\n\n let instanced: Buffer;\n try {\n const subsetFont = await loadSubsetFont();\n instanced = await subsetFont(raw, bmpCharset(), {\n targetFormat: 'sfnt',\n variationAxes,\n // Keep every common name record. harfbuzz drops the ones not in\n // this list; our downstream rewrites need 1/2/4/6/16/17 intact.\n preserveNameIds: [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,\n 20, 21, 22, 23, 24, 25,\n ],\n });\n } catch (err) {\n return {\n warnings: [\n `Variable font instancing for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${(err as Error).message}`,\n ],\n };\n }\n\n opts.memoryCache?.set(key, instanced);\n await opts.diskCache?.set(key, instanced);\n return {\n source: {\n data: instanced,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(instanced),\n },\n warnings: [],\n };\n}\n","/**\n * `ResolvedFont[]` ⇄ `RasterizeFontFace[]` — the one encoder/decoder pair for\n * shipping font bytes to the pptx rasterizer.\n *\n * The docx side encodes (core-docx, from `resolveDocumentFonts`) and the\n * rasterizer side decodes (jto-cli, before handing the faces to a\n * `FontStager`). Keeping both halves here means the two cannot drift on\n * base64 handling or on the family-name convention.\n *\n * FAMILY NAMES STAY UNSYNTHESIZED. The wire carries the catalog family\n * (\"Inter\"); the stager applies `synthesizeFamilyName` +\n * `rewriteFontFamilyName` to produce the sub-family the presentation\n * actually references (\"Inter Light\"). Encoding a pre-synthesized name here\n * would make the stager apply the suffix twice.\n *\n * Buffer-dependent → Node-only. Exported from `@json-to-office/shared/fonts/node`.\n */\n\nimport type { ResolvedFont, ResolvedFontSource } from './types';\nimport type { RasterizeFontFace } from '../types/services';\nimport type { GenerationWarning } from '../types/warnings';\n\n/**\n * Formats the rasterizer's native stagers can actually register.\n *\n * All three stagers (fontconfig, macOS Core Text, Windows GDI) write every\n * staged source as a `.ttf` and register it as a raw sfnt, and they rename\n * the face through `rewriteFontFamilyName`, which returns the buffer\n * UNCHANGED for anything without an sfnt header. So a WOFF/WOFF2 (or EOT, or\n * PostScript) source is staged as bytes no font system parses, under the\n * catalog family rather than the synthesized sub-family the presentation\n * references — it renders as fallback text, silently.\n *\n * Shipping those bytes anyway costs wire size, disk writes, and a distinct\n * rasterizer cache key for a render that is identical to the fontless one.\n * An allowlist (rather than a WOFF denylist) keeps any format added to\n * `ResolvedFontSource['format']` later excluded until a stager can handle it.\n */\nconst STAGEABLE_FORMATS = new Set<ResolvedFontSource['format']>(['ttf', 'otf']);\n\n/**\n * Flatten resolved fonts into the serializable wire faces (one face per\n * source variant). Entries with no sources — safe-only fonts, which the\n * renderer resolves against system faces — carry no bytes and are skipped,\n * as are sources in a format no stager can register.\n *\n * @param warnings - sink for one warning per dropped source, shaped like every\n * other generation warning so a caller can hand in the same array it already\n * collects. Both docx entry paths do: a dropped face renders as a fallback,\n * which is precisely the silent substitution this pipeline exists to make\n * visible, so it must not be discoverable only by reading the code.\n */\nexport function toRasterizeFontFaces(\n fonts: readonly ResolvedFont[],\n warnings?: GenerationWarning[]\n): RasterizeFontFace[] {\n const faces: RasterizeFontFace[] = [];\n for (const font of fonts) {\n if (font.sources.length === 0) continue;\n for (const source of font.sources) {\n if (!STAGEABLE_FORMATS.has(source.format)) {\n warnings?.push({\n component: 'fontRegistry',\n severity: 'warning',\n context: { code: 'FONT_FORMAT_NOT_RASTERIZABLE' },\n message:\n `\"${font.family}\" weight ${source.weight}` +\n `${source.italic ? ' italic' : ''} is ${source.format}; the rasterizer's ` +\n `font stagers only register TTF/OTF, so this face is omitted and the ` +\n `visual renders with a fallback face.`,\n });\n continue;\n }\n faces.push({\n family: font.family,\n weight: source.weight,\n italic: source.italic,\n data: source.data.toString('base64'),\n format: source.format as RasterizeFontFace['format'],\n });\n }\n }\n return faces;\n}\n\n/**\n * Inverse of {@link toRasterizeFontFaces}: regroup wire faces back into\n * `ResolvedFont[]` so the existing `FontStager.stage(ResolvedFont[], …)`\n * signature needs no change. Grouping is by exact (case-sensitive) family,\n * matching how the registry keys resolved fonts.\n */\nexport function fromRasterizeFontFaces(\n faces: readonly RasterizeFontFace[]\n): ResolvedFont[] {\n const byFamily = new Map<string, ResolvedFont>();\n for (const face of faces) {\n let font = byFamily.get(face.family);\n if (!font) {\n font = { family: face.family, sources: [], warnings: [] };\n byFamily.set(face.family, font);\n }\n const source: ResolvedFontSource = {\n data: Buffer.from(face.data, 'base64'),\n weight: face.weight,\n italic: face.italic,\n format: face.format ?? 'ttf',\n };\n font.sources.push(source);\n }\n return [...byFamily.values()];\n}\n"],"mappings":"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","names":["readFile"]}
1
+ {"version":3,"sources":["../../src/fonts/sources/file-loader.ts","../../src/fonts/cache/disk-cache.ts","../../src/fonts/sources/variable-fetcher.ts","../../src/fonts/rasterize-faces.ts"],"sourcesContent":["/**\n * Load a .ttf/.otf file from disk.\n * Node-only — called from the render pipeline.\n */\n\nimport { readFile } from 'fs/promises';\nimport { isAbsolute, resolve as resolvePath } from 'path';\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\n\nexport interface FileSourceInput {\n path: string;\n weight?: number;\n italic?: boolean;\n baseDir?: string;\n}\n\n/** Read a font file and wrap as a ResolvedFontSource. */\nexport async function loadFileFontSource(\n input: FileSourceInput\n): Promise<ResolvedFontSource> {\n const fullPath = isAbsolute(input.path)\n ? input.path\n : resolvePath(input.baseDir ?? process.cwd(), input.path);\n const data = await readFile(fullPath);\n const format = detectFontFormat(data);\n if (format === 'unknown') {\n throw new Error(\n `Font file at \"${fullPath}\" is not a recognized font file (expected TTF/OTF/WOFF/WOFF2)`\n );\n }\n // No format rejection here: bytes flow to the LibreOffice preview\n // stager, which handles WOFF/WOFF2 natively via fontconfig on\n // Linux/macOS. Office output never embeds these bytes — substitute/\n // custom modes rely on recipient-side fonts.\n return {\n data,\n weight: input.weight ?? 400,\n italic: input.italic ?? false,\n format,\n };\n}\n","/**\n * On-disk cache for fetched Google Fonts TTFs.\n * Optional — only active when a cacheDir is provided. Node-only.\n */\n\nimport { createHash } from 'crypto';\nimport { mkdir, readFile, writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nexport class FontDiskCache {\n private readonly dir: string;\n // In-flight promise dedupes the first-write mkdir across concurrent set()\n // calls. Without it, two simultaneous cold-cache writes could both see\n // `ensured=false`, both issue mkdir, and both flip the flag afterwards —\n // harmless today (recursive mkdir is idempotent) but the pattern is\n // right and leaves room to add per-directory locks if we ever need to.\n private ensurePromise: Promise<void> | null = null;\n\n constructor(dir: string) {\n this.dir = dir;\n }\n\n private ensureDir(): Promise<void> {\n if (!this.ensurePromise) {\n this.ensurePromise = mkdir(this.dir, { recursive: true }).then(\n () => undefined\n );\n }\n return this.ensurePromise;\n }\n\n private pathFor(key: string): string {\n const hash = createHash('sha256').update(key).digest('hex').slice(0, 24);\n return join(this.dir, `${hash}.bin`);\n }\n\n async get(key: string): Promise<Buffer | undefined> {\n try {\n return await readFile(this.pathFor(key));\n } catch {\n return undefined;\n }\n }\n\n async set(key: string, value: Buffer): Promise<void> {\n await this.ensureDir();\n await writeFile(this.pathFor(key), value);\n }\n}\n","/**\n * Variable-font instancer. Fetches a variable font once (disk-cached; TTF,\n * OTF, or WOFF/WOFF2 — fontverter converts compressed containers before\n * instancing), then pins its `wght` axis (plus any additional axes) to\n * produce a clean static TTF per requested weight. Uses harfbuzz via\n * `subset-font` — pure JS + WASM, no native toolchain.\n *\n * Why this exists. Google Fonts serves pre-instanced static TTFs for many\n * families, but the instancing step is lossy: Inter Thin (100) and\n * ExtraLight (200) both ship with `OS/2.usWeightClass=250` and near-\n * identical glyph outlines (xAvgCharWidth differs by 1.8%, glyf table\n * differs by 83 bytes out of 135 KB). Pinning the upstream variable TTF's\n * `wght` axis at exactly 100 vs 200 produces properly distinct instances.\n *\n * Cache strategy:\n * 1. Raw variable font cached at key `varsrc|<url>` — one download per URL\n * per process (+ optional disk layer).\n * 2. Instanced static TTF cached at `variable2|<url>|<weight>|<italic>` —\n * avoids re-running harfbuzz for weights we've already produced.\n *\n * Full-glyph retention. subset-font's `text` parameter drives which\n * codepoints' glyphs survive. We pass every BMP codepoint so the output\n * is effectively a full-glyph static (not a subset) for any Latin /\n * Cyrillic / Greek / Vietnamese-covering family — which includes every\n * entry in our POPULAR_GOOGLE_FONTS catalog. Supplementary-plane glyphs\n * (emoji) would be dropped, but those aren't in the variable families we\n * target. `preserveNameIds` keeps the human-readable name records our\n * downstream normalization expects.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\nimport { rewriteFontSubfamilyNames } from './ttf-name';\nimport { isAllowedFontUrl } from './url-allowlist';\n\n// `subset-font` carries a harfbuzz WASM payload and is Node-only. Lazy-load\n// so a browser bundler that chases the generic `sources/` tree doesn't pull\n// it in. Cached across calls so the WASM heap is created once per process.\nlet subsetFontPromise: Promise<typeof import('subset-font').default> | null =\n null;\nfunction loadSubsetFont(): Promise<typeof import('subset-font').default> {\n if (!subsetFontPromise) {\n subsetFontPromise = import('subset-font').then((m) => m.default);\n }\n return subsetFontPromise;\n}\n\nexport interface VariableFetchOptions {\n url: string;\n weight: number;\n italic: boolean;\n /** Extra axis pins merged on top of the derived `wght` pin (e.g. `ital`,\n * `opsz`, `slnt`). Rare — the `weight`/`italic` pair is usually enough. */\n axes?: Record<string, number>;\n /** Family label used in error messages and diagnostics. */\n familyLabel?: string;\n fetchTimeoutMs?: number;\n fetcher?: typeof fetch;\n memoryCache?: {\n get(key: string): Buffer | undefined;\n set(key: string, value: Buffer): void;\n };\n diskCache?: {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n };\n}\n\nfunction rawCacheKey(url: string): string {\n return `varsrc|${url}`;\n}\n\nfunction instanceCacheKey(\n url: string,\n weight: number,\n italic: boolean,\n axes?: Record<string, number>\n): string {\n // Axes go into the key deterministically so different axis pins don't\n // collide. Sorted so `{a:1,b:2}` and `{b:2,a:1}` hash the same.\n const axisPart = axes\n ? '|' +\n Object.entries(axes)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}=${v}`)\n .join(',')\n : '';\n // `variable2`: v2 stamps standard subfamily names into the instanced\n // output — bumped so persistent disk caches drop pre-stamp instances.\n return `variable2|${url}|${weight}|${italic ? 'i' : 'r'}${axisPart}`;\n}\n\n/**\n * String covering every assigned BMP codepoint (0x20-0xFFFF minus surrogate\n * range). Built lazily on first use — ~127 KiB of UTF-16 memory (0xFFFF\n * codepoints × 2 bytes per UTF-16 code unit, minus the surrogate range)\n * held for the lifetime of the process, which is negligible next to the\n * WASM heap harfbuzz already carries.\n */\nlet cachedBmpCharset: string | null = null;\nfunction bmpCharset(): string {\n if (cachedBmpCharset) return cachedBmpCharset;\n let s = '';\n for (let cp = 0x20; cp <= 0xffff; cp++) {\n // Surrogate range is structurally invalid as standalone codepoints —\n // harfbuzz rejects them. Skip.\n if (cp >= 0xd800 && cp <= 0xdfff) continue;\n s += String.fromCodePoint(cp);\n }\n cachedBmpCharset = s;\n return s;\n}\n\ntype FetchResult = { buf: Buffer } | { error: string };\n\nasync function fetchVariableSource(\n opts: VariableFetchOptions\n): Promise<FetchResult> {\n if (!isAllowedFontUrl(opts.url)) {\n return { error: 'host not in allowlist or non-HTTPS' };\n }\n const key = rawCacheKey(opts.url);\n const mem = opts.memoryCache?.get(key);\n if (mem) return { buf: mem };\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return { buf: disk };\n }\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 10000);\n try {\n const f = opts.fetcher ?? fetch;\n // redirect: 'manual' so the allowlist can't be bypassed via Location.\n let res = await f(opts.url, { signal: ctrl.signal, redirect: 'manual' });\n let hops = 0;\n while (res.status >= 300 && res.status < 400 && res.status !== 304) {\n const next = res.headers.get('location');\n if (!next) return { error: `${res.status} with no Location` };\n const resolved = new URL(next, opts.url).toString();\n if (!isAllowedFontUrl(resolved)) {\n return { error: `redirect to disallowed host: ${resolved}` };\n }\n if (++hops > 3) return { error: 'too many redirects' };\n res = await f(resolved, { signal: ctrl.signal, redirect: 'manual' });\n }\n if (!res.ok) return { error: `HTTP ${res.status} ${res.statusText}` };\n const ab = await res.arrayBuffer();\n const buf = Buffer.from(ab);\n // Sanity-check: reject sub-1KB or non-TTF responses up front. The\n // instancer would fail loudly on garbage, but a clear \"wrong URL\"\n // signal here shortens the debug cycle.\n if (buf.length < 1024)\n return { error: `response too small (${buf.length}B)` };\n const format = detectFontFormat(buf);\n // WOFF/WOFF2 sources are fine: subset-font funnels every input through\n // fontverter (sfnt/woff/woff2 → truetype) before harfbuzz sees it, and\n // the instanced output is always plain sfnt. Needed in practice —\n // rsms/inter publishes its italic variable master only as woff2.\n if (\n format !== 'ttf' &&\n format !== 'otf' &&\n format !== 'woff' &&\n format !== 'woff2'\n ) {\n return { error: `unexpected font format: ${format}` };\n }\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n return { buf };\n } catch (err) {\n return { error: (err as Error).message };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function fetchVariableFontSource(\n opts: VariableFetchOptions\n): Promise<{ source?: ResolvedFontSource; warnings?: string[] }> {\n const key = instanceCacheKey(opts.url, opts.weight, opts.italic, opts.axes);\n const mem = opts.memoryCache?.get(key);\n if (mem) {\n return {\n source: {\n data: mem,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(mem),\n },\n warnings: [],\n };\n }\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return {\n source: {\n data: disk,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(disk),\n },\n warnings: [],\n };\n }\n\n const fetched = await fetchVariableSource(opts);\n if ('error' in fetched) {\n return {\n warnings: [\n `Variable font fetch \"${opts.url}\" for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${fetched.error}; falling back to host defaults.`,\n ],\n };\n }\n const raw = fetched.buf;\n\n // Harfbuzz refuses to emit WOFF2 for subset-font's default SFNT target,\n // but we need plain SFNT anyway — Office embeds TTFs, not compressed\n // formats. Pin the weight (and any extra axes) and preserve the name\n // records that our downstream name rewrites (`rewriteFontFamilyName`,\n // `rewriteFontSubfamilyNames`) depend on.\n //\n // Note: italic is encoded by URL (separate italic master), not by axis pin.\n // The `ital` axis exists on some fonts but not others (Inter ships a\n // separate InterVariable-Italic.ttf instead). Callers that want to force\n // an axis pin can pass `axes: { ital: 1 }` explicitly.\n const variationAxes: Record<string, number> = {\n wght: opts.weight,\n ...(opts.axes ?? {}),\n };\n\n let instanced: Buffer;\n try {\n const subsetFont = await loadSubsetFont();\n instanced = await subsetFont(raw, bmpCharset(), {\n targetFormat: 'sfnt',\n variationAxes,\n // Keep every common name record. harfbuzz drops the ones not in\n // this list; our downstream rewrites need 1/2/4/6/16/17 intact.\n preserveNameIds: [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,\n 20, 21, 22, 23, 24, 25,\n ],\n });\n } catch (err) {\n return {\n warnings: [\n `Variable font instancing for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${(err as Error).message}`,\n ],\n };\n }\n\n // harfbuzz preserves the source's name records verbatim, so the instanced\n // static still carries the variable font's default-instance subfamily\n // (typically \"Regular\") in nameID 2/17 — which would trip\n // validateFontMetadata's FONT_METADATA_DEFECT warning for every non-\n // Regular weight. Stamp the standard subfamily for the pinned pair.\n instanced = rewriteFontSubfamilyNames(instanced, opts.weight, opts.italic);\n\n opts.memoryCache?.set(key, instanced);\n await opts.diskCache?.set(key, instanced);\n return {\n source: {\n data: instanced,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(instanced),\n },\n warnings: [],\n };\n}\n","/**\n * `ResolvedFont[]` ⇄ `RasterizeFontFace[]` — the one encoder/decoder pair for\n * shipping font bytes to the pptx rasterizer.\n *\n * The docx side encodes (core-docx, from `resolveDocumentFonts`) and the\n * rasterizer side decodes (jto-cli, before handing the faces to a\n * `FontStager`). Keeping both halves here means the two cannot drift on\n * base64 handling or on the family-name convention.\n *\n * FAMILY NAMES STAY UNSYNTHESIZED. The wire carries the catalog family\n * (\"Inter\"); the stager applies `synthesizeFamilyName` +\n * `rewriteFontFamilyName` to produce the sub-family the presentation\n * actually references (\"Inter Light\"). Encoding a pre-synthesized name here\n * would make the stager apply the suffix twice.\n *\n * Buffer-dependent → Node-only. Exported from `@json-to-office/shared/fonts/node`.\n */\n\nimport type { ResolvedFont, ResolvedFontSource } from './types';\nimport type { RasterizeFontFace } from '../types/services';\nimport type { GenerationWarning } from '../types/warnings';\n\n/**\n * Formats the rasterizer's native stagers can actually register.\n *\n * All three stagers (fontconfig, macOS Core Text, Windows GDI) write every\n * staged source as a `.ttf` and register it as a raw sfnt, and they rename\n * the face through `rewriteFontFamilyName`, which returns the buffer\n * UNCHANGED for anything without an sfnt header. So a WOFF/WOFF2 (or EOT, or\n * PostScript) source is staged as bytes no font system parses, under the\n * catalog family rather than the synthesized sub-family the presentation\n * references — it renders as fallback text, silently.\n *\n * Shipping those bytes anyway costs wire size, disk writes, and a distinct\n * rasterizer cache key for a render that is identical to the fontless one.\n * An allowlist (rather than a WOFF denylist) keeps any format added to\n * `ResolvedFontSource['format']` later excluded until a stager can handle it.\n */\nconst STAGEABLE_FORMATS = new Set<ResolvedFontSource['format']>(['ttf', 'otf']);\n\n/**\n * Flatten resolved fonts into the serializable wire faces (one face per\n * source variant). Entries with no sources — safe-only fonts, which the\n * renderer resolves against system faces — carry no bytes and are skipped,\n * as are sources in a format no stager can register.\n *\n * @param warnings - sink for one warning per dropped source, shaped like every\n * other generation warning so a caller can hand in the same array it already\n * collects. Both docx entry paths do: a dropped face renders as a fallback,\n * which is precisely the silent substitution this pipeline exists to make\n * visible, so it must not be discoverable only by reading the code.\n */\nexport function toRasterizeFontFaces(\n fonts: readonly ResolvedFont[],\n warnings?: GenerationWarning[]\n): RasterizeFontFace[] {\n const faces: RasterizeFontFace[] = [];\n for (const font of fonts) {\n if (font.sources.length === 0) continue;\n for (const source of font.sources) {\n if (!STAGEABLE_FORMATS.has(source.format)) {\n warnings?.push({\n component: 'fontRegistry',\n severity: 'warning',\n context: { code: 'FONT_FORMAT_NOT_RASTERIZABLE' },\n message:\n `\"${font.family}\" weight ${source.weight}` +\n `${source.italic ? ' italic' : ''} is ${source.format}; the rasterizer's ` +\n `font stagers only register TTF/OTF, so this face is omitted and the ` +\n `visual renders with a fallback face.`,\n });\n continue;\n }\n faces.push({\n family: font.family,\n weight: source.weight,\n italic: source.italic,\n data: source.data.toString('base64'),\n format: source.format as RasterizeFontFace['format'],\n });\n }\n }\n return faces;\n}\n\n/**\n * Inverse of {@link toRasterizeFontFaces}: regroup wire faces back into\n * `ResolvedFont[]` so the existing `FontStager.stage(ResolvedFont[], …)`\n * signature needs no change. Grouping is by exact (case-sensitive) family,\n * matching how the registry keys resolved fonts.\n */\nexport function fromRasterizeFontFaces(\n faces: readonly RasterizeFontFace[]\n): ResolvedFont[] {\n const byFamily = new Map<string, ResolvedFont>();\n for (const face of faces) {\n let font = byFamily.get(face.family);\n if (!font) {\n font = { family: face.family, sources: [], warnings: [] };\n byFamily.set(face.family, font);\n }\n const source: ResolvedFontSource = {\n data: Buffer.from(face.data, 'base64'),\n weight: face.weight,\n italic: face.italic,\n format: face.format ?? 'ttf',\n };\n font.sources.push(source);\n }\n return [...byFamily.values()];\n}\n"],"mappings":"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","names":["readFile"]}
package/dist/index.d.ts CHANGED
@@ -202,14 +202,24 @@ interface SynthesizedFamily {
202
202
  declare function synthesizeFamilyName(family: string, weight: number | undefined, italic: boolean): SynthesizedFamily;
203
203
 
204
204
  /**
205
- * Rewrite a TTF/OTF's `name` table so `nameID` 1 / 4 / 6 / 16 carry the
206
- * supplied synthetic family name. Used by the preview-side font stagers
207
- * so that running-text references like `"Inter Light"` resolve to the
208
- * correct face when the stager registers it with Core Text / fontconfig
209
- * / GDI (all of which index by the font's internal `name` table rather
210
- * than the filename).
211
- *
212
- * The transform rebuilds the whole font: new `name` table bytes, new
205
+ * Name-table rewriting for TTF/OTF sfnt fonts. Two public transforms share
206
+ * the rebuild machinery:
207
+ *
208
+ * - `rewriteFontFamilyName` rewrite `nameID` 1 / 4 / 6 / 16 to a
209
+ * synthetic family name. Used by the preview-side font stagers so that
210
+ * running-text references like `"Inter Light"` resolve to the correct
211
+ * face when the stager registers it with Core Text / fontconfig / GDI
212
+ * (all of which index by the font's internal `name` table rather than
213
+ * the filename).
214
+ *
215
+ * - `rewriteFontSubfamilyNames` — rewrite `nameID` 2 / 17 to the standard
216
+ * subfamily strings for a (weight, italic) pair. Used by the variable-
217
+ * font instancer: harfbuzz preserves the source font's name records
218
+ * verbatim, so an instanced Bold would otherwise keep the variable
219
+ * font's default-instance "Regular" subfamily and trip
220
+ * `validateFontMetadata`.
221
+ *
222
+ * Each transform rebuilds the whole font: new `name` table bytes, new
213
223
  * table directory with shifted offsets, recomputed per-table checksums,
214
224
  * and the magic `head.checkSumAdjustment` recomputed against the whole
215
225
  * output buffer. Nothing else is touched.