UNPKG

lightningcss-plugin-utopia

Version:

A LightningCSS plugin to generate fluid typography and scale based on Utopia.fyi

275 lines (260 loc) 7.18 kB
import { calculateClamps, calculateSpaceScale, calculateTypeScale } from "utopia-core"; /** * @param {import("lightningcss").TokenOrValue[]} tokens * @returns {number} */ const parseNumber = (tokens) => { const firstToken = tokens[0]; if (firstToken.type !== "token") { throw "[lightningcss-plugin-utopia] Invalid type `" + firstToken.type + "`, expected `token`"; } if (firstToken.value.type !== "number") { throw "[lightningcss-plugin-utopia] Invalid token type `" + firstToken.value.type + "`, expected `number`"; } return firstToken.value.value; }; /** * @param {import("lightningcss").TokenOrValue[]} tokens * @returns {string} */ const parseString = (tokens) => { const firstToken = tokens[0]; if (firstToken.type !== "token") { throw "[lightningcss-plugin-utopia] Invalid type `" + firstToken.type + "`, expected `token`"; } if (firstToken.value.type !== "string") { throw "[lightningcss-plugin-utopia] Invalid token type `" + firstToken.value.type + "`, expected `string`"; } return firstToken.value.value; }; /** * @param {import("lightningcss").TokenOrValue[]} tokens * @returns {number[]} */ const parseNumbersArray = (tokens) => { const result = []; for (const token of tokens) { if (token.type === "token" && token.value.type === "number") { result.push(token.value.value); } } return result; }; /** * @param {import("lightningcss").TokenOrValue[]} tokens * @returns {[number, number][]} */ const parseNumberPairsArray = (tokens) => { /** @type {number[][]} */ let result = undefined; for (const token of tokens) { if (token.type === "token") { switch (token.value.type) { case "square-bracket-block": if (result === undefined) { result = []; } else { result.push([]); } break; case "number": result[result.length - 1].push(token.value.value); break; } } } return result; }; /** * @param {import("lightningcss").TokenOrValue[]} tokens * @returns {string[]} */ const parseStringsArray = (tokens) => { const result = []; for (const token of tokens) { if (token.type === "token" && token.value.type === "string") { result.push(token.value.value); } } return result; }; /** * @typedef {{property: "custom", value: import("lightningcss").CustomProperty}} CustomDeclaration * @typedef {import("utopia-core").UtopiaSpaceConfig & {"prefix": string}} UtopiaSpaceConfig * @typedef {import("utopia-core").UtopiaTypeConfig & {"prefix": string}} UtopiaTypeConfig * @typedef {import("utopia-core").UtopiaClampsConfig & {"prefix": string}} UtopiaClampsConfig */ /** * @param {CustomDeclaration[]} declarations * @returns {UtopiaSpaceConfig} */ const extractSpaceScaleConfig = (declarations) => { /** @type {UtopiaSpaceConfig} */ const config = {}; for (const { value: { name, value } } of declarations) { /** @type {keyof UtopiaSpaceConfig} */ const field = name; switch (field) { case "minWidth": config[field] = parseNumber(value); break; case "maxWidth": config[field] = parseNumber(value); break; case "minSize": config[field] = parseNumber(value); break; case "maxSize": config[field] = parseNumber(value); break; case "negativeSteps": config[field] = parseNumbersArray(value); break; case "positiveSteps": config[field] = parseNumbersArray(value); break; case "customSizes": config[field] = parseStringsArray(value); break; case "relativeTo": config[field] = parseString(value); break; case "prefix": config[field] = parseString(value); break; } } return config; }; /** * @param {CustomDeclaration[]} declarations * @returns {UtopiaTypeConfig} */ const extractTypeScaleConfig = (declarations) => { /** @type {UtopiaTypeConfig} */ const config = {}; for (const { value: { name, value } } of declarations) { /** @type {keyof UtopiaTypeConfig} */ const field = name; switch (field) { case "minWidth": config[field] = parseNumber(value); break; case "maxWidth": config[field] = parseNumber(value); break; case "negativeSteps": config[field] = parseNumber(value); break; case "positiveSteps": config[field] = parseNumber(value); break; case "minFontSize": config[field] = parseNumber(value); break; case "maxFontSize": config[field] = parseNumber(value); break; case "minTypeScale": config[field] = parseNumber(value); break; case "maxTypeScale": config[field] = parseNumber(value); break; case "relativeTo": config[field] = parseString(value); break; case "labelStyle": config[field] = parseString(value); break; case "prefix": config[field] = parseString(value); break; } } return config; }; /** * @param {CustomDeclaration[]} declarations * @returns {UtopiaClampsConfig} */ const extractClampsConfig = (declarations) => { /** @type {UtopiaClampsConfig} */ const config = {}; for (const { value: { name, value } } of declarations) { /** @type {keyof UtopiaClampsConfig} */ const field = name; switch (field) { case "minWidth": config[field] = parseNumber(value); break; case "maxWidth": config[field] = parseNumber(value); break; case "relativeTo": config[field] = parseString(value); break; case "pairs": config[field] = parseNumberPairsArray(value); break; case "prefix": config[field] = parseString(value); break; } } return config; }; /** @type {import("lightningcss").Visitor} */ export const utopiaVisitor = { Rule: { custom: { utopia(rule) { /** @type {import("lightningcss").ReturnedDeclaration[]} */ let declarations = []; if (rule.prelude.value === "spaceScale") { const config = extractSpaceScaleConfig(rule.body.value.declarations); const spaceScale = calculateSpaceScale(config); declarations = [...spaceScale.sizes, ...spaceScale.oneUpPairs, ...spaceScale.customPairs] .map(step => ({ property: `--${config.prefix || "space"}-${step.label}`, raw: step.clamp, })); } else if (rule.prelude.value === "typeScale") { const config = extractTypeScaleConfig(rule.body.value.declarations); const typeScale = calculateTypeScale(config); declarations = typeScale.map(step => ({ property: `--${config.prefix || "type"}-${step.label}`, raw: step.clamp, })); } else if (rule.prelude.value === "clamps") { const config = extractClampsConfig(rule.body.value.declarations); const clamps = calculateClamps(config); declarations = clamps.map(step => ({ property: `--${config.prefix || "step"}-${step.label}`, raw: step.clamp, })); } else { throw "[lightningcss-plugin-utopia] Unsupported @utopia rule: " + rule.prelude.value; } return { type: "style", value: { loc: rule.loc, selectors: [[{ type: "nesting" }]], declarations: { declarations } } }; } } } }; /** @type {import("lightningcss").CustomAtRules} */ export const customAtRules = { utopia: { prelude: "<custom-ident>", body: "declaration-list" }, };