Skip to content

Commit

Permalink
chore: add support for nested CSS
Browse files Browse the repository at this point in the history
  • Loading branch information
onmax committed Nov 14, 2024
1 parent b726fa2 commit 4f4c8f6
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 30 deletions.
78 changes: 48 additions & 30 deletions src/plugins/transform.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createUnplugin } from 'unplugin'
import { parse, walk } from 'css-tree'
import type { CssNode, StyleSheet } from 'css-tree'
import { walk, parse } from 'css-tree'
import MagicString from 'magic-string'
import { transform } from 'esbuild'
import type { TransformOptions } from 'esbuild'
import type { ESBuildOptions } from 'vite'
import { dirname } from 'pathe'
import { withLeadingSlash } from 'ufo'
import type { RemoteFontSource } from 'unifont'

import type { Awaitable, FontFaceData } from '../types'
import type { GenericCSSFamily } from '../css/parse'
import { extractEndOfFirstChild, extractFontFamilies, extractGeneric } from '../css/parse'
Expand Down Expand Up @@ -103,37 +103,55 @@ export const FontFamilyInjectionPlugin = (options: FontFamilyInjectionPluginOpti

// Collect existing `@font-face` declarations (to skip adding them)
const existingFontFamilies = new Set<string>()
walk(ast, {
visit: 'Declaration',
enter(node) {
if (this.atrule?.name === 'font-face' && node.property === 'font-family') {
for (const family of extractFontFamilies(node)) {
existingFontFamilies.add(family)

// For nested CSS we need to keep track how long the parent selector is
function processNode(node: CssNode, parentOffset = 0) {
walk(node, {
visit: 'Declaration',
enter(node) {
if (this.atrule?.name === 'font-face' && node.property === 'font-family') {
for (const family of extractFontFamilies(node)) {
existingFontFamilies.add(family)
}
}
}
},
})
},
})

walk(ast, {
visit: 'Declaration',
enter(node) {
if (((node.property !== 'font-family' && node.property !== 'font') && (!options.processCSSVariables || !node.property.startsWith('--'))) || this.atrule?.name === 'font-face') {
return
}
walk(node, {
visit: 'Declaration',
enter(node) {
if (((node.property !== 'font-family' && node.property !== 'font') && (!options.processCSSVariables || !node.property.startsWith('--'))) || this.atrule?.name === 'font-face') {
return
}

// Only add @font-face for the first font-family in the list and treat the rest as fallbacks
const [fontFamily, ...fallbacks] = extractFontFamilies(node)
if (fontFamily && !existingFontFamilies.has(fontFamily)) {
promises.push(addFontFaceDeclaration(fontFamily, node.value.type !== 'Raw'
? {
fallbacks,
generic: extractGeneric(node),
index: extractEndOfFirstChild(node)!,
}
: undefined))
}
},
})
// Only add @font-face for the first font-family in the list and treat the rest as fallbacks
const [fontFamily, ...fallbacks] = extractFontFamilies(node)
if (fontFamily && !existingFontFamilies.has(fontFamily)) {
promises.push(addFontFaceDeclaration(fontFamily, node.value.type !== 'Raw'
? {
fallbacks,
generic: extractGeneric(node),
index: extractEndOfFirstChild(node)! + parentOffset,
}
: undefined))
}
},
})

// Nested CSS
walk(node, {
visit: 'Raw',
enter(node) {
const nestedRaw = parse(node.value, { positions: true }) as StyleSheet
const isNestedCss = nestedRaw.children.some(child => child.type === 'Rule')
if (!isNestedCss) return
parentOffset += node.loc!.start.offset
processNode(nestedRaw, parentOffset)
},
})
}

processNode(ast)

await Promise.all(promises)

Expand Down
35 changes: 35 additions & 0 deletions test/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,41 @@ describe('parsing css', () => {
expect([...extracted]).toEqual(['Press Start 2P'])
}
})

it('should handle nested CSS', async () => {
const expected = await transform(`.parent { div { font-family: 'Poppins'; } p { font-family: 'Poppins'; @media (min-width: 768px) { @media (prefers-reduced-motion: reduce) { a { font-family: 'Lato'; } } } } }`)
expect(expected).toMatchInlineSnapshot(`
"@font-face {
font-family: 'Lato';
src: url("/lato.woff2") format(woff2);
font-display: swap;
}
@font-face {
font-family: "Lato Fallback: Times New Roman";
src: local("Times New Roman");
size-adjust: 107.2%;
ascent-override: 92.0709%;
descent-override: 19.8694%;
line-gap-override: 0%;
}
@font-face {
font-family: 'Poppins';
src: url("/poppins.woff2") format(woff2);
font-display: swap;
}
@font-face {
font-family: "Poppins Fallback: Times New Roman";
src: local("Times New Roman");
size-adjust: 123.0769%;
ascent-override: 85.3125%;
descent-override: 28.4375%;
line-gap-override: 8.125%;
}
.parent { div { font-family: 'Poppins', "Poppins Fallback: Times New Roman"; } p { font-family: 'Poppins', "Poppins Fallback: Times New Roman"; @media (min-width: 768px) { @media (prefers-reduced-motion: reduce) { a { font-family: 'Lato', "Lato Fallback: Times New Roman"; } } } } }"
`)
})
})

describe('error handling', () => {
Expand Down

0 comments on commit 4f4c8f6

Please sign in to comment.