Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <hello@ayuhito.com>",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
49 changes: 48 additions & 1 deletion src/variable-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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;
}
}
}
Expand Down Expand Up @@ -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;
};
100 changes: 100 additions & 0 deletions tests/variable-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
addAndMergeAxesRange,
fetchAllCSS,
generateCSSLinks,
parseCSS,
parseVariable,
sortAxes,
} from '../src/variable-parser';
Expand Down Expand Up @@ -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);
Expand Down