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.
- package/.env.example +20 -0
- package/.eslintrc.json +35 -0
- package/.github/workflows/build.yml +26 -0
- package/.github/workflows/coverage.yml +26 -0
- package/.github/workflows/sync-npm.yml +154 -0
- package/.nyc_output/1f0406b8-bb70-495d-8f8a-521fdd81b500.json +1 -0
- package/.nyc_output/6390956f-4f8a-4adb-9256-4a1c7e34a52d.json +1 -0
- package/.nyc_output/processinfo/1f0406b8-bb70-495d-8f8a-521fdd81b500.json +1 -0
- package/.nyc_output/processinfo/6390956f-4f8a-4adb-9256-4a1c7e34a52d.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/@types.d.ts +3 -0
- package/coverage/coverage-final.json +4 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +198 -0
- package/coverage-demo.test.ts +8 -0
- package/dist/coverage-demo.test.js +10 -0
- package/dist/coverage-demo.test.js.map +1 -0
- package/dist/globals.js +24 -0
- package/dist/globals.js.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.js +166 -0
- package/dist/index.test.js.map +1 -0
- package/dist/package.json +130 -0
- package/dist/snapapi-screenshot.test.js +44 -0
- package/dist/snapapi-screenshot.test.js.map +1 -0
- package/dist/src/coverage-demo.js +7 -0
- package/dist/src/coverage-demo.js.map +1 -0
- package/dist/src/utils-extra.test.js +137 -0
- package/dist/src/utils-extra.test.js.map +1 -0
- package/dist/src/utils.test.js +65 -0
- package/dist/src/utils.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils.js +61 -0
- package/dist/utils.js.map +1 -0
- package/fetch-page-assets.test.ts +448 -0
- package/index.d.ts +173 -0
- package/index.js +628 -249
- package/index.test.ts +774 -0
- package/index.ts +155 -1530
- package/package.json +87 -15
- package/r2.js +163 -0
- package/readme.md +126 -72
- package/scripts/patch-fetch-page-assets.mjs +13 -0
- package/scripts/sync-from-npm.mjs +115 -0
- package/snapapi-screenshot.test.ts +46 -0
- package/src/coverage-demo.ts +3 -0
- package/src/utils-extra.test.ts +108 -0
- package/src/utils.test.ts +36 -0
- package/temp-block-test.js +19 -0
- package/tsconfig.json +25 -4
- package/utils.ts +56 -0
- package/vendor/fetch-page-assets/LICENSE.MD +21 -0
- package/vendor/fetch-page-assets/README.md +117 -0
- package/vendor/fetch-page-assets/index.js +362 -0
- 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
|
|
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
|
-
|
|
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 =
|
|
176
|
-
const
|
|
177
|
-
const
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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':
|
|
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
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const [
|
|
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 +
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 +=
|
|
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
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[^>]*
|
|
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) =>
|
|
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) =>
|
|
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,
|
|
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.
|
|
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
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
|
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;
|