html-to-gutenberg 4.2.5 → 4.2.7

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.
Files changed (3) hide show
  1. package/index.js +14 -0
  2. package/index.ts +837 -408
  3. package/package.json +2 -2
package/index.ts CHANGED
@@ -1,67 +1,23 @@
1
- import axios from 'axios';
1
+ import presetReact from '@babel/preset-react';
2
+ import * as babel from '@babel/core';
2
3
  import * as cheerio from 'cheerio';
3
- import convert from 'node-html-to-jsx';
4
+ import scopeCss from 'css-scoping';
5
+ import extractAssets from 'fetch-page-assets';
4
6
  import fs from 'fs';
5
- import path from 'path';
6
- import * as babel from '@babel/core';
7
- import presetReact from '@babel/preset-react';
8
- import { transform } from '@svgr/core';
9
- import extractAssets from 'fetch-page-assets/index.js';
10
7
  import icon from 'html-screenshots';
11
- import imageToBase64 from 'image-to-base64';
8
+ import { createRequire } from 'module';
9
+ import convert from 'node-html-to-jsx';
10
+ import path from 'path';
12
11
 
13
12
  import {
14
13
  imports,
15
- panels,
16
14
  images,
17
15
  characters
18
16
  } from './globals.js';
19
17
 
20
- interface Attributes {
21
- [key: string]: {
22
- attribute?: string,
23
- selector?: string,
24
- default?: string,
25
- type?: string
26
- }
27
- }
28
-
29
- interface Panel {
30
- type?: string,
31
- title?: string,
32
- attributes?: any,
33
- };
18
+ const require = createRequire(import.meta.url);
19
+ const { version } = require('./package.json');
34
20
 
35
- interface Image {
36
- randomUrlVariable: string,
37
- randomAltVariable: string,
38
- imgClass: string,
39
- }
40
-
41
- interface ImageProperties {
42
- imgTag: any,
43
- imgSrc: string,
44
- imgAlt: string,
45
- imgClass: string,
46
- isBackground: boolean,
47
- type?: string,
48
- attribute?: string,
49
- prefix?: string,
50
- }
51
- interface BlockOptions {
52
- name: string,
53
- prefix: string,
54
- category: string,
55
- basePath: string,
56
- htmlContent?: string,
57
- cssFiles?: string[],
58
- jsFiles?: string[],
59
- generateIconPreview?: boolean,
60
- shouldSaveFiles?: boolean,
61
- }
62
-
63
- let js = '';
64
- let css = '';
65
21
  const block = async (
66
22
  htmlContent,
67
23
  options = {
@@ -70,27 +26,40 @@ const block = async (
70
26
  category: 'common',
71
27
  basePath: process.cwd(),
72
28
  shouldSaveFiles: true,
29
+ generateIconPreview: false,
30
+ jsFiles: [],
31
+ cssFiles: [],
32
+ source: null,
73
33
  }
74
34
  ) => {
35
+ const panels = [];
75
36
  const styles = [];
76
37
  const scripts = [];
77
38
  const attributes = {};
39
+ const formVars = {};
40
+
41
+ const { name, prefix, source } = options;
78
42
 
79
- function parseStyleString(style) {
80
- const entries = style.split(';').filter(Boolean).map(rule => {
81
- const [key, value] = rule.split(':');
82
- if (!key || !value) return null;
43
+ let js = '';
44
+ let css = '';
45
+ let phpEmailData = '';
46
+ let emailTemplate = '';
83
47
 
84
- const camelKey = key.trim().replace(/-([a-z])/g, (_, char) => char.toUpperCase());
85
- return [camelKey, value.trim()];
86
- }).filter(Boolean);
48
+ function hasTailwindCdnSource(jsFiles) {
49
+ const tailwindCdnRegex = /https:\/\/(cdn\.tailwindcss\.com(\?[^"'\s]*)?|cdn\.jsdelivr\.net\/npm\/@tailwindcss\/browser@4(\.\d+){0,2})/;
87
50
 
88
- const styleObject = Object.fromEntries(entries);
51
+ return jsFiles.some(url => tailwindCdnRegex.test(url));
52
+ }
53
+
54
+ function replaceSourceUrlVars(str, source) {
55
+ if (!source) return str;
89
56
 
90
- return JSON.stringify(styleObject).replace(/"([^"]+)":/g, '$1:'); // Strip object keys' quotes
57
+ const escapedSource = source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
+ const pattern = new RegExp(`var\.url\+'${escapedSource}([^']+)'`, 'g');
59
+ return str.replace(pattern, (match, path) => `\${vars.url}${path}`);
91
60
  }
92
61
 
93
- function sanitizeAndReplaceNumbers(str) {
62
+ function sanitizeAndReplaceLeadingNumbers(str) {
94
63
  const numberWords = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
95
64
  let firstNumberReplaced = false;
96
65
 
@@ -100,85 +69,132 @@ const block = async (
100
69
  .replace(/\d/g, (digit) => {
101
70
  if (!firstNumberReplaced) {
102
71
  firstNumberReplaced = true;
72
+
103
73
  return numberWords[parseInt(digit)] + digit;
104
74
  }
105
- return digit; // keep the rest as-is or you can modify as needed
75
+ return digit;
106
76
  })
107
77
  .replace(/^[^a-z]+/, '');
108
78
  }
109
79
 
110
- const convertName = (name) => {
111
- return (name || '').replace(new RegExp(/\W|_/, 'g'), '-').toLowerCase();
80
+ const replaceUnderscoresSpacesAndUppercaseLetters = (name = '') => {
81
+ return name.replace(new RegExp(/\W|_/, 'g'), '-').toLowerCase();
112
82
  };
83
+
113
84
  const saveFile = (fileName, contents, options) => {
114
85
  try {
115
86
  const filePath = path.join(options.basePath, fileName);
87
+
116
88
  fs.writeFileSync(filePath, contents);
89
+
117
90
  return contents;
118
91
  } catch (error) {
119
92
  logError(error);
120
93
  }
121
94
  };
122
- const parseRequirements = async (files) => {
95
+
96
+ function replaceRelativeUrls(html, replacer) {
97
+ const urlAttributes = [
98
+ 'src', 'href', 'action', 'srcset', 'poster', 'data', 'formaction'
99
+ ];
100
+
101
+ const regex = new RegExp(
102
+ `\\b(${urlAttributes.join('|')}|data-[a-zA-Z0-9_-]+)\\s*=\\s*(['"])(?!https?:|//|mailto:|tel:|#)([^'"]+)\\2`,
103
+ 'gi'
104
+ );
105
+
106
+ return html.replace(regex, (_match, attr, quote, url) => {
107
+ const newUrl = replacer(url);
108
+ return `${attr}=${quote}${newUrl}${quote}`;
109
+ });
110
+ }
111
+
112
+
113
+ function replaceRelativeUrlsInCss(css, replacer) {
114
+ const regex = /url\(\s*(['"]?)(.+?)\1\s*\)/gi;
115
+
116
+ return css.replace(regex, (match, quote, url) => {
117
+ if (/^(https?:|\/\/|data:|mailto:|tel:|#)/.test(url.trim())) {
118
+ return match;
119
+ }
120
+ const newUrl = replacer(url);
121
+ return `url(${quote}${newUrl}${quote})`;
122
+ });
123
+ }
124
+
125
+ function replaceRelativeUrlsInHtml(html, baseUrl) {
126
+ return replaceRelativeUrls(html, (url) => {
127
+ return new URL(url, baseUrl).href;
128
+ });
129
+ }
130
+
131
+ function replaceRelativeUrlsInCssWithBase(css, cssFileUrl) {
132
+ return replaceRelativeUrlsInCss(css, (url) => {
133
+ if (/^(https?:|\/\/|data:|mailto:|tel:|#)/.test(url.trim())) {
134
+ return url;
135
+ }
136
+ return new URL(url, cssFileUrl).href;
137
+ });
138
+ }
139
+
140
+ const parseRequirements = async (files, options) => {
141
+ const { source } = options;
123
142
  let output = '';
143
+
124
144
  for (const file of files) {
125
145
  try {
126
- const { data } = await axios.get(file, { responseType: 'text' });
127
- output += data;
146
+ const response = await fetch(file);
147
+
148
+ if (!response.ok) {
149
+ throw new Error(`HTTP error! Status: ${response.status}`);
150
+ }
151
+
152
+ let data = await response.text();
153
+
154
+ if (source) {
155
+ data = replaceRelativeUrlsInCssWithBase(data, file);
156
+ }
157
+
158
+ output += `${data}\n\n`;
128
159
  } catch (error) {
129
160
  logError(error);
130
161
  }
131
162
  }
163
+
132
164
  return output;
133
165
  };
134
- const convertToUnderscores = (string) => {
166
+
167
+ const convertDashesSpacesAndUppercaseToUnderscoresAndLowercase = (string) => {
135
168
  if (string) {
136
- return `${string.replaceAll('-', '_').replaceAll(' ', '_').toLowerCase()}${generateRandomVariableName(
137
- 'func',
138
- 3
139
- )}`;
169
+ return `${string.replaceAll('-', '_').replaceAll(' ', '_').toLowerCase()}`;
140
170
  }
141
171
 
142
172
  return '';
143
173
  };
144
174
 
175
+ const newName = replaceUnderscoresSpacesAndUppercaseLetters(name);
176
+ const blockName = `${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(prefix))}/${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(name))}`;
177
+ const blockNameHandle = `${prefix}-${newName}`;
178
+
145
179
  const getPhp = (options) => {
146
180
  const { name, prefix, jsFiles, cssFiles } = options;
147
- const newName = convertName(name);
148
- const phpName = convertToUnderscores(name);
149
- const phpPrefix = convertToUnderscores(prefix);
150
-
151
- const inlineRemoteLoader = `
152
- var remoteUrls = ${JSON.stringify(jsFiles)};
153
-
154
- (function loadScripts() {
155
- window._loadedRemoteScripts = window._loadedRemoteScripts || new Set();
156
-
157
- remoteUrls.forEach((url) => {
158
- if (window._loadedRemoteScripts.has(url)) return;
159
-
160
- const script = document.createElement('script');
161
- script.src = url;
162
- script.async = true;
163
- document.head.appendChild(script);
164
-
165
- window._loadedRemoteScripts.add(url);
166
- });
167
- })();
168
- `;
169
-
181
+ const phpName = convertDashesSpacesAndUppercaseToUnderscoresAndLowercase(name);
182
+ const phpPrefix = `${functionSufix}${convertDashesSpacesAndUppercaseToUnderscoresAndLowercase(prefix)}`;
183
+ 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();' : '';
184
+ 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})})();' : '';
185
+ 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}`;
170
186
 
171
187
  const enqueueRemoteStyles = cssFiles
172
188
  .map((remoteUrl) => {
173
189
  return `
174
190
  wp_enqueue_style(
175
- '${prefix}-${newName}-${remoteUrl
191
+ '${blockNameHandle}-${remoteUrl
176
192
  .split('/')
177
193
  .pop()
178
194
  .replace(/\.\w+$/, '')}',
179
195
  '${remoteUrl}',
180
196
  array(),
181
- null // Set to null if you don't have a version
197
+ null
182
198
  );
183
199
  `;
184
200
  })
@@ -187,7 +203,7 @@ const block = async (
187
203
  return `<?php
188
204
  /*
189
205
  * Plugin Name: ${name}
190
- * Version: 1.0
206
+ * Version: ${version}
191
207
  * Author: Html to Gutenberg
192
208
  * Author URI: https://www.html-to-gutenberg.io/
193
209
  * Description: A custom editable block built with Html to Gutenberg
@@ -198,59 +214,44 @@ const block = async (
198
214
  exit;
199
215
  }
200
216
 
201
- function ${phpPrefix}_${phpName}_add_custom_editor_styles() {
202
- echo '<style>
203
- .block-editor-block-types-list__list-item {
204
- width: 100% !important;
205
- }
206
-
207
- .block-editor-block-list__layout.is-root-container > :where(:not(.alignleft):not(.alignright):not(.alignfull)) {
208
- max-width: 100%;
209
- margin: 0;
210
- }
211
-
212
- [aria-label="Empty block; start writing or type forward slash to choose a block"] {
213
- max-width: 1200px !important;
214
- }
215
-
216
- span.block-editor-block-types-list__item-icon img {
217
- max-width: 100%;
218
- width: 100%;
219
- margin: 0;
220
- display: block;
221
- }
217
+ function parse_form_placeholders_${functionSufix}($content, $post_data)
218
+ {
219
+ return preg_replace_callback('/{{(.*?)}}/', function ($matches) use ($post_data) {
220
+ $key = trim($matches[1]);
221
+ return isset($post_data[$key]) ? sanitize_text_field($post_data[$key]) : '';
222
+ }, $content);
223
+ }
222
224
 
223
- span.block-editor-block-icon.has-colors {
224
- width: 100%;
225
- all: inherit;
226
- }
225
+ add_action('wp_ajax_send_test_email_${functionSufix}}', function() {
226
+ //check_ajax_referer('wp_rest');
227
227
 
228
- span.block-editor-block-icon.has-colors svg {
229
- margin-left: auto;
230
- margin-right: auto;
231
- }
228
+ $post_data = $_POST;
229
+ $to = sanitize_email(parse_form_placeholders_${functionSufix}($post_data['to'], $post_data));
230
+ $from = sanitize_email(parse_form_placeholders_${functionSufix}($post_data['from'], $post_data));
231
+ $subject = sanitize_text_field(parse_form_placeholders_${functionSufix}($post_data['subject'], $post_data));
232
+ $message = wp_kses_post(parse_form_placeholders_${functionSufix}($post_data['message'], $post_data));
232
233
 
233
- span.block-editor-block-icon.has-colors {
234
- order: 2;
235
- flex: 0 0 100%;
236
- width: 100%;
237
- }
234
+ if (empty($to) || empty($from)) {
235
+ wp_send_json_error('Missing email addresses.');
236
+ }
238
237
 
239
- .block-editor-block-card {
240
- display: flex !important;
241
- flex-wrap: wrap;
242
- }
238
+ $sent = wp_mail($to, $subject, $message, [
239
+ 'Content-Type: text/html; charset=UTF-8',
240
+ 'From: ' . $from
241
+ ]);
243
242
 
244
- .block-editor-inserter__preview-content-missing {
245
- display: none !important;
246
- }
243
+ if ($sent) {
244
+ wp_send_json_success('Email sent');
245
+ } else {
246
+ error_log('Failed to send. To: ' . $to . ' | From: ' . $from);
247
+ wp_send_json_error('Failed to send email.');
248
+ }
249
+ });
247
250
 
248
- span.block-editor-block-icon.block-editor-block-switcher__toggle.has-colors img {
249
- display: none;
250
- }
251
-
251
+ ${phpEmailData}
252
252
 
253
- </style>';
253
+ function ${phpPrefix}_${phpName}_add_custom_editor_styles() {
254
+ 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>';
254
255
  }
255
256
 
256
257
  add_action('admin_footer', function () {
@@ -258,7 +259,7 @@ const block = async (
258
259
 
259
260
  if ($screen && method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) {
260
261
  $href = esc_url(plugins_url('editor.css', __FILE__));
261
- echo "<link rel='stylesheet' id='${prefix}-${newName}-style' href='$href' type='text/css' media='all' />";
262
+ echo "<link rel='stylesheet' id='${blockNameHandle}-style' href='$href' type='text/css' media='all' />";
262
263
  }
263
264
  });
264
265
 
@@ -272,39 +273,38 @@ const block = async (
272
273
  remove_action('wp_footer', 'wp_global_styles_render_svg_filters');
273
274
  }, 100);
274
275
 
275
-
276
276
  function ${phpPrefix}_${phpName}_editor_assets() {
277
277
  $filepath = plugin_dir_path(__FILE__) . 'block.js';
278
278
  $version = file_exists($filepath) ? filemtime($filepath) : time();
279
279
 
280
280
  wp_enqueue_script(
281
- '${prefix}-${newName}',
281
+ '${blockNameHandle}',
282
282
  plugins_url( 'block.js', __FILE__ ),
283
283
  array( 'wp-blocks', 'wp-components', 'wp-element' ,'wp-editor'),
284
284
  $version
285
285
  );
286
286
 
287
- wp_localize_script( '${prefix}-${newName}', 'vars', array( 'url' => plugin_dir_url( __FILE__ ) ) );
287
+ wp_localize_script( '${blockNameHandle}', 'vars', array( 'url' => plugin_dir_url( __FILE__ ) ) );
288
+
289
+ ${enqueueRemoteStyles}
290
+
291
+ ${phpPrefix}_${phpName}_add_custom_editor_styles();
292
+
293
+ wp_dequeue_style('${blockNameHandle}-frontend');
294
+ wp_deregister_style('${blockNameHandle}-frontend');
288
295
 
289
296
  wp_enqueue_script(
290
- '${prefix}-${newName}-remote-loader',
291
- plugins_url('remote-loader.js', __FILE__),
292
- array(),
293
- null,
294
- true
297
+ '${blockNameHandle}-remote-loader',
298
+ plugins_url('remote-loader.js', __FILE__),
299
+ array(),
300
+ null,
301
+ true
295
302
  );
296
303
 
297
304
  wp_add_inline_script(
298
- '${prefix}-${newName}-remote-loader',
305
+ '${blockNameHandle}-remote-loader',
299
306
  ${JSON.stringify(inlineRemoteLoader)}
300
307
  );
301
-
302
- ${enqueueRemoteStyles}
303
-
304
- ${phpPrefix}_${phpName}_add_custom_editor_styles();
305
-
306
- wp_dequeue_style('${prefix}-${newName}-frontend');
307
- wp_deregister_style('${prefix}-${newName}-frontend');
308
308
  }
309
309
 
310
310
  add_action('enqueue_block_editor_assets', function () {
@@ -314,177 +314,175 @@ const block = async (
314
314
  wp_dequeue_style('wp-format-library');
315
315
  }, 100);
316
316
 
317
- add_action( 'enqueue_block_assets', '${phpPrefix}_${phpName}_block_assets' );
318
317
 
319
- if (!is_admin()) {
320
- wp_register_style(
321
- '${prefix}-${newName}-frontend',
322
- plugins_url('style.css', __FILE__)
318
+ add_action('init', function () {
319
+ wp_register_script(
320
+ '${blockNameHandle}-scripts',
321
+ plugins_url('scripts.js', __FILE__),
322
+ array(),
323
+ null,
324
+ true
323
325
  );
324
- }
325
326
 
326
- function ${phpPrefix}_${phpName}_block_assets() {
327
-
328
- if ( ! is_admin() ) {
329
- wp_enqueue_style('${prefix}-${newName}-frontend');
330
- }
327
+ wp_register_style(
328
+ '${blockNameHandle}-frontend',
329
+ plugins_url('style.css', __FILE__)
330
+ );
331
331
 
332
- wp_enqueue_script(
333
- '${prefix}-${newName}-remote-loader',
332
+ wp_register_script(
333
+ '${blockNameHandle}-remote-loader',
334
334
  plugins_url('remote-loader.js', __FILE__),
335
335
  array(),
336
336
  null,
337
337
  true
338
338
  );
339
+ });
339
340
 
340
-
341
- wp_add_inline_script(
342
- '${prefix}-${newName}-remote-loader',
343
- ${JSON.stringify(inlineRemoteLoader)}
344
- );
345
- }
346
- `;
347
- };
348
-
349
- function scopeCss(css, blockClass) {
350
- const GLOBAL_SELECTORS = ['html', 'body', ':root', ':host', '::backdrop'];
351
- const lines = css.split('\n');
341
+ add_action( 'wp_enqueue_scripts', '${phpPrefix}_${phpName}_block_assets', 999 );
352
342
 
353
- let result = '';
354
- const nestingStack = [];
355
- let selectorBuffer = [];
356
- let insidePropertyBlock = false;
357
- let insideRule = false;
358
- let currentIsScoped = false;
359
-
360
- function scopeSelector(sel) {
361
- sel = sel.trim();
362
-
363
- const whereMatch = sel.match(/^([^ :]+)(:where\(.*\))$/);
364
- if (whereMatch) {
365
- const base = whereMatch[1];
366
- const pseudo = whereMatch[2];
367
- const scopedBase = scopeSelector(base);
368
- return `${scopedBase}${pseudo}`;
369
- }
343
+ function ${phpPrefix}_${phpName}_block_assets() {
344
+ global $wp_query;
370
345
 
371
- if (sel.startsWith(':where(') && sel.endsWith(')')) {
372
- const inner = sel.slice(7, -1);
373
- const scopedInner = inner
374
- .split(',')
375
- .map(scopeSelector)
376
- .join(', ');
377
- return `:where(${scopedInner})`;
378
- }
346
+ $used = false;
379
347
 
380
- const isGlobal = GLOBAL_SELECTORS.some(g => sel.startsWith(g));
381
- const isAlreadyScoped = sel.startsWith(blockClass) || sel.includes(` ${blockClass}`);
348
+ if (!empty($wp_query->posts)) {
349
+ foreach ($wp_query->posts as $post) {
350
+ $blocks = parse_blocks($post->post_content);
382
351
 
383
- if (!isGlobal && !isAlreadyScoped && sel.length > 0) {
384
- return `${blockClass} ${sel}`;
385
- }
386
- return sel;
352
+ foreach ($blocks as $block) {
353
+ if ($block['blockName'] === '${blockName}') {
354
+ $used = true;
355
+ break 2;
356
+ }
357
+ }
358
+ }
387
359
  }
388
360
 
389
- for (const rawLine of lines) {
390
- const line = rawLine.trim();
361
+ if ($used) {
362
+ $handle = '${blockNameHandle}';
391
363
 
392
- if (line.startsWith('@property')) {
393
- insidePropertyBlock = true;
394
- nestingStack.push('@property');
395
- result += `${rawLine}\n`;
396
- continue;
397
- }
398
-
399
- if (insidePropertyBlock) {
400
- result += `${rawLine}\n`;
401
- if (line === '}') {
402
- nestingStack.pop();
403
- insidePropertyBlock = false;
404
- }
405
- continue;
406
- }
407
-
408
- if (line.startsWith('@') && line.endsWith('{')) {
409
- nestingStack.push('@block');
410
- result += `${rawLine}\n`;
411
- continue;
412
- }
364
+ wp_enqueue_style($handle . '-frontend');
413
365
 
414
- if (line === '}') {
415
- if (nestingStack.length) nestingStack.pop();
416
- result += `${rawLine}\n`;
417
- insideRule = false;
418
- currentIsScoped = false;
419
- continue;
420
- }
366
+ wp_enqueue_script($handle . '-scripts');
421
367
 
422
- if (!insideRule && !line.startsWith('@')) {
423
- selectorBuffer.push(rawLine);
368
+ wp_localize_script(
369
+ $handle . '-scripts',
370
+ 'vars',
371
+ array(
372
+ 'postId' => get_queried_object_id(),
373
+ 'ajaxUrl' => admin_url('admin-ajax.php')
374
+ )
375
+ );
424
376
 
425
- if (line.endsWith('{')) {
426
- insideRule = true;
377
+ wp_enqueue_script($handle . '-remote-loader');
427
378
 
428
- const fullSelectorLine = selectorBuffer.join('\n');
429
- const selectorPart = fullSelectorLine.split('{')[0];
430
- const selectors = selectorPart.split(',').map(s => s.trim());
379
+ wp_add_inline_script(
380
+ $handle . '-remote-loader',
381
+ ${JSON.stringify(inlineRemoteLoader)}
382
+ );
383
+ }
384
+ }
385
+ `;
386
+ };
431
387
 
432
- const scopedSelectors = selectors.map(scopeSelector);
433
- currentIsScoped = selectors.some(sel => scopeSelector(sel) !== sel);
388
+ function preprocessSvgAttributes(code) {
389
+ return code.replace(/(<svg[\s\S]*?>[\s\S]*?<\/svg>)/gi, (svgBlock) => {
390
+ let processed = svgBlock.replace(/([a-zA-Z0-9]+)-([a-zA-Z0-9]+)=/g, (match, p1, p2) => {
391
+ const camel = p1 + p2.charAt(0).toUpperCase() + p2.slice(1);
392
+ return camel + '=';
393
+ });
394
+ return processed;
395
+ });
396
+ }
434
397
 
435
- result += `${scopedSelectors.join(',\n')} {\n`;
436
- selectorBuffer = [];
437
- }
398
+ function unwrapBody(code) {
399
+ try {
400
+ return code.replace(/<\/?(html|body)[^>]*>/gi, '');
401
+ } catch (e) {
402
+ return code;
403
+ }
404
+ }
438
405
 
439
- continue;
440
- }
406
+ function transformBlockFile(blockCode) {
407
+ let test = '';
441
408
 
442
- if (insideRule && line.includes(':')) {
443
- const declarationMatch = line.match(/^([a-zA-Z0-9\-\_]+)\s*:\s*(.+?)(;?)$/);
444
- if (declarationMatch) {
445
- let [_, prop, value, semi] = declarationMatch;
446
- result += ` ${prop}: ${value}${semi}\n`;
447
- continue;
448
- }
449
- }
409
+ try {
410
+ test = babel.transformSync(blockCode, {
411
+ presets: [[presetReact, { pragma: 'wp.element.createElement' }]],
412
+ filename: 'block.js'
413
+ });
414
+ } catch (error) {
415
+ console.log(error);
450
416
 
451
- result += `${rawLine}\n`;
452
417
  }
453
418
 
454
- return result;
419
+ return test;
455
420
  }
456
421
 
457
422
  const saveFiles = async (options) => {
458
- const { cssFiles = [], shouldSaveFiles, name, prefix } = options;
459
- css = await parseRequirements(cssFiles);
423
+ const { cssFiles = [], jsFiles = [], shouldSaveFiles, name, prefix } = options;
424
+ 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;
425
+ const hasTailwind = tailwindRegex.test(htmlContent);
426
+ const hasTailwindCdn = hasTailwindCdnSource(jsFiles);
427
+
428
+ css = hasTailwind && hasTailwindCdn ? '' : `
429
+ *:not(.components-button) {
430
+ all: revert-layer;
431
+ }\n`;
432
+
433
+ css += await parseRequirements(cssFiles, options);
434
+
435
+ console.log('[CSS BEFORE]', css);
436
+
460
437
 
461
438
  for (const style of styles) {
462
- css = style.type === 'inline' ? `
463
- *:not(.components-button) {
464
- all: revert-layer;
465
- }
466
-
467
- ${style.content}` : `${style.content}`;
439
+ css += style.content;
468
440
  }
469
441
 
470
- const scopedCssFrontend = scopeCss(css, `.wp-block-${sanitizeAndReplaceNumbers(convertName(prefix))}-${sanitizeAndReplaceNumbers(convertName(name))}`);
471
- const editorStyleFile = scopeCss(css, `[data-type="${sanitizeAndReplaceNumbers(convertName(prefix))}/${sanitizeAndReplaceNumbers(convertName(name))}"]`);
442
+ css = css.replace(/[^{}]+:is\([^)]*\[.*?['"].*?\)[^{}]*\{[^}]*\}/g, '');
443
+
444
+ const scopedCssFrontend = scopeCss(css, `.wp-block-${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(prefix))}-${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(name))}`);
445
+ const editorStyleFile = scopeCss(css, `[data-type="${blockName}"]`);
472
446
  const scriptFile = js;
447
+
448
+ htmlContent = htmlContent
449
+ .replace(/<head[\s\S]*?<\/head>/gi, '')
450
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
451
+ .replace(/<style[\s\S]*?<\/style>/gi, '');
452
+
453
+
454
+ let blockCode = await getBlock(options);
455
+
456
+ blockCode = blockCode.replaceAll(' / dangerouslySetInnerHTML', ' dangerouslySetInnerHTML')
457
+
473
458
  const indexFile = getPhp(options);
474
- const blockCode = await getBlock(htmlContent, options);
459
+ let blockFile = '';
460
+
461
+ try {
462
+ blockFile = transformBlockFile(blockCode).code
463
+ ?.replace(/name: \"\{field.name\}\"/g, 'name: field.name')
464
+ ?.replace(/key: \"\{index\}\"/g, 'key: index')
465
+ } catch (error) {
466
+
467
+ console.log(error);
468
+
469
+ }
470
+
471
+ console.log(blockFile);
475
472
 
476
- const blockFile = babel.transformSync(blockCode, {
477
- presets: [[presetReact, { pragma: 'wp.element.createElement' }]],
478
- filename: 'block.js',
479
- });
480
473
 
481
474
  if (shouldSaveFiles) {
482
- saveFile('style.css', scopedCssFrontend, options);
483
- saveFile('editor.css', editorStyleFile, options);
484
- saveFile('scripts.js', scriptFile, options);
485
- saveFile('index.php', indexFile, options);
486
- saveFile('block.js', blockFile.code, options);
487
- saveFile('remote-loader.js', '// This file intentionally left blank.', options);
475
+ try {
476
+ saveFile('style.css', scopedCssFrontend, options);
477
+ saveFile('editor.css', editorStyleFile, options);
478
+ saveFile('scripts.js', `${scriptFile}\n\n${emailTemplate}`, options);
479
+ saveFile('index.php', indexFile, options);
480
+ saveFile('block.js', blockFile, options);
481
+ saveFile('remote-loader.js', '', options);
482
+ } catch (error) {
483
+ console.log(error);
484
+
485
+ }
488
486
  }
489
487
 
490
488
  return {
@@ -492,7 +490,7 @@ const block = async (
492
490
  'editor.css': editorStyleFile,
493
491
  'scripts.js': scriptFile,
494
492
  'index.php': indexFile,
495
- 'block.js': blockFile.code,
493
+ 'block.js': blockFile,
496
494
  }
497
495
 
498
496
  };
@@ -503,22 +501,24 @@ const block = async (
503
501
 
504
502
  const generateRandomVariableName = (prefix = 'content', length = 3) => {
505
503
  let suffix = '';
504
+
506
505
  for (let i = 0; i < length; i++) {
507
506
  suffix += characters.charAt(
508
507
  Math.floor(Math.random() * characters.length)
509
508
  );
510
509
  }
510
+
511
511
  return `${prefix}${suffix}`;
512
512
  };
513
- const setAttributeContent = (randomVariableName, content) => {
514
- attributes[randomVariableName] = {
515
- type: 'string', default: content.replace(/(?:<[^>]*>|[^<"]*")|"/g, (match) => {
516
- if (match === '"') {
517
- return '&quot;';
518
- }
519
- return match;
520
- })
521
- };
513
+
514
+ const functionSufix = generateRandomVariableName('func');
515
+
516
+ const setRandomAttributeContent = (randomVariableName, content) => {
517
+ const isArray = Array.isArray(content);
518
+
519
+
520
+ attributes[randomVariableName] = { type: isArray ? 'array' : 'string', default: content };
521
+
522
522
  };
523
523
 
524
524
  const hasAbsoluteKeyword = (str) => {
@@ -551,6 +551,7 @@ const block = async (
551
551
  width: '100%',
552
552
  height: '100%',
553
553
  zIndex: 10,
554
+ cursor: 'pointer'
554
555
  }}
555
556
  ></div>
556
557
  </div>
@@ -559,12 +560,12 @@ const block = async (
559
560
  `;
560
561
  };
561
562
 
562
-
563
563
  const replaceHtmlImage = (html, image) => {
564
564
  const { randomUrlVariable } = image;
565
565
  const regex = new RegExp(`<img\\s+[^>]*src=\\{[^}]*${randomUrlVariable}[^}]*\\}[^>]*>`, 'gi');
566
566
  return html.replace(regex, getImageTemplate(image));
567
567
  };
568
+
568
569
  const replaceImageComponents = (html) => {
569
570
  images.forEach((image) => {
570
571
  html = replaceHtmlImage(html, image);
@@ -577,9 +578,10 @@ const block = async (
577
578
  if (htmlContent) {
578
579
  const newHtml = await extractAssets(htmlContent, {
579
580
  basePath,
581
+ saveFile: true,
580
582
  verbose: false,
581
- saveFile: false,
582
583
  });
584
+
583
585
  return cheerio.load(newHtml, {
584
586
  xmlMode: true,
585
587
  decodeEntities: false,
@@ -642,53 +644,191 @@ const block = async (
642
644
  };
643
645
  const setImageAttribute = (properties) => {
644
646
  const { imgTag, imgSrc, imgAlt, attribute, type, prefix } = properties;
645
- const newPrefix = prefix ? convertName(prefix) : 'wp';
647
+ const newPrefix = prefix ? replaceUnderscoresSpacesAndUppercaseLetters(prefix) : 'wp';
646
648
  const randomVariable = generateRandomVariableName(`${type}${newPrefix}`);
649
+
650
+ let imgSrcWithoutOrigin = imgSrc;
651
+ try {
652
+ if (typeof imgSrc === 'string') {
653
+ if (/^https?:\/\//.test(imgSrc)) {
654
+ const urlObj = new URL(imgSrc);
655
+ imgSrcWithoutOrigin = urlObj.pathname + urlObj.search + urlObj.hash;
656
+ } else if (source && imgSrc.startsWith(source)) {
657
+ imgSrcWithoutOrigin = imgSrc.slice(source.length);
658
+ if (imgSrcWithoutOrigin.startsWith('/')) imgSrcWithoutOrigin = imgSrcWithoutOrigin.slice(1);
659
+ }
660
+ }
661
+ } catch (e) {
662
+ imgSrcWithoutOrigin = imgSrc;
663
+ }
664
+ let imgSrcNoLeadingSlash = imgSrcWithoutOrigin;
665
+ if (typeof imgSrcNoLeadingSlash === 'string' && imgSrcNoLeadingSlash.startsWith('/')) {
666
+ imgSrcNoLeadingSlash = imgSrcNoLeadingSlash.slice(1);
667
+ }
647
668
  attributes[randomVariable] = {
648
669
  attribute,
649
670
  type: 'string',
650
671
  selector: 'img',
651
- default: attribute === 'alt' ? imgAlt : `var.url+'${imgSrc}'`,
672
+ default: attribute === 'alt' ? imgAlt : `{vars.url}${imgSrcNoLeadingSlash}`.replace(/^\u007f/, '$'),
652
673
  };
674
+
653
675
  imgTag.attr(attribute, `{attributes.${randomVariable}}`);
676
+
654
677
  return randomVariable;
655
678
  };
679
+
656
680
  const processImage = (properties) => {
657
681
  const { imgClass, type } = properties;
682
+
658
683
  const randomUrlVariable = setImageAttribute({
659
684
  ...properties,
660
685
  attribute: 'src',
661
686
  prefix: 'Url',
662
687
  });
688
+
663
689
  const randomAltVariable = setImageAttribute({
664
690
  ...properties,
665
691
  attribute: 'alt',
666
692
  prefix: 'Alt',
667
693
  });
694
+
668
695
  if (type !== 'background') {
669
696
  images.push({ randomUrlVariable, randomAltVariable, imgClass });
697
+
670
698
  return;
671
699
  }
700
+
672
701
  createPanel({
673
702
  type: 'media',
674
703
  title: 'Background Image',
675
704
  attributes: [randomUrlVariable, randomAltVariable],
676
705
  });
677
706
  };
707
+
708
+ const createPanelsForForm = () => {
709
+ const randomFormIdVariable = generateRandomVariableName('form');
710
+ const randomHiddenFieldsAttr = generateRandomVariableName('hiddenFields');
711
+ const randomSendEmailVariable = generateRandomVariableName('send');
712
+ const randomEmailFromVariable = generateRandomVariableName('emailFrom');
713
+ const randomEmailToVariable = generateRandomVariableName('emailTo');
714
+ const randomEmailSubjectVariable = generateRandomVariableName('emailSubj');
715
+ const randomEmailMessageVariable = generateRandomVariableName('emailMsg');
716
+ const randomTestFeedbackAttr = generateRandomVariableName('emailTestMsg');
717
+
718
+ Object.assign(formVars, {
719
+ randomFormIdVariable,
720
+ randomSendEmailVariable,
721
+ randomEmailFromVariable,
722
+ randomEmailToVariable,
723
+ randomEmailSubjectVariable,
724
+ randomEmailMessageVariable,
725
+ randomTestFeedbackAttr,
726
+ randomHiddenFieldsAttr
727
+ });
728
+
729
+ setRandomAttributeContent(randomFormIdVariable, `form-${randomFormIdVariable}`);
730
+ setRandomAttributeContent(randomSendEmailVariable, false);
731
+ setRandomAttributeContent(randomEmailFromVariable, '');
732
+ setRandomAttributeContent(randomEmailToVariable, '');
733
+ setRandomAttributeContent(randomEmailSubjectVariable, 'New Form Submission');
734
+ setRandomAttributeContent(randomEmailMessageVariable, 'Form data:\n{{fields}}');
735
+ setRandomAttributeContent(randomTestFeedbackAttr, '');
736
+ setRandomAttributeContent(randomHiddenFieldsAttr, []);
737
+
738
+ createPanel({
739
+ type: 'formSettings',
740
+ title: 'Form Settings',
741
+ attributes: [randomFormIdVariable, randomSendEmailVariable],
742
+ });
743
+
744
+ createPanel({
745
+ type: 'emailSettings',
746
+ title: 'Email Settings',
747
+ attributes: [
748
+ randomSendEmailVariable,
749
+ randomEmailFromVariable,
750
+ randomEmailToVariable,
751
+ randomEmailSubjectVariable,
752
+ randomEmailMessageVariable,
753
+ randomTestFeedbackAttr,
754
+ randomFormIdVariable
755
+ ],
756
+ });
757
+
758
+ createPanel({
759
+ type: 'hiddenFields',
760
+ title: 'Hidden Fields',
761
+ attributes: [randomHiddenFieldsAttr],
762
+ });
763
+ };
764
+
765
+ const getFormVariables = () => ({ ...formVars });
766
+
767
+ const transformFormToDynamicJSX = (htmlContent) => {
768
+ const regex = /<form([\s\S]*?)>([\s\S]*?)<\/form>/gi
769
+ const formExists = regex.test(htmlContent);
770
+
771
+ if (!formExists) {
772
+ return htmlContent;
773
+ }
774
+
775
+
776
+ return htmlContent.replace(
777
+ /<form([\s\S]*?)>([\s\S]*?)<\/form>/gi,
778
+ (_match, formAttributes, innerContent) => {
779
+ createPanelsForForm();
780
+
781
+ const {
782
+ randomFormIdVariable,
783
+ randomHiddenFieldsAttr
784
+ } = getFormVariables();
785
+
786
+ return `
787
+ <form
788
+ ${formAttributes.trim()}
789
+ id={attributes.${randomFormIdVariable}}
790
+ >
791
+ ${innerContent}
792
+
793
+ { (attributes.${randomHiddenFieldsAttr} || []).map((field, index) => (<input key={index} name={field.name} value={field.value} type="hidden" /> )) }
794
+ </form>
795
+ `;
796
+ }
797
+ );
798
+ };
799
+
678
800
  const getFixedHtml = (html) => {
801
+ function parseStyleString(style) {
802
+ const entries = style.split(';').filter(Boolean).map(rule => {
803
+ const [key, value] = rule.split(':');
804
+ if (!key || !value) return null;
805
+ const camelKey = key.trim().replace(/-([a-z])/g, (_, char) => char.toUpperCase());
806
+ return [camelKey, value.trim()];
807
+ }).filter(Boolean);
808
+ const styleObject = Object.fromEntries(entries);
809
+ return JSON.stringify(styleObject).replace(/"([^"\n]+)":/g, '$1:');
810
+ }
679
811
  return html
812
+ .replace(/style="([^"]+)"/g, (_, styleString) => {
813
+ const styleObj = parseStyleString(styleString);
814
+ return `style={${styleObj}}`;
815
+ })
680
816
  .replace(/ onChange="{" \(newtext\)=""\>/gi, ' onChange={ (newtext) => ')
681
817
  .replace(/\<\/RichText\>/gi, '')
682
818
  .replace(/value="{(.*?)}"/gi, 'value={$1}')
683
819
  .replace(/"{attributes.(.*?)}"/gi, '{attributes.$1}');
684
820
  };
821
+
685
822
  const processImages = (imgTag) => {
686
823
  const properties = getImageProperties(imgTag);
687
824
  const { isBackground } = properties;
825
+
688
826
  if (!isBackground) {
689
827
  processImage({ ...properties, type: 'image' });
828
+
690
829
  return;
691
830
  }
831
+
692
832
  processImage({ ...properties, type: 'background' });
693
833
  };
694
834
  const loopImages = ($) => {
@@ -696,9 +836,11 @@ const block = async (
696
836
  processImages($(img));
697
837
  });
698
838
  };
839
+
699
840
  const getHtml = ($) => {
700
841
  return $.html({ xml: false, decodeEntities: false });
701
842
  };
843
+
702
844
  const processEditImages = async (options) => {
703
845
  const $ = await loadHtml(options);
704
846
  loopImages($);
@@ -715,43 +857,52 @@ const block = async (
715
857
  }}
716
858
  /><`;
717
859
  };
860
+
718
861
  const convertToRichText = (variableContent) => {
719
862
  const randomVariable = generateRandomVariableName('content');
720
- setAttributeContent(randomVariable, variableContent);
863
+
864
+ setRandomAttributeContent(randomVariable, variableContent);
865
+
721
866
  return getRichTextTemplate(randomVariable, variableContent);
722
867
  };
868
+
723
869
  const parseContent = (content) => {
724
870
  return content.replace(/>([^<]+)</g, (match, variableContent) => {
725
- if (match.replace(/\s\S/g, '').replace(/(<|>)/g, '').trim() === '') {
871
+ const regex = /{|}|\(|\)|=>/;
872
+
873
+ if (regex.test(variableContent.trim())) {
874
+ return match;
875
+ }
876
+
877
+ if (variableContent.trim() === '') {
726
878
  return match;
727
879
  }
880
+
728
881
  return convertToRichText(variableContent);
729
882
  });
730
883
  };
731
- const editJsxContent = async (options) => {
732
- let content;
733
- if (options.htmlContent) {
734
- content = options.htmlContent.replaceAll(/<!--(.*?)-->/gs, '');
735
- }
884
+
885
+ const getEditJsxContent = async (options) => {
886
+ let content = transformFormToDynamicJSX(options.htmlContent);
887
+
888
+ content = content.replaceAll(/<!--(.*?)-->/gs, '');
889
+
736
890
  content = `<div className="custom-block">${content}</div>`;
891
+
737
892
  return await processEditImages({
738
893
  ...options,
739
- htmlContent: convert(parseContent(content)),
894
+ htmlContent: parseContent(content),
740
895
  });
741
896
  };
897
+
742
898
  const createPanel = (values) => {
743
899
  if (values.attributes && values.attributes.length > 0) {
744
900
  panels.push(values);
745
901
  }
746
902
  };
747
- const getSvgTemplate = (_match, group1, group3, randomSVGVariable) => {
748
- return `
749
- <svg
750
- ${group1}
751
- dangerouslySetInnerHTML={ { __html: attributes.${randomSVGVariable} }}
752
- >
753
- ${group3}
754
- `;
903
+
904
+ const getSvgTemplate = (_match, group1, _group3, randomSVGVariable) => {
905
+ return `<svg ${group1} dangerouslySetInnerHTML={ { __html: attributes.${randomSVGVariable} }}></svg>`;
755
906
  };
756
907
  const replaceSVGImages = async (html) => {
757
908
  const regex = /<\s*svg\b((?:[^>'"]|"[^"]*"|'[^']*')*)>(\s*(?:[^<]|<(?!\/svg\s*>))*)(<\/\s*svg\s*>)/gim;
@@ -770,17 +921,15 @@ const block = async (
770
921
  const content = group2.trim();
771
922
  if (content) {
772
923
  const randomSVGVariable = generateRandomVariableName('svg');
773
- setAttributeContent(randomSVGVariable, content);
924
+ setRandomAttributeContent(randomSVGVariable, content.replaceAll('className', 'class'));
774
925
  createPanel({
775
926
  type: 'svg',
776
927
  title: 'SVG Markup',
777
928
  attributes: [randomSVGVariable],
778
929
  });
779
930
 
780
- const replacement = await transform(
781
- getSvgTemplate(fullMatch, group1, group3, randomSVGVariable),
782
- { jsxRuntime: 'classic' }
783
- );
931
+
932
+ const replacement = getSvgTemplate(fullMatch, group1, group3, randomSVGVariable)
784
933
 
785
934
  result += replacement;
786
935
  } else {
@@ -791,6 +940,9 @@ const block = async (
791
940
  }
792
941
 
793
942
  result += html.slice(lastIndex);
943
+
944
+ console.log(result);
945
+
794
946
  return result;
795
947
  };
796
948
  const getSvgPanelTemplate = (panel) => {
@@ -822,6 +974,7 @@ const block = async (
822
974
  ? `${panel.attributes[0]}: media.url,
823
975
  ${panel.attributes[1]}: media.alt`
824
976
  : '';
977
+
825
978
  return panel.attributes &&
826
979
  panel.attributes[0] &&
827
980
  attributes[panel.attributes[0]]
@@ -830,10 +983,7 @@ const block = async (
830
983
  <PanelRow>
831
984
  <div>
832
985
  <MediaUpload
833
- onSelect={ (media) => {
834
- setAttributes({
835
- ${mediaAtts}
836
- });
986
+ onSelect={ (media) => { setAttributes({ ${mediaAtts} });
837
987
  } }
838
988
  type="image"
839
989
  value={ attributes.${panel.attributes?.[0]} }
@@ -851,12 +1001,272 @@ const block = async (
851
1001
  : '';
852
1002
  };
853
1003
 
854
- const getPanelTemplate = (panel) => {
1004
+ const getFormSettingsPanelTemplate = (panel) => {
1005
+ const [formIdAttr, sendEmailAttr] = panel.attributes;
1006
+
1007
+ return `
1008
+ <PanelBody title="${panel.title}" initialOpen={true}>
1009
+ <TextControl
1010
+ label="Form ID"
1011
+ disabled="true"
1012
+ value={attributes.${formIdAttr}}
1013
+ onChange={(val) => setAttributes({ ${formIdAttr}: val })}
1014
+ />
1015
+ <ToggleControl
1016
+ label="Send Email on Submit"
1017
+ checked={attributes.${sendEmailAttr}}
1018
+ onChange={(val) => setAttributes({ ${sendEmailAttr}: val })}
1019
+ />
1020
+ </PanelBody>
1021
+ `;
1022
+ };
1023
+
1024
+ const getPhpEmailData = (formIdAttr, fromAttr, toAttr, subjectAttr, messageAttr) => {
1025
+ return `
1026
+ add_action('wp_ajax_send_email_${formIdAttr}', function() {
1027
+ //check_ajax_referer('wp_rest');
1028
+
1029
+ $post_id = $_POST['postId'];
1030
+
1031
+ if (!$post_id) {
1032
+ wp_send_json_error('Missing post ID.');
1033
+ }
1034
+
1035
+ $post_data = $_POST;
1036
+ $post = get_post($post_id);
1037
+
1038
+ if (!$post) {
1039
+ wp_send_json_error('Post not found.');
1040
+ }
1041
+
1042
+ $blocks = parse_blocks($post->post_content);
1043
+
1044
+ $target_block = null;
1045
+
1046
+ foreach ($blocks as $block) {
1047
+ if ($block['blockName'] === '${blockName}') {
1048
+ $target_block = $block;
1049
+ break;
1050
+ }
1051
+ }
1052
+
1053
+ if (!$target_block) {
1054
+ wp_send_json_error('Block not found.');
1055
+ }
1056
+
1057
+ $attrs = $target_block['attrs'] ?? [];
1058
+
1059
+ $to = sanitize_email(parse_form_placeholders_${functionSufix}($attrs['${toAttr}'] ?? '', $post_data));
1060
+ $from = sanitize_email(parse_form_placeholders_${functionSufix}($attrs['${fromAttr}'] ?? '', $post_data));
1061
+ $subject = sanitize_text_field(parse_form_placeholders_${functionSufix}($attrs['${subjectAttr}'] ?? '', $post_data));
1062
+ $message = wp_kses_post(parse_form_placeholders_${functionSufix}($attrs['${messageAttr}'] ?? '', $post_data));
1063
+
1064
+ if (empty($to) || empty($from)) {
1065
+ wp_send_json_error('Missing email addresses.');
1066
+ }
1067
+
1068
+ $sent = wp_mail($to, $subject, $message, [
1069
+ 'Content-Type: text/html; charset=UTF-8',
1070
+ 'From: ' . $from
1071
+ ]);
1072
+
1073
+ if ($sent) {
1074
+ wp_send_json_success('Email sent');
1075
+ } else {
1076
+ error_log('Failed to send. To: ' . $to . ' | From: ' . $from);
1077
+ wp_send_json_error('Failed to send email.');
1078
+ }
1079
+ });
1080
+ `;
1081
+ }
1082
+
1083
+ const getEmailSettingsPanelTemplate = (panel) => {
1084
+ const [
1085
+ sendEmailAttr,
1086
+ fromAttr,
1087
+ toAttr,
1088
+ subjectAttr,
1089
+ messageAttr,
1090
+ testFeedbackAttr,
1091
+ formIdAttr,
1092
+ ] = panel.attributes;
1093
+
1094
+ emailTemplate += sendEmailAttr ? getEmailSaveTemplate(formIdAttr) : '';
1095
+ phpEmailData += getPhpEmailData(formIdAttr, fromAttr, toAttr, subjectAttr, messageAttr);
1096
+
1097
+ return `
1098
+ { attributes.${sendEmailAttr} && (
1099
+ <PanelBody title="${panel.title}" initialOpen={true}>
1100
+ <TextControl
1101
+ label="From"
1102
+ value={attributes.${fromAttr}}
1103
+ onChange={(val) => setAttributes({ ${fromAttr}: val })}
1104
+ />
1105
+ <TextControl
1106
+ label="To"
1107
+ value={attributes.${toAttr}}
1108
+ onChange={(val) => setAttributes({ ${toAttr}: val })}
1109
+ />
1110
+ <TextControl
1111
+ label="Subject"
1112
+ value={attributes.${subjectAttr}}
1113
+ onChange={(val) => setAttributes({ ${subjectAttr}: val })}
1114
+ />
1115
+ <TextareaControl
1116
+ label="Message Template (HTML Supported)"
1117
+ help="Use {{fieldName}} to insert field values."
1118
+ value={attributes.${messageAttr}}
1119
+ onChange={(val) => setAttributes({ ${messageAttr}: val })}
1120
+ />
1121
+
1122
+ <Button
1123
+ variant="primary"
1124
+ onClick={() => {
1125
+ const form = document.getElementById('form-${formIdAttr}');
1126
+ const inputs = form.querySelectorAll('input, select, textarea');
1127
+ const body = new URLSearchParams();
1128
+
1129
+ inputs.forEach(input => {
1130
+ if (input.name && !input.disabled) {
1131
+ body.append(input.name, input.value);
1132
+ }
1133
+ });
1134
+
1135
+ body.append('action', 'send_test_email_${functionSufix}');
1136
+ body.append('from', attributes?.${fromAttr} || '');
1137
+ body.append('to', attributes?.${toAttr} || '');
1138
+ body.append('subject', attributes?.${subjectAttr} || '');
1139
+ body.append('message', attributes?.${messageAttr} || '');
1140
+
1141
+ fetch(vars.ajaxUrl, {
1142
+ method: 'POST',
1143
+ body
1144
+ })
1145
+ .then(res => res.json())
1146
+ .then(data => {
1147
+ setAttributes({ ${testFeedbackAttr}: data.success ? 'Test Email Sent!' : data.error });
1148
+ })
1149
+ .catch(() => {
1150
+ setAttributes({ ${testFeedbackAttr}: 'Failed to send test email.' });
1151
+ });
1152
+ }}
1153
+ >
1154
+ Send Test Email
1155
+ </Button>
1156
+
1157
+ {attributes.${testFeedbackAttr} && (
1158
+ <Notice status="info" isDismissible={false}>
1159
+ {attributes.${testFeedbackAttr}}
1160
+ </Notice>
1161
+ )}
1162
+ </PanelBody>
1163
+ )}
1164
+ `;
1165
+ };
1166
+
1167
+ const getEmailSaveTemplate = (formIdAttr) => {
1168
+ return `
1169
+ document.addEventListener('DOMContentLoaded', function() {
1170
+ const form = document.getElementById('form-${formIdAttr}');
1171
+
1172
+ form.addEventListener('submit', function(event) {
1173
+ event.preventDefault();
1174
+
1175
+ const inputs = form.querySelectorAll('input, select, textarea');
1176
+ const body = new URLSearchParams();
1177
+
1178
+ inputs.forEach(input => {
1179
+ if (input.name && !input.disabled) {
1180
+ body.append(input.name, input.value);
1181
+ }
1182
+ });
1183
+
1184
+ body.append('action', 'send_email_${formIdAttr}');
1185
+ body.append('postId', vars.postId);
1186
+
1187
+ fetch(vars.ajaxUrl, {
1188
+ method: 'POST',
1189
+ body
1190
+ })
1191
+ .then(res => res.json())
1192
+ .then(data => {
1193
+ console.log(data.success ? 'Test Email Sent!' : data.error);
1194
+ })
1195
+ .catch(() => {
1196
+ console.log('Failed to send test email.');
1197
+ });
1198
+
1199
+ form.reset()
1200
+ });
1201
+ });
1202
+ `;
1203
+ };
1204
+
1205
+ const getHiddenFieldsPanelTemplate = (panel) => {
1206
+ const [hiddenFieldsAttr] = panel.attributes;
1207
+
1208
+ return `
1209
+ <PanelBody title="${panel.title}" initialOpen={false}>
1210
+ { (attributes.${hiddenFieldsAttr} || []).map((field, index) => (
1211
+ <div key={index} style={{ marginBottom: '10px', borderBottom: '1px solid #ddd', paddingBottom: '10px' }}>
1212
+ <TextControl
1213
+ label="Name"
1214
+ value={field.name}
1215
+ onChange={(val) => {
1216
+ const newFields = [...attributes.${hiddenFieldsAttr}];
1217
+ newFields[index].name = val;
1218
+ setAttributes({ ${hiddenFieldsAttr}: newFields });
1219
+ }}
1220
+ />
1221
+ <TextControl
1222
+ label="Value"
1223
+ value={field.value}
1224
+ onChange={(val) => {
1225
+ const newFields = [...attributes.${hiddenFieldsAttr}];
1226
+ newFields[index].value = val;
1227
+ setAttributes({ ${hiddenFieldsAttr}: newFields });
1228
+ }}
1229
+ />
1230
+ <Button
1231
+ variant="secondary"
1232
+ onClick={() => {
1233
+ const newFields = [...attributes.${hiddenFieldsAttr}];
1234
+ newFields.splice(index, 1);
1235
+ setAttributes({ ${hiddenFieldsAttr}: newFields });
1236
+ }}
1237
+ >
1238
+ Remove
1239
+ </Button>
1240
+ </div>
1241
+ ))}
1242
+
1243
+ <Button
1244
+ variant="primary"
1245
+ onClick={() => {
1246
+ const newFields = [...(attributes.${hiddenFieldsAttr} || [])];
1247
+ newFields.push({ name: '', value: '' });
1248
+ setAttributes({ ${hiddenFieldsAttr}: newFields });
1249
+ }}
1250
+ >
1251
+ Add Hidden Field
1252
+ </Button>
1253
+ </PanelBody>
1254
+ `;
1255
+ };
1256
+
1257
+
1258
+ const findAndGetPanelTemplate = (panel) => {
855
1259
  switch (panel.type) {
856
1260
  case 'svg':
857
1261
  return getSvgPanelTemplate(panel);
858
1262
  case 'media':
859
1263
  return getMediaPanelTemplate(panel);
1264
+ case 'formSettings':
1265
+ return getFormSettingsPanelTemplate(panel);
1266
+ case 'emailSettings':
1267
+ return getEmailSettingsPanelTemplate(panel);
1268
+ case 'hiddenFields':
1269
+ return getHiddenFieldsPanelTemplate(panel);
860
1270
  default:
861
1271
  return '';
862
1272
  }
@@ -865,7 +1275,7 @@ const block = async (
865
1275
  const getPanelsTemplate = () => {
866
1276
  return panels
867
1277
  .map((panel) => {
868
- return getPanelTemplate(panel);
1278
+ return findAndGetPanelTemplate(panel);
869
1279
  })
870
1280
  .join('\n');
871
1281
  };
@@ -877,28 +1287,35 @@ const block = async (
877
1287
  </Panel>`;
878
1288
  };
879
1289
 
880
- const getSaveContent = (editContent) => {
1290
+ const buildSaveContent = (editContent) => {
881
1291
  return editContent.replace(
882
1292
  /<RichText((.|\n)*?)value=\{(.*?)\}((.|\n)*?)\/>/gi,
883
1293
  '<RichText.Content value={$3} />'
884
- );
885
- };
886
-
887
- const saveHtmlContent = (editContent) => {
888
- return getSaveContent(editContent).replace(/className=/gi, 'class=');
1294
+ )
1295
+ .replaceAll('class=', 'className=')
1296
+ .replace(
1297
+ /<MediaUpload\b[^>]*>([\s\S]*?(<img\b[^>]*>*\/>)[\s\S]*?)\/>/g,
1298
+ (_match, _attributes, img) => {
1299
+ return img.replace(/onClick={[^}]+}\s*/, '');
1300
+ }
1301
+ );
889
1302
  };
890
1303
 
891
1304
  const removeHref = (match) => {
892
1305
  return match.replace(/href="(.*?)"/, '');
893
1306
  };
1307
+
894
1308
  const replaceRichText = (match, group1, _group2, group3) => {
895
1309
  return removeHref(match)
896
1310
  .replace(group1, '<span')
897
1311
  .replace(group3, '</span>');
898
1312
  };
1313
+
899
1314
  const processLinks = (options) => {
900
- let htmlContent = options.htmlContent
901
- ? options.htmlContent.replace(
1315
+ let { htmlContent } = options;
1316
+
1317
+ htmlContent = htmlContent
1318
+ ? htmlContent.replace(
902
1319
  /(<a)[^>]*>([\s\S]*?)(<\/a>)/gim,
903
1320
  replaceRichText
904
1321
  )
@@ -935,84 +1352,62 @@ const block = async (
935
1352
  );
936
1353
  };
937
1354
 
938
- const transformOnClickEvent = (img) => {
939
- return img.replace(/onClick={[^}]+}\s*/, '');
940
- };
941
-
942
- const processSaveImages = (htmlString) => {
943
- return htmlString.replace(
944
- /<MediaUpload\b[^>]*>([\s\S]*?(<img\b[^>]*>*\/>)[\s\S]*?)\/>/g,
945
- (_match, _attributes, img) => transformOnClickEvent(img)
946
- );
947
- };
948
-
949
1355
  const getComponentAttributes = () => {
950
- return JSON.stringify(attributes, null, 2);
1356
+ return Object.entries(attributes)
1357
+ .map(([key, value]) => {
1358
+ if (typeof value === 'object' && value !== null) {
1359
+ return `${key}: { ${Object.entries(value).map(([k, v]) => `${k}: \`${v}\``).join(', ')} }`;
1360
+ } else {
1361
+ return `${key}:\`${value}\``;
1362
+ }
1363
+ })
1364
+ .join(',\n');
951
1365
  };
952
1366
 
953
1367
  const getEdit = async (options) => {
954
1368
  let { htmlContent } = options;
955
1369
 
956
1370
  if (htmlContent) {
957
- htmlContent = await editJsxContent(processLinks(options));
958
- return (await replaceSVGImages(htmlContent.replaceAll('../', '').replace(/style="([^"]+)"/g, (_, styleString) => {
959
- const styleObj = parseStyleString(styleString);
960
- return `style={${styleObj}}`;
961
- })));
1371
+ options.htmlContent = unwrapBody(htmlContent);
1372
+ const postProcessLinks = processLinks(options);
1373
+ const postGetEditJsx = await getEditJsxContent(postProcessLinks);
1374
+ const preConvert = await postGetEditJsx.replace(/<\/br>/g, '<br/>').replace(/<\/hr>/g, '<hr/>')
1375
+ return convert(preConvert)
962
1376
  }
1377
+
963
1378
  return '';
964
1379
  };
965
- const getSave = (edit) => {
966
- return processSaveImages(saveHtmlContent(edit));
967
- };
968
- const getBlock = async (htmlContent, settings) => {
1380
+
1381
+ const parseBlockAttributes = () => {
1382
+ const attrs = `{${getComponentAttributes()}}`;
1383
+ return replaceSourceUrlVars(attrs, options.source);
1384
+ }
1385
+
1386
+ const getBlock = async (settings) => {
969
1387
  let {
970
- prefix,
971
1388
  name,
972
1389
  category,
973
1390
  generateIconPreview,
974
- basePath,
975
- cssFiles,
976
- jsFiles,
977
1391
  } = settings;
978
- const newName = convertName(name);
979
- const newPrefix = convertName(prefix);
980
- cssFiles = cssFiles || [];
981
- jsFiles = jsFiles || [];
982
- let iconPreview = "'shield'";
983
- let edit = await getEdit(settings);
984
- edit = edit.replace(
985
- /dangerouslySetInnerHTML="{" {="" __html:="" (.*?)="" }}=""/gm,
986
- `dangerouslySetInnerHTML={{ __html: $1 }}`
987
- );
988
- const save = getSave(edit);
1392
+
1393
+ const iconPreview = generateIconPreview ? `(<img src={vars.url + 'preview.jpeg'} />)` : "'shield'";
1394
+ const edit = await getEdit(settings);
1395
+ const save = buildSaveContent(edit);
989
1396
  const blockPanels = createPanels();
990
- const blockAttributes = `${JSON.parse(
991
- JSON.stringify(getComponentAttributes(), null, 2)
992
- ).replace(/"var.url\+\'(.*?)\'(.*?)"/g, "vars.url+'$1'$2").replaceAll("var(.*?).url\+'(http.*?)'", 'http$2')}`;
993
- if (generateIconPreview) {
994
- try {
995
- await icon(htmlContent, { basePath, cssFiles, jsFiles });
996
- iconPreview = `(<img src="data:image/jpeg;base64,${await imageToBase64(
997
- path.join(basePath, 'preview.jpeg')
998
- )}" />)`;
999
- } catch (error) {
1000
- console.log(`There was an error generating preview. ${error.message}`);
1001
- }
1002
- }
1397
+
1003
1398
  const output = `
1004
1399
  (function () {
1005
1400
  ${imports}
1006
1401
 
1007
- registerBlockType('${sanitizeAndReplaceNumbers(newPrefix)}/${sanitizeAndReplaceNumbers(newName)}', {
1402
+ registerBlockType('${blockName}', {
1008
1403
  title: '${name}',
1009
1404
  icon: ${iconPreview},
1010
1405
  category: '${category}',
1011
- attributes: ${blockAttributes},
1406
+ attributes: ${parseBlockAttributes()},
1012
1407
  edit(props) {
1013
- const { attributes, setAttributes } = props;
1408
+ const { attributes, setAttributes } = props;
1014
1409
 
1015
- return (
1410
+ return (
1016
1411
  <div>
1017
1412
  <InspectorControls>
1018
1413
  ${blockPanels}
@@ -1023,22 +1418,22 @@ const block = async (
1023
1418
  );
1024
1419
  },
1025
1420
  save(props) {
1026
- const { attributes } = props;
1421
+ const { attributes } = props;
1027
1422
 
1028
- return (
1029
- ${save}
1030
- );
1423
+ return (
1424
+ ${save}
1425
+ );
1031
1426
  },
1032
1427
  });
1033
1428
  })();`;
1034
- if (generateIconPreview) {
1035
- return output.replace(/icon: \s * (')([^']*)(')/, 'icon: $2');
1036
- }
1429
+
1037
1430
  return output;
1038
1431
  };
1432
+
1039
1433
  const setupVariables = async (htmlContent, options) => {
1434
+
1040
1435
  const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
1041
- const linkRegex = /<link\s+[^>]*href=["']([^"']+)["'][^>]*>/gi;
1436
+ const linkRegex = /<link\b[^>]*((\brel=["']stylesheet["'])|\bhref=["'][^"']+\.css["'])[^>]*>/gi;
1042
1437
 
1043
1438
  let match;
1044
1439
 
@@ -1061,10 +1456,15 @@ const block = async (
1061
1456
 
1062
1457
  await Promise.all(fetchCssPromises);
1063
1458
 
1064
- css = styles.map((style) => {
1459
+ css += styles.map((style) => {
1065
1460
  return `${style.content}`;
1066
1461
  }).join('\n');
1067
1462
 
1463
+
1464
+ console.log('[CSSFETCHED]', css);
1465
+
1466
+
1467
+
1068
1468
  const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1069
1469
  const scriptSrcRegex =
1070
1470
  /<script\s+[^>]*src=["']([^"']+)["'][^>]*>\s*<\/script>/gi;
@@ -1079,6 +1479,7 @@ const block = async (
1079
1479
  });
1080
1480
 
1081
1481
  const fetchJsPromises = [];
1482
+
1082
1483
  while ((jsMatch = scriptSrcRegex.exec(htmlContent)) !== null) {
1083
1484
  const url = jsMatch[1];
1084
1485
  const fetchJsPromise = fetch(url)
@@ -1092,7 +1493,7 @@ const block = async (
1092
1493
 
1093
1494
  await Promise.all(fetchJsPromises);
1094
1495
 
1095
- js = scripts.map((script) => script.content).join('\n');
1496
+ js += scripts.map((script) => script.content).join('\n');
1096
1497
 
1097
1498
  let {
1098
1499
  basePath = process.cwd(),
@@ -1100,9 +1501,24 @@ const block = async (
1100
1501
  jsFiles = [],
1101
1502
  name = 'My block',
1102
1503
  } = options;
1103
- const newDir = path.join(basePath, convertName(name));
1504
+
1505
+ const newDir = path.join(basePath, replaceUnderscoresSpacesAndUppercaseLetters(name));
1506
+
1507
+ const $ = cheerio.load(htmlContent, {
1508
+ xmlMode: true,
1509
+ decodeEntities: false,
1510
+ });
1511
+
1512
+ $('head, script, style').remove();
1513
+
1514
+ htmlContent = $('body').html();
1515
+
1516
+ options.html
1517
+
1518
+
1104
1519
  try {
1105
1520
  fs.mkdirSync(newDir, { recursive: true });
1521
+
1106
1522
  return {
1107
1523
  ...options,
1108
1524
  jsFiles,
@@ -1114,6 +1530,19 @@ const block = async (
1114
1530
  logError(error);
1115
1531
  }
1116
1532
  };
1533
+
1534
+ if (source) {
1535
+ htmlContent = replaceRelativeUrlsInHtml(htmlContent, source);
1536
+ htmlContent = replaceRelativeUrlsInCssWithBase(htmlContent, source);
1537
+ }
1538
+
1539
+ try {
1540
+ icon(htmlContent, { basePath: path.join(options.basePath, replaceUnderscoresSpacesAndUppercaseLetters(options.name)) });
1541
+ } catch (error) {
1542
+ console.log(`There was an error generating preview. ${error.message}`);
1543
+ }
1544
+
1117
1545
  return saveFiles(await setupVariables(htmlContent, options));
1118
1546
  };
1119
- export default block;
1547
+
1548
+ export default block;