html-to-gutenberg 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,1589 @@
1
+ import presetReact from '@babel/preset-react';
2
+ import { transform } from '@svgr/core';
3
+ import * as babel from '@babel/core';
4
+ import * as cheerio from 'cheerio';
5
+ import scopeCss from 'css-scoping';
6
+ import extractAssets from 'fetch-page-assets';
7
+ import fs from 'fs';
8
+ import icon from 'html-screenshots';
9
+ import imageToBase64 from 'image-to-base64';
10
+ import { createRequire } from 'module';
11
+ import convert from 'node-html-to-jsx';
12
+ import path from 'path';
13
+
14
+ import {
15
+ imports,
16
+ images,
17
+ characters
18
+ } from './globals.js';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const { version } = require('./package.json');
22
+
23
+ const block = async (
24
+ htmlContent,
25
+ options = {
26
+ name: 'My block',
27
+ prefix: 'wp',
28
+ category: 'common',
29
+ basePath: process.cwd(),
30
+ shouldSaveFiles: true,
31
+ generateIconPreview: false,
32
+ jsFiles: [],
33
+ cssFiles: [],
34
+ source: null,
35
+ }
36
+ ) => {
37
+ const panels = [];
38
+ const styles = [];
39
+ const scripts = [];
40
+ const attributes = {};
41
+ const formVars = {};
42
+
43
+ const { name, prefix, source } = options;
44
+
45
+ let js = '';
46
+ let css = '';
47
+ let phpEmailData = '';
48
+ let emailTemplate = '';
49
+
50
+ function hasTailwindCdnSource(jsFiles) {
51
+ const tailwindCdnRegex = /https:\/\/(cdn\.tailwindcss\.com(\?[^"'\s]*)?|cdn\.jsdelivr\.net\/npm\/@tailwindcss\/browser@4(\.\d+){0,2})/;
52
+
53
+ return jsFiles.some(url => tailwindCdnRegex.test(url));
54
+ }
55
+
56
+ function parseInlineStyle(styleString) {
57
+ if (!styleString || typeof styleString !== 'string') return '';
58
+
59
+ const styleEntries = styleString
60
+ .split(';')
61
+ .map(rule => rule.trim())
62
+ .filter(Boolean)
63
+ .map(rule => {
64
+ const [property, value] = rule.split(':').map(part => part.trim());
65
+
66
+ if (!property || !value) return null;
67
+
68
+ const camelCasedProperty = property.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
69
+
70
+ return [camelCasedProperty, value];
71
+ })
72
+ .filter(Boolean);
73
+
74
+ const styleObject = Object.fromEntries(styleEntries);
75
+
76
+ return JSON.stringify(styleObject).replace(/"([^"]+)":/g, '$1:');
77
+ }
78
+
79
+ function sanitizeAndReplaceLeadingNumbers(input) {
80
+ const numberWords = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
81
+ let hasReplacedFirstNumber = false;
82
+
83
+ return input
84
+ .toLowerCase()
85
+ .replace(/[\s\-_]/g, '')
86
+ .replace(/\d/g, (digit) => {
87
+ if (hasReplacedFirstNumber) return digit;
88
+
89
+ hasReplacedFirstNumber = true;
90
+ return numberWords[parseInt(digit)] + digit;
91
+ })
92
+ .replace(/^[^a-z]+/, '');
93
+ }
94
+
95
+ const replaceUnderscoresSpacesAndUppercaseLetters = (input = '') => {
96
+ const nonWordOrUnderscore = /\W|_/g;
97
+ return input.replace(nonWordOrUnderscore, '-').toLowerCase();
98
+ };
99
+
100
+ const buildFilePath = (basePath, fileName) => {
101
+ return path.join(basePath, fileName);
102
+ };
103
+
104
+ const writeFile = (filePath, data) => {
105
+ fs.writeFileSync(filePath, data);
106
+ };
107
+
108
+ const saveFile = (fileName, contents, options) => {
109
+ try {
110
+ const filePath = buildFilePath(options.basePath, fileName);
111
+ writeFile(filePath, contents);
112
+ return contents;
113
+ } catch (error) {
114
+ logError(error);
115
+ }
116
+ };
117
+
118
+ function buildRelativeUrlRegex() {
119
+ const attributes = [
120
+ 'src',
121
+ 'href',
122
+ 'action',
123
+ 'srcset',
124
+ 'poster',
125
+ 'data',
126
+ 'formaction'
127
+ ];
128
+
129
+ const protocolsToIgnore = ['https?:', '//', 'mailto:', 'tel:', '#'].join('|');
130
+
131
+ return new RegExp(
132
+ `\\b(${attributes.join('|')}|data-[a-zA-Z0-9_-]+)\\s*=\\s*(['"])(?!${protocolsToIgnore})([^'"]+)\\2`,
133
+ 'gi'
134
+ );
135
+ }
136
+
137
+ function replaceRelativeUrls(html, replacer) {
138
+ const regex = buildRelativeUrlRegex();
139
+ return html.replace(regex, (_match, attribute, quote, url) => {
140
+ const updatedUrl = replacer(url);
141
+ return `${attribute}=${quote}${updatedUrl}${quote}`;
142
+ });
143
+ }
144
+
145
+ function getRelativeUrlCssRegex() {
146
+ const ignoredProtocols = ['https?:', '//', 'data:', 'mailto:', 'tel:', '#'].join('|');
147
+ return new RegExp(
148
+ `url\\(\\s*(['"]?)(?!${ignoredProtocols})([^'")]+)\\1\\s*\\)`,
149
+ 'gi'
150
+ );
151
+ }
152
+
153
+ function replaceRelativeUrlsInCss(css, replacer) {
154
+ const regex = getRelativeUrlCssRegex();
155
+
156
+ return css.replace(regex, (_match, quote, url) => {
157
+ const updatedUrl = replacer(url);
158
+ return `url(${quote}${updatedUrl}${quote})`;
159
+ });
160
+ }
161
+
162
+ function resolveUrl(relativePath, baseUrl) {
163
+ return new URL(relativePath, baseUrl).href;
164
+ }
165
+
166
+ function replaceRelativeUrlsInHtml(html, baseUrl) {
167
+ return replaceRelativeUrls(html, (url) => resolveUrl(url, baseUrl));
168
+ }
169
+
170
+ function replaceRelativeUrlsInCssWithBase(css, cssFileUrl) {
171
+ return replaceRelativeUrlsInCss(css, (url) => resolveUrl(url, cssFileUrl));
172
+ }
173
+
174
+ const parseRequirements = async (files, options) => {
175
+ const { source } = options;
176
+ let output = '';
177
+
178
+ for (const file of files) {
179
+ try {
180
+ const response = await fetch(file);
181
+
182
+ if (!response.ok) {
183
+ throw new Error(`HTTP error! Status: ${response.status}`);
184
+ }
185
+
186
+ let data = await response.text();
187
+
188
+ if (source) {
189
+ data = replaceRelativeUrlsInCssWithBase(data, file);
190
+ }
191
+
192
+ output += `${data}\n\n`;
193
+ } catch (error) {
194
+ logError(error);
195
+ }
196
+ }
197
+
198
+ return output;
199
+ };
200
+
201
+ const convertDashesSpacesAndUppercaseToUnderscoresAndLowercase = (input = '') => {
202
+ if (!input) return '';
203
+
204
+ return input
205
+ .replaceAll('-', '_')
206
+ .replaceAll(' ', '_')
207
+ .toLowerCase();
208
+ };
209
+
210
+ const newName = replaceUnderscoresSpacesAndUppercaseLetters(name);
211
+ const blockName = `${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(prefix))}/${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(name))}`;
212
+ const blockNameHandle = `${prefix}-${newName}`;
213
+
214
+ const getPhp = (options) => {
215
+ const { name, prefix, jsFiles, cssFiles } = options;
216
+ const phpName = convertDashesSpacesAndUppercaseToUnderscoresAndLowercase(name);
217
+ const phpPrefix = `${functionSufix}${convertDashesSpacesAndUppercaseToUnderscoresAndLowercase(prefix)}`;
218
+ 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();': '';
219
+ 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})})();' : '';
220
+ 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}`;
221
+
222
+ const enqueueRemoteStyles = cssFiles
223
+ .map((remoteUrl) => {
224
+ return `
225
+ wp_enqueue_style(
226
+ '${blockNameHandle}-${remoteUrl
227
+ .split('/')
228
+ .pop()
229
+ .replace(/\.\w+$/, '')}',
230
+ '${remoteUrl}',
231
+ array(),
232
+ null
233
+ );
234
+ `;
235
+ })
236
+ .join('\n');
237
+
238
+ return `<?php
239
+ /*
240
+ * Plugin Name: ${name}
241
+ * Version: ${version}
242
+ * Author: Html to Gutenberg
243
+ * Author URI: https://www.html-to-gutenberg.io/
244
+ * Description: A custom editable block built with Html to Gutenberg
245
+
246
+ */
247
+
248
+ if ( ! defined( 'ABSPATH' ) ) {
249
+ exit;
250
+ }
251
+
252
+ function parse_form_placeholders_${functionSufix}($content, $post_data)
253
+ {
254
+ return preg_replace_callback('/{{(.*?)}}/', function ($matches) use ($post_data) {
255
+ $key = trim($matches[1]);
256
+ return isset($post_data[$key]) ? sanitize_text_field($post_data[$key]) : '';
257
+ }, $content);
258
+ }
259
+
260
+ add_action('wp_ajax_send_test_email_${functionSufix}}', function() {
261
+ //check_ajax_referer('wp_rest');
262
+
263
+ $post_data = $_POST;
264
+ $to = sanitize_email(parse_form_placeholders_${functionSufix}($post_data['to'], $post_data));
265
+ $from = sanitize_email(parse_form_placeholders_${functionSufix}($post_data['from'], $post_data));
266
+ $subject = sanitize_text_field(parse_form_placeholders_${functionSufix}($post_data['subject'], $post_data));
267
+ $message = wp_kses_post(parse_form_placeholders_${functionSufix}($post_data['message'], $post_data));
268
+
269
+ if (empty($to) || empty($from)) {
270
+ wp_send_json_error('Missing email addresses.');
271
+ }
272
+
273
+ $sent = wp_mail($to, $subject, $message, [
274
+ 'Content-Type: text/html; charset=UTF-8',
275
+ 'From: ' . $from
276
+ ]);
277
+
278
+ if ($sent) {
279
+ wp_send_json_success('Email sent');
280
+ } else {
281
+ error_log('Failed to send. To: ' . $to . ' | From: ' . $from);
282
+ wp_send_json_error('Failed to send email.');
283
+ }
284
+ });
285
+
286
+ ${phpEmailData}
287
+
288
+ function ${phpPrefix}_${phpName}_add_custom_editor_styles() {
289
+ 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>';
290
+ }
291
+
292
+ add_action('admin_footer', function () {
293
+ $screen = get_current_screen();
294
+
295
+ if ($screen && method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) {
296
+ $href = esc_url(plugins_url('editor.css', __FILE__));
297
+ echo "<link rel='stylesheet' id='${blockNameHandle}-style' href='$href' type='text/css' media='all' />";
298
+ }
299
+ });
300
+
301
+ add_action( 'enqueue_block_editor_assets', '${phpPrefix}_${phpName}_editor_assets' );
302
+
303
+ add_action('wp_enqueue_scripts', function() {
304
+ wp_dequeue_style('wp-fonts-local');
305
+ wp_deregister_style('wp-fonts-local');
306
+ wp_dequeue_style('global-styles');
307
+ wp_deregister_style('global-styles');
308
+ remove_action('wp_footer', 'wp_global_styles_render_svg_filters');
309
+ }, 100);
310
+
311
+ function ${phpPrefix}_${phpName}_editor_assets() {
312
+ $filepath = plugin_dir_path(__FILE__) . 'block.js';
313
+ $version = file_exists($filepath) ? filemtime($filepath) : time();
314
+
315
+ wp_enqueue_script(
316
+ '${blockNameHandle}',
317
+ plugins_url( 'block.js', __FILE__ ),
318
+ array( 'wp-blocks', 'wp-components', 'wp-element' ,'wp-editor'),
319
+ $version
320
+ );
321
+
322
+ wp_localize_script( '${blockNameHandle}', 'vars', array( 'url' => plugin_dir_url( __FILE__ ) ) );
323
+
324
+ ${enqueueRemoteStyles}
325
+
326
+ ${phpPrefix}_${phpName}_add_custom_editor_styles();
327
+
328
+ wp_dequeue_style('${blockNameHandle}-frontend');
329
+ wp_deregister_style('${blockNameHandle}-frontend');
330
+
331
+ wp_enqueue_script(
332
+ '${blockNameHandle}-remote-loader',
333
+ plugins_url('remote-loader.js', __FILE__),
334
+ array(),
335
+ null,
336
+ true
337
+ );
338
+
339
+ wp_add_inline_script(
340
+ '${blockNameHandle}-remote-loader',
341
+ ${JSON.stringify(inlineRemoteLoader)}
342
+ );
343
+ }
344
+
345
+ add_action('enqueue_block_editor_assets', function () {
346
+ wp_dequeue_style('wp-block-library');
347
+ wp_dequeue_style('wp-block-library-theme');
348
+ wp_dequeue_style('wc-block-style');
349
+ wp_dequeue_style('wp-format-library');
350
+ }, 100);
351
+
352
+
353
+ add_action('init', function () {
354
+ wp_register_script(
355
+ '${blockNameHandle}-scripts',
356
+ plugins_url('scripts.js', __FILE__),
357
+ array(),
358
+ null,
359
+ true
360
+ );
361
+
362
+ wp_register_style(
363
+ '${blockNameHandle}-frontend',
364
+ plugins_url('style.css', __FILE__)
365
+ );
366
+
367
+ wp_register_script(
368
+ '${blockNameHandle}-remote-loader',
369
+ plugins_url('remote-loader.js', __FILE__),
370
+ array(),
371
+ null,
372
+ true
373
+ );
374
+ });
375
+
376
+ add_action( 'wp_enqueue_scripts', '${phpPrefix}_${phpName}_block_assets', 999 );
377
+
378
+ function ${phpPrefix}_${phpName}_block_assets() {
379
+ global $wp_query;
380
+
381
+ $used = false;
382
+
383
+ if (!empty($wp_query->posts)) {
384
+ foreach ($wp_query->posts as $post) {
385
+ $blocks = parse_blocks($post->post_content);
386
+
387
+ foreach ($blocks as $block) {
388
+ if ($block['blockName'] === '${blockName}') {
389
+ $used = true;
390
+ break 2;
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ if ($used) {
397
+ $handle = '${blockNameHandle}';
398
+
399
+ wp_enqueue_style($handle . '-frontend');
400
+
401
+ wp_enqueue_script($handle . '-scripts');
402
+
403
+ wp_localize_script(
404
+ $handle . '-scripts',
405
+ 'vars',
406
+ array(
407
+ 'postId' => get_queried_object_id(),
408
+ 'ajaxUrl' => admin_url('admin-ajax.php')
409
+ )
410
+ );
411
+
412
+ wp_enqueue_script($handle . '-remote-loader');
413
+
414
+ wp_add_inline_script(
415
+ $handle . '-remote-loader',
416
+ ${JSON.stringify(inlineRemoteLoader)}
417
+ );
418
+ }
419
+ }
420
+ `;
421
+ };
422
+
423
+ function transformBlockFile(code) {
424
+ const transformOptions = {
425
+ presets: [[presetReact, { pragma: 'wp.element.createElement' }]],
426
+ filename: 'block.js',
427
+ };
428
+
429
+ return babel.transformSync(code, transformOptions);
430
+ }
431
+
432
+ const saveFiles = async (options) => {
433
+ const { cssFiles = [], jsFiles = [], shouldSaveFiles, name, prefix } = options;
434
+ 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;
435
+ const hasTailwind = tailwindRegex.test(htmlContent);
436
+ const hasTailwindCdn = hasTailwindCdnSource(jsFiles);
437
+
438
+ css = hasTailwind && hasTailwindCdn ? '' : `
439
+ *:not(.components-button) {
440
+ all: revert-layer;
441
+ }\n`;
442
+
443
+ css += await parseRequirements(cssFiles, options);
444
+
445
+ for (const style of styles) {
446
+ css += style.content;
447
+ }
448
+
449
+ const scopedCssFrontend = scopeCss(css, `.wp-block-${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(prefix))}-${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(name))}`);
450
+ const editorStyleFile = scopeCss(css, `[data-type="${blockName}"]`);
451
+ const scriptFile = js;
452
+ let blockCode = await getBlock(htmlContent, options);
453
+
454
+ blockCode = blockCode
455
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
456
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
457
+ .replace(/<link[^>]*?>/gi, '');
458
+
459
+ const indexFile = getPhp(options);
460
+
461
+ const blockFile = transformBlockFile(blockCode).code
462
+ .replace(/name: \"\{field.name\}\"/g, 'name: field.name')
463
+ .replace(/key: \"\{index\}\"/g, 'key: index')
464
+
465
+ if (shouldSaveFiles) {
466
+ saveFile('style.css', scopedCssFrontend, options);
467
+ saveFile('editor.css', editorStyleFile, options);
468
+ saveFile('scripts.js', `${scriptFile}\n\n${emailTemplate}`, options);
469
+ saveFile('index.php', indexFile, options);
470
+ saveFile('block.js', blockFile, options);
471
+ saveFile('remote-loader.js', '', options);
472
+ }
473
+
474
+ return {
475
+ 'style.css': scopedCssFrontend,
476
+ 'editor.css': editorStyleFile,
477
+ 'scripts.js': scriptFile,
478
+ 'index.php': indexFile,
479
+ 'block.js': blockFile,
480
+ }
481
+
482
+ };
483
+
484
+ const logError = (error) => {
485
+ console.error(`[Error] ${error.message}`);
486
+ };
487
+
488
+ const generateRandomVariableName = (prefix = 'content', length = 3) => {
489
+ let suffix = '';
490
+
491
+ for (let i = 0; i < length; i++) {
492
+ suffix += characters.charAt(
493
+ Math.floor(Math.random() * characters.length)
494
+ );
495
+ }
496
+
497
+ return `${prefix}${suffix}`;
498
+ };
499
+
500
+ const functionSufix = generateRandomVariableName('func', 6);
501
+
502
+ const setRandomAttributeContent = (randomVariableName, content) => {
503
+ let newContent = content;
504
+ const isArray = Array.isArray(content);
505
+
506
+ if (typeof content === 'string') {
507
+ newContent = content.replace(/(?:<[^>]*>|[^<"]*")|"/g, (match) => {
508
+ if (match === '"') {
509
+ return '&quot;';
510
+ }
511
+ return match;
512
+ })
513
+ }
514
+
515
+ attributes[randomVariableName] = { type: isArray ? 'array' : 'string', default: newContent };
516
+ };
517
+
518
+ const hasAbsoluteKeyword = (str) => {
519
+ return !!(str && str.toLowerCase().includes('absolute'));
520
+ };
521
+
522
+ const getImageTemplate = (image) => {
523
+ const { randomUrlVariable, randomAltVariable, imgClass } = image;
524
+ return `
525
+ <MediaUpload
526
+ onSelect={(media) => {
527
+ setAttributes({
528
+ ${randomUrlVariable}: media.url,
529
+ ${randomAltVariable}: media.alt
530
+ });
531
+ }}
532
+ type="image"
533
+ render={({ open }) => (
534
+ <div style={{ position: 'relative' }}>
535
+ <img
536
+ src={attributes.${randomUrlVariable}}
537
+ alt={attributes.${randomAltVariable}}
538
+ className="${imgClass}"
539
+ />
540
+ <div
541
+ onClick={open}
542
+ style={{
543
+ position: 'absolute',
544
+ bottom: '0',
545
+ width: '100%',
546
+ height: '100%',
547
+ zIndex: 10,
548
+ cursor: 'pointer'
549
+ }}
550
+ ></div>
551
+ </div>
552
+ )}
553
+ />
554
+ `;
555
+ };
556
+
557
+ const replaceHtmlImage = (html, image) => {
558
+ const { randomUrlVariable } = image;
559
+ const regex = new RegExp(`<img\\s+[^>]*src=\\{[^}]*${randomUrlVariable}[^}]*\\}[^>]*>`, 'gi');
560
+ return html.replace(regex, getImageTemplate(image));
561
+ };
562
+
563
+ const replaceImageComponents = (html) => {
564
+ images.forEach((image) => {
565
+ html = replaceHtmlImage(html, image);
566
+ });
567
+
568
+ return html;
569
+ };
570
+ const loadHtml = async (options) => {
571
+ const { basePath, htmlContent } = options;
572
+ if (htmlContent) {
573
+ const newHtml = await extractAssets(htmlContent, {
574
+ basePath,
575
+ saveFile: true,
576
+ verbose: false,
577
+ });
578
+
579
+ return cheerio.load(newHtml, {
580
+ xmlMode: true,
581
+ decodeEntities: false,
582
+ });
583
+ }
584
+ };
585
+ const getImageSource = (imgTag) => {
586
+ return imgTag.attr('src') || '';
587
+ };
588
+ const getImageAlt = (imgTag) => {
589
+ return imgTag.attr('alt') || '';
590
+ };
591
+ const getParentElement = (imgTag) => {
592
+ return imgTag.parent();
593
+ };
594
+ const getImageStyle = (imgTag) => {
595
+ return imgTag.attr('style') || '';
596
+ };
597
+ const getImageClass = (imgTag) => {
598
+ return imgTag.attr('class') || imgTag.attr('className') || '';
599
+ };
600
+ const getPreviousStyle = (parentElement) => {
601
+ return parentElement.attr('style') || '';
602
+ };
603
+ const getParentClass = (parentElement) => {
604
+ return parentElement.attr('class') || parentElement.attr('className') || '';
605
+ };
606
+ const isBackgroundImage = (
607
+ imgStyle,
608
+ imgClass,
609
+ previousStyle,
610
+ previousClass
611
+ ) => {
612
+ return (
613
+ hasAbsoluteKeyword(imgStyle) ||
614
+ hasAbsoluteKeyword(imgClass) ||
615
+ hasAbsoluteKeyword(previousStyle) ||
616
+ hasAbsoluteKeyword(previousClass)
617
+ );
618
+ };
619
+ const getImageProperties = (imgTag) => {
620
+ const parentElement = getParentElement(imgTag);
621
+ const imgStyle = getImageStyle(imgTag);
622
+ const imgClass = getImageClass(imgTag);
623
+ const previousStyle = getPreviousStyle(parentElement);
624
+ const previousClass = getParentClass(parentElement);
625
+ const isBackground = isBackgroundImage(
626
+ imgStyle,
627
+ imgClass,
628
+ previousStyle,
629
+ previousClass
630
+ );
631
+ return {
632
+ imgTag,
633
+ imgClass,
634
+ isBackground,
635
+ imgSrc: getImageSource(imgTag),
636
+ imgAlt: getImageAlt(imgTag),
637
+ };
638
+ };
639
+ const setImageAttribute = (properties) => {
640
+ const { imgTag, imgSrc, imgAlt, attribute, type, prefix } = properties;
641
+ const newPrefix = prefix ? replaceUnderscoresSpacesAndUppercaseLetters(prefix) : 'wp';
642
+ const randomVariable = generateRandomVariableName(`${type}${newPrefix}`);
643
+
644
+ attributes[randomVariable] = {
645
+ attribute,
646
+ type: 'string',
647
+ selector: 'img',
648
+ default: attribute === 'alt' ? imgAlt : `var.url+'${imgSrc}'`,
649
+ };
650
+
651
+ imgTag.attr(attribute, `{attributes.${randomVariable}}`);
652
+
653
+ return randomVariable;
654
+ };
655
+
656
+ const processImage = (properties) => {
657
+ const { imgClass, type } = properties;
658
+
659
+ const randomUrlVariable = setImageAttribute({
660
+ ...properties,
661
+ attribute: 'src',
662
+ prefix: 'Url',
663
+ });
664
+
665
+ const randomAltVariable = setImageAttribute({
666
+ ...properties,
667
+ attribute: 'alt',
668
+ prefix: 'Alt',
669
+ });
670
+
671
+ if (type !== 'background') {
672
+ images.push({ randomUrlVariable, randomAltVariable, imgClass });
673
+
674
+ return;
675
+ }
676
+
677
+ createPanel({
678
+ type: 'media',
679
+ title: 'Background Image',
680
+ attributes: [randomUrlVariable, randomAltVariable],
681
+ });
682
+ };
683
+
684
+ const createPanelsForForm = () => {
685
+ const randomFormIdVariable = generateRandomVariableName('form');
686
+ const randomHiddenFieldsAttr = generateRandomVariableName('hiddenFields');
687
+ const randomSendEmailVariable = generateRandomVariableName('send');
688
+ const randomEmailFromVariable = generateRandomVariableName('emailFrom');
689
+ const randomEmailToVariable = generateRandomVariableName('emailTo');
690
+ const randomEmailSubjectVariable = generateRandomVariableName('emailSubj');
691
+ const randomEmailMessageVariable = generateRandomVariableName('emailMsg');
692
+ const randomTestFeedbackAttr = generateRandomVariableName('emailTestMsg');
693
+
694
+ Object.assign(formVars, {
695
+ randomFormIdVariable,
696
+ randomSendEmailVariable,
697
+ randomEmailFromVariable,
698
+ randomEmailToVariable,
699
+ randomEmailSubjectVariable,
700
+ randomEmailMessageVariable,
701
+ randomTestFeedbackAttr,
702
+ randomHiddenFieldsAttr
703
+ });
704
+
705
+ setRandomAttributeContent(randomFormIdVariable, `form-${randomFormIdVariable}`);
706
+ setRandomAttributeContent(randomSendEmailVariable, false);
707
+ setRandomAttributeContent(randomEmailFromVariable, '');
708
+ setRandomAttributeContent(randomEmailToVariable, '');
709
+ setRandomAttributeContent(randomEmailSubjectVariable, 'New Form Submission');
710
+ setRandomAttributeContent(randomEmailMessageVariable, 'Form data:\n{{fields}}');
711
+ setRandomAttributeContent(randomTestFeedbackAttr, '');
712
+ setRandomAttributeContent(randomHiddenFieldsAttr, []);
713
+
714
+ createPanel({
715
+ type: 'formSettings',
716
+ title: 'Form Settings',
717
+ attributes: [randomFormIdVariable, randomSendEmailVariable],
718
+ });
719
+
720
+ createPanel({
721
+ type: 'emailSettings',
722
+ title: 'Email Settings',
723
+ attributes: [
724
+ randomSendEmailVariable,
725
+ randomEmailFromVariable,
726
+ randomEmailToVariable,
727
+ randomEmailSubjectVariable,
728
+ randomEmailMessageVariable,
729
+ randomTestFeedbackAttr,
730
+ randomFormIdVariable
731
+ ],
732
+ });
733
+
734
+ createPanel({
735
+ type: 'hiddenFields',
736
+ title: 'Hidden Fields',
737
+ attributes: [randomHiddenFieldsAttr],
738
+ });
739
+ };
740
+
741
+ const getFormVariables = () => ({ ...formVars });
742
+
743
+ const transformFormToDynamicJSX = (htmlContent) => {
744
+ const regex = /<form([\s\S]*?)>([\s\S]*?)<\/form>/gi
745
+ const formExists = regex.test(htmlContent);
746
+
747
+ if (!formExists) {
748
+ return htmlContent;
749
+ }
750
+
751
+
752
+ return htmlContent.replace(
753
+ /<form([\s\S]*?)>([\s\S]*?)<\/form>/gi,
754
+ (_match, formAttributes, innerContent) => {
755
+ createPanelsForForm();
756
+
757
+ const {
758
+ randomFormIdVariable,
759
+ randomHiddenFieldsAttr
760
+ } = getFormVariables();
761
+
762
+ return `
763
+ <form
764
+ ${formAttributes.trim()}
765
+ id={attributes.${randomFormIdVariable}}
766
+ >
767
+ ${innerContent}
768
+
769
+ { (attributes.${randomHiddenFieldsAttr} || []).map((field, index) => (<input key={index} name={field.name} value={field.value} type="hidden" /> )) }
770
+ </form>
771
+ `;
772
+ }
773
+ );
774
+ };
775
+
776
+ const getFixedHtml = (html) => {
777
+ const onChangeRegex = / onChange="{" \(newtext\)=""\>/gi;
778
+ const closeRichTextRegex = /<\/RichText>/gi;
779
+ const valueBindingRegex = /value="{(.*?)}"/gi;
780
+ const attributeBindingRegex = /"{attributes\.(.*?)}"/gi;
781
+
782
+ const fixOnChange = ' onChange={ (newtext) => ';
783
+ const fixValueBinding = 'value={$1}';
784
+ const fixAttributeBinding = '{attributes.$1}';
785
+
786
+ return html
787
+ .replace(onChangeRegex, fixOnChange)
788
+ .replace(closeRichTextRegex, '')
789
+ .replace(valueBindingRegex, fixValueBinding)
790
+ .replace(attributeBindingRegex, fixAttributeBinding);
791
+ };
792
+
793
+
794
+ const processImages = (imgTag) => {
795
+ const properties = getImageProperties(imgTag);
796
+ const type = properties.isBackground ? 'background' : 'image';
797
+
798
+ processImage({ ...properties, type });
799
+ };
800
+
801
+
802
+ const loopImages = ($) => {
803
+ $('img').each((_index, img) => {
804
+ processImages($(img));
805
+ });
806
+ };
807
+
808
+ const getHtml = ($) => {
809
+ return $.html({ xml: false, decodeEntities: false });
810
+ };
811
+
812
+ const processEditImages = async (options) => {
813
+ const $ = await loadHtml(options);
814
+ loopImages($);
815
+ return replaceImageComponents(getFixedHtml(getHtml($)));
816
+ };
817
+ const getRichTextTemplate = (randomVariable, variableContent) => {
818
+ return `
819
+ ><RichText
820
+ tagName="span"
821
+ value={attributes.${randomVariable}}
822
+ default="${variableContent.trim().replace(/"/g, '&quot;')}"
823
+ onChange={ (newtext) => {
824
+ setAttributes({ ${randomVariable}: newtext });
825
+ }}
826
+ /><`;
827
+ };
828
+
829
+ const convertToRichText = (variableContent) => {
830
+ const randomVariable = generateRandomVariableName('content');
831
+
832
+ setRandomAttributeContent(randomVariable, variableContent);
833
+
834
+ return getRichTextTemplate(randomVariable, variableContent);
835
+ };
836
+
837
+ const containsCodeSyntax = (text) => {
838
+ const codePattern = /{|}|\(|\)|=>/;
839
+ return codePattern.test(text.trim());
840
+ };
841
+
842
+ const isEmptyText = (text) => {
843
+ return text.trim() === '';
844
+ };
845
+
846
+ const parseContent = (content) => {
847
+ return content.replace(/>([^<]+)</g, (match, text) => {
848
+ const hasCodeSyntax = containsCodeSyntax(text);
849
+ const isEmpty = isEmptyText(text);
850
+
851
+ if (hasCodeSyntax || isEmpty) {
852
+ return match;
853
+ }
854
+
855
+ return convertToRichText(text);
856
+ });
857
+ };
858
+
859
+ const removeHtmlComments = (html) => {
860
+ return html.replaceAll(/<!--(.*?)-->/gs, '');
861
+ };
862
+
863
+ const wrapWithContainer = (html) => {
864
+ return `<div className="custom-block">${html}</div>`;
865
+ };
866
+
867
+ const getEditJsxContent = async (options) => {
868
+ const dynamicContent = transformFormToDynamicJSX(htmlContent);
869
+ const withoutComments = removeHtmlComments(dynamicContent);
870
+ const wrappedContent = wrapWithContainer(withoutComments);
871
+
872
+ const parsedContent = convert(parseContent(wrappedContent));
873
+
874
+ return await processEditImages({
875
+ ...options,
876
+ htmlContent: parsedContent,
877
+ });
878
+ };
879
+
880
+ const hasAttributes = (panel) => {
881
+ return Array.isArray(panel.attributes) && panel.attributes.length > 0;
882
+ };
883
+
884
+ const createPanel = (panel) => {
885
+ if (!hasAttributes(panel)) return;
886
+
887
+ panels.push(panel);
888
+ };
889
+
890
+ const getSvgTemplate = (_match, attributesString, closingTag, variableName) => {
891
+ return `
892
+ <svg
893
+ ${attributesString}
894
+ dangerouslySetInnerHTML={{ __html: attributes.${variableName} }}
895
+ >
896
+ ${closingTag}
897
+ `;
898
+ };
899
+
900
+ const findSVGMatches = (html) => {
901
+ const svgRegex = /<\s*svg\b((?:[^>'"]|"[^"]*"|'[^']*')*)>(\s*(?:[^<]|<(?!\/svg\s*>))*)(<\/\s*svg\s*>)/gim;
902
+ return [...html.matchAll(svgRegex)];
903
+ };
904
+
905
+ const parseSVGMatch = (match) => {
906
+ const [fullSvg, attributes, content, closingTag] = match;
907
+ const start = match.index;
908
+ const end = start + fullSvg.length;
909
+
910
+ return { fullSvg, attributes, content, closingTag, start, end };
911
+ };
912
+
913
+ const hasContent = (content) => content.trim() !== '';
914
+
915
+ const handleSVGContent = (content) => {
916
+ const variableName = generateRandomVariableName('svg');
917
+ const cleanedContent = content.replaceAll('className', 'class');
918
+
919
+ setRandomAttributeContent(variableName, cleanedContent);
920
+
921
+ createPanel({
922
+ type: 'svg',
923
+ title: 'SVG Markup',
924
+ attributes: [variableName],
925
+ });
926
+
927
+ return variableName;
928
+ };
929
+
930
+ const transformSVG = async (fullSvg, attributes, closingTag, variableName) => {
931
+ const template = getSvgTemplate(fullSvg, attributes, closingTag, variableName);
932
+ return await transform(template, { jsxRuntime: 'classic' });
933
+ };
934
+
935
+ const replaceSVGImages = async (html) => {
936
+ const matches = findSVGMatches(html);
937
+
938
+ let output = '';
939
+ let cursor = 0;
940
+
941
+ for (const match of matches) {
942
+ const { fullSvg, attributes, content, closingTag, start, end } = parseSVGMatch(match);
943
+
944
+ output += html.slice(cursor, start);
945
+
946
+ if (hasContent(content)) {
947
+ const variableName = handleSVGContent(content);
948
+
949
+ const transformed = await transformSVG(
950
+ fullSvg,
951
+ attributes,
952
+ closingTag,
953
+ variableName
954
+ );
955
+
956
+ output += transformed;
957
+ } else {
958
+ output += fullSvg;
959
+ }
960
+
961
+ cursor = end;
962
+ }
963
+
964
+ output += html.slice(cursor);
965
+
966
+ return output;
967
+ };
968
+
969
+ const getSvgPanelTemplate = (panel) => {
970
+ return panel.attributes && attributes[panel.attributes]
971
+ ? `
972
+ { (
973
+ <PanelBody title="${panel.title}">
974
+ <PanelRow>
975
+ <div>
976
+ <TextareaControl
977
+ label="SVG Content"
978
+ help="Enter your SVG content..."
979
+ value={ attributes.${panel.attributes} }
980
+ onChange={ ( value ) => {
981
+ setAttributes({ ${panel.attributes}: value });
982
+ } }
983
+ />
984
+ </div>
985
+ </PanelRow>
986
+ </PanelBody>
987
+ )}
988
+ `
989
+ : '';
990
+ };
991
+
992
+ const getMediaPanelTemplate = (panel) => {
993
+ const mediaAtts =
994
+ panel.attributes?.[0] && panel.attributes[1]
995
+ ? `${panel.attributes[0]}: media.url,
996
+ ${panel.attributes[1]}: media.alt`
997
+ : '';
998
+
999
+ return panel.attributes &&
1000
+ panel.attributes[0] &&
1001
+ attributes[panel.attributes[0]]
1002
+ ? `
1003
+ <PanelBody title="${panel.title}">
1004
+ <PanelRow>
1005
+ <div>
1006
+ <MediaUpload
1007
+ onSelect={ (media) => {
1008
+ setAttributes({
1009
+ ${mediaAtts}
1010
+ });
1011
+ } }
1012
+ type="image"
1013
+ value={ attributes.${panel.attributes?.[0]} }
1014
+ render={({ open }) => (
1015
+ <Button variant="secondary" style={{ marginBottom: "20px" }} onClick={ open }>Select Image</Button>
1016
+ )}
1017
+ />
1018
+ {attributes.${panel.attributes?.[0]} && (
1019
+ <img src={attributes.${panel.attributes?.[0]}} alt={attributes.${panel.attributes?.[1]}} />
1020
+ )}
1021
+ </div>
1022
+ </PanelRow>
1023
+ </PanelBody>
1024
+ `
1025
+ : '';
1026
+ };
1027
+
1028
+ const getFormSettingsPanelTemplate = (panel) => {
1029
+ const [formIdAttr, sendEmailAttr] = panel.attributes;
1030
+
1031
+ return `
1032
+ <PanelBody title="${panel.title}" initialOpen={true}>
1033
+ <TextControl
1034
+ label="Form ID"
1035
+ disabled="true"
1036
+ value={attributes.${formIdAttr}}
1037
+ onChange={(val) => setAttributes({ ${formIdAttr}: val })}
1038
+ />
1039
+ <ToggleControl
1040
+ label="Send Email on Submit"
1041
+ checked={attributes.${sendEmailAttr}}
1042
+ onChange={(val) => setAttributes({ ${sendEmailAttr}: val })}
1043
+ />
1044
+ </PanelBody>
1045
+ `;
1046
+ };
1047
+
1048
+ const getPhpEmailData = (formIdAttr, fromAttr, toAttr, subjectAttr, messageAttr) => {
1049
+ return `
1050
+ add_action('wp_ajax_send_email_${formIdAttr}', function() {
1051
+ //check_ajax_referer('wp_rest');
1052
+
1053
+ $post_id = $_POST['postId'];
1054
+
1055
+ if (!$post_id) {
1056
+ wp_send_json_error('Missing post ID.');
1057
+ }
1058
+
1059
+ $post_data = $_POST;
1060
+ $post = get_post($post_id);
1061
+
1062
+ if (!$post) {
1063
+ wp_send_json_error('Post not found.');
1064
+ }
1065
+
1066
+ $blocks = parse_blocks($post->post_content);
1067
+
1068
+ $target_block = null;
1069
+
1070
+ foreach ($blocks as $block) {
1071
+ if ($block['blockName'] === '${blockName}') {
1072
+ $target_block = $block;
1073
+ break;
1074
+ }
1075
+ }
1076
+
1077
+ if (!$target_block) {
1078
+ wp_send_json_error('Block not found.');
1079
+ }
1080
+
1081
+ $attrs = $target_block['attrs'] ?? [];
1082
+
1083
+ $to = sanitize_email(parse_form_placeholders_${functionSufix}($attrs['${toAttr}'] ?? '', $post_data));
1084
+ $from = sanitize_email(parse_form_placeholders_${functionSufix}($attrs['${fromAttr}'] ?? '', $post_data));
1085
+ $subject = sanitize_text_field(parse_form_placeholders_${functionSufix}($attrs['${subjectAttr}'] ?? '', $post_data));
1086
+ $message = wp_kses_post(parse_form_placeholders_${functionSufix}($attrs['${messageAttr}'] ?? '', $post_data));
1087
+
1088
+ if (empty($to) || empty($from)) {
1089
+ wp_send_json_error('Missing email addresses.');
1090
+ }
1091
+
1092
+ $sent = wp_mail($to, $subject, $message, [
1093
+ 'Content-Type: text/html; charset=UTF-8',
1094
+ 'From: ' . $from
1095
+ ]);
1096
+
1097
+ if ($sent) {
1098
+ wp_send_json_success('Email sent');
1099
+ } else {
1100
+ error_log('Failed to send. To: ' . $to . ' | From: ' . $from);
1101
+ wp_send_json_error('Failed to send email.');
1102
+ }
1103
+ });
1104
+ `;
1105
+ }
1106
+
1107
+ const getEmailSettingsPanelTemplate = (panel) => {
1108
+ const [
1109
+ sendEmailAttr,
1110
+ fromAttr,
1111
+ toAttr,
1112
+ subjectAttr,
1113
+ messageAttr,
1114
+ testFeedbackAttr,
1115
+ formIdAttr,
1116
+ ] = panel.attributes;
1117
+
1118
+ emailTemplate += sendEmailAttr ? getEmailSaveTemplate(formIdAttr) : '';
1119
+ phpEmailData += getPhpEmailData(formIdAttr, fromAttr, toAttr, subjectAttr, messageAttr);
1120
+
1121
+ return `
1122
+ { attributes.${sendEmailAttr} && (
1123
+ <PanelBody title="${panel.title}" initialOpen={true}>
1124
+ <TextControl
1125
+ label="From"
1126
+ value={attributes.${fromAttr}}
1127
+ onChange={(val) => setAttributes({ ${fromAttr}: val })}
1128
+ />
1129
+ <TextControl
1130
+ label="To"
1131
+ value={attributes.${toAttr}}
1132
+ onChange={(val) => setAttributes({ ${toAttr}: val })}
1133
+ />
1134
+ <TextControl
1135
+ label="Subject"
1136
+ value={attributes.${subjectAttr}}
1137
+ onChange={(val) => setAttributes({ ${subjectAttr}: val })}
1138
+ />
1139
+ <TextareaControl
1140
+ label="Message Template (HTML Supported)"
1141
+ help="Use {{fieldName}} to insert field values."
1142
+ value={attributes.${messageAttr}}
1143
+ onChange={(val) => setAttributes({ ${messageAttr}: val })}
1144
+ />
1145
+
1146
+ <Button
1147
+ variant="primary"
1148
+ onClick={() => {
1149
+ const form = document.getElementById('form-${formIdAttr}');
1150
+ const inputs = form.querySelectorAll('input, select, textarea');
1151
+ const body = new URLSearchParams();
1152
+
1153
+ inputs.forEach(input => {
1154
+ if (input.name && !input.disabled) {
1155
+ body.append(input.name, input.value);
1156
+ }
1157
+ });
1158
+
1159
+ body.append('action', 'send_test_email_${functionSufix}');
1160
+ body.append('from', attributes?.${fromAttr} || '');
1161
+ body.append('to', attributes?.${toAttr} || '');
1162
+ body.append('subject', attributes?.${subjectAttr} || '');
1163
+ body.append('message', attributes?.${messageAttr} || '');
1164
+
1165
+ fetch(vars.ajaxUrl, {
1166
+ method: 'POST',
1167
+ body
1168
+ })
1169
+ .then(res => res.json())
1170
+ .then(data => {
1171
+ setAttributes({ ${testFeedbackAttr}: data.success ? 'Test Email Sent!' : data.error });
1172
+ })
1173
+ .catch(() => {
1174
+ setAttributes({ ${testFeedbackAttr}: 'Failed to send test email.' });
1175
+ });
1176
+ }}
1177
+ >
1178
+ Send Test Email
1179
+ </Button>
1180
+
1181
+ {attributes.${testFeedbackAttr} && (
1182
+ <Notice status="info" isDismissible={false}>
1183
+ {attributes.${testFeedbackAttr}}
1184
+ </Notice>
1185
+ )}
1186
+ </PanelBody>
1187
+ )}
1188
+ `;
1189
+ };
1190
+
1191
+ const getEmailSaveTemplate = (formIdAttr) => {
1192
+ return `
1193
+ document.addEventListener('DOMContentLoaded', function() {
1194
+ const form = document.getElementById('form-${formIdAttr}');
1195
+
1196
+ form.addEventListener('submit', function(event) {
1197
+ event.preventDefault();
1198
+
1199
+ const inputs = form.querySelectorAll('input, select, textarea');
1200
+ const body = new URLSearchParams();
1201
+
1202
+ inputs.forEach(input => {
1203
+ if (input.name && !input.disabled) {
1204
+ body.append(input.name, input.value);
1205
+ }
1206
+ });
1207
+
1208
+ body.append('action', 'send_email_${formIdAttr}');
1209
+ body.append('postId', vars.postId);
1210
+
1211
+ fetch(vars.ajaxUrl, {
1212
+ method: 'POST',
1213
+ body
1214
+ })
1215
+ .then(res => res.json())
1216
+ .then(data => {
1217
+ console.log(data.success ? 'Test Email Sent!' : data.error);
1218
+ })
1219
+ .catch(() => {
1220
+ console.log('Failed to send test email.');
1221
+ });
1222
+
1223
+ form.reset()
1224
+ });
1225
+ });
1226
+ `;
1227
+ };
1228
+
1229
+ const getHiddenFieldsPanelTemplate = (panel) => {
1230
+ const [hiddenFieldsAttr] = panel.attributes;
1231
+
1232
+ return `
1233
+ <PanelBody title="${panel.title}" initialOpen={false}>
1234
+ { (attributes.${hiddenFieldsAttr} || []).map((field, index) => (
1235
+ <div key={index} style={{ marginBottom: '10px', borderBottom: '1px solid #ddd', paddingBottom: '10px' }}>
1236
+ <TextControl
1237
+ label="Name"
1238
+ value={field.name}
1239
+ onChange={(val) => {
1240
+ const newFields = [...attributes.${hiddenFieldsAttr}];
1241
+ newFields[index].name = val;
1242
+ setAttributes({ ${hiddenFieldsAttr}: newFields });
1243
+ }}
1244
+ />
1245
+ <TextControl
1246
+ label="Value"
1247
+ value={field.value}
1248
+ onChange={(val) => {
1249
+ const newFields = [...attributes.${hiddenFieldsAttr}];
1250
+ newFields[index].value = val;
1251
+ setAttributes({ ${hiddenFieldsAttr}: newFields });
1252
+ }}
1253
+ />
1254
+ <Button
1255
+ variant="secondary"
1256
+ onClick={() => {
1257
+ const newFields = [...attributes.${hiddenFieldsAttr}];
1258
+ newFields.splice(index, 1);
1259
+ setAttributes({ ${hiddenFieldsAttr}: newFields });
1260
+ }}
1261
+ >
1262
+ Remove
1263
+ </Button>
1264
+ </div>
1265
+ ))}
1266
+
1267
+ <Button
1268
+ variant="primary"
1269
+ onClick={() => {
1270
+ const newFields = [...(attributes.${hiddenFieldsAttr} || [])];
1271
+ newFields.push({ name: '', value: '' });
1272
+ setAttributes({ ${hiddenFieldsAttr}: newFields });
1273
+ }}
1274
+ >
1275
+ Add Hidden Field
1276
+ </Button>
1277
+ </PanelBody>
1278
+ `;
1279
+ };
1280
+
1281
+
1282
+ const findAndGetPanelTemplate = (panel) => {
1283
+ switch (panel.type) {
1284
+ case 'svg':
1285
+ return getSvgPanelTemplate(panel);
1286
+ case 'media':
1287
+ return getMediaPanelTemplate(panel);
1288
+ case 'formSettings':
1289
+ return getFormSettingsPanelTemplate(panel);
1290
+ case 'emailSettings':
1291
+ return getEmailSettingsPanelTemplate(panel);
1292
+ case 'hiddenFields':
1293
+ return getHiddenFieldsPanelTemplate(panel);
1294
+ default:
1295
+ return '';
1296
+ }
1297
+ };
1298
+
1299
+ const getPanelsTemplate = () => {
1300
+ return panels
1301
+ .map((panel) => {
1302
+ return findAndGetPanelTemplate(panel);
1303
+ })
1304
+ .join('\n');
1305
+ };
1306
+
1307
+ const createPanels = () => {
1308
+ return `
1309
+ <Panel>
1310
+ ${getPanelsTemplate()}
1311
+ </Panel>`;
1312
+ };
1313
+
1314
+ const buildSaveContent = (editContent) => {
1315
+ return editContent.replace(
1316
+ /<RichText((.|\n)*?)value=\{(.*?)\}((.|\n)*?)\/>/gi,
1317
+ '<RichText.Content value={$3} />'
1318
+ )
1319
+ .replace(/className=/gi, 'class=')
1320
+ .replace(
1321
+ /<MediaUpload\b[^>]*>([\s\S]*?(<img\b[^>]*>*\/>)[\s\S]*?)\/>/g,
1322
+ (_match, _attributes, img) => {
1323
+ return img.replace(/onClick={[^}]+}\s*/, '');
1324
+ }
1325
+ );
1326
+ };
1327
+
1328
+ const removeHref = (match) => {
1329
+ return match.replace(/href="(.*?)"/, '');
1330
+ };
1331
+
1332
+ const replaceRichText = (match, group1, _group2, group3) => {
1333
+ return removeHref(match)
1334
+ .replace(group1, '<span')
1335
+ .replace(group3, '</span>');
1336
+ };
1337
+
1338
+ const processLinks = (options) => {
1339
+ let { htmlContent } = options;
1340
+
1341
+ htmlContent = htmlContent
1342
+ ? htmlContent.replace(
1343
+ /(<a)[^>]*>([\s\S]*?)(<\/a>)/gim,
1344
+ replaceRichText
1345
+ )
1346
+ : undefined;
1347
+
1348
+ htmlContent = unwrapAnchor(htmlContent);
1349
+
1350
+ return {
1351
+ ...options,
1352
+ htmlContent,
1353
+ };
1354
+ };
1355
+
1356
+ const mergeAttributes = (attrString) => {
1357
+ const attrRegex = /(\S+)=["'](.*?)["']/g;
1358
+ const attrs = {};
1359
+ let match;
1360
+
1361
+ while ((match = attrRegex.exec(attrString)) !== null) {
1362
+ attrs[match[1]] = match[2];
1363
+ }
1364
+ return attrs;
1365
+ };
1366
+
1367
+ const unwrapAnchor = (htmlContent) => {
1368
+
1369
+ return htmlContent.replace(
1370
+ /<span([^>]*)>\s*<a([^>]*)>(.*?)<\/a>\s*<\/span>/gi,
1371
+ (_, spanAttrs, anchorAttrs, content) => {
1372
+ const spanAttributes = mergeAttributes(spanAttrs);
1373
+ const anchorAttributes = mergeAttributes(anchorAttrs);
1374
+ const allAttributes = { ...spanAttributes, ...anchorAttributes };
1375
+
1376
+ const attrString = Object.entries(allAttributes)
1377
+ .map(([key, value]) => `${key}="${value}"`)
1378
+ .join(' ');
1379
+
1380
+ return `<a ${attrString}>${content}</a>`;
1381
+ }
1382
+ );
1383
+ };
1384
+
1385
+
1386
+ const getComponentAttributes = () => {
1387
+ return JSON.stringify(attributes, null, 2);
1388
+ };
1389
+
1390
+ const getEdit = async (options) => {
1391
+ let { htmlContent } = options;
1392
+
1393
+ if (!htmlContent) {
1394
+ return '';
1395
+ }
1396
+
1397
+ const processedOptions = processLinks(options);
1398
+ const jsxContent = await getEditJsxContent(processedOptions);
1399
+
1400
+ const replacedContent = jsxContent
1401
+ .replace(/<article\b([^>]*)>/gi, '<div$1>')
1402
+ .replace(/<\/article>/gi, '</div>')
1403
+ .replaceAll('../', '')
1404
+ .replace(/style="([^"]+)"/g, (_, styleString) => {
1405
+ const styleObj = parseInlineStyle(styleString);
1406
+ return `style={${styleObj}}`;
1407
+ });
1408
+
1409
+ return await replaceSVGImages(replacedContent);
1410
+ };
1411
+
1412
+
1413
+ const parseBlockAttributes = () => {
1414
+ const attributes = getComponentAttributes();
1415
+ const jsonString = JSON.stringify(attributes, null, 2);
1416
+
1417
+ return jsonString
1418
+ .replace(/"var.url\+\'(.*?)\'(.*?)"/g, "vars.url+'$1'$2")
1419
+ .replace(/var(.*?).url\+'(http.*?)'/g, 'http$2');
1420
+ };
1421
+
1422
+ const fixDangerouslySetInnerHTML = (edit) => {
1423
+ const pattern = /dangerouslySetInnerHTML="{" {="" __html:="" (.*?)="" }}=""/gm;
1424
+ const replacement = 'dangerouslySetInnerHTML={{ __html: $1 }}';
1425
+
1426
+ return edit.replace(pattern, replacement);
1427
+ };
1428
+
1429
+ const getBlock = async (htmlContent, settings) => {
1430
+ let {
1431
+ name,
1432
+ category,
1433
+ generateIconPreview,
1434
+ basePath,
1435
+ cssFiles = [],
1436
+ jsFiles = [],
1437
+ } = settings;
1438
+
1439
+ let iconPreview = "'shield'";
1440
+ let edit = await getEdit(settings);
1441
+
1442
+ edit = fixDangerouslySetInnerHTML(edit);
1443
+
1444
+ const save = buildSaveContent(edit);
1445
+ const blockPanels = createPanels();
1446
+ const blockAttributes = parseBlockAttributes();
1447
+
1448
+ if (generateIconPreview) {
1449
+ try {
1450
+ await icon(htmlContent, { basePath, cssFiles, jsFiles });
1451
+ iconPreview = `(<img src="data:image/jpeg;base64,${await imageToBase64(
1452
+ path.join(basePath, 'preview.jpeg')
1453
+ )}" />)`;
1454
+ } catch (error) {
1455
+ console.log(`There was an error generating preview. ${error.message}`);
1456
+ }
1457
+ }
1458
+
1459
+ const output = `
1460
+ (function () {
1461
+ ${imports}
1462
+
1463
+ registerBlockType('${blockName}', {
1464
+ title: '${name}',
1465
+ icon: ${iconPreview},
1466
+ category: '${category}',
1467
+ attributes: ${blockAttributes},
1468
+ edit(props) {
1469
+ const { attributes, setAttributes } = props;
1470
+
1471
+ return (
1472
+ <div>
1473
+ <InspectorControls>
1474
+ ${blockPanels}
1475
+ </InspectorControls>
1476
+
1477
+ ${edit}
1478
+ </div>
1479
+ );
1480
+ },
1481
+ save(props) {
1482
+ const { attributes } = props;
1483
+
1484
+ return (
1485
+ ${save}
1486
+ );
1487
+ },
1488
+ });
1489
+ })();`;
1490
+
1491
+ if (generateIconPreview) {
1492
+ return output.replace(/icon: \s * (')([^']*)(')/, 'icon: $2');
1493
+ }
1494
+
1495
+ return output;
1496
+ };
1497
+
1498
+ const setupVariables = async (htmlContent, options) => {
1499
+ const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
1500
+ const linkRegex = /<link\s+[^>]*href=["']([^"']+)["'][^>]*>/gi;
1501
+
1502
+ let match;
1503
+
1504
+ htmlContent = htmlContent.replace(styleRegex, (_fullMatch, cssContent) => {
1505
+ styles.push({ type: 'inline', content: cssContent.trim() });
1506
+ return '';
1507
+ });
1508
+
1509
+ const fetchCssPromises = [];
1510
+ while ((match = linkRegex.exec(htmlContent)) !== null) {
1511
+ const url = match[1];
1512
+ const fetchCssPromise = fetch(url)
1513
+ .then((response) => response.text())
1514
+ .then((css) => styles.push({ type: 'external', content: css }))
1515
+ .catch(() => console.warn(`Failed to fetch: ${url}`));
1516
+ fetchCssPromises.push(fetchCssPromise);
1517
+ }
1518
+
1519
+ htmlContent = htmlContent.replace(linkRegex, '');
1520
+
1521
+ await Promise.all(fetchCssPromises);
1522
+
1523
+ css += styles.map((style) => {
1524
+ return `${style.content}`;
1525
+ }).join('\n');
1526
+
1527
+
1528
+ const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1529
+ const scriptSrcRegex =
1530
+ /<script\s+[^>]*src=["']([^"']+)["'][^>]*>\s*<\/script>/gi;
1531
+
1532
+ let jsMatch;
1533
+
1534
+ htmlContent = htmlContent.replace(scriptRegex, (_fullMatch, jsContent) => {
1535
+ if (jsContent.trim()) {
1536
+ scripts.push({ type: 'inline', content: jsContent });
1537
+ }
1538
+ return '';
1539
+ });
1540
+
1541
+ const fetchJsPromises = [];
1542
+
1543
+ while ((jsMatch = scriptSrcRegex.exec(htmlContent)) !== null) {
1544
+ const url = jsMatch[1];
1545
+ const fetchJsPromise = fetch(url)
1546
+ .then((response) => response.text())
1547
+ .then((js) => scripts.push({ type: 'external', content: js }))
1548
+ .catch(() => console.warn(`Failed to fetch script: ${url}`));
1549
+ fetchJsPromises.push(fetchJsPromise);
1550
+ }
1551
+
1552
+ htmlContent = htmlContent.replace(scriptSrcRegex, '');
1553
+
1554
+ await Promise.all(fetchJsPromises);
1555
+
1556
+ js += scripts.map((script) => script.content).join('\n');
1557
+
1558
+ let {
1559
+ basePath = process.cwd(),
1560
+ cssFiles = [],
1561
+ jsFiles = [],
1562
+ name = 'My block',
1563
+ } = options;
1564
+
1565
+ const newDir = path.join(basePath, replaceUnderscoresSpacesAndUppercaseLetters(name));
1566
+
1567
+ try {
1568
+ fs.mkdirSync(newDir, { recursive: true });
1569
+
1570
+ return {
1571
+ ...options,
1572
+ jsFiles,
1573
+ cssFiles,
1574
+ htmlContent,
1575
+ basePath: newDir,
1576
+ };
1577
+ } catch (error) {
1578
+ logError(error);
1579
+ }
1580
+ };
1581
+
1582
+ if (source) {
1583
+ htmlContent = replaceRelativeUrlsInHtml(htmlContent, source);
1584
+ htmlContent = replaceRelativeUrlsInCssWithBase(htmlContent, source);
1585
+ }
1586
+
1587
+ return saveFiles(await setupVariables(htmlContent, options));
1588
+ };
1589
+ export default block;