import { Features } from '.'
import { atRule, context, type AstNode } from './ast'
import * as CSS from './css-parser'
import * as ValueParser from './value-parser'
import { walk, WalkAction } from './walk'
type LoadStylesheet = (
id: string,
basedir: string,
) => Promise<{
path: string
base: string
content: string
}>
export async function substituteAtImports(
ast: AstNode[],
base: string,
loadStylesheet: LoadStylesheet,
recurseCount = 0,
track = false,
) {
let features = Features.None
let promises: Promise<void>[] = []
walk(ast, (node) => {
if (node.kind === 'at-rule' && (node.name === '@import' || node.name === '@reference')) {
let parsed = parseImportParams(ValueParser.parse(node.params))
if (parsed === null) return
if (node.name === '@reference') {
parsed.media = 'reference'
}
features |= Features.AtImport
let { uri, layer, media, supports } = parsed
if (uri.startsWith('data:')) return
if (uri.startsWith('http://') || uri.startsWith('https://')) return
let contextNode = context({}, [])
promises.push(
(async () => {
if (recurseCount > 100) {
throw new Error(
`Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`,
)
}
let loaded = await loadStylesheet(uri, base)
let ast = CSS.parse(loaded.content, { from: track ? loaded.path : undefined })
await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1, track)
contextNode.nodes = buildImportNodes(
node,
[context({ base: loaded.base }, ast)],
layer,
media,
supports,
)
})(),
)
return WalkAction.ReplaceSkip(contextNode)
}
})
if (promises.length > 0) {
await Promise.all(promises)
}
return features
}
export function parseImportParams(params: ValueParser.ValueAstNode[]) {
let uri
let layer: string | null = null
let media: string | null = null
let supports: string | null = null
for (let i = 0; i < params.length; i++) {
let node = params[i]
if (node.kind === 'separator') continue
if (node.kind === 'word' && !uri) {
if (!node.value) return null
if (node.value[0] !== '"' && node.value[0] !== "'") return null
uri = node.value.slice(1, -1)
continue
}
if (node.kind === 'function' && node.value.toLowerCase() === 'url') {
return null
}
if (!uri) return null
if (
(node.kind === 'word' || node.kind === 'function') &&
node.value.toLowerCase() === 'layer'
) {
if (layer) return null
if (supports) {
throw new Error(
'`layer(…)` in an `@import` should come before any other functions or conditions',
)
}
if ('nodes' in node) {
layer = ValueParser.toCss(node.nodes)
} else {
layer = ''
}
continue
}
if (node.kind === 'function' && node.value.toLowerCase() === 'supports') {
if (supports) return null
supports = ValueParser.toCss(node.nodes)
continue
}
media = ValueParser.toCss(params.slice(i))
break
}
if (!uri) return null
return { uri, layer, media, supports }
}
function buildImportNodes(
importNode: AstNode,
importedAst: AstNode[],
layer: string | null,
media: string | null,
supports: string | null,
): AstNode[] {
let root = importedAst
if (layer !== null) {
let node = atRule('@layer', layer, root)
node.src = importNode.src
root = [node]
}
if (media !== null) {
let node = atRule('@media', media, root)
node.src = importNode.src
root = [node]
}
if (supports !== null) {
let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)
node.src = importNode.src
root = [node]
}
return root
}