wp-migrate-core 0.1.0-demo

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.
@@ -0,0 +1,603 @@
1
+ const GUTENBERG_BLOCK_PATTERN = /<!--\s*(\/)?wp:([a-z0-9-]+(?:\/[a-z0-9-]+)?)(?:\s+(\{[\s\S]*?\}))?\s*(\/)?-->/gi;
2
+ const SHORTCODE_PATTERN = /\[(?!\/)([a-z][a-z0-9_-]*)(?:\s[^\]]*)?\]/gi;
3
+ const NATIVE_GUTENBERG_BLOCKS = new Map([
4
+ ["core/paragraph", { kind: "paragraph", conversion: "native" }],
5
+ ["core/heading", { kind: "heading", conversion: "native" }],
6
+ ["core/list", { kind: "list", conversion: "native" }],
7
+ ["core/list-item", { kind: "list", conversion: "native" }],
8
+ ["core/quote", { kind: "quote", conversion: "native" }],
9
+ ["core/code", { kind: "code", conversion: "native" }],
10
+ ["core/preformatted", { kind: "code", conversion: "native" }],
11
+ ["core/image", { kind: "image", conversion: "manual" }],
12
+ ["core/gallery", { kind: "gallery", conversion: "manual" }],
13
+ ["core/columns", { kind: "columns", conversion: "native" }],
14
+ ["core/column", { kind: "column", conversion: "native" }],
15
+ ["core/group", { kind: "group", conversion: "native" }],
16
+ ["core/buttons", { kind: "group", conversion: "native" }],
17
+ ["core/button", { kind: "button", conversion: "native" }],
18
+ ["core/separator", { kind: "separator", conversion: "native" }],
19
+ ["core/spacer", { kind: "spacer", conversion: "native" }],
20
+ ["core/html", { kind: "html", conversion: "legacy-html" }],
21
+ ["core/embed", { kind: "embed", conversion: "legacy-html" }],
22
+ ["core/shortcode", { kind: "shortcode", conversion: "manual" }]
23
+ ]);
24
+ const DYNAMIC_GUTENBERG_BLOCKS = new Set([
25
+ "core/archives",
26
+ "core/calendar",
27
+ "core/categories",
28
+ "core/latest-comments",
29
+ "core/latest-posts",
30
+ "core/loginout",
31
+ "core/navigation",
32
+ "core/post-comments-form",
33
+ "core/post-template",
34
+ "core/query",
35
+ "core/query-no-results",
36
+ "core/query-pagination",
37
+ "core/query-pagination-next",
38
+ "core/query-pagination-numbers",
39
+ "core/query-pagination-previous",
40
+ "core/rss",
41
+ "core/search",
42
+ "core/tag-cloud"
43
+ ]);
44
+ const FORM_GUTENBERG_BLOCKS = new Set([
45
+ "contact-form-7/contact-form-selector",
46
+ "formidable/simple-form",
47
+ "gravityforms/form",
48
+ "jetpack/contact-form",
49
+ "wpforms/form-selector"
50
+ ]);
51
+ const ELEMENTOR_NATIVE_WIDGETS = new Map([
52
+ ["heading", "heading"],
53
+ ["text-editor", "html"],
54
+ ["button", "button"],
55
+ ["divider", "separator"],
56
+ ["spacer", "spacer"]
57
+ ]);
58
+ const ELEMENTOR_FORM_WIDGETS = new Set(["form", "wp-widget-wpforms-widget", "wp-widget-gform_widget"]);
59
+ const ELEMENTOR_QUERY_WIDGETS = new Set([
60
+ "archive-posts",
61
+ "loop-carousel",
62
+ "loop-grid",
63
+ "portfolio",
64
+ "posts",
65
+ "products",
66
+ "woocommerce-products"
67
+ ]);
68
+ const BLOCKING_SHORTCODES = new Set([
69
+ "contact-form-7",
70
+ "elementor-template",
71
+ "gravityform",
72
+ "learndash_course_grid",
73
+ "product",
74
+ "products",
75
+ "tutor_course",
76
+ "woocommerce_cart",
77
+ "woocommerce_checkout",
78
+ "wpforms"
79
+ ]);
80
+ const SAFE_ELEMENTOR_HREF_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
81
+ /**
82
+ * Parse a bounded WXR export into a target-neutral migration model.
83
+ *
84
+ * This deliberately does not claim to be a complete XML or WordPress parser. It
85
+ * is dependency-free so 0.1.0-demo can make the conversion boundary visible.
86
+ */
87
+ export function parseWxr(xml, options = {}) {
88
+ const records = [];
89
+ const projectIssues = [];
90
+ const channelHeader = xml.slice(0, xml.search(/<item\b/i) === -1 ? xml.length : xml.search(/<item\b/i));
91
+ const itemPattern = /<item\b[^>]*>([\s\S]*?)<\/item>/gi;
92
+ let itemMatch;
93
+ while ((itemMatch = itemPattern.exec(xml)) !== null) {
94
+ const itemXml = itemMatch[1] ?? "";
95
+ const postType = cleanField(readTag(itemXml, "wp:post_type"));
96
+ if (postType !== "page" && postType !== "post") {
97
+ continue;
98
+ }
99
+ const status = normalizeStatus(cleanField(readTag(itemXml, "wp:status")));
100
+ if (!options.includeDrafts && status !== "publish") {
101
+ continue;
102
+ }
103
+ const rawId = cleanField(readTag(itemXml, "wp:post_id"));
104
+ const wordpressId = Number.parseInt(rawId, 10);
105
+ if (!Number.isFinite(wordpressId)) {
106
+ projectIssues.push({
107
+ id: `project:WXR_ITEM_MISSING_ID:${projectIssues.length + 1}`,
108
+ severity: "warning",
109
+ code: "WXR_ITEM_MISSING_ID",
110
+ sourceId: "wp:unknown",
111
+ title: "WordPress item is missing its ID",
112
+ message: `Skipped a ${postType} without a numeric wp:post_id.`,
113
+ requiredAction: "Inspect the WXR export and restore the missing post identifier."
114
+ });
115
+ continue;
116
+ }
117
+ records.push(parseItem(itemXml, postType, status, wordpressId));
118
+ }
119
+ if (records.length === 0) {
120
+ projectIssues.push({
121
+ id: "project:WXR_NO_ITEMS:1",
122
+ severity: "warning",
123
+ code: "WXR_NO_ITEMS",
124
+ sourceId: "project",
125
+ title: "No eligible WordPress content found",
126
+ message: "The WXR input contains no posts or pages included by the current scan settings.",
127
+ requiredAction: "Confirm that the export includes published posts or pages. Drafts are excluded by default; library users can pass { includeDrafts: true } when appropriate."
128
+ });
129
+ }
130
+ const recordIssues = records.flatMap((record) => record.issues);
131
+ const issues = [...projectIssues, ...recordIssues];
132
+ const source = compactOptionalObject({
133
+ title: cleanOptionalField(readTag(channelHeader, "title")),
134
+ url: cleanOptionalField(readTag(channelHeader, "link"))
135
+ });
136
+ return {
137
+ site: {
138
+ title: source.title ?? "WordPress migration",
139
+ ...(source.url === undefined ? {} : { url: source.url })
140
+ },
141
+ source,
142
+ records,
143
+ issues,
144
+ summary: summarize(records, issues)
145
+ };
146
+ }
147
+ export const inspectWxr = parseWxr;
148
+ function parseItem(itemXml, type, status, wordpressId) {
149
+ const sourceId = `wp:${type}:${wordpressId}`;
150
+ const route = cleanOptionalField(readTag(itemXml, "link"));
151
+ const collector = createIssueCollector(sourceId, route);
152
+ const rawContent = unwrapXmlValue(readTag(itemXml, "content:encoded"));
153
+ const meta = parsePostMeta(itemXml);
154
+ const elementorData = meta._elementor_data?.[0];
155
+ const hasGutenberg = /<!--\s*wp:/i.test(rawContent);
156
+ const hasElementor = typeof elementorData === "string" && elementorData.trim() !== "";
157
+ const editor = hasElementor ? (hasGutenberg ? "mixed" : "elementor") : hasGutenberg ? "gutenberg" : "classic";
158
+ const nodes = [];
159
+ if (hasGutenberg) {
160
+ nodes.push(...parseGutenberg(rawContent, sourceId, collector));
161
+ }
162
+ else if (rawContent.trim() !== "") {
163
+ nodes.push(createClassicNode(rawContent, `${sourceId}:classic:1`));
164
+ }
165
+ if (hasElementor && elementorData !== undefined) {
166
+ nodes.push(...parseElementor(elementorData, sourceId, collector));
167
+ }
168
+ scanShortcodes(rawContent, sourceId, collector);
169
+ const title = cleanField(readTag(itemXml, "title"));
170
+ const slug = cleanField(readTag(itemXml, "wp:post_name")) || slugify(title) || String(wordpressId);
171
+ return {
172
+ sourceId,
173
+ wordpressId,
174
+ type,
175
+ status,
176
+ title,
177
+ slug,
178
+ ...(route === undefined ? {} : { route }),
179
+ ...optionalProperty("publishedAt", cleanOptionalField(readTag(itemXml, "wp:post_date_gmt")) ?? cleanOptionalField(readTag(itemXml, "wp:post_date"))),
180
+ ...optionalProperty("modifiedAt", cleanOptionalField(readTag(itemXml, "wp:post_modified_gmt")) ?? cleanOptionalField(readTag(itemXml, "wp:post_modified"))),
181
+ ...optionalProperty("author", cleanOptionalField(readTag(itemXml, "dc:creator"))),
182
+ editor,
183
+ rawContent,
184
+ meta,
185
+ terms: parseTerms(itemXml),
186
+ nodes,
187
+ issues: collector.issues
188
+ };
189
+ }
190
+ function parseGutenberg(content, sourceId, collector) {
191
+ const roots = [];
192
+ const stack = [];
193
+ let cursor = 0;
194
+ let ordinal = 0;
195
+ let match;
196
+ GUTENBERG_BLOCK_PATTERN.lastIndex = 0;
197
+ while ((match = GUTENBERG_BLOCK_PATTERN.exec(content)) !== null) {
198
+ if (stack.length === 0) {
199
+ appendLooseHtml(content.slice(cursor, match.index), roots, sourceId, () => ++ordinal);
200
+ }
201
+ const closing = match[1] === "/";
202
+ const blockName = normalizeBlockName(match[2] ?? "unknown");
203
+ const selfClosing = match[4] === "/";
204
+ if (closing) {
205
+ const frame = stack.pop();
206
+ if (frame === undefined || frame.blockName !== blockName) {
207
+ collector.add("warning", "GUTENBERG_UNMATCHED_CLOSE", `Found an unmatched closing marker for ${blockName}.`, "Inspect the original block markup and repair the affected content.", { evidence: match[0].slice(0, 160) });
208
+ if (frame !== undefined) {
209
+ stack.push(frame);
210
+ }
211
+ }
212
+ else {
213
+ const rawHtml = content.slice(frame.contentStart, match.index).trim();
214
+ if (rawHtml !== "") {
215
+ frame.node.rawHtml = rawHtml;
216
+ const text = htmlToText(rawHtml);
217
+ if (text !== "") {
218
+ frame.node.text = text;
219
+ }
220
+ }
221
+ }
222
+ cursor = GUTENBERG_BLOCK_PATTERN.lastIndex;
223
+ continue;
224
+ }
225
+ const nodeId = `${sourceId}:gutenberg:${++ordinal}`;
226
+ const attributes = parseBlockAttributes(match[3], blockName, nodeId, collector);
227
+ const classification = classifyGutenbergBlock(blockName);
228
+ const node = {
229
+ id: nodeId,
230
+ source: "gutenberg",
231
+ sourceType: blockName,
232
+ kind: classification.kind,
233
+ conversion: classification.conversion,
234
+ attributes,
235
+ children: []
236
+ };
237
+ const parent = stack.at(-1)?.node;
238
+ (parent?.children ?? roots).push(node);
239
+ reportGutenbergCompatibility(node, collector);
240
+ if (!selfClosing) {
241
+ stack.push({ blockName, node, contentStart: GUTENBERG_BLOCK_PATTERN.lastIndex });
242
+ }
243
+ cursor = GUTENBERG_BLOCK_PATTERN.lastIndex;
244
+ }
245
+ if (stack.length === 0) {
246
+ appendLooseHtml(content.slice(cursor), roots, sourceId, () => ++ordinal);
247
+ }
248
+ else {
249
+ for (const frame of stack) {
250
+ collector.add("warning", "GUTENBERG_UNCLOSED_BLOCK", `Block ${frame.blockName} is missing its closing marker.`, "Repair the Gutenberg block markup or accept the preserved HTML fallback.", { nodeId: frame.node.id });
251
+ frame.node.conversion = "manual";
252
+ const rawHtml = content.slice(frame.contentStart).trim();
253
+ if (rawHtml !== "") {
254
+ frame.node.rawHtml = rawHtml;
255
+ }
256
+ }
257
+ }
258
+ return roots;
259
+ }
260
+ function parseElementor(serialized, sourceId, collector) {
261
+ let value;
262
+ try {
263
+ value = JSON.parse(decodeXmlEntities(serialized));
264
+ if (typeof value === "string") {
265
+ value = JSON.parse(value);
266
+ }
267
+ }
268
+ catch (error) {
269
+ collector.add("blocker", "ELEMENTOR_INVALID_DATA", "Elementor data exists but is not valid JSON.", "Re-export the page from a working WordPress installation.", { evidence: error instanceof Error ? error.message : String(error) });
270
+ return [];
271
+ }
272
+ if (!Array.isArray(value)) {
273
+ collector.add("blocker", "ELEMENTOR_INVALID_DATA", "Elementor data does not contain the expected top-level element array.", "Extract _elementor_data from a working WordPress installation.");
274
+ return [];
275
+ }
276
+ let ordinal = 0;
277
+ return value.flatMap((element) => convertElementorElement(element, sourceId, collector, () => ++ordinal));
278
+ }
279
+ function convertElementorElement(value, sourceId, collector, nextOrdinal) {
280
+ if (!isUnknownRecord(value)) {
281
+ return [];
282
+ }
283
+ const nodeId = `${sourceId}:elementor:${nextOrdinal()}`;
284
+ const elementType = typeof value.elType === "string" ? value.elType : "unknown";
285
+ const widgetType = typeof value.widgetType === "string" ? value.widgetType : undefined;
286
+ const settings = isUnknownRecord(value.settings) ? value.settings : {};
287
+ const childValues = Array.isArray(value.elements) ? value.elements : [];
288
+ const children = childValues.flatMap((child) => convertElementorElement(child, sourceId, collector, nextOrdinal));
289
+ if (elementType === "section" || elementType === "container") {
290
+ return [createMutableNode(nodeId, "elementor", elementType, "section", "native", settings, children)];
291
+ }
292
+ if (elementType === "column") {
293
+ return [createMutableNode(nodeId, "elementor", elementType, "column", "native", settings, children)];
294
+ }
295
+ if (elementType !== "widget" || widgetType === undefined) {
296
+ const node = createMutableNode(nodeId, "elementor", elementType, "unknown", "manual", settings, children);
297
+ collector.add("warning", "ELEMENTOR_WIDGET_UNKNOWN", `Unknown Elementor element type ${elementType}.`, "Replace it with an Astro component or preserve its rendered HTML.", { nodeId, evidence: elementType });
298
+ return [node];
299
+ }
300
+ if (ELEMENTOR_FORM_WIDGETS.has(widgetType)) {
301
+ const node = createMutableNode(nodeId, "elementor", widgetType, "form", "blocked", settings, children);
302
+ collector.add("blocker", "ELEMENTOR_FORM_UNSUPPORTED", `Elementor widget ${widgetType} submits data and cannot be migrated as static content.`, "Choose a form backend and rebuild this form explicitly.", { nodeId, evidence: widgetType });
303
+ return [node];
304
+ }
305
+ if (ELEMENTOR_QUERY_WIDGETS.has(widgetType)) {
306
+ const node = createMutableNode(nodeId, "elementor", widgetType, "query", "blocked", settings, children);
307
+ collector.add("blocker", "ELEMENTOR_QUERY_UNSUPPORTED", `Elementor widget ${widgetType} depends on a WordPress query.`, "Map the query to an Astro content collection and verify its filtering and ordering.", { nodeId, evidence: widgetType });
308
+ return [node];
309
+ }
310
+ if (widgetType === "image") {
311
+ const node = createMutableNode(nodeId, "elementor", widgetType, "image", "manual", settings, children);
312
+ collector.add("warning", "ELEMENTOR_IMAGE_REMOTE_MEDIA", "Elementor image widgets are withheld until their media is added locally.", "Download or import the image into local Astro assets, verify it, and rebuild this widget.", { nodeId });
313
+ return [node];
314
+ }
315
+ if (widgetType === "button") {
316
+ const href = getNestedString(settings, "link", "url");
317
+ if (href !== undefined && !isSafeElementorHref(href)) {
318
+ const node = createMutableNode(nodeId, "elementor", widgetType, "button", "manual", settings, children);
319
+ collector.add("warning", "ELEMENTOR_BUTTON_UNSAFE_URL", "Elementor button has an unsafe link and was withheld.", "Replace the link with an http, https, mailto, tel, or relative URL before publishing.", { nodeId });
320
+ return [node];
321
+ }
322
+ }
323
+ const kind = ELEMENTOR_NATIVE_WIDGETS.get(widgetType);
324
+ if (kind !== undefined) {
325
+ const node = createMutableNode(nodeId, "elementor", widgetType, kind, kind === "html" ? "legacy-html" : "native", settings, children);
326
+ const text = elementorWidgetText(widgetType, settings);
327
+ if (text !== undefined) {
328
+ node.text = text;
329
+ }
330
+ return [node];
331
+ }
332
+ const node = createMutableNode(nodeId, "elementor", widgetType, "unknown", "manual", settings, children);
333
+ collector.add("warning", "ELEMENTOR_WIDGET_UNKNOWN", `Elementor widget ${widgetType} has no 0.1.0-demo adapter.`, "Replace it with an Astro component or preserve its rendered HTML.", { nodeId, evidence: widgetType });
334
+ return [node];
335
+ }
336
+ function scanShortcodes(content, sourceId, collector) {
337
+ SHORTCODE_PATTERN.lastIndex = 0;
338
+ const seen = new Set();
339
+ let match;
340
+ while ((match = SHORTCODE_PATTERN.exec(content)) !== null) {
341
+ const shortcode = (match[1] ?? "unknown").toLowerCase();
342
+ if (seen.has(shortcode)) {
343
+ continue;
344
+ }
345
+ seen.add(shortcode);
346
+ const blocked = BLOCKING_SHORTCODES.has(shortcode);
347
+ collector.add(blocked ? "blocker" : "warning", "SHORTCODE_UNSUPPORTED", `Shortcode [${shortcode}] cannot be executed by Astro.`, blocked
348
+ ? "Choose a replacement integration and rebuild this behavior."
349
+ : "Replace the shortcode or accept a static HTML fallback.", { evidence: match[0].slice(0, 200) });
350
+ }
351
+ }
352
+ function classifyGutenbergBlock(blockName) {
353
+ const native = NATIVE_GUTENBERG_BLOCKS.get(blockName);
354
+ if (native !== undefined) {
355
+ return native;
356
+ }
357
+ if (DYNAMIC_GUTENBERG_BLOCKS.has(blockName)) {
358
+ return { kind: "query", conversion: "blocked" };
359
+ }
360
+ if (FORM_GUTENBERG_BLOCKS.has(blockName)) {
361
+ return { kind: "form", conversion: "blocked" };
362
+ }
363
+ return { kind: "unknown", conversion: "manual" };
364
+ }
365
+ function reportGutenbergCompatibility(node, collector) {
366
+ if (node.sourceType === "core/image" || node.sourceType === "core/gallery") {
367
+ collector.add("warning", "GUTENBERG_MEDIA_UNSUPPORTED", `Gutenberg ${node.sourceType === "core/image" ? "image" : "gallery"} media is withheld until local assets are added.`, "Import approved local media, write appropriate alternative text, and rebuild this content deliberately.", { nodeId: node.id, evidence: node.sourceType });
368
+ return;
369
+ }
370
+ if (DYNAMIC_GUTENBERG_BLOCKS.has(node.sourceType) || FORM_GUTENBERG_BLOCKS.has(node.sourceType)) {
371
+ collector.add("blocker", "GUTENBERG_DYNAMIC_BLOCK", `Dynamic Gutenberg block ${node.sourceType} depends on WordPress runtime behavior.`, "Map the block to an Astro data source and verify the generated behavior.", { nodeId: node.id, evidence: node.sourceType });
372
+ return;
373
+ }
374
+ if (node.sourceType === "core/shortcode") {
375
+ collector.add("warning", "SHORTCODE_UNSUPPORTED", "The Gutenberg Shortcode block cannot execute inside Astro.", "Replace the shortcode with static content or an explicit Astro integration.", { nodeId: node.id, evidence: node.sourceType });
376
+ return;
377
+ }
378
+ if (!NATIVE_GUTENBERG_BLOCKS.has(node.sourceType)) {
379
+ collector.add("warning", "GUTENBERG_UNKNOWN_BLOCK", `Gutenberg block ${node.sourceType} has no 0.1.0-demo adapter.`, "Add an adapter or preserve the block's rendered HTML.", { nodeId: node.id, evidence: node.sourceType });
380
+ }
381
+ }
382
+ function parseBlockAttributes(serialized, blockName, nodeId, collector) {
383
+ if (serialized === undefined || serialized.trim() === "") {
384
+ return {};
385
+ }
386
+ try {
387
+ const value = JSON.parse(serialized);
388
+ if (isUnknownRecord(value)) {
389
+ return value;
390
+ }
391
+ }
392
+ catch (error) {
393
+ collector.add("warning", "GUTENBERG_INVALID_ATTRIBUTES", `Block ${blockName} contains invalid JSON attributes.`, "Repair the block attributes or accept the preserved HTML fallback.", { nodeId, evidence: error instanceof Error ? error.message : String(error) });
394
+ }
395
+ return {};
396
+ }
397
+ function parsePostMeta(itemXml) {
398
+ const values = {};
399
+ const pattern = /<wp:postmeta\b[^>]*>([\s\S]*?)<\/wp:postmeta>/gi;
400
+ let match;
401
+ while ((match = pattern.exec(itemXml)) !== null) {
402
+ const block = match[1] ?? "";
403
+ const key = cleanField(readTag(block, "wp:meta_key"));
404
+ if (key === "") {
405
+ continue;
406
+ }
407
+ const value = unwrapXmlValue(readTag(block, "wp:meta_value"));
408
+ (values[key] ??= []).push(value);
409
+ }
410
+ return values;
411
+ }
412
+ function parseTerms(itemXml) {
413
+ const terms = [];
414
+ const pattern = /<category\b([^>]*)>([\s\S]*?)<\/category>/gi;
415
+ let match;
416
+ while ((match = pattern.exec(itemXml)) !== null) {
417
+ const attributes = match[1] ?? "";
418
+ const domain = decodeXmlEntities(readAttribute(attributes, "domain") ?? "");
419
+ const nicename = decodeXmlEntities(readAttribute(attributes, "nicename") ?? "");
420
+ const name = cleanField(match[2] ?? "");
421
+ terms.push({ domain, nicename, name });
422
+ }
423
+ return terms;
424
+ }
425
+ function createIssueCollector(sourceId, route) {
426
+ const issues = [];
427
+ return {
428
+ issues,
429
+ add(severity, code, message, requiredAction, details = {}) {
430
+ issues.push({
431
+ id: `${sourceId}:${code}:${issues.length + 1}`,
432
+ severity,
433
+ code,
434
+ sourceId,
435
+ ...(route === undefined ? {} : { route }),
436
+ ...(details.nodeId === undefined ? {} : { nodeId: details.nodeId }),
437
+ title: message,
438
+ message,
439
+ ...(details.evidence === undefined ? {} : { evidence: details.evidence }),
440
+ requiredAction
441
+ });
442
+ }
443
+ };
444
+ }
445
+ function createMutableNode(id, source, sourceType, kind, conversion, attributes, children) {
446
+ return { id, source, sourceType, kind, conversion, attributes, children };
447
+ }
448
+ function createClassicNode(rawHtml, id) {
449
+ const text = htmlToText(rawHtml);
450
+ return {
451
+ id,
452
+ source: "classic",
453
+ sourceType: "classic/html",
454
+ kind: "html",
455
+ conversion: "legacy-html",
456
+ attributes: {},
457
+ children: [],
458
+ ...(text === "" ? {} : { text }),
459
+ rawHtml
460
+ };
461
+ }
462
+ function appendLooseHtml(rawHtml, destination, sourceId, nextOrdinal) {
463
+ if (rawHtml.trim() === "") {
464
+ return;
465
+ }
466
+ const node = createClassicNode(rawHtml, `${sourceId}:gutenberg-loose:${nextOrdinal()}`);
467
+ destination.push({ ...node, attributes: {}, children: [] });
468
+ }
469
+ function summarize(records, issues) {
470
+ const nodes = records.flatMap((record) => flattenNodes(record.nodes));
471
+ return {
472
+ records: records.length,
473
+ pages: records.filter((record) => record.type === "page").length,
474
+ posts: records.filter((record) => record.type === "post").length,
475
+ nodes: nodes.length,
476
+ nativeNodes: nodes.filter((node) => node.conversion === "native").length,
477
+ manualNodes: nodes.filter((node) => node.conversion === "manual" || node.conversion === "legacy-html").length,
478
+ blockedNodes: nodes.filter((node) => node.conversion === "blocked").length,
479
+ reviewItems: nodes.filter((node) => node.conversion === "manual" || node.conversion === "legacy-html" || node.conversion === "blocked").length,
480
+ warnings: issues.filter((issue) => issue.severity === "warning").length,
481
+ blockers: issues.filter((issue) => issue.severity === "blocker").length
482
+ };
483
+ }
484
+ function flattenNodes(nodes) {
485
+ return nodes.flatMap((node) => [node, ...flattenNodes(node.children)]);
486
+ }
487
+ function readTag(xml, tagName) {
488
+ const escaped = escapeRegExp(tagName);
489
+ const match = new RegExp(`<${escaped}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${escaped}>`, "i").exec(xml);
490
+ return match?.[1] ?? "";
491
+ }
492
+ function readAttribute(attributes, attributeName) {
493
+ const escaped = escapeRegExp(attributeName);
494
+ const match = new RegExp(`(?:^|\\s)${escaped}=(?:"([^"]*)"|'([^']*)')`, "i").exec(attributes);
495
+ return match?.[1] ?? match?.[2];
496
+ }
497
+ function unwrapXmlValue(value) {
498
+ const trimmed = value.trim();
499
+ const cdata = /^<!\[CDATA\[([\s\S]*)\]\]>$/.exec(trimmed);
500
+ return cdata?.[1] ?? decodeXmlEntities(trimmed);
501
+ }
502
+ function cleanField(value) {
503
+ return decodeXmlEntities(unwrapXmlValue(value)).trim();
504
+ }
505
+ function cleanOptionalField(value) {
506
+ const cleaned = cleanField(value);
507
+ return cleaned === "" ? undefined : cleaned;
508
+ }
509
+ function decodeXmlEntities(value) {
510
+ return value
511
+ .replace(/&lt;/g, "<")
512
+ .replace(/&gt;/g, ">")
513
+ .replace(/&quot;/g, '"')
514
+ .replace(/&apos;/g, "'")
515
+ .replace(/&#(\d+);/g, (_match, decimal) => String.fromCodePoint(Number.parseInt(decimal, 10)))
516
+ .replace(/&#x([0-9a-f]+);/gi, (_match, hexadecimal) => String.fromCodePoint(Number.parseInt(hexadecimal, 16)))
517
+ .replace(/&amp;/g, "&");
518
+ }
519
+ function htmlToText(value) {
520
+ return decodeXmlEntities(value.replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
521
+ }
522
+ function normalizeBlockName(value) {
523
+ return value.includes("/") ? value.toLowerCase() : `core/${value.toLowerCase()}`;
524
+ }
525
+ function normalizeStatus(value) {
526
+ switch (value) {
527
+ case "publish":
528
+ case "draft":
529
+ case "future":
530
+ case "pending":
531
+ case "private":
532
+ case "trash":
533
+ case "inherit":
534
+ return value;
535
+ default:
536
+ return "unknown";
537
+ }
538
+ }
539
+ function elementorWidgetText(widgetType, settings) {
540
+ const candidate = widgetType === "heading" ? settings.title : widgetType === "text-editor" ? settings.editor : settings.text;
541
+ return typeof candidate === "string" && candidate.trim() !== "" ? htmlToText(candidate) : undefined;
542
+ }
543
+ function getNestedString(values, key, nestedKey) {
544
+ const value = values[key];
545
+ if (!isUnknownRecord(value)) {
546
+ return undefined;
547
+ }
548
+ const nested = value[nestedKey];
549
+ return typeof nested === "string" && nested.trim() !== "" ? nested : undefined;
550
+ }
551
+ function isSafeElementorHref(value) {
552
+ const href = value.trim();
553
+ if (href === "" || /[\u0000-\u001f\u007f-\u009f]/.test(href)) {
554
+ return false;
555
+ }
556
+ const decodedHref = decodeElementorHrefEntities(href).trim();
557
+ if (decodedHref === "" || /[\u0000-\u001f\u007f-\u009f]/.test(decodedHref)) {
558
+ return false;
559
+ }
560
+ const normalized = decodedHref.replace(/\s+/g, "");
561
+ if (normalized.startsWith("//") || normalized.startsWith("\\")) {
562
+ return false;
563
+ }
564
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(normalized)?.[1]?.toLowerCase();
565
+ return scheme === undefined || SAFE_ELEMENTOR_HREF_SCHEMES.has(scheme);
566
+ }
567
+ function decodeElementorHrefEntities(value) {
568
+ let decoded = value;
569
+ for (let pass = 0; pass < 2; pass += 1) {
570
+ const next = decodeXmlEntities(decoded)
571
+ .replace(/&colon;/gi, ":")
572
+ .replace(/&newline;/gi, "\n")
573
+ .replace(/&tab;/gi, "\t");
574
+ if (next === decoded) {
575
+ return next;
576
+ }
577
+ decoded = next;
578
+ }
579
+ return decoded;
580
+ }
581
+ function slugify(value) {
582
+ return value
583
+ .toLowerCase()
584
+ .normalize("NFKD")
585
+ .replace(/[\u0300-\u036f]/g, "")
586
+ .replace(/[^a-z0-9]+/g, "-")
587
+ .replace(/^-|-$/g, "");
588
+ }
589
+ function escapeRegExp(value) {
590
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
591
+ }
592
+ function isUnknownRecord(value) {
593
+ return typeof value === "object" && value !== null && !Array.isArray(value);
594
+ }
595
+ function optionalProperty(key, value) {
596
+ return value === undefined ? {} : { [key]: value };
597
+ }
598
+ function compactOptionalObject(values) {
599
+ return {
600
+ ...(values.title === undefined ? {} : { title: values.title }),
601
+ ...(values.url === undefined ? {} : { url: values.url })
602
+ };
603
+ }
@@ -0,0 +1,2 @@
1
+ import type { MigrationProject } from "./types.js";
2
+ export declare function generateAstroProject(project: MigrationProject, outDir: string): Promise<void>;