html-to-gutenberg 4.2.8 → 4.2.10

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 (60) hide show
  1. package/.env.example +20 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/workflows/build.yml +26 -0
  4. package/.github/workflows/coverage.yml +26 -0
  5. package/.github/workflows/sync-npm.yml +154 -0
  6. package/.nyc_output/1f0406b8-bb70-495d-8f8a-521fdd81b500.json +1 -0
  7. package/.nyc_output/6390956f-4f8a-4adb-9256-4a1c7e34a52d.json +1 -0
  8. package/.nyc_output/processinfo/1f0406b8-bb70-495d-8f8a-521fdd81b500.json +1 -0
  9. package/.nyc_output/processinfo/6390956f-4f8a-4adb-9256-4a1c7e34a52d.json +1 -0
  10. package/.nyc_output/processinfo/index.json +1 -0
  11. package/@types.d.ts +3 -0
  12. package/coverage/coverage-final.json +4 -0
  13. package/coverage/lcov-report/base.css +224 -0
  14. package/coverage/lcov-report/block-navigation.js +87 -0
  15. package/coverage/lcov-report/prettify.css +1 -0
  16. package/coverage/lcov-report/prettify.js +2 -0
  17. package/coverage/lcov-report/sorter.js +210 -0
  18. package/coverage/lcov.info +198 -0
  19. package/coverage-demo.test.ts +8 -0
  20. package/dist/coverage-demo.test.js +10 -0
  21. package/dist/coverage-demo.test.js.map +1 -0
  22. package/dist/globals.js +24 -0
  23. package/dist/globals.js.map +1 -0
  24. package/dist/index.js +36 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/index.test.js +166 -0
  27. package/dist/index.test.js.map +1 -0
  28. package/dist/package.json +130 -0
  29. package/dist/snapapi-screenshot.test.js +44 -0
  30. package/dist/snapapi-screenshot.test.js.map +1 -0
  31. package/dist/src/coverage-demo.js +7 -0
  32. package/dist/src/coverage-demo.js.map +1 -0
  33. package/dist/src/utils-extra.test.js +137 -0
  34. package/dist/src/utils-extra.test.js.map +1 -0
  35. package/dist/src/utils.test.js +65 -0
  36. package/dist/src/utils.test.js.map +1 -0
  37. package/dist/tsconfig.tsbuildinfo +1 -0
  38. package/dist/utils.js +61 -0
  39. package/dist/utils.js.map +1 -0
  40. package/fetch-page-assets.test.ts +448 -0
  41. package/index.d.ts +173 -0
  42. package/index.js +628 -249
  43. package/index.test.ts +774 -0
  44. package/index.ts +155 -1530
  45. package/package.json +87 -15
  46. package/r2.js +163 -0
  47. package/readme.md +126 -72
  48. package/scripts/patch-fetch-page-assets.mjs +13 -0
  49. package/scripts/sync-from-npm.mjs +115 -0
  50. package/snapapi-screenshot.test.ts +46 -0
  51. package/src/coverage-demo.ts +3 -0
  52. package/src/utils-extra.test.ts +108 -0
  53. package/src/utils.test.ts +36 -0
  54. package/temp-block-test.js +19 -0
  55. package/tsconfig.json +25 -4
  56. package/utils.ts +56 -0
  57. package/vendor/fetch-page-assets/LICENSE.MD +21 -0
  58. package/vendor/fetch-page-assets/README.md +117 -0
  59. package/vendor/fetch-page-assets/index.js +362 -0
  60. package/vendor/fetch-page-assets/package.json +48 -0
package/index.js CHANGED
@@ -1,13 +1,21 @@
1
+ import fetch from 'node-fetch';
1
2
  import presetReact from '@babel/preset-react';
2
3
  import * as babel from '@babel/core';
3
4
  import * as cheerio from 'cheerio';
4
5
  import scopeCss from 'css-scoping';
5
6
  import extractAssets from 'fetch-page-assets';
6
7
  import fs from 'fs';
7
- import icon from 'html-screenshots';
8
+ import dotenv from 'dotenv';
8
9
  import { createRequire } from 'module';
9
10
  import convert from 'node-html-to-jsx';
10
11
  import path from 'path';
12
+ import {
13
+ createFileRecord,
14
+ createJobId,
15
+ inferContentType,
16
+ uploadBufferToR2,
17
+ zipEntriesToBuffer,
18
+ } from './r2.js';
11
19
 
12
20
  import {
13
21
  imports,
@@ -18,32 +26,411 @@ import {
18
26
  const require = createRequire(import.meta.url);
19
27
  const { version } = require('./package.json');
20
28
 
29
+ export const createProfiler = (enabled) => {
30
+ const marks = new Map();
31
+
32
+ return {
33
+ start(label) {
34
+ if (enabled) {
35
+ marks.set(label, process.hrtime.bigint());
36
+ }
37
+ },
38
+ end(label) {
39
+ if (enabled && marks.has(label)) {
40
+ const start = marks.get(label);
41
+ const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
42
+ console.log(`[profile] ${label}: ${elapsedMs.toFixed(2)}ms`);
43
+ marks.delete(label);
44
+ }
45
+ },
46
+ };
47
+ };
48
+
49
+ export const findSelfClosingJsxEnd = (content, startIndex) => {
50
+ let inSingleQuote = false;
51
+ let inDoubleQuote = false;
52
+ let inTemplateQuote = false;
53
+ let braceDepth = 0;
54
+ let parenDepth = 0;
55
+
56
+ for (let i = startIndex; i < content.length - 1; i++) {
57
+ const char = content[i];
58
+ const next = content[i + 1];
59
+ const prev = i > 0 ? content[i - 1] : '';
60
+ const escaped = prev === '\\';
61
+
62
+ if (!escaped) {
63
+ if (!inDoubleQuote && !inTemplateQuote && char === '\'') {
64
+ inSingleQuote = !inSingleQuote;
65
+ continue;
66
+ }
67
+ if (!inSingleQuote && !inTemplateQuote && char === '"') {
68
+ inDoubleQuote = !inDoubleQuote;
69
+ continue;
70
+ }
71
+ if (!inSingleQuote && !inDoubleQuote && char === '`') {
72
+ inTemplateQuote = !inTemplateQuote;
73
+ continue;
74
+ }
75
+ }
76
+
77
+ if (inSingleQuote || inDoubleQuote || inTemplateQuote) {
78
+ continue;
79
+ }
80
+
81
+ if (char === '{') {
82
+ braceDepth++;
83
+ continue;
84
+ }
85
+
86
+ if (char === '}') {
87
+ braceDepth = Math.max(0, braceDepth - 1);
88
+ continue;
89
+ }
90
+
91
+ if (char === '(') {
92
+ parenDepth++;
93
+ continue;
94
+ }
95
+
96
+ if (char === ')') {
97
+ parenDepth = Math.max(0, parenDepth - 1);
98
+ continue;
99
+ }
100
+
101
+ if (char === '/' && next === '>' && braceDepth === 0 && parenDepth === 0) {
102
+ return i + 2;
103
+ }
104
+ }
105
+
106
+ return -1;
107
+ };
108
+
109
+ export const replaceSelfClosingJsxComponent = (content, componentName, replacer) => {
110
+ const openTag = `<${componentName}`;
111
+
112
+ if (!content.includes(openTag)) {
113
+ return content;
114
+ }
115
+
116
+ let cursor = 0;
117
+ let result = '';
118
+
119
+ while (cursor < content.length) {
120
+ const start = content.indexOf(openTag, cursor);
121
+
122
+ if (start === -1) {
123
+ result += content.slice(cursor);
124
+ break;
125
+ }
126
+
127
+ result += content.slice(cursor, start);
128
+ const end = findSelfClosingJsxEnd(content, start);
129
+
130
+ if (end === -1) {
131
+ result += content.slice(start);
132
+ break;
133
+ }
134
+
135
+ result += replacer(content.slice(start, end));
136
+ cursor = end;
137
+ }
138
+
139
+ return result;
140
+ };
141
+
142
+ export const getMediaUploadSaveTemplate = (image) => {
143
+ if (!image) {
144
+ return '';
145
+ }
146
+
147
+ const { randomUrlVariable, randomAltVariable, imgClass } = image;
148
+ const classNameAttr = imgClass ? ` className="${imgClass}"` : '';
149
+
150
+ return `<img src={attributes.${randomUrlVariable}} alt={attributes.${randomAltVariable}}${classNameAttr} />`;
151
+ };
152
+
153
+ export const replaceMediaUploadComponents = (content, imageRegistry) => {
154
+ let imageIndex = 0;
155
+
156
+ return replaceSelfClosingJsxComponent(content, 'MediaUpload', () => {
157
+ const template = getMediaUploadSaveTemplate(imageRegistry[imageIndex]);
158
+ imageIndex++;
159
+ return template;
160
+ });
161
+ };
162
+
163
+ export const replaceRichTextComponents = (content) => {
164
+ return replaceSelfClosingJsxComponent(content, 'RichText', (componentSource) => {
165
+ const valueMatch = componentSource.match(/\bvalue=\{([^}]+)\}/);
166
+
167
+ if (!valueMatch) {
168
+ return componentSource;
169
+ }
170
+
171
+ return `<RichText.Content value={${valueMatch[1]}} />`;
172
+ });
173
+ };
174
+
175
+ export const buildAssetExtractionOptions = (basePath, options = {}) => ({
176
+ basePath,
177
+ saveFile: false,
178
+ verbose: false,
179
+ maxRetryAttempts: 1,
180
+ retryDelay: 0,
181
+ concurrency: 8,
182
+ uploadToR2: options.uploadToR2 || false,
183
+ returnDetails: options.returnDetails || false,
184
+ jobId: options.jobId,
185
+ r2Prefix: options.r2Prefix,
186
+ });
187
+
188
+ export const slugifyBlockValue = (value = '') => {
189
+ return String(value)
190
+ .replace(/\W|_/g, '-')
191
+ .replace(/-+/g, '-')
192
+ .replace(/^-|-$/g, '')
193
+ .toLowerCase();
194
+ };
195
+
196
+ export const formatCategoryLabel = (category = '') => {
197
+ return String(category)
198
+ .split(/[-_\s]+/)
199
+ .filter(Boolean)
200
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
201
+ .join(' ');
202
+ };
203
+
204
+ export const normalizeBlockOptions = (options = {}) => {
205
+ const hasOwn = (key) => Object.prototype.hasOwnProperty.call(options, key);
206
+ const title = options.title ?? options.name ?? 'My block';
207
+ const slug = slugifyBlockValue(options.slug ?? title) || 'my-block';
208
+ const namespace = options.namespace ?? options.prefix ?? 'wp';
209
+ const baseUrl = options.baseUrl ?? options.source ?? null;
210
+ const outputPath = options.outputPath ?? options.basePath ?? process.cwd();
211
+ const hasExplicitLocalOutputPreference =
212
+ hasOwn('writeFiles') ||
213
+ hasOwn('outputPath') ||
214
+ hasOwn('shouldSaveFiles') ||
215
+ hasOwn('basePath');
216
+ const hasExplicitJobPreference = hasOwn('uploadToR2') || hasOwn('jobId');
217
+ const outputMode =
218
+ options.outputMode ??
219
+ (hasExplicitJobPreference ? 'job' : hasExplicitLocalOutputPreference ? 'legacy' : 'job');
220
+ const writeFiles = options.writeFiles ?? options.shouldSaveFiles ?? (outputMode === 'legacy');
221
+ const generatePreviewImage =
222
+ options.generatePreviewImage ?? options.generateIconPreview ?? false;
223
+
224
+ return {
225
+ ...options,
226
+ title,
227
+ name: title,
228
+ slug,
229
+ namespace,
230
+ prefix: namespace,
231
+ baseUrl,
232
+ source: baseUrl,
233
+ category: options.category ?? 'common',
234
+ registerCategoryIfMissing: options.registerCategoryIfMissing ?? false,
235
+ outputPath,
236
+ basePath: outputPath,
237
+ writeFiles,
238
+ shouldSaveFiles: writeFiles,
239
+ generatePreviewImage,
240
+ generateIconPreview: generatePreviewImage,
241
+ jsFiles: options.jsFiles ?? [],
242
+ cssFiles: options.cssFiles ?? [],
243
+ outputMode,
244
+ uploadToR2: options.uploadToR2 ?? outputMode === 'job',
245
+ jobId: options.jobId,
246
+ };
247
+ };
248
+
249
+ export const replaceRelativeUrls = (html, replacer) => {
250
+ const urlAttributes = [
251
+ 'src', 'href', 'action', 'srcset', 'poster', 'data', 'formaction'
252
+ ];
253
+
254
+ const regex = new RegExp(
255
+ `\\b(${urlAttributes.join('|')}|data-[a-zA-Z0-9_-]+)\\s*=\\s*(['"])(?!https?:|//|mailto:|tel:|#)([^'"]+)\\2`,
256
+ 'gi'
257
+ );
258
+
259
+ return html.replace(regex, (_match, attr, quote, url) => {
260
+ const newUrl = replacer(url);
261
+ return `${attr}=${quote}${newUrl}${quote}`;
262
+ });
263
+ };
264
+
265
+ export const replaceRelativeUrlsInCss = (css, replacer) => {
266
+ const regex = /url\(\s*(['"]?)(.+?)\1\s*\)/gi;
267
+
268
+ return css.replace(regex, (match, quote, url) => {
269
+ if (/^(https?:|\/\/|data:|mailto:|tel:|#)/.test(url.trim())) {
270
+ return match;
271
+ }
272
+
273
+ const newUrl = replacer(url);
274
+ return `url(${quote}${newUrl}${quote})`;
275
+ });
276
+ };
277
+
278
+ export const replaceRelativeUrlsInHtml = (html, baseUrl) => {
279
+ return replaceRelativeUrls(html, (url) => {
280
+ return new URL(url, baseUrl).href;
281
+ });
282
+ };
283
+
284
+ export const replaceRelativeUrlsInCssWithBase = (css, cssFileUrl) => {
285
+ return replaceRelativeUrlsInCss(css, (url) => {
286
+ return new URL(url, cssFileUrl).href;
287
+ });
288
+ };
289
+
290
+ export const unwrapBody = (code) => {
291
+ try {
292
+ return code.replace(/<\/?(html|body)[^>]*>/gi, '');
293
+ } catch (_error) {
294
+ return code;
295
+ }
296
+ };
297
+
298
+ export const transformBlockFile = (blockCode) => {
299
+ let transformedCode = '';
300
+
301
+ try {
302
+ transformedCode = babel.transformSync(blockCode, {
303
+ presets: [[presetReact, { pragma: 'wp.element.createElement' }]],
304
+ filename: 'block.js'
305
+ });
306
+ } catch (error) {
307
+ console.log(error);
308
+ }
309
+
310
+ return transformedCode;
311
+ };
312
+
313
+ export const getSnapApiUrl = () => {
314
+ return process.env.SNAPAPI_URL || 'https://api.snapapi.pics/v1/screenshot';
315
+ };
316
+
317
+ const uploadJobPackage = async ({ jobId, generatedFiles, assetFiles, previewArtifact }) => {
318
+ const allFiles = [];
319
+ const zipEntries = [];
320
+ let sourceIndex = 1;
321
+ let assetIndex = 1;
322
+
323
+ for (const [name, contents] of Object.entries(generatedFiles)) {
324
+ const body = typeof contents === 'string' ? Buffer.from(contents, 'utf8') : Buffer.from(contents || '');
325
+ const storageKey = `generated/${jobId}/${name}`;
326
+ const uploadResult = await uploadBufferToR2({
327
+ storageKey,
328
+ body,
329
+ contentType: inferContentType(name),
330
+ });
331
+
332
+ allFiles.push(
333
+ createFileRecord({
334
+ id: `file_${sourceIndex++}`,
335
+ name,
336
+ kind: 'source',
337
+ storageKey: uploadResult.storageKey,
338
+ size: uploadResult.size,
339
+ type: uploadResult.type,
340
+ url: uploadResult.url,
341
+ })
342
+ );
343
+
344
+ zipEntries.push({ name, body });
345
+ }
346
+
347
+ for (const assetFile of assetFiles) {
348
+ allFiles.push({
349
+ id: `file_${sourceIndex + assetIndex - 1}`,
350
+ name: assetFile.name,
351
+ type: assetFile.type,
352
+ size: assetFile.size,
353
+ path: assetFile.path,
354
+ url: assetFile.url,
355
+ kind: assetFile.kind || 'asset',
356
+ });
357
+
358
+ if (assetFile.buffer) {
359
+ zipEntries.push({
360
+ name: path.posix.relative(`generated/${jobId}`, assetFile.path.replace(/^\//, '')),
361
+ body: assetFile.buffer,
362
+ });
363
+ }
364
+
365
+ assetIndex++;
366
+ }
367
+
368
+ if (previewArtifact) {
369
+ allFiles.push(previewArtifact.file);
370
+ zipEntries.push({
371
+ name: previewArtifact.file.name,
372
+ body: previewArtifact.buffer,
373
+ });
374
+ }
375
+
376
+ const zipBuffer = await zipEntriesToBuffer(zipEntries);
377
+ const bundleUpload = await uploadBufferToR2({
378
+ storageKey: `generated/${jobId}/output.zip`,
379
+ body: zipBuffer,
380
+ contentType: 'application/zip',
381
+ });
382
+ const bundleFile = createFileRecord({
383
+ id: 'file_bundle',
384
+ name: 'output.zip',
385
+ kind: 'bundle',
386
+ storageKey: bundleUpload.storageKey,
387
+ size: bundleUpload.size,
388
+ type: bundleUpload.type,
389
+ url: bundleUpload.url,
390
+ });
391
+
392
+ return {
393
+ jobId,
394
+ status: 'completed',
395
+ output: {
396
+ files: allFiles,
397
+ bundle: {
398
+ id: bundleFile.id,
399
+ name: bundleFile.name,
400
+ type: bundleFile.type,
401
+ size: bundleFile.size,
402
+ path: bundleFile.path,
403
+ url: bundleFile.url,
404
+ zipUrl: bundleUpload.url,
405
+ },
406
+ },
407
+ };
408
+ };
409
+
21
410
  const block = async (
22
411
  htmlContent,
23
- options = {
24
- name: 'My block',
25
- prefix: 'wp',
26
- category: 'common',
27
- basePath: process.cwd(),
28
- shouldSaveFiles: true,
29
- generateIconPreview: false,
30
- jsFiles: [],
31
- cssFiles: [],
32
- source: null,
33
- }
412
+ rawOptions = {}
34
413
  ) => {
414
+ const options = normalizeBlockOptions(rawOptions);
35
415
  const panels = [];
36
416
  const styles = [];
37
417
  const scripts = [];
38
418
  const attributes = {};
39
419
  const formVars = {};
420
+ const extractedAssets = [];
421
+ images.length = 0;
40
422
 
41
- const { name, prefix, source } = options;
423
+ const { name, prefix, source, slug, registerCategoryIfMissing } = options;
424
+ const outputMode = options.outputMode;
425
+ const useR2Storage = options.uploadToR2;
426
+ const jobId = options.jobId || createJobId();
42
427
 
43
428
  let js = '';
44
429
  let css = '';
45
430
  let phpEmailData = '';
46
431
  let emailTemplate = '';
432
+ let previewArtifact = null;
433
+ const profiler = createProfiler(process.env.HTG_PROFILE === '1');
47
434
 
48
435
  function hasTailwindCdnSource(jsFiles) {
49
436
  const tailwindCdnRegex = /https:\/\/(cdn\.tailwindcss\.com(\?[^"'\s]*)?|cdn\.jsdelivr\.net\/npm\/@tailwindcss\/browser@4(\.\d+){0,2})/;
@@ -77,9 +464,7 @@ const block = async (
77
464
  .replace(/^[^a-z]+/, '');
78
465
  }
79
466
 
80
- const replaceUnderscoresSpacesAndUppercaseLetters = (name = '') => {
81
- return name.replace(new RegExp(/\W|_/, 'g'), '-').toLowerCase();
82
- };
467
+ const replaceUnderscoresSpacesAndUppercaseLetters = (name = '') => slugifyBlockValue(name);
83
468
 
84
469
  const saveFile = (fileName, contents, options) => {
85
470
  try {
@@ -93,50 +478,6 @@ const block = async (
93
478
  }
94
479
  };
95
480
 
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
481
  const parseRequirements = async (files, options) => {
141
482
  const { source } = options;
142
483
  let output = '';
@@ -172,9 +513,10 @@ const block = async (
172
513
  return '';
173
514
  };
174
515
 
175
- const newName = replaceUnderscoresSpacesAndUppercaseLetters(name);
176
- const blockName = `${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(prefix))}/${sanitizeAndReplaceLeadingNumbers(replaceUnderscoresSpacesAndUppercaseLetters(name))}`;
177
- const blockNameHandle = `${prefix}-${newName}`;
516
+ const newName = slug;
517
+ const normalizedNamespace = replaceUnderscoresSpacesAndUppercaseLetters(prefix);
518
+ const blockName = `${sanitizeAndReplaceLeadingNumbers(normalizedNamespace)}/${sanitizeAndReplaceLeadingNumbers(newName)}`;
519
+ const blockNameHandle = `${normalizedNamespace}-${newName}`;
178
520
 
179
521
  const getPhp = (options) => {
180
522
  const { name, prefix, jsFiles, cssFiles } = options;
@@ -385,40 +727,6 @@ const block = async (
385
727
  `;
386
728
  };
387
729
 
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
- }
397
-
398
- function unwrapBody(code) {
399
- try {
400
- return code.replace(/<\/?(html|body)[^>]*>/gi, '');
401
- } catch (e) {
402
- return code;
403
- }
404
- }
405
-
406
- function transformBlockFile(blockCode) {
407
- let test = '';
408
-
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);
416
-
417
- }
418
-
419
- return test;
420
- }
421
-
422
730
  const saveFiles = async (options) => {
423
731
  const { cssFiles = [], jsFiles = [], shouldSaveFiles, name, prefix } = options;
424
732
  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;
@@ -430,10 +738,7 @@ const block = async (
430
738
  all: revert-layer;
431
739
  }\n`;
432
740
 
433
- css += await parseRequirements(cssFiles, options);
434
-
435
- console.log('[CSS BEFORE]', css);
436
-
741
+ css += await parseRequirements(cssFiles, options);
437
742
 
438
743
  for (const style of styles) {
439
744
  css += style.content;
@@ -450,47 +755,42 @@ const block = async (
450
755
  .replace(/<script[\s\S]*?<\/script>/gi, '')
451
756
  .replace(/<style[\s\S]*?<\/style>/gi, '');
452
757
 
453
-
758
+ profiler.start('getBlock');
454
759
  let blockCode = await getBlock(options);
455
-
760
+ profiler.end('getBlock');
761
+
762
+ // Ensure all <img> tags are self-closing for valid JSX
763
+ blockCode = blockCode.replace(/<img([^>/]*?)>/g, '<img$1 />');
456
764
  blockCode = blockCode.replaceAll(' / dangerouslySetInnerHTML', ' dangerouslySetInnerHTML')
457
765
 
458
766
  const indexFile = getPhp(options);
459
767
  let blockFile = '';
460
768
 
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);
472
-
769
+ profiler.start('transformBlockFile');
770
+ blockFile = transformBlockFile(blockCode).code
771
+ ?.replace(/name: \"\{field.name\}\"/g, 'name: field.name')
772
+ ?.replace(/key: \"\{index\}\"/g, 'key: index');
773
+ profiler.end('transformBlockFile');
473
774
 
474
- if (shouldSaveFiles) {
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
- }
775
+ const finalScriptsFile = `${scriptFile}\n\n${emailTemplate}`;
776
+ const remoteLoaderFile = '';
777
+
778
+ if (shouldSaveFiles && outputMode === 'legacy') {
779
+ saveFile('style.css', scopedCssFrontend, options);
780
+ saveFile('editor.css', editorStyleFile, options);
781
+ saveFile('scripts.js', finalScriptsFile, options);
782
+ saveFile('index.php', indexFile, options);
783
+ saveFile('block.js', blockFile, options);
784
+ saveFile('remote-loader.js', remoteLoaderFile, options);
486
785
  }
487
786
 
488
787
  return {
489
788
  'style.css': scopedCssFrontend,
490
789
  'editor.css': editorStyleFile,
491
- 'scripts.js': scriptFile,
790
+ 'scripts.js': finalScriptsFile,
492
791
  'index.php': indexFile,
493
792
  'block.js': blockFile,
793
+ 'remote-loader.js': remoteLoaderFile,
494
794
  }
495
795
 
496
796
  };
@@ -576,11 +876,20 @@ const block = async (
576
876
  const loadHtml = async (options) => {
577
877
  const { basePath, htmlContent } = options;
578
878
  if (htmlContent) {
579
- const newHtml = await extractAssets(htmlContent, {
580
- basePath,
581
- saveFile: true,
582
- verbose: false,
583
- });
879
+ const extracted = await extractAssets(
880
+ htmlContent,
881
+ buildAssetExtractionOptions(basePath, {
882
+ uploadToR2: useR2Storage,
883
+ returnDetails: useR2Storage,
884
+ jobId,
885
+ r2Prefix: `generated/${jobId}/assets`,
886
+ })
887
+ );
888
+ const newHtml = typeof extracted === 'string' ? extracted : extracted.html;
889
+
890
+ if (typeof extracted !== 'string' && Array.isArray(extracted.assets)) {
891
+ extractedAssets.push(...extracted.assets);
892
+ }
584
893
 
585
894
  return cheerio.load(newHtml, {
586
895
  xmlMode: true,
@@ -644,7 +953,7 @@ const block = async (
644
953
  };
645
954
  const setImageAttribute = (properties) => {
646
955
  const { imgTag, imgSrc, imgAlt, attribute, type, prefix } = properties;
647
- const newPrefix = prefix ? replaceUnderscoresSpacesAndUppercaseLetters(prefix) : 'wp';
956
+ const newPrefix = replaceUnderscoresSpacesAndUppercaseLetters(prefix);
648
957
  const randomVariable = generateRandomVariableName(`${type}${newPrefix}`);
649
958
 
650
959
  let imgSrcWithoutOrigin = imgSrc;
@@ -890,10 +1199,12 @@ const block = async (
890
1199
 
891
1200
  content = `<div className="custom-block">${content}</div>`;
892
1201
 
893
- return await processEditImages({
1202
+ const processedImages = await processEditImages({
894
1203
  ...options,
895
1204
  htmlContent: parseContent(content),
896
1205
  });
1206
+
1207
+ return replaceSVGImages(processedImages);
897
1208
  };
898
1209
 
899
1210
  const createPanel = (values) => {
@@ -906,49 +1217,42 @@ const block = async (
906
1217
  return `<svg ${group1} dangerouslySetInnerHTML={ { __html: attributes.${randomSVGVariable} }}></svg>`;
907
1218
  };
908
1219
  const replaceSVGImages = async (html) => {
909
- const regex = /<\s*svg\b((?:[^>'"]|"[^"]*"|'[^']*')*)>(\s*(?:[^<]|<(?!\/svg\s*>))*)(<\/\s*svg\s*>)/gim;
910
-
1220
+ // Improved SVG handling: preserve nesting and avoid splitting SVG from parent
1221
+ const svgRegex = /(<svg[\s\S]*?<\/svg>)/gi;
911
1222
  let result = '';
912
1223
  let lastIndex = 0;
913
- const matches = [...html.matchAll(regex)];
914
-
915
- for (const match of matches) {
916
- const [fullMatch, group1, group2, group3] = match;
1224
+ let match;
1225
+
1226
+ while ((match = svgRegex.exec(html)) !== null) {
1227
+ const [svgBlock] = match;
917
1228
  const start = match.index;
918
- const end = start + fullMatch.length;
919
-
920
- result += html.slice(lastIndex, start);
921
-
922
- const content = group2.trim();
923
- if (content) {
1229
+ const end = start + svgBlock.length;
1230
+
1231
+ // Add preceding HTML
1232
+ result += html.slice(lastIndex, start);
1233
+
1234
+ // Extract attributes and inner content
1235
+ const attrMatch = svgBlock.match(/^<svg([^>]*)>([\s\S]*?)<\/svg>$/i);
1236
+ if (attrMatch) {
1237
+ const group1 = attrMatch[1];
1238
+ const group2 = attrMatch[2];
924
1239
  const randomSVGVariable = generateRandomVariableName('svg');
925
- setRandomAttributeContent(randomSVGVariable, content.replaceAll('className', 'class'));
1240
+ setRandomAttributeContent(randomSVGVariable, group2.replaceAll('className', 'class'));
926
1241
  createPanel({
927
1242
  type: 'svg',
928
1243
  title: 'SVG Markup',
929
1244
  attributes: [randomSVGVariable],
930
1245
  });
931
-
932
-
933
- const replacement = getSvgTemplate(fullMatch, group1, group3, randomSVGVariable)
934
-
935
- result += replacement;
936
- } else {
937
- result += fullMatch;
1246
+ // Replace with JSX template, preserving parent context
1247
+ result += getSvgTemplate(svgBlock, group1, '</svg>', randomSVGVariable);
938
1248
  }
939
-
940
1249
  lastIndex = end;
941
1250
  }
942
-
943
1251
  result += html.slice(lastIndex);
944
-
945
- console.log(result);
946
-
947
1252
  return result;
948
1253
  };
949
1254
  const getSvgPanelTemplate = (panel) => {
950
- return panel.attributes && attributes[panel.attributes]
951
- ? `
1255
+ return `
952
1256
  { (
953
1257
  <PanelBody title="${panel.title}">
954
1258
  <PanelRow>
@@ -966,7 +1270,6 @@ const block = async (
966
1270
  </PanelBody>
967
1271
  )}
968
1272
  `
969
- : '';
970
1273
  };
971
1274
 
972
1275
  const getMediaPanelTemplate = (panel) => {
@@ -976,10 +1279,7 @@ const block = async (
976
1279
  ${panel.attributes[1]}: media.alt`
977
1280
  : '';
978
1281
 
979
- return panel.attributes &&
980
- panel.attributes[0] &&
981
- attributes[panel.attributes[0]]
982
- ? `
1282
+ return `
983
1283
  <PanelBody title="${panel.title}">
984
1284
  <PanelRow>
985
1285
  <div>
@@ -998,8 +1298,7 @@ const block = async (
998
1298
  </div>
999
1299
  </PanelRow>
1000
1300
  </PanelBody>
1001
- `
1002
- : '';
1301
+ `;
1003
1302
  };
1004
1303
 
1005
1304
  const getFormSettingsPanelTemplate = (panel) => {
@@ -1092,7 +1391,7 @@ const block = async (
1092
1391
  formIdAttr,
1093
1392
  ] = panel.attributes;
1094
1393
 
1095
- emailTemplate += sendEmailAttr ? getEmailSaveTemplate(formIdAttr) : '';
1394
+ emailTemplate += getEmailSaveTemplate(formIdAttr);
1096
1395
  phpEmailData += getPhpEmailData(formIdAttr, fromAttr, toAttr, subjectAttr, messageAttr);
1097
1396
 
1098
1397
  return `
@@ -1289,17 +1588,10 @@ const block = async (
1289
1588
  };
1290
1589
 
1291
1590
  const buildSaveContent = (editContent) => {
1292
- return editContent.replace(
1293
- /<RichText((.|\n)*?)value=\{(.*?)\}((.|\n)*?)\/>/gi,
1294
- '<RichText.Content value={$3} />'
1295
- )
1296
- .replaceAll('class=', 'className=')
1297
- .replace(
1298
- /<MediaUpload\b[^>]*>([\s\S]*?(<img\b[^>]*>*\/>)[\s\S]*?)\/>/g,
1299
- (_match, _attributes, img) => {
1300
- return img.replace(/onClick={[^}]+}\s*/, '');
1301
- }
1302
- );
1591
+ let output = replaceRichTextComponents(editContent);
1592
+ output = output.replaceAll('class=', 'className=');
1593
+ output = replaceMediaUploadComponents(output, images);
1594
+ return output;
1303
1595
  };
1304
1596
 
1305
1597
  const removeHref = (match) => {
@@ -1322,45 +1614,16 @@ const block = async (
1322
1614
  )
1323
1615
  : undefined;
1324
1616
 
1325
- htmlContent = unwrapAnchor(htmlContent);
1326
-
1327
1617
  return {
1328
1618
  ...options,
1329
1619
  htmlContent,
1330
1620
  };
1331
1621
  };
1332
1622
 
1333
- const unwrapAnchor = (htmlContent) => {
1334
- return htmlContent.replace(
1335
- /<span([^>]*)>\s*<a([^>]*)>(.*?)<\/a>\s*<\/span>/gi,
1336
- (_, spanAttrs, anchorAttrs, content) => {
1337
- const allAttrs = {};
1338
- const attrRegex = /(\S+)=["'](.*?)["']/g;
1339
-
1340
- let match;
1341
- while ((match = attrRegex.exec(spanAttrs)) !== null) {
1342
- allAttrs[match[1]] = match[2];
1343
- }
1344
-
1345
- while ((match = attrRegex.exec(anchorAttrs)) !== null) {
1346
- allAttrs[match[1]] = match[2];
1347
- }
1348
-
1349
- return `<a ${Object.entries(allAttrs)
1350
- .map(([key, value]) => `${key}="${value}"`)
1351
- .join(' ')}>${content}</a>`;
1352
- }
1353
- );
1354
- };
1355
-
1356
1623
  const getComponentAttributes = () => {
1357
1624
  return Object.entries(attributes)
1358
1625
  .map(([key, value]) => {
1359
- if (typeof value === 'object' && value !== null) {
1360
- return `${key}: { ${Object.entries(value).map(([k, v]) => `${k}: \`${v}\``).join(', ')} }`;
1361
- } else {
1362
- return `${key}:\`${value}\``;
1363
- }
1626
+ return `${key}: { ${Object.entries(value).map(([k, v]) => `${k}: \`${v}\``).join(', ')} }`;
1364
1627
  })
1365
1628
  .join(',\n');
1366
1629
  };
@@ -1369,11 +1632,14 @@ const block = async (
1369
1632
  let { htmlContent } = options;
1370
1633
 
1371
1634
  if (htmlContent) {
1635
+ profiler.start('getEdit');
1372
1636
  options.htmlContent = unwrapBody(htmlContent);
1373
1637
  const postProcessLinks = processLinks(options);
1374
1638
  const postGetEditJsx = await getEditJsxContent(postProcessLinks);
1375
1639
  const preConvert = await postGetEditJsx.replace(/<\/br>/g, '<br/>').replace(/<\/hr>/g, '<hr/>')
1376
- return convert(preConvert)
1640
+ const converted = convert(preConvert);
1641
+ profiler.end('getEdit');
1642
+ return converted;
1377
1643
  }
1378
1644
 
1379
1645
  return '';
@@ -1392,13 +1658,54 @@ const block = async (
1392
1658
  } = settings;
1393
1659
 
1394
1660
  const iconPreview = generateIconPreview ? `(<img src={vars.url + 'preview.jpeg'} />)` : "'shield'";
1661
+ const categoryRegistration = registerCategoryIfMissing
1662
+ ? `
1663
+ (function ensureBlockCategory() {
1664
+ if (
1665
+ typeof wp === 'undefined' ||
1666
+ !wp.blocks ||
1667
+ !wp.blocks.setCategories ||
1668
+ !wp.blocks.store ||
1669
+ !wp.data ||
1670
+ !wp.data.select
1671
+ ) {
1672
+ return;
1673
+ }
1674
+
1675
+ const categorySelector = wp.data.select(wp.blocks.store);
1676
+ const existingCategories =
1677
+ categorySelector && typeof categorySelector.getCategories === 'function'
1678
+ ? categorySelector.getCategories()
1679
+ : [];
1680
+
1681
+ if (existingCategories.some((item) => item && item.slug === ${JSON.stringify(category)})) {
1682
+ return;
1683
+ }
1684
+
1685
+ wp.blocks.setCategories([
1686
+ ...existingCategories,
1687
+ {
1688
+ slug: ${JSON.stringify(category)},
1689
+ title: ${JSON.stringify(formatCategoryLabel(category) || category)},
1690
+ },
1691
+ ]);
1692
+ })();
1693
+ `
1694
+ : '';
1695
+ profiler.start('getBlock:getEdit');
1395
1696
  const edit = await getEdit(settings);
1697
+ profiler.end('getBlock:getEdit');
1698
+ profiler.start('getBlock:buildSaveContent');
1396
1699
  const save = buildSaveContent(edit);
1700
+ profiler.end('getBlock:buildSaveContent');
1701
+ profiler.start('getBlock:createPanels');
1397
1702
  const blockPanels = createPanels();
1703
+ profiler.end('getBlock:createPanels');
1398
1704
 
1399
1705
  const output = `
1400
1706
  (function () {
1401
1707
  ${imports}
1708
+ ${categoryRegistration}
1402
1709
 
1403
1710
  registerBlockType('${blockName}', {
1404
1711
  title: '${name}',
@@ -1434,7 +1741,7 @@ const block = async (
1434
1741
  const setupVariables = async (htmlContent, options) => {
1435
1742
 
1436
1743
  const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
1437
- const linkRegex = /<link\b[^>]*((\brel=["']stylesheet["'])|\bhref=["'][^"']+\.css["'])[^>]*>/gi;
1744
+ const linkRegex = /<link\b[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+\.css(?:\?[^"']*)?)["'][^>]*>/gi;
1438
1745
 
1439
1746
  let match;
1440
1747
 
@@ -1447,7 +1754,13 @@ const block = async (
1447
1754
  while ((match = linkRegex.exec(htmlContent)) !== null) {
1448
1755
  const url = match[1];
1449
1756
  const fetchCssPromise = fetch(url)
1450
- .then((response) => response.text())
1757
+ .then((response) => {
1758
+ if (!response.ok) {
1759
+ throw new Error(`HTTP error! Status: ${response.status}`);
1760
+ }
1761
+
1762
+ return response.text();
1763
+ })
1451
1764
  .then((css) => styles.push({ type: 'external', content: css }))
1452
1765
  .catch(() => console.warn(`Failed to fetch: ${url}`));
1453
1766
  fetchCssPromises.push(fetchCssPromise);
@@ -1461,30 +1774,24 @@ const block = async (
1461
1774
  return `${style.content}`;
1462
1775
  }).join('\n');
1463
1776
 
1464
-
1465
- console.log('[CSSFETCHED]', css);
1466
-
1467
-
1468
-
1469
- const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1777
+ const scriptRegex = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
1470
1778
  const scriptSrcRegex =
1471
1779
  /<script\s+[^>]*src=["']([^"']+)["'][^>]*>\s*<\/script>/gi;
1472
1780
 
1473
1781
  let jsMatch;
1474
1782
 
1475
- htmlContent = htmlContent.replace(scriptRegex, (_fullMatch, jsContent) => {
1476
- if (jsContent.trim()) {
1477
- scripts.push({ type: 'inline', content: jsContent });
1478
- }
1479
- return '';
1480
- });
1481
-
1482
1783
  const fetchJsPromises = [];
1483
1784
 
1484
1785
  while ((jsMatch = scriptSrcRegex.exec(htmlContent)) !== null) {
1485
1786
  const url = jsMatch[1];
1486
1787
  const fetchJsPromise = fetch(url)
1487
- .then((response) => response.text())
1788
+ .then((response) => {
1789
+ if (!response.ok) {
1790
+ throw new Error(`HTTP error! Status: ${response.status}`);
1791
+ }
1792
+
1793
+ return response.text();
1794
+ })
1488
1795
  .then((js) => scripts.push({ type: 'external', content: js }))
1489
1796
  .catch(() => console.warn(`Failed to fetch script: ${url}`));
1490
1797
  fetchJsPromises.push(fetchJsPromise);
@@ -1492,6 +1799,13 @@ const block = async (
1492
1799
 
1493
1800
  htmlContent = htmlContent.replace(scriptSrcRegex, '');
1494
1801
 
1802
+ htmlContent = htmlContent.replace(scriptRegex, (_fullMatch, jsContent) => {
1803
+ if (jsContent.trim()) {
1804
+ scripts.push({ type: 'inline', content: jsContent });
1805
+ }
1806
+ return '';
1807
+ });
1808
+
1495
1809
  await Promise.all(fetchJsPromises);
1496
1810
 
1497
1811
  js += scripts.map((script) => script.content).join('\n');
@@ -1501,9 +1815,10 @@ const block = async (
1501
1815
  cssFiles = [],
1502
1816
  jsFiles = [],
1503
1817
  name = 'My block',
1818
+ slug = replaceUnderscoresSpacesAndUppercaseLetters(name),
1504
1819
  } = options;
1505
1820
 
1506
- const newDir = path.join(basePath, replaceUnderscoresSpacesAndUppercaseLetters(name));
1821
+ const newDir = path.join(basePath, slug);
1507
1822
 
1508
1823
  const $ = cheerio.load(htmlContent, {
1509
1824
  xmlMode: true,
@@ -1514,22 +1829,17 @@ const block = async (
1514
1829
 
1515
1830
  htmlContent = $('body').html();
1516
1831
 
1517
- options.html
1518
-
1519
-
1520
- try {
1832
+ if (outputMode === 'legacy' && options.shouldSaveFiles) {
1521
1833
  fs.mkdirSync(newDir, { recursive: true });
1522
-
1523
- return {
1524
- ...options,
1525
- jsFiles,
1526
- cssFiles,
1527
- htmlContent,
1528
- basePath: newDir,
1529
- };
1530
- } catch (error) {
1531
- logError(error);
1532
1834
  }
1835
+
1836
+ return {
1837
+ ...options,
1838
+ jsFiles,
1839
+ cssFiles,
1840
+ htmlContent,
1841
+ basePath: newDir,
1842
+ };
1533
1843
  };
1534
1844
 
1535
1845
  if (source) {
@@ -1537,13 +1847,82 @@ const block = async (
1537
1847
  htmlContent = replaceRelativeUrlsInCssWithBase(htmlContent, source);
1538
1848
  }
1539
1849
 
1540
- try {
1541
- icon(htmlContent, { basePath: path.join(options.basePath, replaceUnderscoresSpacesAndUppercaseLetters(options.name)) });
1542
- } catch (error) {
1543
- console.log(`There was an error generating preview. ${error.message}`);
1850
+
1851
+
1852
+ // Screenshot generation using SnapAPI
1853
+ dotenv.config({ quiet: true });
1854
+ if (options.generateIconPreview && options.source) {
1855
+ try {
1856
+ const snapApiKey = process.env.SNAPAPI_KEY;
1857
+ if (!snapApiKey) {
1858
+ throw new Error('SNAPAPI_KEY is not set in environment variables.');
1859
+ }
1860
+ const snapApiUrl = getSnapApiUrl();
1861
+ const snapApiBody = {
1862
+ url: options.source,
1863
+ fullPage: true,
1864
+ delay: 4000,
1865
+ blockAds: true,
1866
+ blockCookieBanners: true
1867
+ };
1868
+ const response = await fetch(snapApiUrl, {
1869
+ method: 'POST',
1870
+ headers: {
1871
+ 'X-Api-Key': snapApiKey,
1872
+ 'Content-Type': 'application/json'
1873
+ },
1874
+ body: JSON.stringify(snapApiBody)
1875
+ });
1876
+ if (!response.ok) {
1877
+ throw new Error(`SnapAPI error: ${response.status}`);
1878
+ }
1879
+ const buffer = Buffer.from(await response.arrayBuffer());
1880
+
1881
+ if (outputMode === 'legacy' && options.shouldSaveFiles) {
1882
+ const previewPath = path.join(options.basePath, options.slug, 'preview.jpeg');
1883
+ fs.writeFileSync(previewPath, buffer);
1884
+ } else if (useR2Storage) {
1885
+ const uploadResult = await uploadBufferToR2({
1886
+ storageKey: `generated/${jobId}/preview.jpeg`,
1887
+ body: buffer,
1888
+ contentType: 'image/jpeg',
1889
+ });
1890
+
1891
+ previewArtifact = {
1892
+ buffer,
1893
+ file: createFileRecord({
1894
+ id: `file_preview`,
1895
+ name: 'preview.jpeg',
1896
+ kind: 'asset',
1897
+ storageKey: uploadResult.storageKey,
1898
+ size: uploadResult.size,
1899
+ type: uploadResult.type,
1900
+ url: uploadResult.url,
1901
+ }),
1902
+ };
1903
+ }
1904
+ } catch (error) {
1905
+ console.log(`There was an error generating preview with SnapAPI. ${error.message}`);
1906
+ }
1907
+ }
1908
+
1909
+ profiler.start('setupVariables');
1910
+ const preparedOptions = await setupVariables(htmlContent, options);
1911
+ profiler.end('setupVariables');
1912
+ profiler.start('saveFiles');
1913
+ const result = await saveFiles(preparedOptions);
1914
+ profiler.end('saveFiles');
1915
+
1916
+ if (outputMode === 'legacy') {
1917
+ return result;
1544
1918
  }
1545
1919
 
1546
- return saveFiles(await setupVariables(htmlContent, options));
1920
+ return uploadJobPackage({
1921
+ jobId,
1922
+ generatedFiles: result,
1923
+ assetFiles: extractedAssets,
1924
+ previewArtifact,
1925
+ });
1547
1926
  };
1548
1927
 
1549
- export default block;
1928
+ export default block;