♻️ Improve theme.json generation with more robust CSS parsing (#3223)
This commit is contained in:
18
.github/workflows/main.yml
vendored
18
.github/workflows/main.yml
vendored
@@ -37,6 +37,24 @@ jobs:
|
|||||||
npm run build
|
npm run build
|
||||||
cat public/build/manifest.json
|
cat public/build/manifest.json
|
||||||
|
|
||||||
|
- name: Validate theme.json
|
||||||
|
run: |
|
||||||
|
THEME_JSON="public/build/assets/theme.json"
|
||||||
|
|
||||||
|
if [ ! -f "$THEME_JSON" ]; then
|
||||||
|
echo "❌ theme.json not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -e '
|
||||||
|
(.settings.color.palette | map(select(.name == "black")) | length > 0) and
|
||||||
|
(.settings.typography.fontFamilies | map(select(.name == "sans")) | length > 0) and
|
||||||
|
(.settings.typography.fontSizes | map(select(.name == "xl")) | length > 0)
|
||||||
|
' "$THEME_JSON" 2>&1 || {
|
||||||
|
echo "❌ Invalid theme.json structure or missing required values"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
php:
|
php:
|
||||||
name: PHP ${{ matrix.php }}
|
name: PHP ${{ matrix.php }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -163,12 +163,20 @@ export function wordpressRollupPlugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a WordPress theme.json file by combining:
|
* Vite plugin that generates a WordPress theme.json file based on Tailwind v4 CSS variables.
|
||||||
* - Base theme.json settings
|
* This allows theme.json settings to stay in sync with your Tailwind design tokens.
|
||||||
* - Tailwind configuration (colors, fonts, font sizes)
|
|
||||||
*
|
*
|
||||||
* The generated theme.json is emitted to public/build/assets/theme.json
|
* CSS variables defined in an @theme block will be transformed into theme.json format:
|
||||||
* and provides WordPress with theme settings that match your Tailwind configuration.
|
*
|
||||||
|
* @example
|
||||||
|
* ```css
|
||||||
|
* @theme {
|
||||||
|
* --color-primary: #000000; -> { name: "primary", color: "#000000" }
|
||||||
|
* --color-red-500: #ef4444; -> { name: "red-500", color: "#ef4444" }
|
||||||
|
* --font-inter: "Inter"; -> { name: "inter", fontFamily: "Inter" }
|
||||||
|
* --text-lg: 1.125rem; -> { name: "lg", size: "1.125rem" }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @param {Object} options Plugin options
|
* @param {Object} options Plugin options
|
||||||
* @param {Object} options.tailwindConfig - The resolved Tailwind configuration object
|
* @param {Object} options.tailwindConfig - The resolved Tailwind configuration object
|
||||||
@@ -185,6 +193,74 @@ export function wordpressThemeJson({
|
|||||||
}) {
|
}) {
|
||||||
let cssContent = null
|
let cssContent = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely extracts content between matched braces, handling:
|
||||||
|
* - Nested braces
|
||||||
|
* - String literals (both single and double quotes)
|
||||||
|
* - CSS comments
|
||||||
|
* - Escaped characters
|
||||||
|
*/
|
||||||
|
function extractThemeContent(css) {
|
||||||
|
const themeMatch = css.match(/@(?:layer\s+)?theme\s*{/s)
|
||||||
|
if (!themeMatch) {
|
||||||
|
return null // No @theme block - that's fine
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = themeMatch.index + themeMatch[0].length
|
||||||
|
let braceCount = 1
|
||||||
|
|
||||||
|
for (let i = startIndex; i < css.length; i++) {
|
||||||
|
// Skip escaped characters
|
||||||
|
if (css[i] === '\\') {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip string literals
|
||||||
|
if (css[i] === '"' || css[i] === "'") {
|
||||||
|
const quote = css[i]
|
||||||
|
i++
|
||||||
|
while (i < css.length) {
|
||||||
|
if (css[i] === '\\') {
|
||||||
|
i++
|
||||||
|
} else if (css[i] === quote) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if (i >= css.length) {
|
||||||
|
throw new Error('Unclosed string literal in CSS')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip CSS comments
|
||||||
|
if (css[i] === '/' && css[i + 1] === '*') {
|
||||||
|
i += 2
|
||||||
|
while (i < css.length) {
|
||||||
|
if (css[i] === '*' && css[i + 1] === '/') {
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if (i >= css.length) {
|
||||||
|
throw new Error('Unclosed comment in CSS')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (css[i] === '{') braceCount++
|
||||||
|
if (css[i] === '}') braceCount--
|
||||||
|
|
||||||
|
if (braceCount === 0) {
|
||||||
|
return css.substring(startIndex, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unclosed @theme block - missing closing brace')
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'wordpress-theme-json',
|
name: 'wordpress-theme-json',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
@@ -198,51 +274,30 @@ export function wordpressThemeJson({
|
|||||||
|
|
||||||
async generateBundle() {
|
async generateBundle() {
|
||||||
if (!cssContent) {
|
if (!cssContent) {
|
||||||
return;
|
return // No CSS file to process
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const baseThemeJson = JSON.parse(
|
const baseThemeJson = JSON.parse(
|
||||||
fs.readFileSync(path.resolve('./theme.json'), 'utf8')
|
fs.readFileSync(path.resolve('./theme.json'), 'utf8')
|
||||||
)
|
)
|
||||||
|
|
||||||
const themeContent = (() => {
|
const themeContent = extractThemeContent(cssContent)
|
||||||
const match = cssContent.match(/@(?:layer\s+)?theme\s*{/s)
|
|
||||||
|
|
||||||
if (!match[0]) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const startIndex = match.index + match[0].length;
|
|
||||||
let braceCount = 1;
|
|
||||||
for (let i = startIndex; i < cssContent.length; i++) {
|
|
||||||
if (cssContent[i] === "{") braceCount++;
|
|
||||||
if (cssContent[i] === "}") braceCount--;
|
|
||||||
if (braceCount === 0) {
|
|
||||||
return cssContent.substring(startIndex, i );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (!themeContent) {
|
if (!themeContent) {
|
||||||
return;
|
return // No @theme block to process
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!themeContent.trim().startsWith(':root')) {
|
// Process any CSS variables in whatever format they exist
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const endIndex = themeContent.lastIndexOf('}');
|
|
||||||
const rootContent = themeContent.slice(themeContent.indexOf('{') + 1, endIndex === -1 ? undefined : endIndex);
|
|
||||||
const colorVariables = {}
|
const colorVariables = {}
|
||||||
|
|
||||||
const colorVarRegex = /--color-([^:]+):\s*([^;}]+)[;}]?/g
|
const colorVarRegex = /--color-([^:]+):\s*([^;}]+)[;}]?/g
|
||||||
let match
|
let match
|
||||||
|
|
||||||
while ((match = colorVarRegex.exec(rootContent)) !== null) {
|
while ((match = colorVarRegex.exec(themeContent)) !== null) {
|
||||||
const [, name, value] = match
|
const [, name, value] = match
|
||||||
colorVariables[name] = value.trim()
|
colorVariables[name] = value.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform colors to theme.json format
|
||||||
const colors = []
|
const colors = []
|
||||||
Object.entries(colorVariables).forEach(([name, value]) => {
|
Object.entries(colorVariables).forEach(([name, value]) => {
|
||||||
if (name.endsWith('-*')) return
|
if (name.endsWith('-*')) return
|
||||||
@@ -271,11 +326,16 @@ export function wordpressThemeJson({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Process any font families
|
||||||
const fontFamilies = []
|
const fontFamilies = []
|
||||||
const fontVarRegex = /--font-([^:]+):\s*([^;}]+)[;}]?/g
|
const fontVarRegex = /--font-([^:]+):\s*([^;}]+)[;}]?/g
|
||||||
while ((match = fontVarRegex.exec(rootContent)) !== null) {
|
while ((match = fontVarRegex.exec(themeContent)) !== null) {
|
||||||
const [, name, value] = match
|
const [, name, value] = match
|
||||||
if (!name.includes('-feature-settings') && !name.includes('-variation-settings')) {
|
// Skip feature settings, variation settings, and any font-* properties
|
||||||
|
if (!name.includes('feature-settings') &&
|
||||||
|
!name.includes('variation-settings') &&
|
||||||
|
!['family', 'size', 'smoothing', 'style', 'weight', 'stretch']
|
||||||
|
.some(prop => name.includes(prop))) {
|
||||||
fontFamilies.push({
|
fontFamilies.push({
|
||||||
name: name,
|
name: name,
|
||||||
slug: name.toLowerCase(),
|
slug: name.toLowerCase(),
|
||||||
@@ -284,11 +344,13 @@ export function wordpressThemeJson({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process any font sizes
|
||||||
const fontSizes = []
|
const fontSizes = []
|
||||||
const fontSizeVarRegex = /--text-([^:]+):\s*([^;}]+)[;}]?/g
|
const fontSizeVarRegex = /--text-([^:]+):\s*([^;}]+)[;}]?/g
|
||||||
while ((match = fontSizeVarRegex.exec(rootContent)) !== null) {
|
while ((match = fontSizeVarRegex.exec(themeContent)) !== null) {
|
||||||
const [, name, value] = match
|
const [, name, value] = match
|
||||||
if (!name.includes('--line-height')) {
|
// Skip line-height entries
|
||||||
|
if (!name.includes('line-height')) {
|
||||||
fontSizes.push({
|
fontSizes.push({
|
||||||
name: name,
|
name: name,
|
||||||
slug: name.toLowerCase(),
|
slug: name.toLowerCase(),
|
||||||
@@ -297,6 +359,7 @@ export function wordpressThemeJson({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build theme.json with whatever variables were found
|
||||||
const themeJson = {
|
const themeJson = {
|
||||||
__processed__: "This file was generated from Tailwind v4 CSS variables",
|
__processed__: "This file was generated from Tailwind v4 CSS variables",
|
||||||
...baseThemeJson,
|
...baseThemeJson,
|
||||||
@@ -308,19 +371,17 @@ export function wordpressThemeJson({
|
|||||||
palette: colors,
|
palette: colors,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
typography: {
|
||||||
|
defaultFontSizes: false,
|
||||||
|
customFontSize: false,
|
||||||
...((!disableTailwindFonts && fontFamilies.length > 0) && {
|
...((!disableTailwindFonts && fontFamilies.length > 0) && {
|
||||||
typography: {
|
|
||||||
...baseThemeJson.settings?.typography,
|
|
||||||
fontFamilies,
|
fontFamilies,
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
...((!disableTailwindFontSizes && fontSizes.length > 0) && {
|
...(!disableTailwindFontSizes && fontSizes.length > 0 && {
|
||||||
typography: {
|
|
||||||
...baseThemeJson.settings?.typography,
|
|
||||||
fontSizes,
|
fontSizes,
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
delete themeJson.__preprocessed__
|
delete themeJson.__preprocessed__
|
||||||
@@ -330,6 +391,9 @@ export function wordpressThemeJson({
|
|||||||
fileName: 'assets/theme.json',
|
fileName: 'assets/theme.json',
|
||||||
source: JSON.stringify(themeJson, null, 2)
|
source: JSON.stringify(themeJson, null, 2)
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.error(error.message)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user