Skip to content

Commit 9e4fd72

Browse files
feat(google): support subsets (#172)
1 parent c8c6b26 commit 9e4fd72

File tree

2 files changed

+108
-14
lines changed

2 files changed

+108
-14
lines changed

src/providers/google.ts

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { FontFaceData, ResolveFontOptions } from '../types'
22

3+
import { findAll, generate, parse } from 'css-tree'
34
import { hash } from 'ohash'
45
import { extractFontFaceData } from '../css/parse'
56
import { $fetch } from '../fetch'
@@ -25,6 +26,37 @@ interface ProviderOption {
2526
}
2627
}
2728

29+
export function splitCssIntoSubsets(input: string): { subset: string | null, css: string }[] {
30+
const data: { subset: string | null, css: string }[] = []
31+
32+
const comments: { value: string, endLine: number }[] = []
33+
const nodes = findAll(
34+
parse(input, {
35+
positions: true,
36+
// Comments are not part of the tree. We rely on the positions to infer the subset
37+
onComment(value, loc) {
38+
comments.push({ value: value.trim(), endLine: loc.end.line })
39+
},
40+
}),
41+
node => node.type === 'Atrule' && node.name === 'font-face',
42+
)
43+
44+
// If there are no comments, we don't associate subsets because we can't
45+
if (comments.length === 0) {
46+
return [{ subset: null, css: input }]
47+
}
48+
49+
for (const node of nodes) {
50+
const comment = comments.filter(comment => comment.endLine < node.loc!.start.line).at(-1)
51+
if (!comment)
52+
continue
53+
54+
data.push({ subset: comment.value, css: generate(node) })
55+
}
56+
57+
return data
58+
}
59+
2860
export default defineFontProvider<ProviderOption>('google', async (_options = {}, ctx) => {
2961
const googleFonts = await ctx.storage.getItem('google:meta.json', () => $fetch<{ familyMetadataList: FontIndexMeta[] }>('https://fonts.google.com/metadata/fonts', { responseType: 'json' }).then(r => r.familyMetadataList))
3062

@@ -82,24 +114,31 @@ export default defineFontProvider<ProviderOption>('google', async (_options = {}
82114
const resolvedFontFaceData: FontFaceData[] = []
83115

84116
for (const extension in userAgents) {
85-
const data = extractFontFaceData(await $fetch<string>('/css2', {
117+
const rawCss = await $fetch<string>('/css2', {
86118
baseURL: 'https://fonts.googleapis.com',
87-
headers: { 'user-agent': userAgents[extension as keyof typeof userAgents] },
119+
headers: {
120+
'user-agent': userAgents[extension as keyof typeof userAgents],
121+
},
88122
query: {
89-
family: `${family}:${resolvedAxes.join(',')}@${resolvedVariants.join(';')}`,
123+
family: `${family}:${resolvedAxes.join(',')}@${resolvedVariants.join(
124+
';',
125+
)}`,
90126
...(glyphs && { text: glyphs }),
91127
},
92-
}))
93-
data.map((f) => {
94-
f.meta ??= {}
95-
f.meta.priority = priority
96-
return f
97128
})
98-
resolvedFontFaceData.push(...data)
129+
const groups = splitCssIntoSubsets(rawCss).filter(group => group.subset ? options.subsets.includes(group.subset) : true)
130+
for (const group of groups) {
131+
const data = extractFontFaceData(group.css)
132+
data.map((f) => {
133+
f.meta ??= {}
134+
f.meta.priority = priority
135+
return f
136+
})
137+
resolvedFontFaceData.push(...data)
138+
}
99139
priority++
100140
}
101141

102-
// TODO: support subsets
103142
return resolvedFontFaceData
104143
}
105144

test/providers/google.test.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ResolveFontOptions } from '../../src'
22
import { describe, expect, it } from 'vitest'
33
import { createUnifont, providers } from '../../src'
4+
import { splitCssIntoSubsets } from '../../src/providers/google'
45
import { getOptimizerIdentityFromUrl, groupBy, pickUniqueBy } from '../utils'
56

67
describe('google', () => {
@@ -16,7 +17,7 @@ describe('google', () => {
1617

1718
const { fonts } = await unifont.resolveFont('Poppins')
1819

19-
expect(fonts).toHaveLength(8)
20+
expect(fonts).toHaveLength(6)
2021
})
2122

2223
it('filters fonts based on provided options', async () => {
@@ -27,13 +28,12 @@ describe('google', () => {
2728
const { fonts } = await unifont.resolveFont('Poppins', {
2829
styles,
2930
weights,
30-
subsets: [],
3131
})
3232

3333
const resolvedStyles = pickUniqueBy(fonts, fnt => fnt.style)
3434
const resolvedWeights = pickUniqueBy(fonts, fnt => String(fnt.weight))
3535

36-
expect(fonts).toHaveLength(4)
36+
expect(fonts).toHaveLength(3)
3737
expect(resolvedStyles).toMatchObject(styles)
3838
expect(resolvedWeights).toMatchObject(weights)
3939
})
@@ -107,7 +107,6 @@ describe('google', () => {
107107
const { fonts } = await unifont.resolveFont('Poppins', {
108108
styles: ['normal'],
109109
weights: ['400'],
110-
subsets: [],
111110
})
112111

113112
// Do not use sanitizeFontSource here, as we must test the optimizer identity in url params
@@ -146,4 +145,60 @@ describe('google', () => {
146145
}
147146
`)
148147
})
148+
149+
it('filters subsets correctly', async () => {
150+
const unifont = await createUnifont([providers.google()])
151+
152+
const { fonts } = await unifont.resolveFont('Roboto', { subsets: ['latin'] })
153+
expect(fonts.length).toEqual(4)
154+
})
155+
156+
describe('splitCssIntoSubsets()', () => {
157+
it('associates subsets and css correctly if there are comments', () => {
158+
expect(
159+
splitCssIntoSubsets(`
160+
/* vietnamese */
161+
@font-face {
162+
font-family: 'A';
163+
}
164+
/* latin-ext */
165+
@font-face {
166+
font-family: 'B';
167+
}
168+
@font-face {
169+
font-family: 'Still B';
170+
}
171+
/* latin */
172+
@font-face {
173+
font-family: 'C';
174+
}
175+
body {
176+
--google-font-color-bungeetint:none;
177+
}
178+
@font-face {
179+
font-family: 'Still C';
180+
}
181+
`),
182+
).toEqual([
183+
{ subset: 'vietnamese', css: '@font-face{font-family:"A"}' },
184+
{ subset: 'latin-ext', css: '@font-face{font-family:"B"}' },
185+
{ subset: 'latin-ext', css: '@font-face{font-family:"Still B"}' },
186+
{ subset: 'latin', css: '@font-face{font-family:"C"}' },
187+
{ subset: 'latin', css: '@font-face{font-family:"Still C"}' },
188+
])
189+
})
190+
})
191+
192+
it('it does not associate subsets if there are no comments', () => {
193+
const input = `
194+
@font-face {
195+
font-family: 'A';
196+
}
197+
@font-face {
198+
font-family: 'B';
199+
}
200+
`
201+
202+
expect(splitCssIntoSubsets(input)).toEqual([{ subset: null, css: input }])
203+
})
149204
})

0 commit comments

Comments
 (0)