import apiFetch from '@wordpress/api-fetch'; import { purgeBites, validateBites, saveBites, } from '@components/Bites/BitesPurgeHelpers'; import { getBiteUtilsPlugin } from '@tailwind/config'; interface Block { attributes: { [key: string]: any }; innerBlocks: Block[]; } interface PageData { properties: string; // array string of refs of reusable blocks reusables: string[]; } export const blockbitePurge = async () => { // Purge bites if (blockbite.data.postType === 'blockbites') { const bitesData = purgeBites(); const validateString = validateBites(bitesData); if (validateString !== '') { const validateAsComponent = `.validate { @apply ${validateString}; }`; createTailwind('validate', validateAsComponent).then(() => { saveBites(bitesData); wp.data.dispatch('biteStore/editor').updateUtils(bitesData.utils); }); } else { // allow creation of bites without any styles saveBites(bitesData); } // TODO: optimize // save bites await purgePost('bites-css'); // save frontend await purgePost('frontend-css'); } else { await purgePost('frontend-css'); } }; const purgePost = async ( handle: 'frontend-css' | 'bites-css' ): Promise => { const blocks = wp.data.select('core/block-editor').getBlocks() as Block[]; // Get prevSiteClasses site style classes const prevSiteClasses = await getStyles(handle); const { properties, reusables } = blockbiteExtract(blocks); // Save styles for reusable blocks let reusablesProperties = ''; if (reusables.length > 0) { if (typeof prevSiteClasses === 'string') { reusablesProperties = await saveStylesWithReusables(reusables); } } // format classes const { biteClasses, tailwindClasses } = formatClasses( prevSiteClasses, `${properties} ${reusablesProperties}` ); // save styles saveStyles(biteClasses, tailwindClasses, handle); }; const formatClasses = (prevSiteClasses: string, newClasses: string) => { const uniqueTailwindClasses = uniqueClasses( `${prevSiteClasses} ${newClasses}` ).split(' '); const utils = wp.data.select('biteStore/editor').getUtils() || []; // check if the biteClasses have a valid tw class const biteClasses = uniqueTailwindClasses .filter( (cls) => cls.startsWith('bit_') && utils.some((util: any) => util.id === cls && util.tw) ) // if have util.related also check if related has tw class .filter((cls) => { const util = utils.find((util: any) => util.id === cls); if (util && util.related) { return util.related.every((related: any) => utils.some((util: any) => util.id === related.id && util.tw) ); } return true; }) .join(' '); const tailwindClasses = uniqueTailwindClasses .filter((cls) => !cls.startsWith('bit_')) .join(' '); return { biteClasses, tailwindClasses }; }; // Recursive function to extract defined properties from blocks and inner blocks export const blockbiteExtract = (blocks: Block[]): PageData => { let properties = ''; let reusables: string[] = []; const recursiveBlocks = (blocks: Block[]): void => { blocks.forEach((block) => { const { attributes, innerBlocks } = block; // Properties to extract, these should reflect only the blockbite classes const propertiesToExtract = [ 'biteClass', 'flexClass', 'mediaClass', 'biteMotionClass', ]; // Extract defined properties from the current block let extractedProperties = ''; propertiesToExtract.forEach((property) => { if (attributes[property] !== undefined && attributes[property] !== '') { extractedProperties += attributes[property] + ' '; } }); // Append the extracted properties if (extractedProperties !== '') { properties += extractedProperties + ' '; } // Check reusable blocks and add to reusables array const ref = attributes.ref; if (ref !== undefined) { reusables.push(ref); } // Recursively extract properties from inner blocks if (innerBlocks.length > 0) { recursiveBlocks(innerBlocks); } }); }; recursiveBlocks(blocks); return { properties: properties.trim(), reusables: reusables }; }; const getReusableBlockStyle = async (ref: string): Promise => { return apiFetch({ path: `/wp/v2/blocks/${ref}` }) .then((result) => result) .catch((error) => console.error(error)); }; export const getStyles = async (handle: string): Promise => { return apiFetch({ path: `${blockbite.api}/editor-styles/?handle=${handle}`, }).then((result: any) => { if (result.tailwind) { return result.tailwind; } else { return ''; } }); }; const saveStylesWithReusables = async ( reusables: string[] ): Promise => { let reusablesProperties = ''; // collect async all reusable blocks from the wp/v2/blocks api const promises = reusables.map(async (ref) => { const result = await getReusableBlockStyle(ref); if (result === undefined) { return ''; } const resuableBlock = wp.blocks.rawHandler({ HTML: result.content.raw, }) as Block[]; const { properties } = blockbiteExtract(resuableBlock); return properties; }); const results = await Promise.all(promises); reusablesProperties = results.filter(Boolean).join(' '); return reusablesProperties; }; export const saveStyles = async ( biteClasses: string, tailwindClasses: string, handle: 'frontend-css' | 'bites-css' ): Promise => { const biteCss = await createTailwind(biteClasses, false); const tailwindCss = await createTailwind(tailwindClasses, false); // data: { content: string } apiFetch({ path: `${blockbite.api}/editor-styles`, method: 'POST', data: { css: `${biteCss} ${tailwindCss}`, tailwind: `${biteClasses} ${tailwindClasses}`, handle: handle, }, }).then(() => { console.log('🍫 Blockbite saved'); blockbite.saving = false; }); }; const generateHeadings = (styles: any) => { let css = ''; styles.forEach((style: any) => { if (style.value === '') { return; } // higher priority of @base styles with inherit property css += `body ${style.name} {\n`; css += ` @apply text-${style.value};\n`; if (style.optional && style.optional.lineHeight) { css += ` line-height: ${style.optional.lineHeight};\n`; } if (style.optional && style.optional.font) { css += ` @apply font-${style.optional.font};\n`; } css += ' }\n'; }); return css; }; const uniqueClasses = (data: string): string => { // Convert data to array let styles = data.split(' '); // Remove duplicates const uniqueStyles = styles.filter((elem, pos) => { return styles.indexOf(elem) === pos && typeof elem !== 'undefined'; }); return uniqueStyles.join(' '); }; export const createTailwind = async ( content: string, component: string | boolean ): Promise => { const store = wp.data.select('biteStore/editor'); const biteUtilPlugin = getBiteUtilsPlugin(store); const newConfig = { ...blockbite.tailwindConfig, important: 'body', plugins: [...(blockbite.tailwindConfig.plugins || []), biteUtilPlugin], }; const tailwindParser = blockbite.createTailwindcss({ tailwindConfig: newConfig, }); let tailwindModules = ` @tailwind components; @tailwind utilities; ` as string; const optin = store.getThemeOptin(); if (optin?.headings) { const { headings } = store.getTheme(); const headingString = generateHeadings(headings); tailwindModules += headingString; } // allows using @apply if (component) { tailwindModules += ` ${component} `; } const data = { content: content, }; try { // Attempt to generate styles const css = await tailwindParser.generateStylesFromContent( tailwindModules, [data.content.trim()] ); return css; } catch (error) { if (error.name === 'CssSyntaxError') { const regex = /`([^`]*)`/g; const match = regex.exec(error.message); if (match) { const className = match[1]; wp.data.dispatch('core/notices').createNotice( 'error', `Error on saving the blockbites, it looks likes there is tailwind class that doesn't exist. Please check your bitestyle that generated this wrong classname: ${className} and remove it. Help us to prevent this error by creating an issue on github`, { id: 'blockbite-notice', isDismissible: true, actions: [ { url: `https://github.com/block-bite/blockbite-dev/issues/new?title=wrong tailwind%20classname%20${className}`, label: 'Create issue on github', }, ], } ); } console.error('Blockbite Tailwind CSS Syntax Error:', error.message); return ''; } else { console.error('Blockbite Tailwind An unexpected error occurred:', error); throw error; } } }; export default { blockbitePurge, createTailwind, getStyles, saveStyles, };