diff --git a/package.json b/package.json index dfc9ae3..f34b727 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "google-font-metadata", "description": "A metadata generator for Google Fonts.", - "version": "6.0.5", + "version": "6.0.6", "author": "Ayuhito ", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/src/variable-parser.ts b/src/variable-parser.ts index dda8905..e780a93 100644 --- a/src/variable-parser.ts +++ b/src/variable-parser.ts @@ -220,6 +220,32 @@ export const parseCSS = (cssTuple: string[][], defSubset?: string) => { } if (rule.type === '@font-face') { + // For each @font-face rule, we need to determine the actual subset + // This could come from a comment (old format) or URL pattern (new format) + let actualSubset = subset; + + // First, look for any URL to extract subset from filename if needed. + for (const subrule of rule.children) { + if (typeof subrule !== 'string' && subrule.props === 'src') { + if (typeof subrule.children === 'string') { + const typeMatch = /(url)\((.+?)\)/g; + const match: string[][] = [ + ...subrule.children.matchAll(typeMatch), + ]; + if (match.length > 0) { + const path: string = match[0][2]; + actualSubset = extractSubsetFromUrl( + path, + subset, + defSubset ?? 'latin', + ); + break; + } + } + } + } + + // Then process all properties with the correct subset for (const subrule of rule.children) { // Type guard to ensure there are children in font-face rules if (typeof subrule === 'string') @@ -246,7 +272,7 @@ export const parseCSS = (cssTuple: string[][], defSubset?: string) => { const path: string = match[0][2]; if (type === 'url') - fontVariants[fontType][fontStyle][subset] = path; + fontVariants[fontType][fontStyle][actualSubset] = path; } } } @@ -297,3 +323,24 @@ export const parseVariable = async (noValidate: boolean) => { } variable font datapoints have been generated.`, ); }; + +/** + * Extract subset from URL filename for numbered subsets. + * Falls back to current subset if no numbered pattern is found. + */ +const extractSubsetFromUrl = ( + url: string, + currentSubset: string, + defSubset: string, +): string => { + // If current subset is not the default, it was set by a comment, so use it + if (currentSubset !== defSubset) return currentSubset; + + // Extract numbered subset from filename pattern like .123.woff2 + const match = url.match(/\.(\d+)\.(woff2?|ttf|otf)$/); + if (match) { + return `[${match[1]}]`; + } + + return currentSubset; +}; diff --git a/tests/variable-parser.test.ts b/tests/variable-parser.test.ts index 153584f..569a6d0 100644 --- a/tests/variable-parser.test.ts +++ b/tests/variable-parser.test.ts @@ -9,6 +9,7 @@ import { addAndMergeAxesRange, fetchAllCSS, generateCSSLinks, + parseCSS, parseVariable, sortAxes, } from '../src/variable-parser'; @@ -233,6 +234,105 @@ describe('Variable Parser', () => { }); }); + describe('Parse CSS with numbered subsets', () => { + it('Handles numbered subsets without comments (new format)', () => { + // Mock CSS data that mimics the new Google Fonts format without comments + // but with numbered subsets in the URL filenames + const cssWithoutComments: string[][] = [ + [ + 'wght.normal', + `@font-face { + font-family: 'Noto Sans JP Variable'; + font-style: normal; + font-weight: 100 900; + src: url(https://fonts.gstatic.com/s/notosansjp/v55/variable-font.0.woff2) format('woff2'); +} +@font-face { + font-family: 'Noto Sans JP Variable'; + font-style: normal; + font-weight: 100 900; + src: url(https://fonts.gstatic.com/s/notosansjp/v55/variable-font.1.woff2) format('woff2'); +} +@font-face { + font-family: 'Noto Sans JP Variable'; + font-style: normal; + font-weight: 100 900; + src: url(https://fonts.gstatic.com/s/notosansjp/v55/variable-font.119.woff2) format('woff2'); +}`, + ], + ]; + + const result = parseCSS(cssWithoutComments); + + // Should extract numbered subsets from URLs: [0], [1], and [119] + expect(result.wght.normal['[0]']).toBe( + 'https://fonts.gstatic.com/s/notosansjp/v55/variable-font.0.woff2', + ); + expect(result.wght.normal['[1]']).toBe( + 'https://fonts.gstatic.com/s/notosansjp/v55/variable-font.1.woff2', + ); + expect(result.wght.normal['[119]']).toBe( + 'https://fonts.gstatic.com/s/notosansjp/v55/variable-font.119.woff2', + ); + }); + + it('Handles numbered subsets with comments (old format)', () => { + // Mock CSS data with comments (old format) + const cssWithComments: string[][] = [ + [ + 'wght.normal', + `/* [0] */ +@font-face { + font-family: 'Noto Sans JP Variable'; + font-style: normal; + font-weight: 100 900; + src: url(https://fonts.gstatic.com/s/notosansjp/v55/variable-font.0.woff2) format('woff2'); +} +/* [1] */ +@font-face { + font-family: 'Noto Sans JP Variable'; + font-style: normal; + font-weight: 100 900; + src: url(https://fonts.gstatic.com/s/notosansjp/v55/variable-font.1.woff2) format('woff2'); +}`, + ], + ]; + + const result = parseCSS(cssWithComments); + + // Should use comment-based subset names [0] and [1] + expect(result.wght.normal['[0]']).toBe( + 'https://fonts.gstatic.com/s/notosansjp/v55/variable-font.0.woff2', + ); + expect(result.wght.normal['[1]']).toBe( + 'https://fonts.gstatic.com/s/notosansjp/v55/variable-font.1.woff2', + ); + }); + + it('Falls back to default subset for non-numbered URLs', () => { + // Mock CSS data with regular URLs (no numbered pattern) + const cssRegular: string[][] = [ + [ + 'wght.normal', + `/* latin */ +@font-face { + font-family: 'Roboto Flex'; + font-style: normal; + font-weight: 100 1000; + src: url(https://fonts.gstatic.com/s/robotoflex/v30/regular.woff2) format('woff2'); +}`, + ], + ]; + + const result = parseCSS(cssRegular); + + // Should use comment-based subset name 'latin' + expect(result.wght.normal.latin).toBe( + 'https://fonts.gstatic.com/s/robotoflex/v30/regular.woff2', + ); + }); + }); + describe('Full parse and order', () => { it('Parses successfully', async () => { await parseVariable(false);