@flightdev/fonts 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ /**
2
+ * @flightdev/fonts - Vite Plugin for Font Optimization
3
+ *
4
+ * Build-time font optimization for Flight Framework.
5
+ * Features:
6
+ * - Automatic character extraction from source files
7
+ * - Unicode-range CSS generation for optimal subsetting
8
+ * - Self-hosting with cache busting
9
+ * - Development mode passthrough for fast iteration
10
+ *
11
+ * This is an OPTIONAL plugin. Developers can choose to use it or not.
12
+ */
13
+ import { join } from 'node:path';
14
+ import { extractCharsFromSource, charsToCodePoints, codePointsToUnicodeRange, getNamedSubsetRanges, detectRequiredSubsets, generateFontFaceCSS, } from '../subset';
15
+ // ============================================================================
16
+ // Vite Plugin
17
+ // ============================================================================
18
+ /**
19
+ * Vite plugin for font optimization.
20
+ *
21
+ * Usage:
22
+ * ```typescript
23
+ * // vite.config.ts
24
+ * import { fontOptimization } from '@flightdev/fonts/vite';
25
+ *
26
+ * export default defineConfig({
27
+ * plugins: [
28
+ * fontOptimization({
29
+ * strategy: 'named-subsets',
30
+ * subsets: ['latin', 'latin-ext'],
31
+ * selfHost: true,
32
+ * }),
33
+ * ],
34
+ * });
35
+ * ```
36
+ */
37
+ export function fontOptimization(options = {}) {
38
+ const { strategy = 'named-subsets', subsets = ['latin'], selfHost = true, outputDir = 'public/_fonts', scanDirs = ['src'], scanExtensions = ['.tsx', '.jsx', '.vue', '.svelte', '.astro', '.html'], excludePatterns = ['node_modules', 'dist', '.git'], generatePreloads = true, injectToHtml = true, verbose = true, } = options;
39
+ let config;
40
+ let isProduction = false;
41
+ let enabled = options.enabled;
42
+ let projectRoot = '';
43
+ let extractedChars = new Set();
44
+ let processedFonts = [];
45
+ // Track registered fonts from Flight's font loader
46
+ const registeredFonts = new Map();
47
+ return {
48
+ name: 'flight-font-optimization',
49
+ enforce: 'pre',
50
+ configResolved(resolvedConfig) {
51
+ config = resolvedConfig;
52
+ isProduction = resolvedConfig.command === 'build';
53
+ projectRoot = resolvedConfig.root;
54
+ // Enable by default only in production
55
+ if (enabled === undefined) {
56
+ enabled = isProduction;
57
+ }
58
+ if (verbose && enabled) {
59
+ console.log('[flight-fonts] Font optimization enabled');
60
+ console.log(`[flight-fonts] Strategy: ${strategy}`);
61
+ }
62
+ },
63
+ buildStart() {
64
+ if (!enabled)
65
+ return;
66
+ // Scan source files for used characters if using 'used-chars' strategy
67
+ if (strategy === 'used-chars') {
68
+ extractedChars = scanSourceFiles(projectRoot, scanDirs, scanExtensions, excludePatterns);
69
+ if (verbose) {
70
+ console.log(`[flight-fonts] Extracted ${extractedChars.size} unique characters`);
71
+ const detectedSubsets = detectRequiredSubsets(extractedChars);
72
+ console.log(`[flight-fonts] Detected subsets: ${detectedSubsets.join(', ')}`);
73
+ }
74
+ }
75
+ },
76
+ transform(code, id) {
77
+ if (!enabled)
78
+ return null;
79
+ // Intercept font loader calls to track registered fonts
80
+ if (id.includes('@flightdev/fonts') || id.includes('fonts/src')) {
81
+ return null;
82
+ }
83
+ // Look for createFontLoader or fonts.load calls
84
+ if (code.includes('fonts.load(') || code.includes('.load(')) {
85
+ // This is a simple detection - in production, use AST parsing
86
+ const fontMatches = code.matchAll(/\.load\s*\(\s*['"]([^'"]+)['"]/g);
87
+ for (const match of fontMatches) {
88
+ const fontFamily = match[1];
89
+ if (!registeredFonts.has(fontFamily)) {
90
+ registeredFonts.set(fontFamily, {
91
+ family: fontFamily,
92
+ weights: ['400'],
93
+ styles: ['normal'],
94
+ subsets: strategy === 'named-subsets' ? subsets : ['latin'],
95
+ display: 'swap',
96
+ });
97
+ }
98
+ }
99
+ }
100
+ return null;
101
+ },
102
+ generateBundle() {
103
+ if (!enabled)
104
+ return;
105
+ // Generate optimized font CSS
106
+ for (const [family, fontDef] of registeredFonts) {
107
+ let unicodeRange;
108
+ switch (strategy) {
109
+ case 'used-chars':
110
+ const codePoints = charsToCodePoints(extractedChars);
111
+ unicodeRange = codePointsToUnicodeRange(codePoints);
112
+ break;
113
+ case 'named-subsets':
114
+ unicodeRange = getNamedSubsetRanges(fontDef.subsets);
115
+ break;
116
+ case 'none':
117
+ default:
118
+ unicodeRange = undefined;
119
+ }
120
+ // Generate CSS for each weight
121
+ const cssRules = [];
122
+ for (const weight of fontDef.weights) {
123
+ for (const style of fontDef.styles) {
124
+ const fontUrl = selfHost
125
+ ? `/_fonts/${family.toLowerCase().replace(/\s/g, '-')}-${weight}.woff2`
126
+ : `https://fonts.gstatic.com/s/${family.toLowerCase().replace(/\s/g, '')}/*/${weight}.woff2`;
127
+ cssRules.push(generateFontFaceCSS(family, fontUrl, {
128
+ weight,
129
+ style,
130
+ display: fontDef.display,
131
+ unicodeRange,
132
+ }));
133
+ }
134
+ }
135
+ processedFonts.push({
136
+ family,
137
+ css: cssRules.join('\n'),
138
+ unicodeRange,
139
+ preloadLinks: generatePreloads ? generatePreloadLinks(family, fontDef.weights) : [],
140
+ });
141
+ }
142
+ if (verbose && processedFonts.length > 0) {
143
+ console.log(`[flight-fonts] Processed ${processedFonts.length} font families`);
144
+ }
145
+ },
146
+ transformIndexHtml(html) {
147
+ if (!enabled || !injectToHtml || processedFonts.length === 0) {
148
+ return html;
149
+ }
150
+ // Inject preload links and font CSS
151
+ const preloadTags = processedFonts
152
+ .flatMap(f => f.preloadLinks)
153
+ .join('\n');
154
+ const fontCSS = processedFonts
155
+ .map(f => f.css)
156
+ .join('\n');
157
+ // Insert before </head>
158
+ const headCloseIndex = html.indexOf('</head>');
159
+ if (headCloseIndex === -1)
160
+ return html;
161
+ const injection = `
162
+ <!-- Flight Font Optimization -->
163
+ ${preloadTags}
164
+ <style id="flight-fonts">
165
+ ${fontCSS}
166
+ </style>
167
+ `;
168
+ return html.slice(0, headCloseIndex) + injection + html.slice(headCloseIndex);
169
+ },
170
+ };
171
+ }
172
+ // ============================================================================
173
+ // Helper Functions
174
+ // ============================================================================
175
+ /**
176
+ * Scan source files for used characters.
177
+ */
178
+ function scanSourceFiles(projectRoot, scanDirs, extensions, excludePatterns) {
179
+ const chars = new Set();
180
+ // This is a simplified implementation
181
+ // In production, use glob or fast-glob for better performance
182
+ const scanDir = (dir) => {
183
+ try {
184
+ const fs = require('node:fs');
185
+ const path = require('node:path');
186
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
187
+ for (const entry of entries) {
188
+ const fullPath = path.join(dir, entry.name);
189
+ // Skip excluded patterns
190
+ if (excludePatterns.some(pattern => fullPath.includes(pattern))) {
191
+ continue;
192
+ }
193
+ if (entry.isDirectory()) {
194
+ scanDir(fullPath);
195
+ }
196
+ else if (entry.isFile()) {
197
+ const ext = path.extname(entry.name);
198
+ if (extensions.includes(ext)) {
199
+ try {
200
+ const content = fs.readFileSync(fullPath, 'utf-8');
201
+ const fileType = ext.slice(1); // Remove the dot
202
+ const fileChars = extractCharsFromSource(content, fileType);
203
+ for (const char of fileChars) {
204
+ chars.add(char);
205
+ }
206
+ }
207
+ catch {
208
+ // Skip files that can't be read
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ catch {
215
+ // Directory doesn't exist or can't be read
216
+ }
217
+ };
218
+ for (const scanDirPath of scanDirs) {
219
+ const fullScanDir = join(projectRoot, scanDirPath);
220
+ scanDir(fullScanDir);
221
+ }
222
+ return chars;
223
+ }
224
+ /**
225
+ * Generate preload link tags for fonts.
226
+ */
227
+ function generatePreloadLinks(family, weights) {
228
+ const links = [];
229
+ for (const weight of weights) {
230
+ const fontPath = `/_fonts/${family.toLowerCase().replace(/\s/g, '-')}-${weight}.woff2`;
231
+ links.push(`<link rel="preload" href="${fontPath}" as="font" type="font/woff2" crossorigin="anonymous">`);
232
+ }
233
+ return links;
234
+ }
235
+ // ============================================================================
236
+ // Exports
237
+ // ============================================================================
238
+ // Types are exported at declaration above
239
+ export default fontOptimization;
240
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","sourceRoot":"","sources":["../../src/plugins/vite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @flightdev/fonts - Font Subsetting Utilities
3
+ *
4
+ * Zero-dependency font subsetting for optimal Core Web Vitals.
5
+ * Extracts used characters and generates unicode-range CSS.
6
+ *
7
+ * For full font file subsetting, install fontkit as a peer dependency.
8
+ */
9
+ /**
10
+ * Unicode range entry for CSS @font-face
11
+ */
12
+ export interface UnicodeRange {
13
+ /** Range name for identification */
14
+ name: string;
15
+ /** CSS unicode-range value (e.g., 'U+0000-00FF') */
16
+ range: string;
17
+ /** Character set description */
18
+ description?: string;
19
+ }
20
+ /**
21
+ * Font subset configuration
22
+ */
23
+ export interface FontSubsetConfig {
24
+ /** Strategy for determining which characters to include */
25
+ strategy: 'used-chars' | 'named-subsets' | 'custom' | 'none';
26
+ /** Named subsets to include (e.g., 'latin', 'latin-ext') */
27
+ subsets?: string[];
28
+ /** Custom characters to include */
29
+ customChars?: string;
30
+ /** Directories to scan for used characters */
31
+ scanDirs?: string[];
32
+ /** File extensions to scan */
33
+ scanExtensions?: string[];
34
+ /** Exclude patterns (glob) */
35
+ excludePatterns?: string[];
36
+ }
37
+ /**
38
+ * Result of character extraction
39
+ */
40
+ export interface ExtractedChars {
41
+ /** Set of unique characters found */
42
+ chars: Set<string>;
43
+ /** Unicode code points */
44
+ codePoints: number[];
45
+ /** Generated unicode-range CSS value */
46
+ unicodeRange: string;
47
+ /** Files that were scanned */
48
+ scannedFiles: string[];
49
+ }
50
+ /**
51
+ * Standard unicode ranges matching Google Fonts subsets.
52
+ * These are the same ranges used by major font services.
53
+ */
54
+ export declare const UNICODE_RANGES: Record<string, UnicodeRange>;
55
+ /**
56
+ * Extract unique characters from text content.
57
+ *
58
+ * @param text - Text to extract characters from
59
+ * @returns Set of unique characters
60
+ */
61
+ export declare function extractCharsFromText(text: string): Set<string>;
62
+ /**
63
+ * Convert a set of characters to sorted code points.
64
+ */
65
+ export declare function charsToCodePoints(chars: Set<string>): number[];
66
+ /**
67
+ * Generate CSS unicode-range from code points.
68
+ *
69
+ * Groups consecutive code points into ranges for smaller CSS.
70
+ *
71
+ * @param codePoints - Sorted array of code points
72
+ * @returns CSS unicode-range value
73
+ */
74
+ export declare function codePointsToUnicodeRange(codePoints: number[]): string;
75
+ /**
76
+ * Get named unicode ranges for specified subsets.
77
+ *
78
+ * @param subsetNames - Array of subset names (e.g., ['latin', 'latin-ext'])
79
+ * @returns Combined unicode-range CSS value
80
+ */
81
+ export declare function getNamedSubsetRanges(subsetNames: string[]): string;
82
+ /**
83
+ * Detect which subsets are needed based on characters.
84
+ *
85
+ * @param chars - Set of characters to analyze
86
+ * @returns Array of subset names that cover the characters
87
+ */
88
+ export declare function detectRequiredSubsets(chars: Set<string>): string[];
89
+ /**
90
+ * Extract text content from source code.
91
+ * Used by the Vite plugin to find used characters.
92
+ *
93
+ * @param sourceCode - Source code content
94
+ * @param fileType - File extension (tsx, vue, svelte, etc.)
95
+ * @returns Set of unique characters
96
+ */
97
+ export declare function extractCharsFromSource(sourceCode: string, fileType: string): Set<string>;
98
+ /**
99
+ * Generate @font-face CSS with unicode-range.
100
+ *
101
+ * @param fontFamily - Font family name
102
+ * @param fontUrl - URL to font file
103
+ * @param options - Font face options
104
+ * @returns CSS @font-face rule
105
+ */
106
+ export declare function generateFontFaceCSS(fontFamily: string, fontUrl: string, options?: {
107
+ weight?: string | number;
108
+ style?: string;
109
+ display?: string;
110
+ unicodeRange?: string;
111
+ format?: string;
112
+ }): string;
113
+ //# sourceMappingURL=subset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subset.d.ts","sourceRoot":"","sources":["../src/subset.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,2DAA2D;IAC3D,QAAQ,EAAE,YAAY,GAAG,eAAe,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC7D,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC3B,qCAAqC;IACrC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,0BAA0B;IAC1B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wCAAwC;IACxC,YAAY,EAAE,MAAM,CAAC;IACrB,8BAA8B;IAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;CAC1B;AAMD;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAuEvD,CAAC;AAMF;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAa9D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAW9D;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAkCrE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAWlE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CA+BlE;AA2BD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAClC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACjB,GAAG,CAAC,MAAM,CAAC,CA+Cb;AAMD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAC/B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IACL,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACd,GACP,MAAM,CAuBR"}
package/dist/subset.js ADDED
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @flightdev/fonts - Font Subsetting Utilities
3
+ *
4
+ * Zero-dependency font subsetting for optimal Core Web Vitals.
5
+ * Extracts used characters and generates unicode-range CSS.
6
+ *
7
+ * For full font file subsetting, install fontkit as a peer dependency.
8
+ */
9
+ // ============================================================================
10
+ // Predefined Unicode Ranges (Google Fonts subsets)
11
+ // ============================================================================
12
+ /**
13
+ * Standard unicode ranges matching Google Fonts subsets.
14
+ * These are the same ranges used by major font services.
15
+ */
16
+ export const UNICODE_RANGES = {
17
+ latin: {
18
+ name: 'latin',
19
+ range: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
20
+ description: 'Basic Latin characters',
21
+ },
22
+ 'latin-ext': {
23
+ name: 'latin-ext',
24
+ range: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF',
25
+ description: 'Extended Latin characters',
26
+ },
27
+ cyrillic: {
28
+ name: 'cyrillic',
29
+ range: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116',
30
+ description: 'Cyrillic characters',
31
+ },
32
+ 'cyrillic-ext': {
33
+ name: 'cyrillic-ext',
34
+ range: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F',
35
+ description: 'Extended Cyrillic characters',
36
+ },
37
+ greek: {
38
+ name: 'greek',
39
+ range: 'U+0370-03FF',
40
+ description: 'Greek characters',
41
+ },
42
+ 'greek-ext': {
43
+ name: 'greek-ext',
44
+ range: 'U+1F00-1FFF',
45
+ description: 'Extended Greek characters',
46
+ },
47
+ vietnamese: {
48
+ name: 'vietnamese',
49
+ range: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB',
50
+ description: 'Vietnamese characters',
51
+ },
52
+ arabic: {
53
+ name: 'arabic',
54
+ range: 'U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF',
55
+ description: 'Arabic characters',
56
+ },
57
+ hebrew: {
58
+ name: 'hebrew',
59
+ range: 'U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F',
60
+ description: 'Hebrew characters',
61
+ },
62
+ devanagari: {
63
+ name: 'devanagari',
64
+ range: 'U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB',
65
+ description: 'Devanagari characters',
66
+ },
67
+ thai: {
68
+ name: 'thai',
69
+ range: 'U+0E01-0E5B, U+200C-200D, U+25CC',
70
+ description: 'Thai characters',
71
+ },
72
+ japanese: {
73
+ name: 'japanese',
74
+ range: 'U+3000-303F, U+3040-309F, U+30A0-30FF, U+FF00-FFEF, U+4E00-9FAF',
75
+ description: 'Japanese characters (Hiragana, Katakana, common Kanji)',
76
+ },
77
+ korean: {
78
+ name: 'korean',
79
+ range: 'U+AC00-D7AF, U+1100-11FF, U+3130-318F, U+A960-A97F, U+D7B0-D7FF',
80
+ description: 'Korean Hangul characters',
81
+ },
82
+ 'chinese-simplified': {
83
+ name: 'chinese-simplified',
84
+ range: 'U+4E00-9FFF, U+3400-4DBF, U+20000-2A6DF',
85
+ description: 'Simplified Chinese characters',
86
+ },
87
+ };
88
+ // ============================================================================
89
+ // Character Extraction
90
+ // ============================================================================
91
+ /**
92
+ * Extract unique characters from text content.
93
+ *
94
+ * @param text - Text to extract characters from
95
+ * @returns Set of unique characters
96
+ */
97
+ export function extractCharsFromText(text) {
98
+ const chars = new Set();
99
+ // Use Array.from to properly handle Unicode surrogate pairs
100
+ for (const char of text) {
101
+ // Skip whitespace and common punctuation for more aggressive subsetting
102
+ const codePoint = char.codePointAt(0);
103
+ if (codePoint !== undefined && codePoint > 0x1F) {
104
+ chars.add(char);
105
+ }
106
+ }
107
+ return chars;
108
+ }
109
+ /**
110
+ * Convert a set of characters to sorted code points.
111
+ */
112
+ export function charsToCodePoints(chars) {
113
+ const codePoints = [];
114
+ for (const char of chars) {
115
+ const cp = char.codePointAt(0);
116
+ if (cp !== undefined) {
117
+ codePoints.push(cp);
118
+ }
119
+ }
120
+ return codePoints.sort((a, b) => a - b);
121
+ }
122
+ /**
123
+ * Generate CSS unicode-range from code points.
124
+ *
125
+ * Groups consecutive code points into ranges for smaller CSS.
126
+ *
127
+ * @param codePoints - Sorted array of code points
128
+ * @returns CSS unicode-range value
129
+ */
130
+ export function codePointsToUnicodeRange(codePoints) {
131
+ if (codePoints.length === 0) {
132
+ return '';
133
+ }
134
+ const ranges = [];
135
+ let rangeStart = codePoints[0];
136
+ let rangeEnd = codePoints[0];
137
+ for (let i = 1; i <= codePoints.length; i++) {
138
+ const current = codePoints[i];
139
+ // Check if current is consecutive with previous
140
+ if (current === rangeEnd + 1) {
141
+ rangeEnd = current;
142
+ }
143
+ else {
144
+ // Output the completed range
145
+ if (rangeStart === rangeEnd) {
146
+ ranges.push(`U+${rangeStart.toString(16).toUpperCase().padStart(4, '0')}`);
147
+ }
148
+ else {
149
+ ranges.push(`U+${rangeStart.toString(16).toUpperCase().padStart(4, '0')}-${rangeEnd.toString(16).toUpperCase().padStart(4, '0')}`);
150
+ }
151
+ // Start new range
152
+ if (current !== undefined) {
153
+ rangeStart = current;
154
+ rangeEnd = current;
155
+ }
156
+ }
157
+ }
158
+ return ranges.join(', ');
159
+ }
160
+ /**
161
+ * Get named unicode ranges for specified subsets.
162
+ *
163
+ * @param subsetNames - Array of subset names (e.g., ['latin', 'latin-ext'])
164
+ * @returns Combined unicode-range CSS value
165
+ */
166
+ export function getNamedSubsetRanges(subsetNames) {
167
+ const ranges = [];
168
+ for (const name of subsetNames) {
169
+ const subset = UNICODE_RANGES[name];
170
+ if (subset) {
171
+ ranges.push(subset.range);
172
+ }
173
+ }
174
+ return ranges.join(', ');
175
+ }
176
+ /**
177
+ * Detect which subsets are needed based on characters.
178
+ *
179
+ * @param chars - Set of characters to analyze
180
+ * @returns Array of subset names that cover the characters
181
+ */
182
+ export function detectRequiredSubsets(chars) {
183
+ const needed = new Set();
184
+ const codePoints = charsToCodePoints(chars);
185
+ // Helper to check if code point is in range
186
+ const inRange = (cp, rangeStr) => {
187
+ const ranges = rangeStr.split(',').map(r => r.trim());
188
+ for (const range of ranges) {
189
+ const match = range.match(/U\+([0-9A-F]+)(?:-([0-9A-F]+))?/i);
190
+ if (match) {
191
+ const start = parseInt(match[1], 16);
192
+ const end = match[2] ? parseInt(match[2], 16) : start;
193
+ if (cp >= start && cp <= end) {
194
+ return true;
195
+ }
196
+ }
197
+ }
198
+ return false;
199
+ };
200
+ // Check each code point against all subsets
201
+ for (const cp of codePoints) {
202
+ for (const [name, subset] of Object.entries(UNICODE_RANGES)) {
203
+ if (inRange(cp, subset.range)) {
204
+ needed.add(name);
205
+ break; // Found a match, move to next code point
206
+ }
207
+ }
208
+ }
209
+ return Array.from(needed);
210
+ }
211
+ // ============================================================================
212
+ // File Scanning (Node.js only, used by Vite plugin)
213
+ // ============================================================================
214
+ /**
215
+ * Patterns to match text content in source files.
216
+ * Excludes code, imports, comments.
217
+ */
218
+ const TEXT_EXTRACTION_PATTERNS = {
219
+ // JSX/TSX text content
220
+ jsxText: />\s*([^<>{]+?)\s*</g,
221
+ // String literals (double quotes)
222
+ doubleQuoteString: /"([^"\\]|\\.)*"/g,
223
+ // String literals (single quotes)
224
+ singleQuoteString: /'([^'\\]|\\.)*'/g,
225
+ // Template literals (backticks)
226
+ templateLiteral: /`([^`\\]|\\.)*`/g,
227
+ // HTML text content
228
+ htmlText: />\s*([^<>]+?)\s*</g,
229
+ // Vue template text
230
+ vueText: />\s*([^<>{]+?)\s*</g,
231
+ // Svelte text
232
+ svelteText: />\s*([^<>{]+?)\s*</g,
233
+ };
234
+ /**
235
+ * Extract text content from source code.
236
+ * Used by the Vite plugin to find used characters.
237
+ *
238
+ * @param sourceCode - Source code content
239
+ * @param fileType - File extension (tsx, vue, svelte, etc.)
240
+ * @returns Set of unique characters
241
+ */
242
+ export function extractCharsFromSource(sourceCode, fileType) {
243
+ const chars = new Set();
244
+ const patterns = [];
245
+ // Select patterns based on file type
246
+ switch (fileType) {
247
+ case 'tsx':
248
+ case 'jsx':
249
+ patterns.push(TEXT_EXTRACTION_PATTERNS.jsxText);
250
+ patterns.push(TEXT_EXTRACTION_PATTERNS.doubleQuoteString);
251
+ patterns.push(TEXT_EXTRACTION_PATTERNS.singleQuoteString);
252
+ patterns.push(TEXT_EXTRACTION_PATTERNS.templateLiteral);
253
+ break;
254
+ case 'vue':
255
+ patterns.push(TEXT_EXTRACTION_PATTERNS.vueText);
256
+ patterns.push(TEXT_EXTRACTION_PATTERNS.doubleQuoteString);
257
+ patterns.push(TEXT_EXTRACTION_PATTERNS.singleQuoteString);
258
+ break;
259
+ case 'svelte':
260
+ patterns.push(TEXT_EXTRACTION_PATTERNS.svelteText);
261
+ patterns.push(TEXT_EXTRACTION_PATTERNS.doubleQuoteString);
262
+ patterns.push(TEXT_EXTRACTION_PATTERNS.singleQuoteString);
263
+ break;
264
+ case 'html':
265
+ patterns.push(TEXT_EXTRACTION_PATTERNS.htmlText);
266
+ break;
267
+ default:
268
+ // Generic: extract all string-like content
269
+ patterns.push(TEXT_EXTRACTION_PATTERNS.doubleQuoteString);
270
+ patterns.push(TEXT_EXTRACTION_PATTERNS.singleQuoteString);
271
+ }
272
+ // Extract text using all applicable patterns
273
+ for (const pattern of patterns) {
274
+ // Reset lastIndex for global regexes
275
+ pattern.lastIndex = 0;
276
+ let match;
277
+ while ((match = pattern.exec(sourceCode)) !== null) {
278
+ const text = match[1] || match[0];
279
+ for (const char of extractCharsFromText(text)) {
280
+ chars.add(char);
281
+ }
282
+ }
283
+ }
284
+ return chars;
285
+ }
286
+ // ============================================================================
287
+ // CSS Generation
288
+ // ============================================================================
289
+ /**
290
+ * Generate @font-face CSS with unicode-range.
291
+ *
292
+ * @param fontFamily - Font family name
293
+ * @param fontUrl - URL to font file
294
+ * @param options - Font face options
295
+ * @returns CSS @font-face rule
296
+ */
297
+ export function generateFontFaceCSS(fontFamily, fontUrl, options = {}) {
298
+ const { weight = '400', style = 'normal', display = 'swap', unicodeRange, format = 'woff2', } = options;
299
+ let css = `@font-face {\n`;
300
+ css += ` font-family: '${fontFamily}';\n`;
301
+ css += ` font-style: ${style};\n`;
302
+ css += ` font-weight: ${weight};\n`;
303
+ css += ` font-display: ${display};\n`;
304
+ css += ` src: url('${fontUrl}') format('${format}');\n`;
305
+ if (unicodeRange) {
306
+ css += ` unicode-range: ${unicodeRange};\n`;
307
+ }
308
+ css += `}\n`;
309
+ return css;
310
+ }
311
+ // ============================================================================
312
+ // Exports
313
+ // ============================================================================
314
+ // Types are exported at declaration above
315
+ //# sourceMappingURL=subset.js.map