@gtkx/cli 2.0.0-beta.5 → 2.0.0-beta.7

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.
Files changed (127) hide show
  1. package/README.md +43 -40
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +28 -8
  4. package/dist/builder.js.map +1 -1
  5. package/dist/cli.js +1 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/codegen/freshness.d.ts.map +1 -1
  8. package/dist/codegen/freshness.js +5 -1
  9. package/dist/codegen/freshness.js.map +1 -1
  10. package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
  11. package/dist/deploy/freedesktop/validate.js +6 -1
  12. package/dist/deploy/freedesktop/validate.js.map +1 -1
  13. package/dist/deploy/settings/libraries.d.ts.map +1 -1
  14. package/dist/deploy/settings/libraries.js +2 -1
  15. package/dist/deploy/settings/libraries.js.map +1 -1
  16. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  17. package/dist/deploy/targets/flatpak-source.js +2 -0
  18. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  19. package/dist/dev/font-dir.d.ts +3 -0
  20. package/dist/dev/font-dir.d.ts.map +1 -0
  21. package/dist/dev/font-dir.js +12 -0
  22. package/dist/dev/font-dir.js.map +1 -0
  23. package/dist/dev/runner-main.d.ts.map +1 -1
  24. package/dist/dev/runner-main.js +2 -0
  25. package/dist/dev/runner-main.js.map +1 -1
  26. package/dist/internal/agent-rules.d.ts.map +1 -1
  27. package/dist/internal/agent-rules.js +7 -2
  28. package/dist/internal/agent-rules.js.map +1 -1
  29. package/dist/internal/banner.d.ts +4 -2
  30. package/dist/internal/banner.d.ts.map +1 -1
  31. package/dist/internal/banner.js +19 -5
  32. package/dist/internal/banner.js.map +1 -1
  33. package/dist/internal/font-path.d.ts +4 -0
  34. package/dist/internal/font-path.d.ts.map +1 -0
  35. package/dist/internal/font-path.js +12 -0
  36. package/dist/internal/font-path.js.map +1 -0
  37. package/dist/internal/font-staging.d.ts +3 -0
  38. package/dist/internal/font-staging.d.ts.map +1 -0
  39. package/dist/internal/font-staging.js +43 -0
  40. package/dist/internal/font-staging.js.map +1 -0
  41. package/dist/internal/source-imports.d.ts +2 -1
  42. package/dist/internal/source-imports.d.ts.map +1 -1
  43. package/dist/internal/source-imports.js +7 -2
  44. package/dist/internal/source-imports.js.map +1 -1
  45. package/dist/internal/xdg-banner.d.ts +4 -0
  46. package/dist/internal/xdg-banner.d.ts.map +1 -0
  47. package/dist/internal/xdg-banner.js +11 -0
  48. package/dist/internal/xdg-banner.js.map +1 -0
  49. package/dist/internal/xdg-data-dirs.d.ts +2 -1
  50. package/dist/internal/xdg-data-dirs.d.ts.map +1 -1
  51. package/dist/internal/xdg-data-dirs.js +1 -1
  52. package/dist/internal/xdg-data-dirs.js.map +1 -1
  53. package/dist/settings/schema.d.ts.map +1 -1
  54. package/dist/settings/schema.js +1 -6
  55. package/dist/settings/schema.js.map +1 -1
  56. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  57. package/dist/vite-plugins/asset-extensions.js +2 -0
  58. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  59. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  60. package/dist/vite-plugins/asset-imports.js +7 -2
  61. package/dist/vite-plugins/asset-imports.js.map +1 -1
  62. package/dist/vite-plugins/asset-specifier.d.ts +5 -1
  63. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  64. package/dist/vite-plugins/asset-specifier.js +10 -1
  65. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  66. package/dist/vite-plugins/css.d.ts.map +1 -1
  67. package/dist/vite-plugins/css.js +3 -2
  68. package/dist/vite-plugins/css.js.map +1 -1
  69. package/dist/vite-plugins/font-name.d.ts +3 -0
  70. package/dist/vite-plugins/font-name.d.ts.map +1 -0
  71. package/dist/vite-plugins/font-name.js +406 -0
  72. package/dist/vite-plugins/font-name.js.map +1 -0
  73. package/dist/vite-plugins/font-worker-env.d.ts +4 -0
  74. package/dist/vite-plugins/font-worker-env.d.ts.map +1 -0
  75. package/dist/vite-plugins/font-worker-env.js +22 -0
  76. package/dist/vite-plugins/font-worker-env.js.map +1 -0
  77. package/dist/vite-plugins/font.d.ts +4 -0
  78. package/dist/vite-plugins/font.d.ts.map +1 -0
  79. package/dist/vite-plugins/font.js +87 -0
  80. package/dist/vite-plugins/font.js.map +1 -0
  81. package/dist/vite-plugins/icons.d.ts.map +1 -1
  82. package/dist/vite-plugins/icons.js +2 -7
  83. package/dist/vite-plugins/icons.js.map +1 -1
  84. package/dist/vite-plugins/index.d.ts.map +1 -1
  85. package/dist/vite-plugins/index.js +2 -0
  86. package/dist/vite-plugins/index.js.map +1 -1
  87. package/dist/vite-plugins/resources.js +1 -1
  88. package/dist/vite-plugins/resources.js.map +1 -1
  89. package/dist/vite-plugins/settings.d.ts.map +1 -1
  90. package/dist/vite-plugins/settings.js +6 -6
  91. package/dist/vite-plugins/settings.js.map +1 -1
  92. package/dist/vite-plugins/undeclared-library.js +3 -3
  93. package/dist/vite-plugins/undeclared-library.js.map +1 -1
  94. package/dist/vitest-plugin.d.ts.map +1 -1
  95. package/dist/vitest-plugin.js +2 -0
  96. package/dist/vitest-plugin.js.map +1 -1
  97. package/env.d.ts +9 -0
  98. package/package.json +14 -12
  99. package/src/builder.ts +28 -8
  100. package/src/cli.ts +1 -1
  101. package/src/codegen/freshness.ts +5 -1
  102. package/src/deploy/freedesktop/validate.ts +8 -1
  103. package/src/deploy/settings/libraries.ts +2 -1
  104. package/src/deploy/targets/flatpak-source.ts +2 -0
  105. package/src/dev/font-dir.ts +16 -0
  106. package/src/dev/runner-main.ts +2 -0
  107. package/src/internal/agent-rules.ts +7 -2
  108. package/src/internal/banner.ts +27 -6
  109. package/src/internal/font-path.ts +15 -0
  110. package/src/internal/font-staging.ts +57 -0
  111. package/src/internal/source-imports.ts +17 -2
  112. package/src/internal/xdg-banner.ts +13 -0
  113. package/src/internal/xdg-data-dirs.ts +1 -1
  114. package/src/settings/schema.ts +1 -8
  115. package/src/vite-plugins/asset-extensions.ts +2 -0
  116. package/src/vite-plugins/asset-imports.ts +9 -1
  117. package/src/vite-plugins/asset-specifier.ts +17 -0
  118. package/src/vite-plugins/css.ts +4 -2
  119. package/src/vite-plugins/font-name.ts +582 -0
  120. package/src/vite-plugins/font-worker-env.ts +29 -0
  121. package/src/vite-plugins/font.ts +147 -0
  122. package/src/vite-plugins/icons.ts +2 -8
  123. package/src/vite-plugins/index.ts +2 -0
  124. package/src/vite-plugins/resources.ts +1 -1
  125. package/src/vite-plugins/settings.ts +7 -7
  126. package/src/vite-plugins/undeclared-library.ts +3 -3
  127. package/src/vitest-plugin.ts +2 -0
@@ -0,0 +1,582 @@
1
+ import { brotliDecompressSync, inflateSync } from "node:zlib";
2
+
3
+ type Base128 = {
4
+ offset: number;
5
+ value: number;
6
+ };
7
+
8
+ type NameHeader = {
9
+ encodingId: number;
10
+ languageId: number;
11
+ length: number;
12
+ nameId: number;
13
+ platformId: number;
14
+ stringOffset: number;
15
+ };
16
+
17
+ type NameRecord = NameHeader & { value: string };
18
+
19
+ type ScoredName = {
20
+ score: number;
21
+ value: string;
22
+ };
23
+
24
+ type Woff2Entry = {
25
+ length: number;
26
+ next: number;
27
+ tag: string;
28
+ };
29
+
30
+ type Woff2Table = {
31
+ length: number;
32
+ offset: number;
33
+ tag: string;
34
+ };
35
+
36
+ type Woff2Directory = {
37
+ end: number;
38
+ tables: Woff2Table[];
39
+ };
40
+
41
+ type Woff2Step = (data: Buffer, offset: number) => number | undefined;
42
+
43
+ const MAX_FONT_BYTES = 256 * 1024 * 1024;
44
+ const MAX_TABLE_BYTES = 16 * 1024 * 1024;
45
+ const TAG_SIZE = 4;
46
+ const TAG_GLYF = "glyf";
47
+ const TAG_LOCA = "loca";
48
+ const TAG_NAME = "name";
49
+ const TAG_TTCF = "ttcf";
50
+ const SIGNATURE_WOFF = "wOFF";
51
+ const SIGNATURE_WOFF2 = "wOF2";
52
+ const SFNT_VERSION_COLLECTION = 0x74_74_63_66;
53
+ const SFNT_VERSIONS = new Set([0x00_01_00_00, 0x4F_54_54_4F, 0x74_72_75_65]);
54
+ const SFNT_TABLE_COUNT_OFFSET = 4;
55
+ const SFNT_DIRECTORY_OFFSET = 12;
56
+ const SFNT_RECORD_SIZE = 16;
57
+ const SFNT_RECORD_OFFSET_FIELD = 8;
58
+ const SFNT_RECORD_LENGTH_FIELD = 12;
59
+ const COLLECTION_COUNT_OFFSET = 8;
60
+ const COLLECTION_DIRECTORY_OFFSET = 12;
61
+ const COLLECTION_ENTRY_SIZE = 4;
62
+ const WOFF_TABLE_COUNT_OFFSET = 12;
63
+ const WOFF_DIRECTORY_OFFSET = 44;
64
+ const WOFF_RECORD_SIZE = 20;
65
+ const WOFF_RECORD_OFFSET_FIELD = 4;
66
+ const WOFF_RECORD_COMPRESSED_FIELD = 8;
67
+ const WOFF_RECORD_ORIGINAL_FIELD = 12;
68
+ const WOFF2_FLAVOR_OFFSET = 4;
69
+ const WOFF2_TABLE_COUNT_OFFSET = 12;
70
+ const WOFF2_COMPRESSED_SIZE_OFFSET = 20;
71
+ const WOFF2_HEADER_SIZE = 48;
72
+ const WOFF2_COLLECTION_FONTS_OFFSET = 4;
73
+ const WOFF2_COLLECTION_FLAVOR_SIZE = 4;
74
+ const WOFF2_TAG_MASK = 0x3F;
75
+ const WOFF2_TAG_ESCAPE = 63;
76
+ const WOFF2_TRANSFORM_SHIFT = 6;
77
+ const WOFF2_KNOWN_TAGS = [
78
+ "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "cvt ", "fpgm",
79
+ "glyf", "loca", "prep", "CFF ", "VORG", "EBDT", "EBLC", "gasp", "hdmx", "kern",
80
+ "LTSH", "PCLT", "VDMX", "vhea", "vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC",
81
+ "JSTF", "MATH", "CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
82
+ "bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar", "gvar", "hsty",
83
+ "just", "lcar", "mort", "morx", "opbd", "prop", "trak", "Zapf", "Silf", "Glat",
84
+ "Gloc", "Feat", "Sill",
85
+ ];
86
+ const BASE128_MAX_BYTES = 5;
87
+ const BASE128_MASK = 0x7F;
88
+ const BASE128_RADIX = 128;
89
+ const BASE128_CONTINUATION = 0x80;
90
+ const UINT32_MAX = 0xFF_FF_FF_FF;
91
+ const WORD_CODE = 253;
92
+ const EXTENDED_CODES = new Map([[254, 506], [255, 253]]);
93
+ const NAME_FORMATS = new Set([0, 1]);
94
+ const NAME_HEADER_SIZE = 6;
95
+ const NAME_RECORD_SIZE = 12;
96
+ const NAME_COUNT_OFFSET = 2;
97
+ const NAME_STORAGE_OFFSET = 4;
98
+ const FAMILY_NAME_IDS = [21, 16, 1];
99
+ const PLATFORM_UNICODE = 0;
100
+ const PLATFORM_MACINTOSH = 1;
101
+ const PLATFORM_WINDOWS = 3;
102
+ const WINDOWS_UTF16_ENCODINGS = new Set([1, 10]);
103
+ const MAC_ROMAN_ENCODING = 0;
104
+ const MAC_ROMAN_LANGUAGE = 0;
105
+ const MAC_ROMAN_HIGH_START = 128;
106
+ const MAC_ROMAN_HIGH =
107
+ "ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü" +
108
+ "†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø" +
109
+ "¿¡¬√ƒ≈∆«»…\u{A0}ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›fifl" +
110
+ "‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔ\u{F8FF}ÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ";
111
+ const LANGUAGE_ENGLISH_US = 0x04_09;
112
+ const LANGUAGE_ENGLISH = 0x09;
113
+ const LANGUAGE_PRIMARY_MASK = 0x03_FF;
114
+ const SCORE_WINDOWS_ENGLISH_US = 100;
115
+ const SCORE_WINDOWS_ENGLISH = 90;
116
+ const SCORE_UNICODE = 80;
117
+ const SCORE_MAC_ROMAN = 70;
118
+ const SCORE_WINDOWS_OTHER = 10;
119
+ const SCORE_NONE = 0;
120
+ const CONTROL_PATTERN = /\p{Cc}/gu;
121
+
122
+ const isValidRange = (data: Buffer, start: number, length: number): boolean =>
123
+ Number.isSafeInteger(start) && Number.isSafeInteger(length) && start >= 0 && length >= 0 &&
124
+ start + length <= data.length;
125
+
126
+ const sliceRange = (data: Buffer, start: number, length: number): Buffer | undefined =>
127
+ isValidRange(data, start, length) ? data.subarray(start, start + length) : undefined;
128
+
129
+ const readUint16 = (data: Buffer, offset: number): number | undefined =>
130
+ sliceRange(data, offset, 2)?.readUInt16BE(0);
131
+
132
+ const readUint32 = (data: Buffer, offset: number): number | undefined =>
133
+ sliceRange(data, offset, 4)?.readUInt32BE(0);
134
+
135
+ const readTag = (data: Buffer, offset: number): string | undefined =>
136
+ sliceRange(data, offset, TAG_SIZE)?.toString("latin1");
137
+
138
+ const inflateTable = (data: Buffer, limit: number): Buffer | undefined => {
139
+ try {
140
+ return inflateSync(data, { maxOutputLength: Math.min(limit, MAX_TABLE_BYTES) });
141
+ } catch {
142
+ return undefined;
143
+ }
144
+ };
145
+
146
+ const brotliFont = (data: Buffer): Buffer | undefined => {
147
+ try {
148
+ return brotliDecompressSync(data, { maxOutputLength: MAX_FONT_BYTES });
149
+ } catch {
150
+ return undefined;
151
+ }
152
+ };
153
+
154
+ const macRomanChar = (byte: number): string => {
155
+ const code = byte < MAC_ROMAN_HIGH_START ? byte : MAC_ROMAN_HIGH.codePointAt(byte - MAC_ROMAN_HIGH_START);
156
+
157
+ return code === undefined ? "" : String.fromCodePoint(code);
158
+ };
159
+
160
+ const decodeMacRoman = (bytes: Buffer): string => [...bytes].map((byte) => macRomanChar(byte)).join("");
161
+
162
+ const decodeUtf16 = (bytes: Buffer): string | undefined => {
163
+ if (bytes.length % 2 !== 0) {
164
+ return undefined;
165
+ }
166
+
167
+ const swapped = Buffer.from(bytes);
168
+
169
+ swapped.swap16();
170
+
171
+ return swapped.toString("utf16le");
172
+ };
173
+
174
+ const isUtf16Name = (header: NameHeader): boolean =>
175
+ header.platformId === PLATFORM_UNICODE ||
176
+ (header.platformId === PLATFORM_WINDOWS && WINDOWS_UTF16_ENCODINGS.has(header.encodingId));
177
+
178
+ const isMacRomanName = (header: NameHeader): boolean =>
179
+ header.platformId === PLATFORM_MACINTOSH &&
180
+ header.encodingId === MAC_ROMAN_ENCODING &&
181
+ header.languageId === MAC_ROMAN_LANGUAGE;
182
+
183
+ const decodeName = (bytes: Buffer, header: NameHeader): string | undefined => {
184
+ if (isUtf16Name(header)) {
185
+ return decodeUtf16(bytes);
186
+ }
187
+
188
+ return isMacRomanName(header) ? decodeMacRoman(bytes) : undefined;
189
+ };
190
+
191
+ const nameRecordHeader = (table: Buffer, offset: number): NameHeader | undefined => {
192
+ const record = sliceRange(table, offset, NAME_RECORD_SIZE);
193
+
194
+ if (record === undefined) {
195
+ return undefined;
196
+ }
197
+
198
+ return {
199
+ encodingId: record.readUInt16BE(2),
200
+ languageId: record.readUInt16BE(4),
201
+ length: record.readUInt16BE(8),
202
+ nameId: record.readUInt16BE(6),
203
+ platformId: record.readUInt16BE(0),
204
+ stringOffset: record.readUInt16BE(10),
205
+ };
206
+ };
207
+
208
+ const nameRecordAt = (table: Buffer, offset: number, storage: number): NameRecord | undefined => {
209
+ const header = nameRecordHeader(table, offset);
210
+
211
+ if (header === undefined) {
212
+ return undefined;
213
+ }
214
+
215
+ const bytes = sliceRange(table, storage + header.stringOffset, header.length);
216
+ const decoded = bytes === undefined ? undefined : decodeName(bytes, header);
217
+ const value = decoded?.replaceAll(CONTROL_PATTERN, "").trim();
218
+
219
+ return value === undefined || value.length === 0 ? undefined : { ...header, value };
220
+ };
221
+
222
+ const nameRecords = (table: Buffer): NameRecord[] => {
223
+ const format = readUint16(table, 0);
224
+ const count = readUint16(table, NAME_COUNT_OFFSET);
225
+ const storage = readUint16(table, NAME_STORAGE_OFFSET);
226
+ const records: NameRecord[] = [];
227
+
228
+ if (format === undefined || count === undefined || storage === undefined || !NAME_FORMATS.has(format)) {
229
+ return records;
230
+ }
231
+
232
+ for (let index = 0; index < count; index += 1) {
233
+ const record = nameRecordAt(table, NAME_HEADER_SIZE + index * NAME_RECORD_SIZE, storage);
234
+
235
+ if (record !== undefined) {
236
+ records.push(record);
237
+ }
238
+ }
239
+
240
+ return records;
241
+ };
242
+
243
+ const windowsLanguageScore = (languageId: number): number => {
244
+ if (languageId === LANGUAGE_ENGLISH_US) {
245
+ return SCORE_WINDOWS_ENGLISH_US;
246
+ }
247
+
248
+ return (languageId & LANGUAGE_PRIMARY_MASK) === LANGUAGE_ENGLISH ? SCORE_WINDOWS_ENGLISH : SCORE_WINDOWS_OTHER;
249
+ };
250
+
251
+ const nameScore = (record: NameRecord): number => {
252
+ if (record.platformId === PLATFORM_WINDOWS && WINDOWS_UTF16_ENCODINGS.has(record.encodingId)) {
253
+ return windowsLanguageScore(record.languageId);
254
+ }
255
+
256
+ if (record.platformId === PLATFORM_UNICODE) {
257
+ return SCORE_UNICODE;
258
+ }
259
+
260
+ return isMacRomanName(record) ? SCORE_MAC_ROMAN : SCORE_NONE;
261
+ };
262
+
263
+ const scoredNames = (records: NameRecord[], nameId: number): ScoredName[] =>
264
+ records
265
+ .filter((record) => record.nameId === nameId)
266
+ .map((record) => ({ score: nameScore(record), value: record.value }))
267
+ .filter((entry) => entry.score > SCORE_NONE);
268
+
269
+ const bestName = (records: NameRecord[], nameId: number): string | undefined =>
270
+ scoredNames(records, nameId).toSorted((left, right) => right.score - left.score)[0]?.value;
271
+
272
+ const familyName = (table: Buffer): string | undefined => {
273
+ const records = nameRecords(table);
274
+
275
+ return FAMILY_NAME_IDS.map((nameId) => bestName(records, nameId)).find((value) => value !== undefined);
276
+ };
277
+
278
+ const sfntTableAt = (data: Buffer, record: number): Buffer | undefined => {
279
+ const tag = readTag(data, record);
280
+ const offset = readUint32(data, record + SFNT_RECORD_OFFSET_FIELD);
281
+ const length = readUint32(data, record + SFNT_RECORD_LENGTH_FIELD);
282
+
283
+ if (tag !== TAG_NAME || offset === undefined || length === undefined) {
284
+ return undefined;
285
+ }
286
+
287
+ return sliceRange(data, offset, length);
288
+ };
289
+
290
+ const sfntNameTable = (data: Buffer, start: number): Buffer | undefined => {
291
+ const version = readUint32(data, start);
292
+ const count = readUint16(data, start + SFNT_TABLE_COUNT_OFFSET);
293
+
294
+ if (version === undefined || count === undefined || !SFNT_VERSIONS.has(version)) {
295
+ return undefined;
296
+ }
297
+
298
+ for (let index = 0; index < count; index += 1) {
299
+ const table = sfntTableAt(data, start + SFNT_DIRECTORY_OFFSET + index * SFNT_RECORD_SIZE);
300
+
301
+ if (table !== undefined) {
302
+ return table;
303
+ }
304
+ }
305
+
306
+ return undefined;
307
+ };
308
+
309
+ const sfntNameTables = (data: Buffer): Buffer[] => {
310
+ const table = sfntNameTable(data, 0);
311
+
312
+ return table === undefined ? [] : [table];
313
+ };
314
+
315
+ const collectionNameTables = (data: Buffer): Buffer[] => {
316
+ const count = readUint32(data, COLLECTION_COUNT_OFFSET) ?? 0;
317
+ const limit = Math.floor((data.length - COLLECTION_DIRECTORY_OFFSET) / COLLECTION_ENTRY_SIZE);
318
+ const tables: Buffer[] = [];
319
+
320
+ for (let index = 0; index < Math.min(count, limit); index += 1) {
321
+ const start = readUint32(data, COLLECTION_DIRECTORY_OFFSET + index * COLLECTION_ENTRY_SIZE);
322
+ const table = start === undefined ? undefined : sfntNameTable(data, start);
323
+
324
+ if (table !== undefined) {
325
+ tables.push(table);
326
+ }
327
+ }
328
+
329
+ return tables;
330
+ };
331
+
332
+ const woffTableData = (compressed: Buffer, compressedLength: number, originalLength: number): Buffer | undefined =>
333
+ compressedLength === originalLength ? compressed : inflateTable(compressed, originalLength);
334
+
335
+ const woffTableAt = (data: Buffer, record: number): Buffer | undefined => {
336
+ const tag = readTag(data, record);
337
+ const offset = readUint32(data, record + WOFF_RECORD_OFFSET_FIELD);
338
+ const compressedLength = readUint32(data, record + WOFF_RECORD_COMPRESSED_FIELD);
339
+ const originalLength = readUint32(data, record + WOFF_RECORD_ORIGINAL_FIELD);
340
+
341
+ if (tag !== TAG_NAME || offset === undefined || compressedLength === undefined || originalLength === undefined) {
342
+ return undefined;
343
+ }
344
+
345
+ const compressed = sliceRange(data, offset, compressedLength);
346
+
347
+ return compressed === undefined ? undefined : woffTableData(compressed, compressedLength, originalLength);
348
+ };
349
+
350
+ const woffNameTables = (data: Buffer): Buffer[] => {
351
+ const count = readUint16(data, WOFF_TABLE_COUNT_OFFSET) ?? 0;
352
+ const tables: Buffer[] = [];
353
+
354
+ for (let index = 0; index < count; index += 1) {
355
+ const table = woffTableAt(data, WOFF_DIRECTORY_OFFSET + index * WOFF_RECORD_SIZE);
356
+
357
+ if (table !== undefined) {
358
+ tables.push(table);
359
+ }
360
+ }
361
+
362
+ return tables;
363
+ };
364
+
365
+ const base128Length = (data: Buffer, start: number): number | undefined => {
366
+ const window = data.subarray(start, start + BASE128_MAX_BYTES);
367
+ const terminator = window.findIndex((byte) => (byte & BASE128_CONTINUATION) === 0);
368
+
369
+ return terminator === -1 ? undefined : terminator + 1;
370
+ };
371
+
372
+ const base128Value = (data: Buffer, start: number, length: number): number => {
373
+ const bytes = data.subarray(start, start + length);
374
+ let value = 0;
375
+
376
+ for (const byte of bytes) {
377
+ value = value * BASE128_RADIX + (byte & BASE128_MASK);
378
+ }
379
+
380
+ return value;
381
+ };
382
+
383
+ const readBase128 = (data: Buffer, start: number): Base128 | undefined => {
384
+ const length = base128Length(data, start);
385
+
386
+ if (length === undefined || data[start] === BASE128_CONTINUATION) {
387
+ return undefined;
388
+ }
389
+
390
+ const value = base128Value(data, start, length);
391
+
392
+ return value > UINT32_MAX ? undefined : { offset: start + length, value };
393
+ };
394
+
395
+ const readExtended255 = (data: Buffer, start: number, code: number): Base128 | undefined => {
396
+ const extra = EXTENDED_CODES.get(code);
397
+
398
+ if (extra === undefined) {
399
+ return { offset: start + 1, value: code };
400
+ }
401
+
402
+ const byte = data[start + 1];
403
+
404
+ return byte === undefined ? undefined : { offset: start + 2, value: byte + extra };
405
+ };
406
+
407
+ const read255Uint16 = (data: Buffer, start: number): Base128 | undefined => {
408
+ const code = data[start];
409
+
410
+ if (code === undefined) {
411
+ return undefined;
412
+ }
413
+
414
+ if (code !== WORD_CODE) {
415
+ return readExtended255(data, start, code);
416
+ }
417
+
418
+ const value = readUint16(data, start + 1);
419
+
420
+ return value === undefined ? undefined : { offset: start + 3, value };
421
+ };
422
+
423
+ const woff2Tag = (data: Buffer, start: number, flags: number): string | undefined =>
424
+ (flags & WOFF2_TAG_MASK) === WOFF2_TAG_ESCAPE
425
+ ? readTag(data, start + 1)
426
+ : WOFF2_KNOWN_TAGS[flags & WOFF2_TAG_MASK];
427
+
428
+ const woff2TagEnd = (start: number, flags: number): number =>
429
+ (flags & WOFF2_TAG_MASK) === WOFF2_TAG_ESCAPE ? start + 1 + TAG_SIZE : start + 1;
430
+
431
+ const isWoff2Transformed = (tag: string, flags: number): boolean =>
432
+ tag === TAG_GLYF || tag === TAG_LOCA
433
+ ? (flags >> WOFF2_TRANSFORM_SHIFT) === 0
434
+ : (flags >> WOFF2_TRANSFORM_SHIFT) !== 0;
435
+
436
+ const woff2TransformedEntry = (data: Buffer, tag: string, original: Base128): Woff2Entry | undefined => {
437
+ const transformed = readBase128(data, original.offset);
438
+
439
+ return transformed === undefined ? undefined : { length: transformed.value, next: transformed.offset, tag };
440
+ };
441
+
442
+ const woff2Entry = (data: Buffer, start: number): Woff2Entry | undefined => {
443
+ const flags = data[start];
444
+
445
+ if (flags === undefined) {
446
+ return undefined;
447
+ }
448
+
449
+ const tag = woff2Tag(data, start, flags);
450
+ const original = readBase128(data, woff2TagEnd(start, flags));
451
+
452
+ if (tag === undefined || original === undefined) {
453
+ return undefined;
454
+ }
455
+
456
+ return isWoff2Transformed(tag, flags)
457
+ ? woff2TransformedEntry(data, tag, original)
458
+ : { length: original.value, next: original.offset, tag };
459
+ };
460
+
461
+ const woff2Directory = (data: Buffer, count: number): Woff2Directory | undefined => {
462
+ const tables: Woff2Table[] = [];
463
+ let cursor = WOFF2_HEADER_SIZE;
464
+ let offset = 0;
465
+
466
+ for (let index = 0; index < count; index += 1) {
467
+ const entry = woff2Entry(data, cursor);
468
+
469
+ if (entry === undefined) {
470
+ return undefined;
471
+ }
472
+
473
+ tables.push({ length: entry.length, offset, tag: entry.tag });
474
+ cursor = entry.next;
475
+ offset += entry.length;
476
+ }
477
+
478
+ return { end: cursor, tables };
479
+ };
480
+
481
+ const skipSequence = (data: Buffer, start: number, count: number, step: Woff2Step): number | undefined => {
482
+ let cursor = start;
483
+
484
+ for (let index = 0; index < count; index += 1) {
485
+ const next = step(data, cursor);
486
+
487
+ if (next === undefined) {
488
+ return undefined;
489
+ }
490
+
491
+ cursor = next;
492
+ }
493
+
494
+ return cursor;
495
+ };
496
+
497
+ const skipIndex = (data: Buffer, offset: number): number | undefined => read255Uint16(data, offset)?.offset;
498
+
499
+ const skipCollectionFont = (data: Buffer, start: number): number | undefined => {
500
+ const tables = read255Uint16(data, start);
501
+
502
+ if (tables === undefined) {
503
+ return undefined;
504
+ }
505
+
506
+ return skipSequence(data, tables.offset + WOFF2_COLLECTION_FLAVOR_SIZE, tables.value, skipIndex);
507
+ };
508
+
509
+ const woff2CollectionEnd = (data: Buffer, start: number): number | undefined => {
510
+ const fonts = read255Uint16(data, start + WOFF2_COLLECTION_FONTS_OFFSET);
511
+
512
+ if (fonts === undefined) {
513
+ return undefined;
514
+ }
515
+
516
+ return skipSequence(data, fonts.offset, fonts.value, skipCollectionFont);
517
+ };
518
+
519
+ const woff2DataStart = (data: Buffer, end: number): number | undefined => {
520
+ const flavor = readUint32(data, WOFF2_FLAVOR_OFFSET);
521
+
522
+ return flavor === SFNT_VERSION_COLLECTION ? woff2CollectionEnd(data, end) : end;
523
+ };
524
+
525
+ const woff2Compressed = (data: Buffer, start: number): Buffer => {
526
+ const size = readUint32(data, WOFF2_COMPRESSED_SIZE_OFFSET);
527
+ const bounded = size === undefined ? undefined : sliceRange(data, start, size);
528
+
529
+ return bounded ?? data.subarray(start);
530
+ };
531
+
532
+ const woff2NameSlices = (font: Buffer, tables: Woff2Table[]): Buffer[] =>
533
+ tables
534
+ .filter((table) => table.tag === TAG_NAME)
535
+ .map((table) => sliceRange(font, table.offset, table.length))
536
+ .filter((table) => table !== undefined);
537
+
538
+ const woff2Tables = (data: Buffer, directory: Woff2Directory): Buffer[] => {
539
+ const start = woff2DataStart(data, directory.end);
540
+ const font = start === undefined ? undefined : brotliFont(woff2Compressed(data, start));
541
+
542
+ return font === undefined ? [] : woff2NameSlices(font, directory.tables);
543
+ };
544
+
545
+ const woff2NameTables = (data: Buffer): Buffer[] => {
546
+ const count = readUint16(data, WOFF2_TABLE_COUNT_OFFSET);
547
+ const directory = count === undefined ? undefined : woff2Directory(data, count);
548
+
549
+ return directory === undefined ? [] : woff2Tables(data, directory);
550
+ };
551
+
552
+ const nameTables = (data: Buffer): Buffer[] => {
553
+ const signature = readTag(data, 0);
554
+
555
+ if (signature === SIGNATURE_WOFF) {
556
+ return woffNameTables(data);
557
+ }
558
+
559
+ if (signature === SIGNATURE_WOFF2) {
560
+ return woff2NameTables(data);
561
+ }
562
+
563
+ return signature === TAG_TTCF ? collectionNameTables(data) : sfntNameTables(data);
564
+ };
565
+
566
+ const familyNames = (font: Buffer): string[] => {
567
+ const names = nameTables(font)
568
+ .map((table) => familyName(table))
569
+ .filter((name) => name !== undefined);
570
+
571
+ return [...new Set(names)];
572
+ };
573
+
574
+ const fontFamilyNames = (font: Buffer): string[] => {
575
+ try {
576
+ return familyNames(font);
577
+ } catch {
578
+ return [];
579
+ }
580
+ };
581
+
582
+ export { fontFamilyNames };
@@ -0,0 +1,29 @@
1
+ import type { UserConfig } from "vite";
2
+ import type { Plugin } from "vitest/config";
3
+ import { createConfigLoader } from "@gtkx/config/internal";
4
+ import { stageProjectFonts } from "../internal/font-staging.js";
5
+ import { prependXdgDataDir } from "../internal/xdg-data-dirs.js";
6
+
7
+ function gtkxFontWorkerEnv(configFile?: string): Plugin {
8
+ const loadConfig = createConfigLoader({ configFile });
9
+
10
+ return {
11
+ name: "gtkx:font-worker-env",
12
+ enforce: "pre",
13
+
14
+ async config(config: UserConfig) {
15
+ const loaded = await loadConfig.load(config.root ?? process.cwd());
16
+ const shareDir = stageProjectFonts(loaded.root);
17
+
18
+ if (shareDir === null) {
19
+ return;
20
+ }
21
+
22
+ return {
23
+ test: { env: { XDG_DATA_DIRS: prependXdgDataDir(shareDir, process.env.XDG_DATA_DIRS) } },
24
+ };
25
+ },
26
+ };
27
+ }
28
+
29
+ export { gtkxFontWorkerEnv };