import fs from 'fs'; import { RewritingStream } from 'parse5-html-rewriting-stream'; import glob from 'fast-glob'; import inquirer from 'inquirer'; import { table } from 'table'; import { executeCommand } from './deploy-utils.mjs'; export async function escapePatterns( themeSlug ) { let patternFiles; if ( themeSlug ) { // If a theme slug is provided, use fast-glob to find all PHP files in the specified theme's directory patternFiles = await glob( `${ themeSlug }/patterns/**/*.php`, { ignore: [ 'node_modules/**', 'vendor/**' ], // Exclude node_modules and vendor directories } ); } else { // If no theme slug is provided, detect changed files via Git const stagedFiles = await executeCommand( 'git diff --name-only --cached' ); const unstagedFiles = await executeCommand( 'git diff --name-only' ); // Combine staged and unstaged files, remove duplicates, and filter for pattern files patternFiles = [ ...new Set( [ ...stagedFiles.split( '\n' ), ...unstagedFiles.split( '\n' ), ] ), ].filter( ( file ) => file.match( /.*\/patterns\/.*.php/g ) ); } // Arrange patterns by theme const themePatterns = patternFiles.reduce( ( acc, file ) => { const themeSlug = file.split( '/' ).shift(); return { ...acc, [ themeSlug ]: ( acc[ themeSlug ] || [] ).concat( file ), }; }, {} ); // Process each theme's patterns for ( const [ themeSlug, patterns ] of Object.entries( themePatterns ) ) { console.log( getPatternTable( themeSlug, patterns ) ); const prompt = await inquirer.prompt( [ { type: 'input', message: 'Verify the theme slug', name: 'themeSlug', default: themeSlug, }, ] ); if ( ! prompt.themeSlug ) { return; } for ( const file of patterns ) { const rewriter = getReWriter( prompt.themeSlug ); const tmpFile = `${ file }-tmp`; const readStream = fs.createReadStream( file, { encoding: 'UTF-8', } ); const writeStream = fs.createWriteStream( tmpFile, { encoding: 'UTF-8', } ); writeStream.on( 'finish', () => { fs.renameSync( tmpFile, file ); } ); readStream.pipe( rewriter ).pipe( writeStream ); } } // Helper functions function getReWriter( themeSlug ) { const rewriter = new RewritingStream(); rewriter.on( 'text', ( _, raw ) => { rewriter.emitRaw( escapeText( raw, themeSlug ) ); } ); rewriter.on( 'startTag', ( startTag, rawHtml ) => { if ( startTag.tagName === 'img' ) { const attrs = startTag.attrs.filter( ( attr ) => [ 'src', 'alt' ].includes( attr.name ) ); attrs.forEach( ( attr ) => { if ( attr.name === 'src' ) { attr.value = escapeImagePath( attr.value ); } else if ( attr.name === 'alt' ) { attr.value = escapeText( attr.value, themeSlug, true ); } } ); } rewriter.emitStartTag( startTag ); } ); rewriter.on( 'comment', ( comment, rawHtml ) => { if ( comment.text.startsWith( '?php' ) ) { rewriter.emitRaw( rawHtml ); return; } // escape the strings in block config (blocks that are represented as comments) // ex: const block = escapeBlockAttrs( comment.text, themeSlug ); rewriter.emitComment( { ...comment, text: block } ); } ); return rewriter; } function escapeBlockAttrs( block, themeSlug ) { // Set isAttr to true if it is an attribute in the result HTML // If set to true, it generates esc_attr_, otherwise it generates esc_html_ const allowedAttrs = [ { name: 'label' }, { name: 'placeholder', isAttr: true }, { name: 'buttonText' }, { name: 'content' }, ]; const start = block.indexOf( '{' ); const end = block.lastIndexOf( '}' ); const configPrefix = block.slice( 0, start ); const config = block.slice( start, end + 1 ); const configSuffix = block.slice( end + 1 ); try { const configJson = JSON.parse( config ); allowedAttrs.forEach( ( attr ) => { if ( ! configJson[ attr.name ] ) return; configJson[ attr.name ] = escapeText( configJson[ attr.name ], themeSlug, attr.isAttr ); } ); return configPrefix + JSON.stringify( configJson ) + configSuffix; } catch ( error ) { // do nothing return block; } } function escapeText( text, themeSlug, isAttr = false ) { const trimmedText = text && text.trim(); if ( ! themeSlug || ! trimmedText || trimmedText.startsWith( ``; } function escapeImagePath( src ) { if ( ! src || src.trim().startsWith( '/${ resultSrc }`; } function getPatternTable( themeSlug, patterns ) { const tableConfig = { columnDefault: { width: 40, }, header: { alignment: 'center', content: `THEME: ${ themeSlug }\n\n Following patterns may get updated with escaped strings and/or image paths`, }, }; return table( patterns.map( ( p ) => [ p ] ), tableConfig ); } }