html-to-gutenberg
Version:
Transform any valid HTML string into fully editable WP Gutenberg blocks in seconds rather than hours.
1,589 lines (1,311 loc) • 48 kB
JavaScript
import presetReact from '@babel/preset-react';
import { transform } from '@svgr/core';
import * as babel from '@babel/core';
import * as cheerio from 'cheerio';
import scopeCss from 'css-scoping';
import extractAssets from 'fetch-page-assets';
import fs from 'fs';
import icon from 'html-screenshots';
import imageToBase64 from 'image-to-base64';
import { createRequire } from 'module';
import convert from 'node-html-to-jsx';
import path from 'path';
import {
imports,
images,
characters
} from './globals.js';
const require = createRequire(import.meta.url);
const { version } = require('./package.json');
const block = async (
htmlContent,
options = {
name: 'My block',
prefix: 'wp',
category: 'common',
basePath: process.cwd(),
shouldSaveFiles: true,
generateIconPreview: false,
jsFiles: [],
cssFiles: [],
source: null,
}
) => {
const panels = [];
const styles = [];
const scripts = [];
const attributes = {};
const formVars = {};
const { name, prefix, source } = options;
let js = '';
let css = '';
let phpEmailData = '';
let emailTemplate = '';
function hasTailwindCdnSource(jsFiles) {
const tailwindCdnRegex = /https:\/\/(cdn\.tailwindcss\.com(\?[^"'\s]*)?|cdn\.jsdelivr\.net\/npm\/@tailwindcss\/browser@4(\.\d+){0,2})/;
return jsFiles.some(url => tailwindCdnRegex.test(url));
}
function parseInlineStyle(styleString) {
if (!styleString || typeof styleString !== 'string') return '';
const styleEntries = styleString
.split(';')
.map(rule => rule.trim())
.filter(Boolean)
.map(rule => {
const [property, value] = rule.split(':').map(part => part.trim());
if (!property || !value) return null;
const camelCasedProperty = property.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
return [camelCasedProperty, value];
})
.filter(Boolean);
const styleObject = Object.fromEntries(styleEntries);
return JSON.stringify(styleObject).replace(/"([^"]+)":/g, '$1:');
}
function sanitizeAndReplaceLeadingNumbers(input) {
const numberWords = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
let hasReplacedFirstNumber = false;
return input
.toLowerCase()
.replace(/[\s\-_]/g, '')
.replace(/\d/g, (digit) => {
if (hasReplacedFirstNumber) return digit;
hasReplacedFirstNumber = true;
return numberWords[parseInt(digit)] + digit;
})
.replace(/^[^a-z]+/, '');
}
const replaceUnderscoresSpacesAndUppercaseLetters = (input = '') => {
const nonWordOrUnderscore = /\W|_/g;
return input.replace(nonWordOrUnderscore, '-').toLowerCase();
};
const buildFilePath = (basePath, fileName) => {
return path.join(basePath, fileName);
};
const writeFile = (filePath, data) => {
fs.writeFileSync(filePath, data);
};
const saveFile = (fileName, contents, options) => {
try {
const filePath = buildFilePath(options.basePath, fileName);
writeFile(filePath, contents);
return contents;
} catch (error) {
logError(error);
}
};
function buildRelativeUrlRegex() {
const attributes = [
'src',
'href',
'action',
'srcset',
'poster',
'data',
'formaction'
];
const protocolsToIgnore = ['https?:', '//', 'mailto:', 'tel:', '#'].join('|');
return new RegExp(
`\\b(${attributes.join('|')}|data-[a-zA-Z0-9_-]+)\\s*=\\s*(['"])(?!${protocolsToIgnore})([^'"]+)\\2`,
'gi'
);
}
function replaceRelativeUrls(html, replacer) {
const regex = buildRelativeUrlRegex();
return html.replace(regex, (_match, attribute, quote, url) => {
const updatedUrl = replacer(url);
return `${attribute}=${quote}${updatedUrl}${quote}`;
});
}
function getRelativeUrlCssRegex() {
const ignoredProtocols = ['https?:', '//', 'data:', 'mailto:', 'tel:', '#'].join('|');
return new RegExp(
`url\\(\\s*(['"]?)(?!${ignoredProtocols})([^'")]+)\\1\\s*\\)`,
'gi'
);
}
function replaceRelativeUrlsInCss(css, replacer) {
const regex = getRelativeUrlCssRegex();
return css.replace(regex, (_match, quote, url) => {
const updatedUrl = replacer(url);
return `url(${quote}${updatedUrl}${quote})`;
});
}
function resolveUrl(relativePath, baseUrl) {
return new URL(relativePath, baseUrl).href;
}
function replaceRelativeUrlsInHtml(html, baseUrl) {
return replaceRelativeUrls(html, (url) => resolveUrl(url, baseUrl));
}
function replaceRelativeUrlsInCssWithBase(css, cssFileUrl) {
return replaceRelativeUrlsInCss(css, (url) => resolveUrl(url, cssFileUrl));
}
const parseRequirements = async (files, options) => {
const { source } = options;
let output = '';
for (const file of files) {
try {
const response = await fetch(file);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
let data = await response.text();
if (source) {
data = replaceRelativeUrlsInCssWithBase(data, file);
}
output += `${data}\n\n`;
} catch (error) {
logError(error);
}
}
return output;
};
const convertDashesSpacesAndUppercaseToUnderscoresAndLowercase = (input = '') => {
if (!input) return '';
return input
.replaceAll('-', '_')
.replaceAll(' ', '_')
.toLowerCase();
};
const newName = replaceUnderscoresSpacesAndUppercaseLetters(name);
const blockName = `${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(prefix))}/${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(name))}`;
const blockNameHandle = `${prefix}-${newName}`;
const getPhp = (options) => {
const { name, prefix, jsFiles, cssFiles } = options;
const phpName = convertDashesSpacesAndUppercaseToUnderscoresAndLowercase(name);
const phpPrefix = `${functionSufix}${convertDashesSpacesAndUppercaseToUnderscoresAndLowercase(prefix)}`;
const tailwindFix = hasTailwindCdnSource(jsFiles) ? 'function forceTailwindUpdate(){let e=setInterval(()=>{if("undefined"!=typeof tailwind){clearInterval(e);let n=document.documentElement.outerHTML;tailwind.config.content=[{raw:n,extension:"html"}]}},100)}forceTailwindUpdate();': '';
const tailwindFooter = hasTailwindCdnSource(jsFiles) ? '(()=>{if(window.__tailwindObserverActive)return;window.__tailwindObserverActive=!0;let e=new MutationObserver(()=>{let t=[...document.querySelectorAll("style")].find(e=>e.innerText.includes("--tw-"));t&&(document.body.appendChild(t),e.disconnect(),window.__tailwindObserverActive=!1)});e.observe(document.documentElement,{childList:!0,subtree:!0})})();' : '';
const inlineRemoteLoader = `var remoteUrls = ${JSON.stringify(jsFiles)};(function loadScripts() {window._loadedRemoteScripts = window._loadedRemoteScripts || new Set();const style = document.createElement('style');remoteUrls.forEach((url) => {if (window._loadedRemoteScripts.has(url)) return;const script = document.createElement('script');script.src = url;document.head.appendChild(script);window._loadedRemoteScripts.add(url);});})();${tailwindFix}${tailwindFooter}`;
const enqueueRemoteStyles = cssFiles
.map((remoteUrl) => {
return `
wp_enqueue_style(
'${blockNameHandle}-${remoteUrl
.split('/')
.pop()
.replace(/\.\w+$/, '')}',
'${remoteUrl}',
array(),
null
);
`;
})
.join('\n');
return `<?php
/*
* Plugin Name: ${name}
* Version: ${version}
* Author: Html to Gutenberg
* Author URI: https://www.html-to-gutenberg.io/
* Description: A custom editable block built with Html to Gutenberg
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function parse_form_placeholders_${functionSufix}($content, $post_data)
{
return preg_replace_callback('/{{(.*?)}}/', function ($matches) use ($post_data) {
$key = trim($matches[1]);
return isset($post_data[$key]) ? sanitize_text_field($post_data[$key]) : '';
}, $content);
}
add_action('wp_ajax_send_test_email_${functionSufix}}', function() {
//check_ajax_referer('wp_rest');
$post_data = $_POST;
$to = sanitize_email(parse_form_placeholders_${functionSufix}($post_data['to'], $post_data));
$from = sanitize_email(parse_form_placeholders_${functionSufix}($post_data['from'], $post_data));
$subject = sanitize_text_field(parse_form_placeholders_${functionSufix}($post_data['subject'], $post_data));
$message = wp_kses_post(parse_form_placeholders_${functionSufix}($post_data['message'], $post_data));
if (empty($to) || empty($from)) {
wp_send_json_error('Missing email addresses.');
}
$sent = wp_mail($to, $subject, $message, [
'Content-Type: text/html; charset=UTF-8',
'From: ' . $from
]);
if ($sent) {
wp_send_json_success('Email sent');
} else {
error_log('Failed to send. To: ' . $to . ' | From: ' . $from);
wp_send_json_error('Failed to send email.');
}
});
${phpEmailData}
function ${phpPrefix}_${phpName}_add_custom_editor_styles() {
echo '<style>span.block-editor-rich-text__editable.rich-text{all:unset!important}a br[data-rich-text-line-break=true],span.block-editor-block-icon.block-editor-block-switcher__toggle.has-colors img{display:none}.block-editor-block-types-list__list-item{width:100%!important}.block-editor-block-list__layout.is-root-container>:where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width:100%;margin:0}[aria-label="Empty block; start writing or type forward slash to choose a block"]{max-width:1200px!important}span.block-editor-block-types-list__item-icon img{max-width:100%;width:100%;margin:0;display:block}span.block-editor-block-icon.has-colors{all:inherit;order:2;flex:0 0 100%;width:100%}span.block-editor-block-icon.has-colors svg{margin-left:auto;margin-right:auto}.block-editor-block-card{display:flex!important;flex-wrap:wrap}.block-editor-inserter__preview-content-missing{display:none!important}</style>';
}
add_action('admin_footer', function () {
$screen = get_current_screen();
if ($screen && method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) {
$href = esc_url(plugins_url('editor.css', __FILE__));
echo "<link rel='stylesheet' id='${blockNameHandle}-style' href='$href' type='text/css' media='all' />";
}
});
add_action( 'enqueue_block_editor_assets', '${phpPrefix}_${phpName}_editor_assets' );
add_action('wp_enqueue_scripts', function() {
wp_dequeue_style('wp-fonts-local');
wp_deregister_style('wp-fonts-local');
wp_dequeue_style('global-styles');
wp_deregister_style('global-styles');
remove_action('wp_footer', 'wp_global_styles_render_svg_filters');
}, 100);
function ${phpPrefix}_${phpName}_editor_assets() {
$filepath = plugin_dir_path(__FILE__) . 'block.js';
$version = file_exists($filepath) ? filemtime($filepath) : time();
wp_enqueue_script(
'${blockNameHandle}',
plugins_url( 'block.js', __FILE__ ),
array( 'wp-blocks', 'wp-components', 'wp-element' ,'wp-editor'),
$version
);
wp_localize_script( '${blockNameHandle}', 'vars', array( 'url' => plugin_dir_url( __FILE__ ) ) );
${enqueueRemoteStyles}
${phpPrefix}_${phpName}_add_custom_editor_styles();
wp_dequeue_style('${blockNameHandle}-frontend');
wp_deregister_style('${blockNameHandle}-frontend');
wp_enqueue_script(
'${blockNameHandle}-remote-loader',
plugins_url('remote-loader.js', __FILE__),
array(),
null,
true
);
wp_add_inline_script(
'${blockNameHandle}-remote-loader',
${JSON.stringify(inlineRemoteLoader)}
);
}
add_action('enqueue_block_editor_assets', function () {
wp_dequeue_style('wp-block-library');
wp_dequeue_style('wp-block-library-theme');
wp_dequeue_style('wc-block-style');
wp_dequeue_style('wp-format-library');
}, 100);
add_action('init', function () {
wp_register_script(
'${blockNameHandle}-scripts',
plugins_url('scripts.js', __FILE__),
array(),
null,
true
);
wp_register_style(
'${blockNameHandle}-frontend',
plugins_url('style.css', __FILE__)
);
wp_register_script(
'${blockNameHandle}-remote-loader',
plugins_url('remote-loader.js', __FILE__),
array(),
null,
true
);
});
add_action( 'wp_enqueue_scripts', '${phpPrefix}_${phpName}_block_assets', 999 );
function ${phpPrefix}_${phpName}_block_assets() {
global $wp_query;
$used = false;
if (!empty($wp_query->posts)) {
foreach ($wp_query->posts as $post) {
$blocks = parse_blocks($post->post_content);
foreach ($blocks as $block) {
if ($block['blockName'] === '${blockName}') {
$used = true;
break 2;
}
}
}
}
if ($used) {
$handle = '${blockNameHandle}';
wp_enqueue_style($handle . '-frontend');
wp_enqueue_script($handle . '-scripts');
wp_localize_script(
$handle . '-scripts',
'vars',
array(
'postId' => get_queried_object_id(),
'ajaxUrl' => admin_url('admin-ajax.php')
)
);
wp_enqueue_script($handle . '-remote-loader');
wp_add_inline_script(
$handle . '-remote-loader',
${JSON.stringify(inlineRemoteLoader)}
);
}
}
`;
};
function transformBlockFile(code) {
const transformOptions = {
presets: [[presetReact, { pragma: 'wp.element.createElement' }]],
filename: 'block.js',
};
return babel.transformSync(code, transformOptions);
}
const saveFiles = async (options) => {
const { cssFiles = [], jsFiles = [], shouldSaveFiles, name, prefix } = options;
const tailwindRegex = /(class|className)\s*=\s*["'][^"']*\b(items-center|justify-center|gap-\d+|rounded(-[a-z]+)?|text-[a-z]+-\d{3}|bg-[a-z]+-\d{3}|w-(full|screen)|h-(full|screen)|max-w-[\w\[\]-]+|p-\d+|m-\d+)\b[^"']*["']/i;
const hasTailwind = tailwindRegex.test(htmlContent);
const hasTailwindCdn = hasTailwindCdnSource(jsFiles);
css = hasTailwind && hasTailwindCdn ? '' : `
*:not(.components-button) {
all: revert-layer;
}\n`;
css += await parseRequirements(cssFiles, options);
for (const style of styles) {
css += style.content;
}
const scopedCssFrontend = scopeCss(css, `.wp-block-${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(prefix))}-${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(name))}`);
const editorStyleFile = scopeCss(css, `[data-type="${blockName}"]`);
const scriptFile = js;
let blockCode = await getBlock(htmlContent, options);
blockCode = blockCode
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<link[^>]*?>/gi, '');
const indexFile = getPhp(options);
const blockFile = transformBlockFile(blockCode).code
.replace(/name: \"\{field.name\}\"/g, 'name: field.name')
.replace(/key: \"\{index\}\"/g, 'key: index')
if (shouldSaveFiles) {
saveFile('style.css', scopedCssFrontend, options);
saveFile('editor.css', editorStyleFile, options);
saveFile('scripts.js', `${scriptFile}\n\n${emailTemplate}`, options);
saveFile('index.php', indexFile, options);
saveFile('block.js', blockFile, options);
saveFile('remote-loader.js', '', options);
}
return {
'style.css': scopedCssFrontend,
'editor.css': editorStyleFile,
'scripts.js': scriptFile,
'index.php': indexFile,
'block.js': blockFile,
}
};
const logError = (error) => {
console.error(`[Error] ${error.message}`);
};
const generateRandomVariableName = (prefix = 'content', length = 3) => {
let suffix = '';
for (let i = 0; i < length; i++) {
suffix += characters.charAt(
Math.floor(Math.random() * characters.length)
);
}
return `${prefix}${suffix}`;
};
const functionSufix = generateRandomVariableName('func', 6);
const setRandomAttributeContent = (randomVariableName, content) => {
let newContent = content;
const isArray = Array.isArray(content);
if (typeof content === 'string') {
newContent = content.replace(/(?:<[^>]*>|[^<"]*")|"/g, (match) => {
if (match === '"') {
return '"';
}
return match;
})
}
attributes[randomVariableName] = { type: isArray ? 'array' : 'string', default: newContent };
};
const hasAbsoluteKeyword = (str) => {
return !!(str && str.toLowerCase().includes('absolute'));
};
const getImageTemplate = (image) => {
const { randomUrlVariable, randomAltVariable, imgClass } = image;
return `
<MediaUpload
onSelect={(media) => {
setAttributes({
${randomUrlVariable}: media.url,
${randomAltVariable}: media.alt
});
}}
type="image"
render={({ open }) => (
<div style={{ position: 'relative' }}>
<img
src={attributes.${randomUrlVariable}}
alt={attributes.${randomAltVariable}}
className="${imgClass}"
/>
<div
onClick={open}
style={{
position: 'absolute',
bottom: '0',
width: '100%',
height: '100%',
zIndex: 10,
cursor: 'pointer'
}}
></div>
</div>
)}
/>
`;
};
const replaceHtmlImage = (html, image) => {
const { randomUrlVariable } = image;
const regex = new RegExp(`<img\\s+[^>]*src=\\{[^}]*${randomUrlVariable}[^}]*\\}[^>]*>`, 'gi');
return html.replace(regex, getImageTemplate(image));
};
const replaceImageComponents = (html) => {
images.forEach((image) => {
html = replaceHtmlImage(html, image);
});
return html;
};
const loadHtml = async (options) => {
const { basePath, htmlContent } = options;
if (htmlContent) {
const newHtml = await extractAssets(htmlContent, {
basePath,
saveFile: true,
verbose: false,
});
return cheerio.load(newHtml, {
xmlMode: true,
decodeEntities: false,
});
}
};
const getImageSource = (imgTag) => {
return imgTag.attr('src') || '';
};
const getImageAlt = (imgTag) => {
return imgTag.attr('alt') || '';
};
const getParentElement = (imgTag) => {
return imgTag.parent();
};
const getImageStyle = (imgTag) => {
return imgTag.attr('style') || '';
};
const getImageClass = (imgTag) => {
return imgTag.attr('class') || imgTag.attr('className') || '';
};
const getPreviousStyle = (parentElement) => {
return parentElement.attr('style') || '';
};
const getParentClass = (parentElement) => {
return parentElement.attr('class') || parentElement.attr('className') || '';
};
const isBackgroundImage = (
imgStyle,
imgClass,
previousStyle,
previousClass
) => {
return (
hasAbsoluteKeyword(imgStyle) ||
hasAbsoluteKeyword(imgClass) ||
hasAbsoluteKeyword(previousStyle) ||
hasAbsoluteKeyword(previousClass)
);
};
const getImageProperties = (imgTag) => {
const parentElement = getParentElement(imgTag);
const imgStyle = getImageStyle(imgTag);
const imgClass = getImageClass(imgTag);
const previousStyle = getPreviousStyle(parentElement);
const previousClass = getParentClass(parentElement);
const isBackground = isBackgroundImage(
imgStyle,
imgClass,
previousStyle,
previousClass
);
return {
imgTag,
imgClass,
isBackground,
imgSrc: getImageSource(imgTag),
imgAlt: getImageAlt(imgTag),
};
};
const setImageAttribute = (properties) => {
const { imgTag, imgSrc, imgAlt, attribute, type, prefix } = properties;
const newPrefix = prefix ? replaceUnderscoresSpacesAndUppercaseLetters(prefix) : 'wp';
const randomVariable = generateRandomVariableName(`${type}${newPrefix}`);
attributes[randomVariable] = {
attribute,
type: 'string',
selector: 'img',
default: attribute === 'alt' ? imgAlt : `var.url+'${imgSrc}'`,
};
imgTag.attr(attribute, `{attributes.${randomVariable}}`);
return randomVariable;
};
const processImage = (properties) => {
const { imgClass, type } = properties;
const randomUrlVariable = setImageAttribute({
...properties,
attribute: 'src',
prefix: 'Url',
});
const randomAltVariable = setImageAttribute({
...properties,
attribute: 'alt',
prefix: 'Alt',
});
if (type !== 'background') {
images.push({ randomUrlVariable, randomAltVariable, imgClass });
return;
}
createPanel({
type: 'media',
title: 'Background Image',
attributes: [randomUrlVariable, randomAltVariable],
});
};
const createPanelsForForm = () => {
const randomFormIdVariable = generateRandomVariableName('form');
const randomHiddenFieldsAttr = generateRandomVariableName('hiddenFields');
const randomSendEmailVariable = generateRandomVariableName('send');
const randomEmailFromVariable = generateRandomVariableName('emailFrom');
const randomEmailToVariable = generateRandomVariableName('emailTo');
const randomEmailSubjectVariable = generateRandomVariableName('emailSubj');
const randomEmailMessageVariable = generateRandomVariableName('emailMsg');
const randomTestFeedbackAttr = generateRandomVariableName('emailTestMsg');
Object.assign(formVars, {
randomFormIdVariable,
randomSendEmailVariable,
randomEmailFromVariable,
randomEmailToVariable,
randomEmailSubjectVariable,
randomEmailMessageVariable,
randomTestFeedbackAttr,
randomHiddenFieldsAttr
});
setRandomAttributeContent(randomFormIdVariable, `form-${randomFormIdVariable}`);
setRandomAttributeContent(randomSendEmailVariable, false);
setRandomAttributeContent(randomEmailFromVariable, '');
setRandomAttributeContent(randomEmailToVariable, '');
setRandomAttributeContent(randomEmailSubjectVariable, 'New Form Submission');
setRandomAttributeContent(randomEmailMessageVariable, 'Form data:\n{{fields}}');
setRandomAttributeContent(randomTestFeedbackAttr, '');
setRandomAttributeContent(randomHiddenFieldsAttr, []);
createPanel({
type: 'formSettings',
title: 'Form Settings',
attributes: [randomFormIdVariable, randomSendEmailVariable],
});
createPanel({
type: 'emailSettings',
title: 'Email Settings',
attributes: [
randomSendEmailVariable,
randomEmailFromVariable,
randomEmailToVariable,
randomEmailSubjectVariable,
randomEmailMessageVariable,
randomTestFeedbackAttr,
randomFormIdVariable
],
});
createPanel({
type: 'hiddenFields',
title: 'Hidden Fields',
attributes: [randomHiddenFieldsAttr],
});
};
const getFormVariables = () => ({ ...formVars });
const transformFormToDynamicJSX = (htmlContent) => {
const regex = /<form([\s\S]*?)>([\s\S]*?)<\/form>/gi
const formExists = regex.test(htmlContent);
if (!formExists) {
return htmlContent;
}
return htmlContent.replace(
/<form([\s\S]*?)>([\s\S]*?)<\/form>/gi,
(_match, formAttributes, innerContent) => {
createPanelsForForm();
const {
randomFormIdVariable,
randomHiddenFieldsAttr
} = getFormVariables();
return `
<form
${formAttributes.trim()}
id={attributes.${randomFormIdVariable}}
>
${innerContent}
{ (attributes.${randomHiddenFieldsAttr} || []).map((field, index) => (<input key={index} name={field.name} value={field.value} type="hidden" /> )) }
</form>
`;
}
);
};
const getFixedHtml = (html) => {
const onChangeRegex = / onChange="{" \(newtext\)=""\>/gi;
const closeRichTextRegex = /<\/RichText>/gi;
const valueBindingRegex = /value="{(.*?)}"/gi;
const attributeBindingRegex = /"{attributes\.(.*?)}"/gi;
const fixOnChange = ' onChange={ (newtext) => ';
const fixValueBinding = 'value={$1}';
const fixAttributeBinding = '{attributes.$1}';
return html
.replace(onChangeRegex, fixOnChange)
.replace(closeRichTextRegex, '')
.replace(valueBindingRegex, fixValueBinding)
.replace(attributeBindingRegex, fixAttributeBinding);
};
const processImages = (imgTag) => {
const properties = getImageProperties(imgTag);
const type = properties.isBackground ? 'background' : 'image';
processImage({ ...properties, type });
};
const loopImages = ($) => {
$('img').each((_index, img) => {
processImages($(img));
});
};
const getHtml = ($) => {
return $.html({ xml: false, decodeEntities: false });
};
const processEditImages = async (options) => {
const $ = await loadHtml(options);
loopImages($);
return replaceImageComponents(getFixedHtml(getHtml($)));
};
const getRichTextTemplate = (randomVariable, variableContent) => {
return `
><RichText
tagName="span"
value={attributes.${randomVariable}}
default="${variableContent.trim().replace(/"/g, '"')}"
onChange={ (newtext) => {
setAttributes({ ${randomVariable}: newtext });
}}
/><`;
};
const convertToRichText = (variableContent) => {
const randomVariable = generateRandomVariableName('content');
setRandomAttributeContent(randomVariable, variableContent);
return getRichTextTemplate(randomVariable, variableContent);
};
const containsCodeSyntax = (text) => {
const codePattern = /{|}|\(|\)|=>/;
return codePattern.test(text.trim());
};
const isEmptyText = (text) => {
return text.trim() === '';
};
const parseContent = (content) => {
return content.replace(/>([^<]+)</g, (match, text) => {
const hasCodeSyntax = containsCodeSyntax(text);
const isEmpty = isEmptyText(text);
if (hasCodeSyntax || isEmpty) {
return match;
}
return convertToRichText(text);
});
};
const removeHtmlComments = (html) => {
return html.replaceAll(/<!--(.*?)-->/gs, '');
};
const wrapWithContainer = (html) => {
return `<div className="custom-block">${html}</div>`;
};
const getEditJsxContent = async (options) => {
const dynamicContent = transformFormToDynamicJSX(htmlContent);
const withoutComments = removeHtmlComments(dynamicContent);
const wrappedContent = wrapWithContainer(withoutComments);
const parsedContent = convert(parseContent(wrappedContent));
return await processEditImages({
...options,
htmlContent: parsedContent,
});
};
const hasAttributes = (panel) => {
return Array.isArray(panel.attributes) && panel.attributes.length > 0;
};
const createPanel = (panel) => {
if (!hasAttributes(panel)) return;
panels.push(panel);
};
const getSvgTemplate = (_match, attributesString, closingTag, variableName) => {
return `
<svg
${attributesString}
dangerouslySetInnerHTML={{ __html: attributes.${variableName} }}
>
${closingTag}
`;
};
const findSVGMatches = (html) => {
const svgRegex = /<\s*svg\b((?:[^>'"]|"[^"]*"|'[^']*')*)>(\s*(?:[^<]|<(?!\/svg\s*>))*)(<\/\s*svg\s*>)/gim;
return [...html.matchAll(svgRegex)];
};
const parseSVGMatch = (match) => {
const [fullSvg, attributes, content, closingTag] = match;
const start = match.index;
const end = start + fullSvg.length;
return { fullSvg, attributes, content, closingTag, start, end };
};
const hasContent = (content) => content.trim() !== '';
const handleSVGContent = (content) => {
const variableName = generateRandomVariableName('svg');
const cleanedContent = content.replaceAll('className', 'class');
setRandomAttributeContent(variableName, cleanedContent);
createPanel({
type: 'svg',
title: 'SVG Markup',
attributes: [variableName],
});
return variableName;
};
const transformSVG = async (fullSvg, attributes, closingTag, variableName) => {
const template = getSvgTemplate(fullSvg, attributes, closingTag, variableName);
return await transform(template, { jsxRuntime: 'classic' });
};
const replaceSVGImages = async (html) => {
const matches = findSVGMatches(html);
let output = '';
let cursor = 0;
for (const match of matches) {
const { fullSvg, attributes, content, closingTag, start, end } = parseSVGMatch(match);
output += html.slice(cursor, start);
if (hasContent(content)) {
const variableName = handleSVGContent(content);
const transformed = await transformSVG(
fullSvg,
attributes,
closingTag,
variableName
);
output += transformed;
} else {
output += fullSvg;
}
cursor = end;
}
output += html.slice(cursor);
return output;
};
const getSvgPanelTemplate = (panel) => {
return panel.attributes && attributes[panel.attributes]
? `
{ (
<PanelBody title="${panel.title}">
<PanelRow>
<div>
<TextareaControl
label="SVG Content"
help="Enter your SVG content..."
value={ attributes.${panel.attributes} }
onChange={ ( value ) => {
setAttributes({ ${panel.attributes}: value });
} }
/>
</div>
</PanelRow>
</PanelBody>
)}
`
: '';
};
const getMediaPanelTemplate = (panel) => {
const mediaAtts =
panel.attributes?.[0] && panel.attributes[1]
? `${panel.attributes[0]}: media.url,
${panel.attributes[1]}: media.alt`
: '';
return panel.attributes &&
panel.attributes[0] &&
attributes[panel.attributes[0]]
? `
<PanelBody title="${panel.title}">
<PanelRow>
<div>
<MediaUpload
onSelect={ (media) => {
setAttributes({
${mediaAtts}
});
} }
type="image"
value={ attributes.${panel.attributes?.[0]} }
render={({ open }) => (
<Button variant="secondary" style={{ marginBottom: "20px" }} onClick={ open }>Select Image</Button>
)}
/>
{attributes.${panel.attributes?.[0]} && (
<img src={attributes.${panel.attributes?.[0]}} alt={attributes.${panel.attributes?.[1]}} />
)}
</div>
</PanelRow>
</PanelBody>
`
: '';
};
const getFormSettingsPanelTemplate = (panel) => {
const [formIdAttr, sendEmailAttr] = panel.attributes;
return `
<PanelBody title="${panel.title}" initialOpen={true}>
<TextControl
label="Form ID"
disabled="true"
value={attributes.${formIdAttr}}
onChange={(val) => setAttributes({ ${formIdAttr}: val })}
/>
<ToggleControl
label="Send Email on Submit"
checked={attributes.${sendEmailAttr}}
onChange={(val) => setAttributes({ ${sendEmailAttr}: val })}
/>
</PanelBody>
`;
};
const getPhpEmailData = (formIdAttr, fromAttr, toAttr, subjectAttr, messageAttr) => {
return `
add_action('wp_ajax_send_email_${formIdAttr}', function() {
//check_ajax_referer('wp_rest');
$post_id = $_POST['postId'];
if (!$post_id) {
wp_send_json_error('Missing post ID.');
}
$post_data = $_POST;
$post = get_post($post_id);
if (!$post) {
wp_send_json_error('Post not found.');
}
$blocks = parse_blocks($post->post_content);
$target_block = null;
foreach ($blocks as $block) {
if ($block['blockName'] === '${blockName}') {
$target_block = $block;
break;
}
}
if (!$target_block) {
wp_send_json_error('Block not found.');
}
$attrs = $target_block['attrs'] ?? [];
$to = sanitize_email(parse_form_placeholders_${functionSufix}($attrs['${toAttr}'] ?? '', $post_data));
$from = sanitize_email(parse_form_placeholders_${functionSufix}($attrs['${fromAttr}'] ?? '', $post_data));
$subject = sanitize_text_field(parse_form_placeholders_${functionSufix}($attrs['${subjectAttr}'] ?? '', $post_data));
$message = wp_kses_post(parse_form_placeholders_${functionSufix}($attrs['${messageAttr}'] ?? '', $post_data));
if (empty($to) || empty($from)) {
wp_send_json_error('Missing email addresses.');
}
$sent = wp_mail($to, $subject, $message, [
'Content-Type: text/html; charset=UTF-8',
'From: ' . $from
]);
if ($sent) {
wp_send_json_success('Email sent');
} else {
error_log('Failed to send. To: ' . $to . ' | From: ' . $from);
wp_send_json_error('Failed to send email.');
}
});
`;
}
const getEmailSettingsPanelTemplate = (panel) => {
const [
sendEmailAttr,
fromAttr,
toAttr,
subjectAttr,
messageAttr,
testFeedbackAttr,
formIdAttr,
] = panel.attributes;
emailTemplate += sendEmailAttr ? getEmailSaveTemplate(formIdAttr) : '';
phpEmailData += getPhpEmailData(formIdAttr, fromAttr, toAttr, subjectAttr, messageAttr);
return `
{ attributes.${sendEmailAttr} && (
<PanelBody title="${panel.title}" initialOpen={true}>
<TextControl
label="From"
value={attributes.${fromAttr}}
onChange={(val) => setAttributes({ ${fromAttr}: val })}
/>
<TextControl
label="To"
value={attributes.${toAttr}}
onChange={(val) => setAttributes({ ${toAttr}: val })}
/>
<TextControl
label="Subject"
value={attributes.${subjectAttr}}
onChange={(val) => setAttributes({ ${subjectAttr}: val })}
/>
<TextareaControl
label="Message Template (HTML Supported)"
help="Use {{fieldName}} to insert field values."
value={attributes.${messageAttr}}
onChange={(val) => setAttributes({ ${messageAttr}: val })}
/>
<Button
variant="primary"
onClick={() => {
const form = document.getElementById('form-${formIdAttr}');
const inputs = form.querySelectorAll('input, select, textarea');
const body = new URLSearchParams();
inputs.forEach(input => {
if (input.name && !input.disabled) {
body.append(input.name, input.value);
}
});
body.append('action', 'send_test_email_${functionSufix}');
body.append('from', attributes?.${fromAttr} || '');
body.append('to', attributes?.${toAttr} || '');
body.append('subject', attributes?.${subjectAttr} || '');
body.append('message', attributes?.${messageAttr} || '');
fetch(vars.ajaxUrl, {
method: 'POST',
body
})
.then(res => res.json())
.then(data => {
setAttributes({ ${testFeedbackAttr}: data.success ? 'Test Email Sent!' : data.error });
})
.catch(() => {
setAttributes({ ${testFeedbackAttr}: 'Failed to send test email.' });
});
}}
>
Send Test Email
</Button>
{attributes.${testFeedbackAttr} && (
<Notice status="info" isDismissible={false}>
{attributes.${testFeedbackAttr}}
</Notice>
)}
</PanelBody>
)}
`;
};
const getEmailSaveTemplate = (formIdAttr) => {
return `
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('form-${formIdAttr}');
form.addEventListener('submit', function(event) {
event.preventDefault();
const inputs = form.querySelectorAll('input, select, textarea');
const body = new URLSearchParams();
inputs.forEach(input => {
if (input.name && !input.disabled) {
body.append(input.name, input.value);
}
});
body.append('action', 'send_email_${formIdAttr}');
body.append('postId', vars.postId);
fetch(vars.ajaxUrl, {
method: 'POST',
body
})
.then(res => res.json())
.then(data => {
console.log(data.success ? 'Test Email Sent!' : data.error);
})
.catch(() => {
console.log('Failed to send test email.');
});
form.reset()
});
});
`;
};
const getHiddenFieldsPanelTemplate = (panel) => {
const [hiddenFieldsAttr] = panel.attributes;
return `
<PanelBody title="${panel.title}" initialOpen={false}>
{ (attributes.${hiddenFieldsAttr} || []).map((field, index) => (
<div key={index} style={{ marginBottom: '10px', borderBottom: '1px solid #ddd', paddingBottom: '10px' }}>
<TextControl
label="Name"
value={field.name}
onChange={(val) => {
const newFields = [...attributes.${hiddenFieldsAttr}];
newFields[index].name = val;
setAttributes({ ${hiddenFieldsAttr}: newFields });
}}
/>
<TextControl
label="Value"
value={field.value}
onChange={(val) => {
const newFields = [...attributes.${hiddenFieldsAttr}];
newFields[index].value = val;
setAttributes({ ${hiddenFieldsAttr}: newFields });
}}
/>
<Button
variant="secondary"
onClick={() => {
const newFields = [...attributes.${hiddenFieldsAttr}];
newFields.splice(index, 1);
setAttributes({ ${hiddenFieldsAttr}: newFields });
}}
>
Remove
</Button>
</div>
))}
<Button
variant="primary"
onClick={() => {
const newFields = [...(attributes.${hiddenFieldsAttr} || [])];
newFields.push({ name: '', value: '' });
setAttributes({ ${hiddenFieldsAttr}: newFields });
}}
>
Add Hidden Field
</Button>
</PanelBody>
`;
};
const findAndGetPanelTemplate = (panel) => {
switch (panel.type) {
case 'svg':
return getSvgPanelTemplate(panel);
case 'media':
return getMediaPanelTemplate(panel);
case 'formSettings':
return getFormSettingsPanelTemplate(panel);
case 'emailSettings':
return getEmailSettingsPanelTemplate(panel);
case 'hiddenFields':
return getHiddenFieldsPanelTemplate(panel);
default:
return '';
}
};
const getPanelsTemplate = () => {
return panels
.map((panel) => {
return findAndGetPanelTemplate(panel);
})
.join('\n');
};
const createPanels = () => {
return `
<Panel>
${getPanelsTemplate()}
</Panel>`;
};
const buildSaveContent = (editContent) => {
return editContent.replace(
/<RichText((.|\n)*?)value=\{(.*?)\}((.|\n)*?)\/>/gi,
'<RichText.Content value={$3} />'
)
.replace(/className=/gi, 'class=')
.replace(
/<MediaUpload\b[^>]*>([\s\S]*?(<img\b[^>]*>*\/>)[\s\S]*?)\/>/g,
(_match, _attributes, img) => {
return img.replace(/onClick={[^}]+}\s*/, '');
}
);
};
const removeHref = (match) => {
return match.replace(/href="(.*?)"/, '');
};
const replaceRichText = (match, group1, _group2, group3) => {
return removeHref(match)
.replace(group1, '<span')
.replace(group3, '</span>');
};
const processLinks = (options) => {
let { htmlContent } = options;
htmlContent = htmlContent
? htmlContent.replace(
/(<a)[^>]*>([\s\S]*?)(<\/a>)/gim,
replaceRichText
)
: undefined;
htmlContent = unwrapAnchor(htmlContent);
return {
...options,
htmlContent,
};
};
const mergeAttributes = (attrString) => {
const attrRegex = /(\S+)=["'](.*?)["']/g;
const attrs = {};
let match;
while ((match = attrRegex.exec(attrString)) !== null) {
attrs[match[1]] = match[2];
}
return attrs;
};
const unwrapAnchor = (htmlContent) => {
return htmlContent.replace(
/<span([^>]*)>\s*<a([^>]*)>(.*?)<\/a>\s*<\/span>/gi,
(_, spanAttrs, anchorAttrs, content) => {
const spanAttributes = mergeAttributes(spanAttrs);
const anchorAttributes = mergeAttributes(anchorAttrs);
const allAttributes = { ...spanAttributes, ...anchorAttributes };
const attrString = Object.entries(allAttributes)
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
return `<a ${attrString}>${content}</a>`;
}
);
};
const getComponentAttributes = () => {
return JSON.stringify(attributes, null, 2);
};
const getEdit = async (options) => {
let { htmlContent } = options;
if (!htmlContent) {
return '';
}
const processedOptions = processLinks(options);
const jsxContent = await getEditJsxContent(processedOptions);
const replacedContent = jsxContent
.replace(/<article\b([^>]*)>/gi, '<div$1>')
.replace(/<\/article>/gi, '</div>')
.replaceAll('../', '')
.replace(/style="([^"]+)"/g, (_, styleString) => {
const styleObj = parseInlineStyle(styleString);
return `style={${styleObj}}`;
});
return await replaceSVGImages(replacedContent);
};
const parseBlockAttributes = () => {
const attributes = getComponentAttributes();
const jsonString = JSON.stringify(attributes, null, 2);
return jsonString
.replace(/"var.url\+\'(.*?)\'(.*?)"/g, "vars.url+'$1'$2")
.replace(/var(.*?).url\+'(http.*?)'/g, 'http$2');
};
const fixDangerouslySetInnerHTML = (edit) => {
const pattern = /dangerouslySetInnerHTML="{" {="" __html:="" (.*?)="" }}=""/gm;
const replacement = 'dangerouslySetInnerHTML={{ __html: $1 }}';
return edit.replace(pattern, replacement);
};
const getBlock = async (htmlContent, settings) => {
let {
name,
category,
generateIconPreview,
basePath,
cssFiles = [],
jsFiles = [],
} = settings;
let iconPreview = "'shield'";
let edit = await getEdit(settings);
edit = fixDangerouslySetInnerHTML(edit);
const save = buildSaveContent(edit);
const blockPanels = createPanels();
const blockAttributes = parseBlockAttributes();
if (generateIconPreview) {
try {
await icon(htmlContent, { basePath, cssFiles, jsFiles });
iconPreview = `(<img src="data:image/jpeg;base64,${await imageToBase64(
path.join(basePath, 'preview.jpeg')
)}" />)`;
} catch (error) {
console.log(`There was an error generating preview. ${error.message}`);
}
}
const output = `
(function () {
${imports}
registerBlockType('${blockName}', {
title: '${name}',
icon: ${iconPreview},
category: '${category}',
attributes: ${blockAttributes},
edit(props) {
const { attributes, setAttributes } = props;
return (
<div>
<InspectorControls>
${blockPanels}
</InspectorControls>
${edit}
</div>
);
},
save(props) {
const { attributes } = props;
return (
${save}
);
},
});
})();`;
if (generateIconPreview) {
return output.replace(/icon: \s * (')([^']*)(')/, 'icon: $2');
}
return output;
};
const setupVariables = async (htmlContent, options) => {
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
const linkRegex = /<link\s+[^>]*href=["']([^"']+)["'][^>]*>/gi;
let match;
htmlContent = htmlContent.replace(styleRegex, (_fullMatch, cssContent) => {
styles.push({ type: 'inline', content: cssContent.trim() });
return '';
});
const fetchCssPromises = [];
while ((match = linkRegex.exec(htmlContent)) !== null) {
const url = match[1];
const fetchCssPromise = fetch(url)
.then((response) => response.text())
.then((css) => styles.push({ type: 'external', content: css }))
.catch(() => console.warn(`Failed to fetch: ${url}`));
fetchCssPromises.push(fetchCssPromise);
}
htmlContent = htmlContent.replace(linkRegex, '');
await Promise.all(fetchCssPromises);
css += styles.map((style) => {
return `${style.content}`;
}).join('\n');
const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
const scriptSrcRegex =
/<script\s+[^>]*src=["']([^"']+)["'][^>]*>\s*<\/script>/gi;
let jsMatch;
htmlContent = htmlContent.replace(scriptRegex, (_fullMatch, jsContent) => {
if (jsContent.trim()) {
scripts.push({ type: 'inline', content: jsContent });
}
return '';
});
const fetchJsPromises = [];
while ((jsMatch = scriptSrcRegex.exec(htmlContent)) !== null) {
const url = jsMatch[1];
const fetchJsPromise = fetch(url)
.then((response) => response.text())
.then((js) => scripts.push({ type: 'external', content: js }))
.catch(() => console.warn(`Failed to fetch script: ${url}`));
fetchJsPromises.push(fetchJsPromise);
}
htmlContent = htmlContent.replace(scriptSrcRegex, '');
await Promise.all(fetchJsPromises);
js += scripts.map((script) => script.content).join('\n');
let {
basePath = process.cwd(),
cssFiles = [],
jsFiles = [],
name = 'My block',
} = options;
const newDir = path.join(basePath, replaceUnderscoresSpacesAndUppercaseLetters(name));
try {
fs.mkdirSync(newDir, { recursive: true });
return {
...options,
jsFiles,
cssFiles,
htmlContent,
basePath: newDir,
};
} catch (error) {
logError(error);
}
};
if (source) {
htmlContent = replaceRelativeUrlsInHtml(htmlContent, source);
htmlContent = replaceRelativeUrlsInCssWithBase(htmlContent, source);
}
return saveFiles(await setupVariables(htmlContent, options));
};
export default block;