@agent-native/core 0.101.7 → 0.101.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +22 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/AGENTS.md +3 -2
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
- package/corpus/templates/content/actions/_database-utils.ts +89 -12
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +18 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +25 -0
- package/corpus/templates/content/app/i18n-data.ts +287 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +47 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +53 -0
- package/corpus/templates/content/shared/api.ts +70 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/nfm.ts +3 -3
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/package.json +2 -2
|
@@ -92,6 +92,11 @@ type MdxNode = {
|
|
|
92
92
|
name?: string;
|
|
93
93
|
value?: string;
|
|
94
94
|
children?: MdxNode[];
|
|
95
|
+
attributes?: Array<{
|
|
96
|
+
type: string;
|
|
97
|
+
name?: string;
|
|
98
|
+
value?: string | null | { type?: string; value?: string };
|
|
99
|
+
}>;
|
|
95
100
|
position?: {
|
|
96
101
|
start?: { offset?: number };
|
|
97
102
|
end?: { offset?: number };
|
|
@@ -270,8 +275,50 @@ export function builderEntryBlocks(entry: BuilderContentEntry): unknown[] {
|
|
|
270
275
|
return [];
|
|
271
276
|
}
|
|
272
277
|
|
|
278
|
+
function isZeroDimension(value: unknown) {
|
|
279
|
+
return value === 0 || value === "0";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isBuilderTrackingPixelBlock(value: unknown) {
|
|
283
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
284
|
+
const block = value as Record<string, unknown>;
|
|
285
|
+
if (block["@type"] !== "@builder.io/sdk:Element" || block.tagName !== "img") {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const properties = block.properties;
|
|
289
|
+
if (
|
|
290
|
+
!properties ||
|
|
291
|
+
typeof properties !== "object" ||
|
|
292
|
+
Array.isArray(properties)
|
|
293
|
+
) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
const props = properties as Record<string, unknown>;
|
|
297
|
+
if (
|
|
298
|
+
props.role !== "presentation" ||
|
|
299
|
+
props.alt !== "" ||
|
|
300
|
+
(props["aria-hidden"] !== true && props["aria-hidden"] !== "true") ||
|
|
301
|
+
!isZeroDimension(props.width) ||
|
|
302
|
+
!isZeroDimension(props.height) ||
|
|
303
|
+
typeof props.src !== "string"
|
|
304
|
+
) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const src = new URL(props.src);
|
|
309
|
+
return (
|
|
310
|
+
(src.hostname === "builder.io" || src.hostname.endsWith(".builder.io")) &&
|
|
311
|
+
src.pathname === "/api/v1/pixel"
|
|
312
|
+
);
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
273
318
|
export function builderBlocksHash(blocks: unknown[]) {
|
|
274
|
-
return stableHash(
|
|
319
|
+
return stableHash(
|
|
320
|
+
blocks.filter((block) => !isBuilderTrackingPixelBlock(block)),
|
|
321
|
+
);
|
|
275
322
|
}
|
|
276
323
|
|
|
277
324
|
export function builderSourceHash(entry: BuilderContentEntry) {
|
|
@@ -485,6 +532,11 @@ async function expectedReadableLayoutFingerprint(blocks: unknown[]) {
|
|
|
485
532
|
const name = componentName(block);
|
|
486
533
|
const options = componentOptions(block);
|
|
487
534
|
if (name === "Text") {
|
|
535
|
+
const table = possibleNativeTableHtml(String(options.text ?? ""));
|
|
536
|
+
if (table) {
|
|
537
|
+
fingerprint.push("prose");
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
488
540
|
for (const unit of markdownUnits(
|
|
489
541
|
htmlToMarkdown(String(options.text ?? "")),
|
|
490
542
|
)) {
|
|
@@ -500,6 +552,10 @@ async function expectedReadableLayoutFingerprint(blocks: unknown[]) {
|
|
|
500
552
|
if (builderImageMarkdown(options)) fingerprint.push("prose");
|
|
501
553
|
continue;
|
|
502
554
|
}
|
|
555
|
+
if (name === "Video") {
|
|
556
|
+
if (builderVideoMdx(options)) fingerprint.push("prose");
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
503
559
|
if (name === "Tabbed Content") {
|
|
504
560
|
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
505
561
|
for (const tab of tabs) {
|
|
@@ -1150,7 +1206,8 @@ async function builderBlockToReadableMdx(
|
|
|
1150
1206
|
const mapping = builderSourceComponentMappingFor(name);
|
|
1151
1207
|
|
|
1152
1208
|
if (name === "Text") {
|
|
1153
|
-
|
|
1209
|
+
const table = await validatedNativeTableHtml(String(options.text ?? ""));
|
|
1210
|
+
return table ?? htmlToMarkdown(String(options.text ?? "")).trim();
|
|
1154
1211
|
}
|
|
1155
1212
|
|
|
1156
1213
|
if (name === "Code Block" || name === "Blog Code Block") {
|
|
@@ -1164,6 +1221,10 @@ async function builderBlockToReadableMdx(
|
|
|
1164
1221
|
return builderImageMarkdown(options);
|
|
1165
1222
|
}
|
|
1166
1223
|
|
|
1224
|
+
if (name === "Video") {
|
|
1225
|
+
return builderVideoMdx(options);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1167
1228
|
if (name === "Tabbed Content") {
|
|
1168
1229
|
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
1169
1230
|
const convertedTabs = await Promise.all(
|
|
@@ -1265,7 +1326,7 @@ function fencedCodeFromMarkdown(markdown: string) {
|
|
|
1265
1326
|
}
|
|
1266
1327
|
|
|
1267
1328
|
interface ReadableEditableSegment {
|
|
1268
|
-
kind: "text" | "code" | "image" | "tab-label";
|
|
1329
|
+
kind: "text" | "table" | "code" | "image" | "video" | "tab-label";
|
|
1269
1330
|
block: Record<string, unknown>;
|
|
1270
1331
|
baseline: string;
|
|
1271
1332
|
}
|
|
@@ -1286,8 +1347,12 @@ function collectReadableEditableSegments(
|
|
|
1286
1347
|
const name = componentName(block);
|
|
1287
1348
|
const options = componentOptions(block);
|
|
1288
1349
|
if (name === "Text") {
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1350
|
+
const rawText = String(options.text ?? "").trim();
|
|
1351
|
+
const table = possibleNativeTableHtml(rawText);
|
|
1352
|
+
const baseline = table ?? htmlToMarkdown(rawText).trim();
|
|
1353
|
+
if (baseline) {
|
|
1354
|
+
segments.push({ kind: table ? "table" : "text", block, baseline });
|
|
1355
|
+
}
|
|
1291
1356
|
continue;
|
|
1292
1357
|
}
|
|
1293
1358
|
if (name === "Code Block" || name === "Blog Code Block") {
|
|
@@ -1300,6 +1365,11 @@ function collectReadableEditableSegments(
|
|
|
1300
1365
|
if (baseline) segments.push({ kind: "image", block, baseline });
|
|
1301
1366
|
continue;
|
|
1302
1367
|
}
|
|
1368
|
+
if (name === "Video") {
|
|
1369
|
+
const baseline = builderVideoMdx(options);
|
|
1370
|
+
if (baseline) segments.push({ kind: "video", block, baseline });
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1303
1373
|
if (name === "Tabbed Content") {
|
|
1304
1374
|
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
1305
1375
|
for (let index = 0; index < tabs.length; index += 1) {
|
|
@@ -1342,6 +1412,9 @@ function builderBlockHasReadableOutput(block: unknown): boolean {
|
|
|
1342
1412
|
if (name === "Image") {
|
|
1343
1413
|
return builderImageMarkdown(options).trim().length > 0;
|
|
1344
1414
|
}
|
|
1415
|
+
if (name === "Video") {
|
|
1416
|
+
return builderVideoMdx(options).trim().length > 0;
|
|
1417
|
+
}
|
|
1345
1418
|
if (name === "Tabbed Content") {
|
|
1346
1419
|
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
1347
1420
|
return tabs.some((tab) => {
|
|
@@ -1367,7 +1440,8 @@ function countExpectedReadableSourceComponentMarkers(
|
|
|
1367
1440
|
name === "Text" ||
|
|
1368
1441
|
name === "Code Block" ||
|
|
1369
1442
|
name === "Blog Code Block" ||
|
|
1370
|
-
name === "Image"
|
|
1443
|
+
name === "Image" ||
|
|
1444
|
+
name === "Video"
|
|
1371
1445
|
) {
|
|
1372
1446
|
continue;
|
|
1373
1447
|
}
|
|
@@ -1485,6 +1559,18 @@ export async function builderReadableBodyToBuilderBlocks(args: {
|
|
|
1485
1559
|
} else if (segment.kind === "text") {
|
|
1486
1560
|
const options = ensureComponentOptions(segment.block, "Text");
|
|
1487
1561
|
options.text = markdownToBuilderTextHtml(nextMarkdown);
|
|
1562
|
+
} else if (segment.kind === "table") {
|
|
1563
|
+
const table = await validatedNativeTableHtml(nextMarkdown);
|
|
1564
|
+
if (!table) {
|
|
1565
|
+
return {
|
|
1566
|
+
blocks: null,
|
|
1567
|
+
warnings: [
|
|
1568
|
+
"Readable Builder table changed into unsupported markup; keep it as a native Content table before pushing.",
|
|
1569
|
+
],
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const options = ensureComponentOptions(segment.block, "Text");
|
|
1573
|
+
options.text = table;
|
|
1488
1574
|
} else if (segment.kind === "image") {
|
|
1489
1575
|
const options = ensureComponentOptions(segment.block, "Image");
|
|
1490
1576
|
const image = parseMarkdownImage(nextMarkdown);
|
|
@@ -1498,6 +1584,22 @@ export async function builderReadableBodyToBuilderBlocks(args: {
|
|
|
1498
1584
|
}
|
|
1499
1585
|
options.image = image.src;
|
|
1500
1586
|
options.altText = image.alt;
|
|
1587
|
+
} else if (segment.kind === "video") {
|
|
1588
|
+
const root = await parseMdxRoot(nextMarkdown);
|
|
1589
|
+
const children = root.children ?? [];
|
|
1590
|
+
const node = children.length === 1 ? children[0] : undefined;
|
|
1591
|
+
if (!node || node.name !== "video") {
|
|
1592
|
+
return {
|
|
1593
|
+
blocks: null,
|
|
1594
|
+
warnings: [
|
|
1595
|
+
"Readable Builder video changed into unsupported markup; keep it as a native Content video before pushing.",
|
|
1596
|
+
],
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
const converted = freshVideoBlock(node, nextMarkdown);
|
|
1600
|
+
const options = ensureComponentOptions(segment.block, "Video");
|
|
1601
|
+
Object.keys(options).forEach((key) => delete options[key]);
|
|
1602
|
+
Object.assign(options, componentOptions(converted));
|
|
1501
1603
|
} else {
|
|
1502
1604
|
const options = ensureComponentOptions(
|
|
1503
1605
|
segment.block,
|
|
@@ -1766,9 +1868,165 @@ function freshTextBlock(markdown: string): unknown {
|
|
|
1766
1868
|
};
|
|
1767
1869
|
}
|
|
1768
1870
|
|
|
1871
|
+
const BUILDER_TABLE_TAGS = new Set([
|
|
1872
|
+
"table",
|
|
1873
|
+
"colgroup",
|
|
1874
|
+
"col",
|
|
1875
|
+
"thead",
|
|
1876
|
+
"tbody",
|
|
1877
|
+
"tfoot",
|
|
1878
|
+
"tr",
|
|
1879
|
+
"th",
|
|
1880
|
+
"td",
|
|
1881
|
+
"p",
|
|
1882
|
+
"strong",
|
|
1883
|
+
"em",
|
|
1884
|
+
"code",
|
|
1885
|
+
"span",
|
|
1886
|
+
"a",
|
|
1887
|
+
"br",
|
|
1888
|
+
]);
|
|
1889
|
+
|
|
1890
|
+
function assertSafeBuilderTableAttribute(
|
|
1891
|
+
tag: string,
|
|
1892
|
+
name: string,
|
|
1893
|
+
value: string | null,
|
|
1894
|
+
) {
|
|
1895
|
+
if (name === "class" && tag === "table" && value === "notion-table") {
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
if (name === "class" && tag === "span" && value === "dark-mode-invert") {
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
if (
|
|
1902
|
+
name === "style" &&
|
|
1903
|
+
(tag === "table" || tag === "col") &&
|
|
1904
|
+
typeof value === "string" &&
|
|
1905
|
+
/^min-width:\s*\d+(?:\.\d+)?px;?$/.test(value.trim())
|
|
1906
|
+
) {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
if (
|
|
1910
|
+
(name === "colspan" || name === "rowspan") &&
|
|
1911
|
+
(tag === "th" || tag === "td") &&
|
|
1912
|
+
typeof value === "string" &&
|
|
1913
|
+
/^[1-9]\d*$/.test(value)
|
|
1914
|
+
) {
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (
|
|
1918
|
+
name === "colwidth" &&
|
|
1919
|
+
(tag === "th" || tag === "td") &&
|
|
1920
|
+
typeof value === "string" &&
|
|
1921
|
+
/^\d+(?:,\d+)*$/.test(value)
|
|
1922
|
+
) {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (
|
|
1926
|
+
name === "href" &&
|
|
1927
|
+
tag === "a" &&
|
|
1928
|
+
typeof value === "string" &&
|
|
1929
|
+
value.trim()
|
|
1930
|
+
) {
|
|
1931
|
+
const href = safeLiteralMediaUrl(value.trim(), "table link");
|
|
1932
|
+
if (href.protocol === "https:" || href.protocol === "http:") return;
|
|
1933
|
+
}
|
|
1934
|
+
throw new Error(`Unsupported Builder table attribute: ${tag}.${name}.`);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function assertSafeBuilderTableNode(node: MdxNode) {
|
|
1938
|
+
if (
|
|
1939
|
+
node.type === "mdxFlowExpression" ||
|
|
1940
|
+
node.type === "mdxTextExpression" ||
|
|
1941
|
+
node.type === "mdxjsEsm"
|
|
1942
|
+
) {
|
|
1943
|
+
throw new Error("Unsupported dynamic syntax inside Builder table.");
|
|
1944
|
+
}
|
|
1945
|
+
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
|
|
1946
|
+
const tag = node.name?.toLowerCase() ?? "";
|
|
1947
|
+
if (!BUILDER_TABLE_TAGS.has(tag)) {
|
|
1948
|
+
throw new Error(
|
|
1949
|
+
`Unsupported Builder table component: <${node.name || "unknown"}>.`,
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
for (const attribute of node.attributes ?? []) {
|
|
1953
|
+
if (
|
|
1954
|
+
attribute.type !== "mdxJsxAttribute" ||
|
|
1955
|
+
!attribute.name ||
|
|
1956
|
+
(attribute.value !== null && typeof attribute.value !== "string")
|
|
1957
|
+
) {
|
|
1958
|
+
throw new Error("Builder table attributes must be literal values.");
|
|
1959
|
+
}
|
|
1960
|
+
assertSafeBuilderTableAttribute(
|
|
1961
|
+
tag,
|
|
1962
|
+
attribute.name.toLowerCase(),
|
|
1963
|
+
attribute.value ?? null,
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
for (const child of node.children ?? []) {
|
|
1968
|
+
assertSafeBuilderTableNode(child);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
function possibleNativeTableHtml(value: string) {
|
|
1973
|
+
const trimmed = value.trim();
|
|
1974
|
+
return /^<table\b[\s\S]*<\/table>$/.test(trimmed) ? trimmed : null;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
async function validatedNativeTableHtml(value: string) {
|
|
1978
|
+
const candidate = possibleNativeTableHtml(value);
|
|
1979
|
+
if (!candidate) return null;
|
|
1980
|
+
const root = await parseMdxRoot(candidate);
|
|
1981
|
+
const children = root.children ?? [];
|
|
1982
|
+
if (children.length !== 1 || children[0]?.name !== "table") return null;
|
|
1983
|
+
assertSafeBuilderTableNode(children[0]);
|
|
1984
|
+
return candidate;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function freshTableBlock(node: MdxNode, raw: string): unknown {
|
|
1988
|
+
assertSafeBuilderTableNode(node);
|
|
1989
|
+
return {
|
|
1990
|
+
"@type": "@builder.io/sdk:Element",
|
|
1991
|
+
"@version": 2,
|
|
1992
|
+
id: `builder-mdx-table-${stableHash(raw).slice(0, 16)}`,
|
|
1993
|
+
component: {
|
|
1994
|
+
name: "Text",
|
|
1995
|
+
options: { text: raw },
|
|
1996
|
+
},
|
|
1997
|
+
responsiveStyles: {
|
|
1998
|
+
large: {
|
|
1999
|
+
display: "flex",
|
|
2000
|
+
flexDirection: "column",
|
|
2001
|
+
position: "relative",
|
|
2002
|
+
},
|
|
2003
|
+
},
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
|
|
1769
2007
|
function freshImageBlock(markdown: string): unknown | null {
|
|
1770
2008
|
const image = parseMarkdownImage(markdown);
|
|
1771
|
-
if (!image?.src)
|
|
2009
|
+
if (!image?.src) {
|
|
2010
|
+
if (markdown.trimStart().startsWith("![")) {
|
|
2011
|
+
throw new Error(
|
|
2012
|
+
"Builder image must use literal Markdown image syntax with an absolute URL.",
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
let imageUrl: URL;
|
|
2018
|
+
try {
|
|
2019
|
+
imageUrl = new URL(image.src);
|
|
2020
|
+
} catch {
|
|
2021
|
+
throw new Error("Builder image src must be an absolute URL.");
|
|
2022
|
+
}
|
|
2023
|
+
if (
|
|
2024
|
+
(imageUrl.protocol !== "https:" && imageUrl.protocol !== "http:") ||
|
|
2025
|
+
imageUrl.username ||
|
|
2026
|
+
imageUrl.password
|
|
2027
|
+
) {
|
|
2028
|
+
throw new Error("Builder image src must use a safe HTTP(S) URL.");
|
|
2029
|
+
}
|
|
1772
2030
|
return {
|
|
1773
2031
|
"@type": "@builder.io/sdk:Element",
|
|
1774
2032
|
"@version": 2,
|
|
@@ -1776,7 +2034,7 @@ function freshImageBlock(markdown: string): unknown | null {
|
|
|
1776
2034
|
component: {
|
|
1777
2035
|
name: "Image",
|
|
1778
2036
|
options: {
|
|
1779
|
-
image:
|
|
2037
|
+
image: imageUrl.toString(),
|
|
1780
2038
|
altText: image.alt,
|
|
1781
2039
|
},
|
|
1782
2040
|
},
|
|
@@ -1790,6 +2048,273 @@ function freshImageBlock(markdown: string): unknown | null {
|
|
|
1790
2048
|
};
|
|
1791
2049
|
}
|
|
1792
2050
|
|
|
2051
|
+
function nodeBlocksWithStandaloneImages(
|
|
2052
|
+
body: string,
|
|
2053
|
+
node: MdxNode,
|
|
2054
|
+
): unknown[] | null {
|
|
2055
|
+
const nodeStart = node.position?.start?.offset;
|
|
2056
|
+
const nodeEnd = node.position?.end?.offset;
|
|
2057
|
+
if (typeof nodeStart !== "number" || typeof nodeEnd !== "number") {
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
const standaloneImages: MdxNode[] = [];
|
|
2061
|
+
const visit = (candidate: MdxNode) => {
|
|
2062
|
+
if (candidate.type === "image") {
|
|
2063
|
+
const start = candidate.position?.start?.offset;
|
|
2064
|
+
const end = candidate.position?.end?.offset;
|
|
2065
|
+
if (typeof start !== "number" || typeof end !== "number") return;
|
|
2066
|
+
const lineStart = body.lastIndexOf("\n", Math.max(0, start - 1)) + 1;
|
|
2067
|
+
const nextNewline = body.indexOf("\n", end);
|
|
2068
|
+
const lineEnd = nextNewline === -1 ? body.length : nextNewline;
|
|
2069
|
+
if (
|
|
2070
|
+
body.slice(lineStart, start).trim() === "" &&
|
|
2071
|
+
body.slice(end, lineEnd).trim() === ""
|
|
2072
|
+
) {
|
|
2073
|
+
standaloneImages.push(candidate);
|
|
2074
|
+
}
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
for (const child of candidate.children ?? []) visit(child);
|
|
2078
|
+
};
|
|
2079
|
+
visit(node);
|
|
2080
|
+
if (standaloneImages.length === 0) return null;
|
|
2081
|
+
|
|
2082
|
+
const blocks: unknown[] = [];
|
|
2083
|
+
let cursor = nodeStart;
|
|
2084
|
+
for (const imageNode of standaloneImages) {
|
|
2085
|
+
const start = imageNode.position!.start!.offset!;
|
|
2086
|
+
const end = imageNode.position!.end!.offset!;
|
|
2087
|
+
const prose = body.slice(cursor, start).trim();
|
|
2088
|
+
if (prose) blocks.push(freshTextBlock(prose));
|
|
2089
|
+
const image = freshImageBlock(body.slice(start, end));
|
|
2090
|
+
if (!image) {
|
|
2091
|
+
throw new Error("Builder standalone image could not be converted.");
|
|
2092
|
+
}
|
|
2093
|
+
blocks.push(image);
|
|
2094
|
+
cursor = end;
|
|
2095
|
+
}
|
|
2096
|
+
const trailingProse = body.slice(cursor, nodeEnd).trim();
|
|
2097
|
+
if (trailingProse) blocks.push(freshTextBlock(trailingProse));
|
|
2098
|
+
return blocks;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function isEmptyEditorBlockSentinel(node: MdxNode, raw: string) {
|
|
2102
|
+
return (
|
|
2103
|
+
node.name === "empty-block" &&
|
|
2104
|
+
(node.attributes ?? []).length === 0 &&
|
|
2105
|
+
(node.children ?? []).length === 0 &&
|
|
2106
|
+
/^<empty-block\s*\/>$/.test(raw)
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
function containsUnsupportedEmptyBlockSyntax(
|
|
2111
|
+
node: MdxNode,
|
|
2112
|
+
raw: string,
|
|
2113
|
+
): boolean {
|
|
2114
|
+
return node.type !== "code" && /<\/?empty-block\b/i.test(raw);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function safeLiteralMediaUrl(value: string, label: string) {
|
|
2118
|
+
let parsed: URL;
|
|
2119
|
+
try {
|
|
2120
|
+
parsed = new URL(value);
|
|
2121
|
+
} catch {
|
|
2122
|
+
throw new Error(`Builder video ${label} must be an absolute URL.`);
|
|
2123
|
+
}
|
|
2124
|
+
if (
|
|
2125
|
+
(parsed.protocol !== "https:" && parsed.protocol !== "http:") ||
|
|
2126
|
+
parsed.username ||
|
|
2127
|
+
parsed.password
|
|
2128
|
+
) {
|
|
2129
|
+
throw new Error(`Builder video ${label} must use a safe HTTP(S) URL.`);
|
|
2130
|
+
}
|
|
2131
|
+
return parsed;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function literalVideoBoolean(value: string | null, name: string) {
|
|
2135
|
+
if (value === null || value === "" || value === name || value === "true") {
|
|
2136
|
+
return true;
|
|
2137
|
+
}
|
|
2138
|
+
if (value === "false") return false;
|
|
2139
|
+
throw new Error(`Builder video attribute ${name} must be a literal boolean.`);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function freshVideoBlock(node: MdxNode, raw: string): unknown {
|
|
2143
|
+
if ((node.children ?? []).length > 0) {
|
|
2144
|
+
throw new Error("Builder video cannot contain child content.");
|
|
2145
|
+
}
|
|
2146
|
+
const allowed = new Set([
|
|
2147
|
+
"src",
|
|
2148
|
+
"poster",
|
|
2149
|
+
"preload",
|
|
2150
|
+
"controls",
|
|
2151
|
+
"autoplay",
|
|
2152
|
+
"muted",
|
|
2153
|
+
"loop",
|
|
2154
|
+
"playsinline",
|
|
2155
|
+
"width",
|
|
2156
|
+
"height",
|
|
2157
|
+
// Content persists title for native playback, but Builder's built-in
|
|
2158
|
+
// Video options do not expose it. Validate that it is literal, then omit.
|
|
2159
|
+
"title",
|
|
2160
|
+
]);
|
|
2161
|
+
const values = new Map<string, string | null>();
|
|
2162
|
+
for (const attribute of node.attributes ?? []) {
|
|
2163
|
+
if (
|
|
2164
|
+
attribute.type !== "mdxJsxAttribute" ||
|
|
2165
|
+
!attribute.name ||
|
|
2166
|
+
(attribute.value !== null && typeof attribute.value !== "string")
|
|
2167
|
+
) {
|
|
2168
|
+
throw new Error("Builder video attributes must be literal values.");
|
|
2169
|
+
}
|
|
2170
|
+
const name = attribute.name.toLowerCase();
|
|
2171
|
+
if (!allowed.has(name)) {
|
|
2172
|
+
throw new Error(`Unsupported Builder video attribute: ${name}.`);
|
|
2173
|
+
}
|
|
2174
|
+
if (values.has(name)) {
|
|
2175
|
+
throw new Error(`Duplicate Builder video attribute: ${name}.`);
|
|
2176
|
+
}
|
|
2177
|
+
values.set(name, attribute.value ?? null);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
const src = values.get("src");
|
|
2181
|
+
if (typeof src !== "string" || !src.trim()) {
|
|
2182
|
+
throw new Error("Builder video requires a literal src attribute.");
|
|
2183
|
+
}
|
|
2184
|
+
const videoUrl = safeLiteralMediaUrl(src.trim(), "src");
|
|
2185
|
+
if (!videoUrl.pathname.toLowerCase().endsWith(".mp4")) {
|
|
2186
|
+
throw new Error("Builder video src must reference an MP4 file.");
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
const options: Record<string, unknown> = {
|
|
2190
|
+
video: videoUrl.toString(),
|
|
2191
|
+
autoPlay: false,
|
|
2192
|
+
controls: false,
|
|
2193
|
+
muted: false,
|
|
2194
|
+
loop: false,
|
|
2195
|
+
playsInline: false,
|
|
2196
|
+
};
|
|
2197
|
+
for (const [attribute, option] of [
|
|
2198
|
+
["autoplay", "autoPlay"],
|
|
2199
|
+
["controls", "controls"],
|
|
2200
|
+
["muted", "muted"],
|
|
2201
|
+
["loop", "loop"],
|
|
2202
|
+
["playsinline", "playsInline"],
|
|
2203
|
+
] as const) {
|
|
2204
|
+
if (values.has(attribute)) {
|
|
2205
|
+
options[option] = literalVideoBoolean(
|
|
2206
|
+
values.get(attribute) ?? null,
|
|
2207
|
+
attribute,
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
const poster = values.get("poster");
|
|
2213
|
+
if (typeof poster === "string" && poster.trim()) {
|
|
2214
|
+
options.posterImage = safeLiteralMediaUrl(
|
|
2215
|
+
poster.trim(),
|
|
2216
|
+
"poster",
|
|
2217
|
+
).toString();
|
|
2218
|
+
} else if (values.has("poster")) {
|
|
2219
|
+
throw new Error("Builder video poster must be a literal URL.");
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
const preload = values.get("preload");
|
|
2223
|
+
if (typeof preload === "string" && preload.trim()) {
|
|
2224
|
+
if (!new Set(["none", "metadata", "auto"]).has(preload)) {
|
|
2225
|
+
throw new Error("Builder video preload must be none, metadata, or auto.");
|
|
2226
|
+
}
|
|
2227
|
+
options.preload = preload;
|
|
2228
|
+
} else if (values.has("preload")) {
|
|
2229
|
+
throw new Error("Builder video preload must have a literal value.");
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
for (const dimension of ["width", "height"] as const) {
|
|
2233
|
+
const value = values.get(dimension);
|
|
2234
|
+
if (typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0) {
|
|
2235
|
+
options[dimension] = Number(value);
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
if (values.has(dimension)) {
|
|
2239
|
+
throw new Error(`Builder video ${dimension} must be a positive integer.`);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const title = values.get("title");
|
|
2244
|
+
if (title === null && values.has("title")) {
|
|
2245
|
+
throw new Error("Builder video title must have a literal value.");
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
return {
|
|
2249
|
+
"@type": "@builder.io/sdk:Element",
|
|
2250
|
+
"@version": 2,
|
|
2251
|
+
id: `builder-mdx-video-${stableHash(raw).slice(0, 16)}`,
|
|
2252
|
+
component: {
|
|
2253
|
+
name: "Video",
|
|
2254
|
+
options,
|
|
2255
|
+
},
|
|
2256
|
+
responsiveStyles: {
|
|
2257
|
+
large: {
|
|
2258
|
+
display: "flex",
|
|
2259
|
+
flexDirection: "column",
|
|
2260
|
+
position: "relative",
|
|
2261
|
+
},
|
|
2262
|
+
},
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function builderVideoMdx(options: Record<string, unknown>) {
|
|
2267
|
+
const source =
|
|
2268
|
+
typeof options.video === "string"
|
|
2269
|
+
? options.video.trim()
|
|
2270
|
+
: typeof options.src === "string"
|
|
2271
|
+
? options.src.trim()
|
|
2272
|
+
: "";
|
|
2273
|
+
if (!source) return "";
|
|
2274
|
+
let src: string;
|
|
2275
|
+
try {
|
|
2276
|
+
src = safeLiteralMediaUrl(source, "src").toString();
|
|
2277
|
+
} catch {
|
|
2278
|
+
return "";
|
|
2279
|
+
}
|
|
2280
|
+
if (!new URL(src).pathname.toLowerCase().endsWith(".mp4")) return "";
|
|
2281
|
+
|
|
2282
|
+
const attributes = [`src="${escapeHtml(src)}"`];
|
|
2283
|
+
const poster =
|
|
2284
|
+
typeof options.posterImage === "string" ? options.posterImage.trim() : "";
|
|
2285
|
+
if (poster) {
|
|
2286
|
+
try {
|
|
2287
|
+
attributes.push(
|
|
2288
|
+
`poster="${escapeHtml(safeLiteralMediaUrl(poster, "poster").toString())}"`,
|
|
2289
|
+
);
|
|
2290
|
+
} catch {
|
|
2291
|
+
return "";
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
if (
|
|
2295
|
+
typeof options.preload === "string" &&
|
|
2296
|
+
new Set(["none", "metadata", "auto"]).has(options.preload)
|
|
2297
|
+
) {
|
|
2298
|
+
attributes.push(`preload="${options.preload}"`);
|
|
2299
|
+
}
|
|
2300
|
+
for (const [option, attribute] of [
|
|
2301
|
+
["autoPlay", "autoplay"],
|
|
2302
|
+
["controls", "controls"],
|
|
2303
|
+
["muted", "muted"],
|
|
2304
|
+
["loop", "loop"],
|
|
2305
|
+
["playsInline", "playsinline"],
|
|
2306
|
+
] as const) {
|
|
2307
|
+
if (options[option] === true) attributes.push(attribute);
|
|
2308
|
+
}
|
|
2309
|
+
for (const dimension of ["width", "height"] as const) {
|
|
2310
|
+
const value = options[dimension];
|
|
2311
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
2312
|
+
attributes.push(`${dimension}="${value}"`);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return `<video ${attributes.join(" ")}></video>`;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
1793
2318
|
function rawBlockForData(
|
|
1794
2319
|
data: BuilderRawRefData,
|
|
1795
2320
|
sidecars: Record<string, string>,
|
|
@@ -1992,6 +2517,17 @@ export async function builderMdxBodyToBuilderBlocks(
|
|
|
1992
2517
|
child.type === "mdxJsxFlowElement" ||
|
|
1993
2518
|
child.type === "mdxJsxTextElement"
|
|
1994
2519
|
) {
|
|
2520
|
+
if (isEmptyEditorBlockSentinel(child, raw)) {
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
if (child.name === "video") {
|
|
2524
|
+
blocks.push(freshVideoBlock(child, raw));
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
if (child.name === "table") {
|
|
2528
|
+
blocks.push(freshTableBlock(child, raw));
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
1995
2531
|
const block = await blockFromMdxComponent(raw, sidecars);
|
|
1996
2532
|
if (block) {
|
|
1997
2533
|
blocks.push(block);
|
|
@@ -2006,6 +2542,16 @@ export async function builderMdxBodyToBuilderBlocks(
|
|
|
2006
2542
|
"Unsupported Builder MDX syntax: import/export statements cannot be pushed to Builder.",
|
|
2007
2543
|
);
|
|
2008
2544
|
}
|
|
2545
|
+
if (containsUnsupportedEmptyBlockSyntax(child, raw)) {
|
|
2546
|
+
throw new Error(
|
|
2547
|
+
"Unsupported Builder MDX component: <empty-block>. Only an attribute-free, self-closing Content editor sentinel can be omitted.",
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
const splitBlocks = nodeBlocksWithStandaloneImages(body, child);
|
|
2551
|
+
if (splitBlocks) {
|
|
2552
|
+
blocks.push(...splitBlocks);
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2009
2555
|
const imageBlock = freshImageBlock(raw);
|
|
2010
2556
|
if (imageBlock) {
|
|
2011
2557
|
blocks.push(imageBlock);
|
|
@@ -2071,10 +2617,134 @@ function inlineHtmlToMarkdown(value: string) {
|
|
|
2071
2617
|
.trim();
|
|
2072
2618
|
}
|
|
2073
2619
|
|
|
2620
|
+
interface HtmlListItem {
|
|
2621
|
+
html: string;
|
|
2622
|
+
children: HtmlList[];
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
interface HtmlList {
|
|
2626
|
+
type: "ul" | "ol";
|
|
2627
|
+
items: HtmlListItem[];
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
function parseHtmlList(value: string): HtmlList | null {
|
|
2631
|
+
const roots: HtmlList[] = [];
|
|
2632
|
+
const lists: HtmlList[] = [];
|
|
2633
|
+
const items: Array<HtmlListItem | null> = [];
|
|
2634
|
+
const tokenPattern = /<\/?(?:ul|ol|li)\b[^>]*>/gi;
|
|
2635
|
+
let cursor = 0;
|
|
2636
|
+
|
|
2637
|
+
const appendText = (text: string) => {
|
|
2638
|
+
const item = items[items.length - 1];
|
|
2639
|
+
if (item) item.html += text;
|
|
2640
|
+
};
|
|
2641
|
+
|
|
2642
|
+
for (const match of value.matchAll(tokenPattern)) {
|
|
2643
|
+
appendText(value.slice(cursor, match.index));
|
|
2644
|
+
cursor = (match.index ?? 0) + match[0].length;
|
|
2645
|
+
const closing = /^<\//.test(match[0]);
|
|
2646
|
+
const tag = match[0].match(/^<\/?(ul|ol|li)\b/i)?.[1]?.toLowerCase();
|
|
2647
|
+
if (!tag) continue;
|
|
2648
|
+
|
|
2649
|
+
if ((tag === "ul" || tag === "ol") && !closing) {
|
|
2650
|
+
const list: HtmlList = { type: tag, items: [] };
|
|
2651
|
+
const parentItem = items[items.length - 1];
|
|
2652
|
+
if (parentItem) parentItem.children.push(list);
|
|
2653
|
+
else roots.push(list);
|
|
2654
|
+
lists.push(list);
|
|
2655
|
+
items.push(null);
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
if ((tag === "ul" || tag === "ol") && closing) {
|
|
2659
|
+
if (lists[lists.length - 1]?.type !== tag) return null;
|
|
2660
|
+
lists.pop();
|
|
2661
|
+
items.pop();
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
if (tag === "li" && !closing) {
|
|
2665
|
+
const list = lists[lists.length - 1];
|
|
2666
|
+
if (!list) return null;
|
|
2667
|
+
const item: HtmlListItem = { html: "", children: [] };
|
|
2668
|
+
list.items.push(item);
|
|
2669
|
+
items[items.length - 1] = item;
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
if (tag === "li" && closing) {
|
|
2673
|
+
if (!items[items.length - 1]) return null;
|
|
2674
|
+
items[items.length - 1] = null;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
appendText(value.slice(cursor));
|
|
2678
|
+
if (lists.length || roots.length !== 1) return null;
|
|
2679
|
+
return roots[0];
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
function renderHtmlListMarkdown(list: HtmlList, depth = 0): string {
|
|
2683
|
+
return list.items
|
|
2684
|
+
.map((item, index) => {
|
|
2685
|
+
const indent = " ".repeat(depth);
|
|
2686
|
+
const marker = list.type === "ol" ? `${index + 1}.` : "-";
|
|
2687
|
+
const body = inlineHtmlToMarkdown(item.html);
|
|
2688
|
+
const line = `${indent}${marker}${body ? ` ${body}` : ""}`;
|
|
2689
|
+
const children = item.children.map((child) =>
|
|
2690
|
+
renderHtmlListMarkdown(child, depth + 1),
|
|
2691
|
+
);
|
|
2692
|
+
return [line, ...children].join("\n");
|
|
2693
|
+
})
|
|
2694
|
+
.join("\n");
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
function replaceHtmlListsWithMarkdown(value: string) {
|
|
2698
|
+
const openingPattern = /<(ul|ol)\b[^>]*>/gi;
|
|
2699
|
+
const listTagPattern = /<\/?(ul|ol)\b[^>]*>/gi;
|
|
2700
|
+
let result = "";
|
|
2701
|
+
let cursor = 0;
|
|
2702
|
+
|
|
2703
|
+
while (cursor < value.length) {
|
|
2704
|
+
openingPattern.lastIndex = cursor;
|
|
2705
|
+
const opening = openingPattern.exec(value);
|
|
2706
|
+
if (!opening) {
|
|
2707
|
+
result += value.slice(cursor);
|
|
2708
|
+
break;
|
|
2709
|
+
}
|
|
2710
|
+
result += value.slice(cursor, opening.index);
|
|
2711
|
+
listTagPattern.lastIndex = opening.index;
|
|
2712
|
+
let depth = 0;
|
|
2713
|
+
let end = -1;
|
|
2714
|
+
for (
|
|
2715
|
+
let tag = listTagPattern.exec(value);
|
|
2716
|
+
tag;
|
|
2717
|
+
tag = listTagPattern.exec(value)
|
|
2718
|
+
) {
|
|
2719
|
+
depth += /^<\//.test(tag[0]) ? -1 : 1;
|
|
2720
|
+
if (depth === 0) {
|
|
2721
|
+
end = listTagPattern.lastIndex;
|
|
2722
|
+
break;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
if (end < 0) {
|
|
2726
|
+
result += value.slice(opening.index);
|
|
2727
|
+
break;
|
|
2728
|
+
}
|
|
2729
|
+
const source = value.slice(opening.index, end);
|
|
2730
|
+
const list = parseHtmlList(source);
|
|
2731
|
+
result += list ? `\n${renderHtmlListMarkdown(list)}\n` : source;
|
|
2732
|
+
cursor = end;
|
|
2733
|
+
}
|
|
2734
|
+
return result;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2074
2737
|
export function htmlToMarkdown(html: string) {
|
|
2075
2738
|
let source = html.trim();
|
|
2076
2739
|
if (!source) return "";
|
|
2077
|
-
source = source
|
|
2740
|
+
source = replaceHtmlListsWithMarkdown(source)
|
|
2741
|
+
.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_match, body) => {
|
|
2742
|
+
const quote = htmlToMarkdown(body);
|
|
2743
|
+
return `\n${quote
|
|
2744
|
+
.split("\n")
|
|
2745
|
+
.map((line) => `> ${line}`.trimEnd())
|
|
2746
|
+
.join("\n")}\n`;
|
|
2747
|
+
})
|
|
2078
2748
|
.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_match, level, body) => {
|
|
2079
2749
|
return `\n${"#".repeat(Number(level))} ${inlineHtmlToMarkdown(body)}\n`;
|
|
2080
2750
|
})
|
|
@@ -2084,7 +2754,7 @@ export function htmlToMarkdown(html: string) {
|
|
|
2084
2754
|
.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_match, body) => {
|
|
2085
2755
|
return `\n- ${inlineHtmlToMarkdown(body)}\n`;
|
|
2086
2756
|
})
|
|
2087
|
-
.replace(/<\/?(ul|ol
|
|
2757
|
+
.replace(/<\/?(ul|ol)[^>]*>/gi, "\n");
|
|
2088
2758
|
return inlineHtmlToMarkdown(source)
|
|
2089
2759
|
.split(/\n{2,}/)
|
|
2090
2760
|
.map((part) => part.trim())
|
|
@@ -2112,10 +2782,17 @@ export function markdownToBuilderTextHtml(markdown: string) {
|
|
|
2112
2782
|
const lines = markdown.trim().split(/\r?\n/);
|
|
2113
2783
|
const html: string[] = [];
|
|
2114
2784
|
let listItems: string[] = [];
|
|
2785
|
+
let listType: "ul" | "ol" | null = null;
|
|
2115
2786
|
const flushList = () => {
|
|
2116
|
-
if (!listItems.length) return;
|
|
2117
|
-
html.push(
|
|
2787
|
+
if (!listItems.length || !listType) return;
|
|
2788
|
+
html.push(`<${listType}>${listItems.join("")}</${listType}>`);
|
|
2118
2789
|
listItems = [];
|
|
2790
|
+
listType = null;
|
|
2791
|
+
};
|
|
2792
|
+
const appendListItem = (type: "ul" | "ol", value: string) => {
|
|
2793
|
+
if (listType && listType !== type) flushList();
|
|
2794
|
+
listType = type;
|
|
2795
|
+
listItems.push(`<li>${inlineMarkdownToHtml(value)}</li>`);
|
|
2119
2796
|
};
|
|
2120
2797
|
for (const line of lines) {
|
|
2121
2798
|
const trimmed = line.trim();
|
|
@@ -2130,9 +2807,22 @@ export function markdownToBuilderTextHtml(markdown: string) {
|
|
|
2130
2807
|
html.push(`<h${level}>${inlineMarkdownToHtml(heading[2])}</h${level}>`);
|
|
2131
2808
|
continue;
|
|
2132
2809
|
}
|
|
2133
|
-
const
|
|
2134
|
-
if (
|
|
2135
|
-
|
|
2810
|
+
const quote = trimmed.match(/^>\s?(.*)$/);
|
|
2811
|
+
if (quote) {
|
|
2812
|
+
flushList();
|
|
2813
|
+
html.push(
|
|
2814
|
+
`<blockquote><p>${inlineMarkdownToHtml(quote[1])}</p></blockquote>`,
|
|
2815
|
+
);
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
const unorderedList = trimmed.match(/^[-*]\s+(.+)$/);
|
|
2819
|
+
if (unorderedList) {
|
|
2820
|
+
appendListItem("ul", unorderedList[1]);
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
const orderedList = trimmed.match(/^\d+\.\s+(.+)$/);
|
|
2824
|
+
if (orderedList) {
|
|
2825
|
+
appendListItem("ol", orderedList[1]);
|
|
2136
2826
|
continue;
|
|
2137
2827
|
}
|
|
2138
2828
|
flushList();
|