import { inferTokenGroup, normalizeTokenGroupComment } from "./categories.js"; import type { ParsedToken, TokenParseOutput } from "./types.js"; export function detectTokenPrefix(names: string[]): string { if (names.length === 0) return "--"; const stripped = names.map((name) => name.slice(2)); let prefix = ""; const first = stripped[0] ?? ""; for (let i = 0; i < first.length; i++) { const ch = first[i]; if (stripped.every((s) => s[i] === ch)) { prefix += ch; } else { break; } } const lastHyphen = prefix.lastIndexOf("-"); if (lastHyphen > 0) { prefix = prefix.slice(0, lastHyphen + 1); } return `--${prefix}`; } export function parseScssVariables(content: string): Map { const vars = new Map(); const scssVarRegex = /^\s*(\$[\w-]+)\s*:\s*(.+?)\s*(?:!default\s*)?;/gm; let match: RegExpExecArray | null; while ((match = scssVarRegex.exec(content)) !== null) { const name = match[1]; const value = match[2].replace(/\s*\/\/.*$/, "").trim(); if (!vars.has(name)) { vars.set(name, value); } } return vars; } export function resolveTokenValue( rawValue: string, scssVars: Map, cssVarValues: Map, depth = 0, ): string { if (depth > 5) return rawValue; let resolved = rawValue; resolved = resolved.replace(/#\{(\$[\w-]+)\}/g, (_, varName) => { const val = scssVars.get(varName); return val !== undefined ? resolveTokenValue(val, scssVars, cssVarValues, depth + 1) : `#{${varName}}`; }); resolved = resolved.replace(/(? { const val = scssVars.get(varName); return val !== undefined ? resolveTokenValue(val, scssVars, cssVarValues, depth + 1) : varName; }); resolved = resolved.replace( /var\((--[\w-]+)(?:\s*,\s*(.+?))?\)/g, (original, tokenName, fallback) => { const tokenVal = cssVarValues.get(tokenName); if (tokenVal !== undefined) { return resolveTokenValue(tokenVal, scssVars, cssVarValues, depth + 1); } if (fallback) { return resolveTokenValue(fallback.trim(), scssVars, cssVarValues, depth + 1); } return original; }, ); return resolved; } export function parseScssTokens(content: string, filePath = "tokens.scss"): TokenParseOutput { const lines = content.split("\n"); const tokens: ParsedToken[] = []; const seenNames = new Set(); let currentCategory = "other"; let hasCommentCategories = false; const scssVars = parseScssVariables(content); const varDeclRegex = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;/; const sectionCommentRegex = /^\s*\/\/\s+([A-Z].+)$/; for (const line of lines) { const commentMatch = line.match(sectionCommentRegex); if (commentMatch) { const normalized = normalizeTokenGroupComment(commentMatch[0]); if (normalized) { currentCategory = normalized; hasCommentCategories = true; } continue; } const varMatch = line.match(varDeclRegex); if (varMatch) { const name = varMatch[1]; const rawValue = varMatch[2]; if (seenNames.has(name)) continue; seenNames.add(name); const inlineComment = line.match(/\/\/\s*(.+)$/); const description = inlineComment ? inlineComment[1].trim() : undefined; const cleanValue = rawValue.replace(/\s*\/\/.*$/, "").trim(); tokens.push({ name, value: cleanValue || undefined, category: hasCommentCategories ? currentCategory : inferTokenGroup(name), description, }); } } for (const [name, value] of scssVars) { if (seenNames.has(name)) continue; seenNames.add(name); tokens.push({ name, value, category: hasCommentCategories ? currentCategory : inferTokenGroup(name), }); } const cssVarValues = new Map(); for (const token of tokens) { if (token.value) { cssVarValues.set(token.name, token.value); } } for (const token of tokens) { if (token.value) { const resolved = resolveTokenValue(token.value, scssVars, cssVarValues); if (resolved !== token.value && !resolved.includes("#{") && !resolved.includes("$")) { token.resolvedValue = resolved; } } } const categories: Record = {}; for (const token of tokens) { categories[token.category] ??= []; categories[token.category].push(token); } return { prefix: detectTokenPrefix(tokens.map((token) => token.name)), categories, total: tokens.length, }; } export function parseCssTokens(content: string, filePath = "tokens.css"): TokenParseOutput { return parseScssTokens(content, filePath); } export const parseTokenFile = parseScssTokens;