| --- |
| import { readFileSync } from 'node:fs'; |
| import { join } from 'node:path'; |
| |
| interface Props { |
| utility: string | string[]; // The utility key(s) from the metadata (e.g., "font-size" or ["font-size", "text-size"]) |
| className?: string; |
| } |
| |
| const { |
| utility, |
| className = "table reference-table" |
| } = Astro.props; |
| |
| // Normalize to array |
| const utilities = Array.isArray(utility) ? utility : [utility]; |
| |
| // Parse CSS variables from _root.scss at build time |
| function parseCSSVariables(): Record<string, string> { |
| try { |
| const projectRoot = process.cwd(); |
| const rootScssPath = join(projectRoot, 'scss/_root.scss'); |
| const scssContent = readFileSync(rootScssPath, 'utf-8'); |
| |
| const cssVarValues: Record<string, string> = {}; |
| const varRegex = /--#\{\$prefix\}([a-z0-9-]+):\s*([^;]+);/gi; |
| let match; |
| |
| while ((match = varRegex.exec(scssContent)) !== null) { |
| const varName = `--bs-${match[1]}`; |
| let value = match[2].trim(); |
| value = value.replace(/#\{[^}]+\}/g, '').trim(); |
| value = value.replace(/\/\/.*$/gm, '').trim(); |
| |
| if (value) { |
| cssVarValues[varName] = value; |
| } |
| } |
| |
| return cssVarValues; |
| } catch (error) { |
| console.warn('Could not parse CSS variables from _root.scss:', error); |
| return {}; |
| } |
| } |
| |
| // Load utilities metadata |
| function loadUtilitiesMetadata(): any { |
| try { |
| const projectRoot = process.cwd(); |
| const metadataPath = join(projectRoot, 'dist/css/bootstrap-utilities.metadata.json'); |
| const metadataContent = readFileSync(metadataPath, 'utf-8'); |
| return JSON.parse(metadataContent); |
| } catch (error) { |
| console.warn('Could not load utilities metadata:', error); |
| return { utilities: {} }; |
| } |
| } |
| |
| // Parse compiled CSS to extract styles for given class selectors |
| function parseCompiledCSS(classNames: string[]): Record<string, string[]> { |
| try { |
| const projectRoot = process.cwd(); |
| const bootstrapCssPath = join(projectRoot, 'dist/css/bootstrap.css'); |
| const cssContent = readFileSync(bootstrapCssPath, 'utf-8'); |
| |
| const classStyles: Record<string, string[]> = {}; |
| |
| classNames.forEach(className => { |
| // Match ONLY single class selectors: .classname { declarations } |
| const escapedClass = className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| const selectorRegex = new RegExp(`(?:^|\\n)\\s*\\.${escapedClass}\\s*\\{([^}]+)\\}`, 'gm'); |
| |
| let match; |
| let foundDeclarations: string[] = []; |
| |
| while ((match = selectorRegex.exec(cssContent)) !== null) { |
| const declarations = match[1] |
| .split(';') |
| .map(decl => decl.trim()) |
| .filter(decl => decl.length > 0) |
| .map(decl => `${decl};`); |
| |
| if (declarations.length > 0) { |
| foundDeclarations = declarations; |
| break; |
| } |
| } |
| |
| classStyles[className] = foundDeclarations; |
| }); |
| |
| return classStyles; |
| } catch (error) { |
| console.warn('Could not parse compiled CSS:', error); |
| return {}; |
| } |
| } |
| |
| const cssVarValues = parseCSSVariables(); |
| const metadata = loadUtilitiesMetadata(); |
| |
| // Collect classes from all specified utilities |
| let allClasses: string[] = []; |
| |
| utilities.forEach(util => { |
| const utilityMeta = metadata.utilities[util]; |
| |
| if (!utilityMeta) { |
| console.warn(`Utility "${util}" not found in metadata. Available utilities: ${Object.keys(metadata.utilities).join(', ')}`); |
| return; |
| } |
| |
| const classes = utilityMeta.classes || []; |
| allClasses = allClasses.concat(classes); |
| }); |
| |
| if (allClasses.length === 0) { |
| throw new Error(`No classes found for utilities: ${utilities.join(', ')}`); |
| } |
| |
| const classStyles = parseCompiledCSS(allClasses); |
| |
| // Function to add CSS variable value comments |
| function addVarComments(cssValue: string): string { |
| const comments: string[] = []; |
| |
| cssValue.replace(/var\((--[a-z0-9-]+)\)/gi, (match, varName) => { |
| const resolvedValue = cssVarValues[varName]; |
| if (resolvedValue) { |
| comments.push(`<span class="fg-3">/* ${resolvedValue} */</span>`); |
| } |
| return match; |
| }); |
| |
| if (comments.length > 0) { |
| const hasSemicolon = cssValue.trimEnd().endsWith(';'); |
| return `${cssValue}${hasSemicolon ? '' : ';'} ${comments.join(' ')}`; |
| } |
| |
| return cssValue; |
| } |
| |
| // Build table data |
| const tableData = allClasses.map(cls => { |
| const styles = classStyles[cls] || []; |
| const formattedStyles = styles.map(style => addVarComments(style)).join('<br/>'); |
| |
| return { |
| class: `.${cls}`, |
| styles: formattedStyles || '<em>Not found</em>' |
| }; |
| }); |
| --- |
| |
| <div class="table-responsive bd-reference-table"> |
| <table class={className}> |
| <thead> |
| <tr> |
| <th scope="col">Class</th> |
| <th scope="col">Styles</th> |
| </tr> |
| </thead> |
| <tbody> |
| {tableData.map((row) => ( |
| <tr> |
| <td>{row.class}</td> |
| <td><Fragment set:html={row.styles} /></td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |