UNPKG

@unocss/svelte-scoped

Version:

Use UnoCSS in a modular fashion with styles being stored only in the Svelte component they are used in: Vite plugin for apps, Svelte preprocessor for component libraries

443 lines (442 loc) 16.9 kB
import { expandVariantGroup, regexScopePlaceholder, toArray, warnOnce } from "@unocss/core"; import * as acorn from "acorn"; import { walk } from "zimmerframe"; import { clone, generate, parse, walk as walk$1 } from "css-tree"; //#region src/_common/transformClasses/findClasses.ts const classesRE$1 = /class=(["'`])([\s\S]*?)\1/g; const classDirectivesRE = /class:(\S+?)="?\{/g; const classDirectivesShorthandRE = /class:([^=>\s/]+)[{>\s/]/g; const classClsxRE = /(?<prefix>class=\{\w*)[[{(]/g; function findClasses(code) { const matchedClasses = [...code.matchAll(classesRE$1)]; const matchedClassDirectives = [...code.matchAll(classDirectivesRE)]; const matchedClassDirectivesShorthand = [...code.matchAll(classDirectivesShorthandRE)]; const matchedClassClsx = [...code.matchAll(classClsxRE)]; const classes = parseMatches(matchedClasses, "regular", 7); const classDirectives = parseMatches(matchedClassDirectives, "directive", 6); const classDirectivesShorthand = parseMatches(matchedClassDirectivesShorthand, "directiveShorthand", 6); const classClsx = parseMatchesWithAcorn(matchedClassClsx, code); return [ ...classes, ...classDirectives, ...classDirectivesShorthand, ...classClsx ]; } function parseMatches(matches, type, prefixLength) { return matches.map((match) => { const body = match[type === "regular" ? 2 : 1]; const start = match.index + prefixLength; return { body: body.trim(), start, end: start + body.length, type }; }).filter(hasBody); } function hasBody(foundClass) { return foundClass.body; } function parseMatchesWithAcorn(matches, code) { return matches.flatMap((match) => { const start = match.index + match.groups.prefix.length; const ast = acorn.parseExpressionAt(code, start, { sourceType: "module", ecmaVersion: 16, locations: true }); const classes = []; function fromProperty(body, node, property) { return { body, start: node.start, end: node.end, type: property.shorthand ? "clsxObjectShorthand" : "clsxObject" }; } function fromString(body, node) { return { body, start: node.start + 1, end: node.end - 1, type: "regular" }; } walk(ast, { property: void 0 }, { Property(node, { visit }) { visit(node.key, { property: node }); }, Identifier(node, { state, next }) { if (state.property) classes.push(fromProperty(node.name, node, state.property)); next(); }, Literal(node, { state, next }) { if (typeof node.value === "string") { const body = node.value; if (state.property) classes.push(fromProperty(body, node, state.property)); else classes.push(fromString(body, node)); } next(); }, TemplateLiteral(node, { state, next }) { if (node.expressions.length === 0 && node.quasis.length === 1) { const body = node.quasis[0].value.raw; if (state.property) classes.push(fromProperty(body, node, state.property)); else classes.push(fromString(body, node)); } next(); } }); return classes; }).filter(hasBody); } //#endregion //#region src/_common/transformClasses/hash.ts function hash(str) { let i; let l; let hval = 2166136261; for (i = 0, l = str.length; i < l; i++) { hval ^= str.charCodeAt(i); hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); } return `00000${(hval >>> 0).toString(36)}`.slice(-6); } //#endregion //#region src/_common/transformClasses/generateClassName.ts function generateClassName(body, options, filename) { const { classPrefix = "uno-", combine = true, hashFn = hash } = options; if (combine) return `${classPrefix}${hashFn(body + filename)}`; else return `_${body}_${hashFn(filename)}`; } //#endregion //#region src/_common/transformClasses/isShortcut.ts function isShortcut(token, shortcuts) { return shortcuts.some((s) => s[0] === token); } //#endregion //#region src/_common/transformClasses/needsGenerated.ts async function needsGenerated(token, uno) { if (uno.config.safelist.includes(token)) return false; return !!await uno.parseToken(token); } //#endregion //#region src/_common/transformClasses/sortClassesIntoCategories.ts async function sortClassesIntoCategories(body, options, uno, filename) { const { combine = true, hashSafelistClasses = false } = options; const rulesToGenerate = {}; const ignore = []; const classes = body.trim().split(/\s+/); const knownClassesToCombine = []; for (const token of classes) { if (!hashSafelistClasses && uno.config.safelist.includes(token)) { ignore.push(token); continue; } if (!(isShortcut(token, uno.config.shortcuts) || await needsGenerated(token, uno))) { ignore.push(token); continue; } if (combine) knownClassesToCombine.push(token); else { const generatedClassName = generateClassName(token, options, filename); rulesToGenerate[generatedClassName] = [token]; } } if (knownClassesToCombine.length) { const generatedClassName = generateClassName(knownClassesToCombine.join(" "), options, filename); rulesToGenerate[generatedClassName] = knownClassesToCombine; } return { rulesToGenerate, ignore }; } //#endregion //#region src/_common/transformClasses/processExpressions.ts const expressionsRE = /\S*\{[^{}]+\}\S*/g; const classesRE = /(?<=\?\s*|:\s*)(["'`])([\s\S]*?)\1/g; async function processExpressions(body, options, uno, filename) { const rulesToGenerate = {}; const updatedExpressions = []; let restOfBody = body; const expressions = [...body.matchAll(expressionsRE)]; for (let [expression] of expressions) { restOfBody = restOfBody.replace(expression, "").trim(); const classes = [...expression.matchAll(classesRE)]; for (const [withQuotes, quoteMark, withoutQuotes] of classes) { const { rulesToGenerate: rulesFromExpression, ignore } = await sortClassesIntoCategories(withoutQuotes, options, uno, filename); Object.assign(rulesToGenerate, rulesFromExpression); const updatedClasses = Object.keys(rulesFromExpression).concat(ignore).join(" "); expression = expression.replace(withQuotes, quoteMark + updatedClasses + quoteMark); } updatedExpressions.push(expression); } return { rulesToGenerate, updatedExpressions, restOfBody }; } //#endregion //#region src/_common/transformClasses/processClassBody.ts async function processClassBody({ body, start, end }, options, uno, filename) { const { rulesToGenerate: rulesFromExpressions, restOfBody, updatedExpressions } = await processExpressions(expandVariantGroup(body), options, uno, filename); const { rulesToGenerate: rulesFromRegularClasses, ignore } = await sortClassesIntoCategories(restOfBody, options, uno, filename); const rulesToGenerate = { ...rulesFromExpressions, ...rulesFromRegularClasses }; if (!Object.keys(rulesToGenerate).length) return {}; return { rulesToGenerate, codeUpdate: { content: Object.keys(rulesFromRegularClasses).concat(ignore).concat(updatedExpressions).join(" "), start, end } }; } //#endregion //#region src/_common/transformClasses/processDirective.ts async function processDirective({ body: token, start, end, type }, options, uno, filename) { if (!(isShortcut(token, uno.config.shortcuts) || await needsGenerated(token, uno))) return; const generatedClassName = generateClassName(token, options, filename); const content = type === "directiveShorthand" ? `${generatedClassName}={${token}}` : generatedClassName; return { rulesToGenerate: { [generatedClassName]: [token] }, codeUpdate: { content, start, end } }; } //#endregion //#region src/_common/transformClasses/processClsx.ts async function processClsx(cls, options, uno, filename) { if (cls.type === "clsxObject") { const { rulesToGenerate, codeUpdate } = await processClassBody({ ...cls, type: "regular" }, options, uno, filename); if (rulesToGenerate && codeUpdate) { codeUpdate.content = `"${codeUpdate.content}"`; return { rulesToGenerate, codeUpdate }; } } else if (cls.type === "clsxObjectShorthand") { const { rulesToGenerate, codeUpdate } = await processDirective({ ...cls, type: "directive" }, options, uno, filename) ?? {}; if (rulesToGenerate && codeUpdate) { codeUpdate.content = `"${codeUpdate.content}": ${cls.body}`; return { rulesToGenerate, codeUpdate }; } } } //#endregion //#region src/_common/transformClasses/processClasses.ts async function processClasses(classes, options, uno, filename) { const result = { rulesToGenerate: {}, codeUpdates: [] }; for (const foundClass of classes) { const { rulesToGenerate, codeUpdate } = await processClass(foundClass, options, uno, filename); if (rulesToGenerate) Object.assign(result.rulesToGenerate, rulesToGenerate); if (codeUpdate) result.codeUpdates.push(codeUpdate); } return result; } async function processClass(foundClass, options, uno, filename) { if (foundClass.type === "regular") return await processClassBody(foundClass, options, uno, filename); if (foundClass.type === "clsxObject" || foundClass.type === "clsxObjectShorthand") return await processClsx(foundClass, options, uno, filename) ?? {}; return await processDirective(foundClass, options, uno, filename) ?? {}; } //#endregion //#region src/_common/transformClasses/index.ts async function transformClasses({ s, filename, uno, options, removeCommentsToMakeGlobalWrappingEasy }) { const classesToProcess = findClasses(s.original); if (!classesToProcess.length) return; const { rulesToGenerate, codeUpdates } = await processClasses(classesToProcess, options, uno, filename); if (!Object.keys(rulesToGenerate).length) return; if (codeUpdates.length) for (const { start, end, content } of codeUpdates) s.overwrite(start, end, content); return { generatedStyles: await generateStyles(rulesToGenerate, uno, removeCommentsToMakeGlobalWrappingEasy) }; } async function generateStyles(rulesToGenerate, uno, minify) { const shortcutsForThisComponent = Object.entries(rulesToGenerate); uno.config.shortcuts.push(...shortcutsForThisComponent); const selectorsToGenerate = Object.keys(rulesToGenerate); const { css } = await uno.generate(selectorsToGenerate, { preflights: false, safelist: false, minify }); return css; } //#endregion //#region src/_common/transformApply/getUtils.ts async function getUtils(body, uno) { return (await parseUtils(expandVariantGroup(body).split(/\s+/g).map((className) => className.trim().replace(/\\/, "")), uno)).sort(([aIndex], [bIndex]) => aIndex - bIndex).sort(([, , , aParent], [, , , bParent]) => (aParent ? uno.parentOrders.get(aParent) ?? 0 : 0) - (bParent ? uno.parentOrders.get(bParent) ?? 0 : 0)).reduce((acc, item) => { const [, selector, body, parent] = item; const sibling = acc.find(([, targetSelector, , targetParent]) => targetSelector === selector && targetParent === parent); if (sibling) sibling[2] += body; else acc.push([...item]); return acc; }, []); } async function parseUtils(classNames, uno) { const foundUtils = []; for (const token of classNames) { const util = await uno.parseToken(token, "-"); if (util) foundUtils.push(util); else warnOnce(`'${token}' not found. You have a typo or need to add a preset.`); } return foundUtils.flat(); } //#endregion //#region src/_common/transformApply/removeOuterQuotes.ts function removeOuterQuotes(input) { if (!input) return ""; return /^(['"]).*\1$/.test(input) ? input.slice(1, -1) : input; } //#endregion //#region src/_common/transformApply/writeUtilStyles.ts function writeUtilStyles([, selector, body, parent], s, node, childNode) { if (!selector) return; const selectorChanged = selector !== ".\\-"; if (!parent && !selectorChanged) return s.appendRight(childNode.loc.end.offset, body); const originalSelector = generate(node.prelude); if (parent && !selectorChanged) { const css = `${parent}{${originalSelector}{${body}}}`; return s.appendLeft(node.loc.end.offset, css); } const rule = `${surroundAllButOriginalSelectorWithGlobal(originalSelector, generateUpdatedSelector(selector.replace(regexScopePlaceholder, " "), node.prelude))}{${body}}`; const css = parent ? `${parent}{${rule}}` : rule; s.appendLeft(node.loc.end.offset, css); } function generateUpdatedSelector(selector, _prelude) { const selectorAST = parse(selector, { context: "selector" }); const prelude = clone(_prelude); prelude.children.forEach((child) => { const parentSelectorAst = clone(selectorAST); parentSelectorAst.children.forEach((i) => { if (i.type === "ClassSelector" && i.name === "\\-") Object.assign(i, clone(child)); }); Object.assign(child, parentSelectorAst); }); return generate(prelude); } function surroundAllButOriginalSelectorWithGlobal(originalSelector, updatedSelector) { const wrapWithGlobal = (str) => `:global(${str})`; const originalSelectors = originalSelector.split(",").map((s) => s.trim()); const updatedSelectors = updatedSelector.split(",").map((s) => s.trim()); return originalSelectors.map((original, index) => { const [prefix, suffix] = updatedSelectors[index].split(original).map((s) => s.trim()); const wrappedPrefix = prefix ? wrapWithGlobal(prefix) : ""; if (!suffix) return `${wrappedPrefix} ${original}`.trim(); const indexOfFirstCombinator = findFirstCombinatorIndex(suffix); if (indexOfFirstCombinator === -1) return `${wrappedPrefix} ${original}${suffix}`.trim(); return `${wrappedPrefix} ${original}${suffix.substring(0, indexOfFirstCombinator).trim()} ${wrapWithGlobal(suffix.substring(indexOfFirstCombinator).trim())}`.trim(); }).join(", "); } function findFirstCombinatorIndex(input) { for (const c of [ " ", ">", "~", "+" ]) { const indexOfFirstCombinator = input.indexOf(c); if (indexOfFirstCombinator !== -1) return indexOfFirstCombinator; } return -1; } //#endregion //#region src/_common/transformApply/index.ts async function transformApply(ctx) { const ast = parse(ctx.s.original, { parseAtrulePrelude: false, positions: true }); if (ast.type !== "StyleSheet") return ctx.s; const stack = []; walk$1(ast, (node) => { if (node.type === "Rule") stack.push(handleApply(ctx, node)); }); await Promise.all(stack); return ctx.s; } /** transformerDirectives's handleApply function checks for style nesting (childNode.type === 'Raw') but we are not supporting it here as it is not valid syntax in Svelte style tags. If browser support becomes mainstream and Svelte updates in kind, we can support that. */ async function handleApply(ctx, node) { const parsePromises = node.block.children.map(async (childNode) => { await parseApply(ctx, node, childNode); }); await Promise.all(parsePromises); } async function parseApply({ s, uno, applyVariables }, node, childNode) { const body = getChildNodeValue(childNode, applyVariables); if (!body) return; const utils = await getUtils(body, uno); if (!utils.length) return; let end = childNode.loc.end.offset; if (s.slice(end, end + 1) === ";") end += 1; s.remove(childNode.loc.start.offset, end); for (const util of utils) writeUtilStyles(util, s, node, childNode); } function getChildNodeValue(childNode, applyVariables) { if (childNode.type === "Atrule" && childNode.name === "apply" && childNode.prelude && childNode.prelude.type === "Raw") return childNode.prelude.value.trim(); if (childNode.type === "Declaration" && applyVariables.includes(childNode.property) && childNode.value.type === "Raw") return removeOuterQuotes(childNode.value.value.trim()); } //#endregion //#region src/_common/transformTheme.ts const themeRE = /theme\((.+?)\)/g; function transformTheme(s, theme) { return s.replace(themeRE, (_, match) => { return getThemeValue(match.slice(1, -1), theme); }); } function getThemeValue(rawArguments, theme) { const keys = rawArguments.split("."); let current = theme; for (const key of keys) { if (key === "__proto__" || key === "constructor") throw new Error(`"${rawArguments}" contains invalid key "${key}"`); if (current[key] === void 0) throw new Error(`"${rawArguments}" is not found in your theme`); else current = current[key]; } return current; } //#endregion //#region src/_common/transformStyle.ts const DEFAULT_APPLY_VARIABLES = ["--at-apply"]; function checkForApply(content, _applyVariables) { if (_applyVariables === false) return { hasApply: false, applyVariables: [] }; const applyVariables = toArray(_applyVariables || DEFAULT_APPLY_VARIABLES); return { hasApply: content.includes("@apply") || applyVariables.some((v) => content.includes(v)), applyVariables }; } async function transformStyle({ s, uno, prepend, applyVariables, transformThemeFn }) { if (applyVariables?.length) await transformApply({ s, uno, applyVariables }); if (transformThemeFn) transformTheme(s, uno.config.theme); if (!s.hasChanged()) return; if (prepend) s.prepend(prepend); } //#endregion export { transformClasses as i, transformStyle as n, themeRE as r, checkForApply as t };