@agent-native/core 0.82.0 → 0.83.0
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 +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/CommandMenu.tsx +34 -12
- package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
- package/corpus/templates/content/.env.local.example +6 -0
- package/corpus/templates/content/AGENTS.md +10 -1
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
- package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
- package/corpus/templates/content/actions/_database-utils.ts +41 -0
- package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
- package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
- package/corpus/templates/content/actions/list-content-databases.ts +41 -33
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
- package/corpus/templates/content/actions/update-document.ts +93 -0
- package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +45 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
- package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
- package/corpus/templates/content/app/global.css +10 -3
- package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
- package/corpus/templates/content/app/i18n-data.ts +212 -0
- package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
- package/corpus/templates/content/app/root.tsx +236 -8
- package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
- package/corpus/templates/content/package.json +3 -1
- package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
- package/corpus/templates/content/scripts/dev-database.mjs +83 -0
- package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +33 -0
- package/corpus/templates/content/shared/api.ts +45 -0
- package/corpus/templates/content/shared/builder-mdx.ts +905 -5
- package/corpus/templates/content/shared/nfm-registry.ts +2 -0
- package/corpus/templates/content/shared/source-component-block.ts +141 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +5 -1
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +229 -131
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +103 -6
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +382 -72
- package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
- package/dist/client/CommandMenu.d.ts +6 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +25 -14
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
parseRegistryBlockData,
|
|
12
12
|
serializeRegistryBlockToMdx,
|
|
13
13
|
} from "./nfm-registry";
|
|
14
|
+
import type {
|
|
15
|
+
SourceComponentData,
|
|
16
|
+
SourceComponentPreview,
|
|
17
|
+
SourceComponentPreviewTable,
|
|
18
|
+
} from "./source-component-block";
|
|
14
19
|
|
|
15
20
|
export const BUILDER_DOCS_CONTENT_ROOT = "content/builder";
|
|
16
21
|
export const BUILDER_DOCS_RAW_ROOT = `${BUILDER_DOCS_CONTENT_ROOT}/.raw`;
|
|
@@ -72,6 +77,11 @@ export interface BuilderBlocksFromMdxResult {
|
|
|
72
77
|
warnings: string[];
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
export interface BuilderReadableBodyMergeResult {
|
|
81
|
+
blocks: unknown[] | null;
|
|
82
|
+
warnings: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
type MdxNode = {
|
|
76
86
|
type: string;
|
|
77
87
|
name?: string;
|
|
@@ -376,6 +386,160 @@ function childBlocks(block: unknown): unknown[] {
|
|
|
376
386
|
return Array.isArray(block.children) ? block.children : [];
|
|
377
387
|
}
|
|
378
388
|
|
|
389
|
+
function isReadableUnsupportedBuilderPlaceholder(value: string) {
|
|
390
|
+
const trimmed = value.trim();
|
|
391
|
+
return (
|
|
392
|
+
/^>\s*Builder .+ component preserved from source\.$/.test(trimmed) ||
|
|
393
|
+
/^<SourceComponent\b/.test(trimmed)
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function countReadableSourceComponentMarkers(markdown: string) {
|
|
398
|
+
return markdownUnits(markdown).filter(isReadableUnsupportedBuilderPlaceholder)
|
|
399
|
+
.length;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function sourceComponentMarkerIdForBlock(block: unknown) {
|
|
403
|
+
return `source-component-builder-${safePathPart(
|
|
404
|
+
builderBlockStableId(block, stableHash(block).slice(0, 12)),
|
|
405
|
+
)}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function builderImageMarkdown(options: Record<string, unknown>) {
|
|
409
|
+
const imageUrl =
|
|
410
|
+
typeof options.image === "string" ? options.image.trim() : "";
|
|
411
|
+
if (!imageUrl) return "";
|
|
412
|
+
const altText =
|
|
413
|
+
typeof options.altText === "string"
|
|
414
|
+
? options.altText.trim()
|
|
415
|
+
: typeof options.alt === "string"
|
|
416
|
+
? options.alt.trim()
|
|
417
|
+
: "";
|
|
418
|
+
return `})`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function escapeMarkdownImageAlt(value: string) {
|
|
424
|
+
return value.replace(/\\/g, "\\\\").replace(/\]/g, "\\]");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function unescapeMarkdownImageAlt(value: string) {
|
|
428
|
+
return value.replace(/\\]/g, "]").replace(/\\\\/g, "\\");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function escapeMarkdownImageUrl(value: string) {
|
|
432
|
+
return value.replace(/\\/g, "%5C").replace(/\)/g, "%29");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function unescapeMarkdownImageUrl(value: string) {
|
|
436
|
+
return value.replace(/%29/gi, ")").replace(/%5C/gi, "\\");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function parseMarkdownImage(markdown: string) {
|
|
440
|
+
const trimmed = markdown.trim();
|
|
441
|
+
const match = trimmed.match(
|
|
442
|
+
/^!\[((?:\\.|[^\]\\])*)\]\((\S+?)(?:\s+"[^"]*")?\)$/,
|
|
443
|
+
);
|
|
444
|
+
if (!match) return null;
|
|
445
|
+
return {
|
|
446
|
+
alt: unescapeMarkdownImageAlt(match[1]),
|
|
447
|
+
src: unescapeMarkdownImageUrl(match[2]),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function readableLayoutFingerprint(markdown: string) {
|
|
452
|
+
const units = markdownUnits(markdown);
|
|
453
|
+
const fingerprint: string[] = [];
|
|
454
|
+
for (const unit of units) {
|
|
455
|
+
if (/^>\s*Builder .+ component preserved from source\.$/.test(unit)) {
|
|
456
|
+
fingerprint.push("source:legacy");
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (/^<SourceComponent\b/.test(unit.trim())) {
|
|
460
|
+
const parsed = await parseRegistryBlockData(unit);
|
|
461
|
+
const id = parsed?.base.id || "missing-id";
|
|
462
|
+
fingerprint.push(`source:${id}`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
fingerprint.push("prose");
|
|
466
|
+
}
|
|
467
|
+
return fingerprint;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function expectedReadableLayoutFingerprint(blocks: unknown[]) {
|
|
471
|
+
const fingerprint: string[] = [];
|
|
472
|
+
for (const block of blocks) {
|
|
473
|
+
if (!isRecord(block)) continue;
|
|
474
|
+
const name = componentName(block);
|
|
475
|
+
const options = componentOptions(block);
|
|
476
|
+
if (name === "Text") {
|
|
477
|
+
for (const unit of markdownUnits(
|
|
478
|
+
htmlToMarkdown(String(options.text ?? "")),
|
|
479
|
+
)) {
|
|
480
|
+
if (unit) fingerprint.push("prose");
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (name === "Code Block" || name === "Blog Code Block") {
|
|
485
|
+
if (readableCodeBlockMarkdown(options).trim()) fingerprint.push("prose");
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (name === "Image") {
|
|
489
|
+
if (builderImageMarkdown(options)) fingerprint.push("prose");
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (name === "Tabbed Content") {
|
|
493
|
+
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
494
|
+
for (const tab of tabs) {
|
|
495
|
+
const content =
|
|
496
|
+
isRecord(tab) && Array.isArray(tab.content) ? tab.content : [];
|
|
497
|
+
if (content.some(builderBlockHasReadableOutput)) {
|
|
498
|
+
fingerprint.push("prose");
|
|
499
|
+
fingerprint.push(
|
|
500
|
+
...(await expectedReadableLayoutFingerprint(content)),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const children = childBlocks(block);
|
|
507
|
+
if (children.length && children.some(builderBlockHasReadableOutput)) {
|
|
508
|
+
fingerprint.push(...(await expectedReadableLayoutFingerprint(children)));
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
fingerprint.push(`source:${sourceComponentMarkerIdForBlock(block)}`);
|
|
512
|
+
}
|
|
513
|
+
return fingerprint;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function validateReadableSourceComponentMarkers(
|
|
517
|
+
markdown: string,
|
|
518
|
+
sidecars: Record<string, string>,
|
|
519
|
+
) {
|
|
520
|
+
const warnings: string[] = [];
|
|
521
|
+
const markers = markdownUnits(markdown).filter((unit) =>
|
|
522
|
+
/^<SourceComponent\b/.test(unit.trim()),
|
|
523
|
+
);
|
|
524
|
+
for (const marker of markers) {
|
|
525
|
+
try {
|
|
526
|
+
const parsed = await parseRegistryBlockData(marker);
|
|
527
|
+
if (parsed?.type !== "source-component") {
|
|
528
|
+
warnings.push("Readable Builder body has an invalid source marker.");
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
rawBlockForData(parsed.data as SourceComponentData, sidecars);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
warnings.push(
|
|
534
|
+
error instanceof Error
|
|
535
|
+
? error.message
|
|
536
|
+
: "Readable Builder body has a source marker that could not be validated.",
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return warnings;
|
|
541
|
+
}
|
|
542
|
+
|
|
379
543
|
function symbolContentEntry(
|
|
380
544
|
symbol: Record<string, unknown>,
|
|
381
545
|
): BuilderContentEntry | null {
|
|
@@ -426,9 +590,319 @@ function blockSummary(block: unknown) {
|
|
|
426
590
|
if (name === "Text" && typeof options.text === "string") {
|
|
427
591
|
return stripHtml(options.text).slice(0, 160);
|
|
428
592
|
}
|
|
593
|
+
if (typeof options.url === "string" && options.url.trim()) {
|
|
594
|
+
return options.url.trim().slice(0, 160);
|
|
595
|
+
}
|
|
596
|
+
if (typeof options.title === "string" && options.title.trim()) {
|
|
597
|
+
return options.title.trim().slice(0, 160);
|
|
598
|
+
}
|
|
429
599
|
return name ?? "Builder block";
|
|
430
600
|
}
|
|
431
601
|
|
|
602
|
+
function countArrayColumns(rows: unknown[]) {
|
|
603
|
+
return rows.reduce<number>((max, row) => {
|
|
604
|
+
if (Array.isArray(row)) return Math.max(max, row.length);
|
|
605
|
+
if (!isRecord(row)) return max;
|
|
606
|
+
for (const key of ["cells", "columns", "values"]) {
|
|
607
|
+
const value = row[key];
|
|
608
|
+
if (Array.isArray(value)) return Math.max(max, value.length);
|
|
609
|
+
}
|
|
610
|
+
return Math.max(max, Object.keys(row).length);
|
|
611
|
+
}, 0);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function tableShapeFromOptions(options: Record<string, unknown>) {
|
|
615
|
+
if (Array.isArray(options.bodyRows)) {
|
|
616
|
+
const columns = options.bodyRows.reduce<number>(
|
|
617
|
+
(max, row) => {
|
|
618
|
+
if (!isRecord(row) || !Array.isArray(row.columns)) return max;
|
|
619
|
+
return Math.max(max, row.columns.length);
|
|
620
|
+
},
|
|
621
|
+
Array.isArray(options.headColumns) ? options.headColumns.length : 0,
|
|
622
|
+
);
|
|
623
|
+
return {
|
|
624
|
+
rows: options.bodyRows.length,
|
|
625
|
+
columns,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const candidates = [
|
|
629
|
+
options.rows,
|
|
630
|
+
options.data,
|
|
631
|
+
isRecord(options.table) ? options.table.rows : undefined,
|
|
632
|
+
isRecord(options.tableData) ? options.tableData.rows : undefined,
|
|
633
|
+
];
|
|
634
|
+
for (const candidate of candidates) {
|
|
635
|
+
if (Array.isArray(candidate)) {
|
|
636
|
+
return {
|
|
637
|
+
rows: candidate.length,
|
|
638
|
+
columns: countArrayColumns(candidate),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const columns = Array.isArray(options.columns) ? options.columns.length : 0;
|
|
643
|
+
if (columns) return { rows: 0, columns };
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function normalizePreviewScalar(value: unknown) {
|
|
648
|
+
if (value === null || value === undefined) return "";
|
|
649
|
+
if (typeof value === "string") return value;
|
|
650
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
651
|
+
return String(value);
|
|
652
|
+
}
|
|
653
|
+
return stableJson(value);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function textFromBuilderBlocks(blocks: unknown[]): string {
|
|
657
|
+
return blocks
|
|
658
|
+
.map((block) => {
|
|
659
|
+
const name = componentName(block);
|
|
660
|
+
const options = componentOptions(block);
|
|
661
|
+
if (name === "Text") {
|
|
662
|
+
return stripHtml(String(options.text ?? "")).trim();
|
|
663
|
+
}
|
|
664
|
+
return textFromBuilderBlocks(childBlocks(block));
|
|
665
|
+
})
|
|
666
|
+
.filter(Boolean)
|
|
667
|
+
.join(" ")
|
|
668
|
+
.trim();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function materialTablePreviewFromOptions(
|
|
672
|
+
options: Record<string, unknown>,
|
|
673
|
+
): SourceComponentPreviewTable | undefined {
|
|
674
|
+
const bodyRows = Array.isArray(options.bodyRows) ? options.bodyRows : [];
|
|
675
|
+
if (!bodyRows.length) return undefined;
|
|
676
|
+
const headColumns = Array.isArray(options.headColumns)
|
|
677
|
+
? options.headColumns
|
|
678
|
+
: [];
|
|
679
|
+
const columnCount = bodyRows.reduce<number>((max, row) => {
|
|
680
|
+
if (!isRecord(row) || !Array.isArray(row.columns)) return max;
|
|
681
|
+
return Math.max(max, row.columns.length);
|
|
682
|
+
}, headColumns.length);
|
|
683
|
+
const columns = Array.from({ length: columnCount }, (_, index) => {
|
|
684
|
+
const head = headColumns[index];
|
|
685
|
+
const label =
|
|
686
|
+
isRecord(head) && typeof head.label === "string" && head.label.trim()
|
|
687
|
+
? head.label.trim()
|
|
688
|
+
: `Column ${index + 1}`;
|
|
689
|
+
return { id: `column-${index + 1}`, label };
|
|
690
|
+
});
|
|
691
|
+
const rows = bodyRows.slice(0, 6).map((row) => {
|
|
692
|
+
const rowColumns =
|
|
693
|
+
isRecord(row) && Array.isArray(row.columns) ? row.columns : [];
|
|
694
|
+
return Object.fromEntries(
|
|
695
|
+
columns.map((column, index) => {
|
|
696
|
+
const cell = rowColumns[index];
|
|
697
|
+
const content =
|
|
698
|
+
isRecord(cell) && Array.isArray(cell.content) ? cell.content : [];
|
|
699
|
+
return [column.id, textFromBuilderBlocks(content)];
|
|
700
|
+
}),
|
|
701
|
+
);
|
|
702
|
+
});
|
|
703
|
+
return {
|
|
704
|
+
columns,
|
|
705
|
+
rows,
|
|
706
|
+
truncated: bodyRows.length > 6,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function tablePreviewFromRows(rows: unknown[]): SourceComponentPreviewTable {
|
|
711
|
+
const records = rows.slice(0, 6).map((row) => {
|
|
712
|
+
if (Array.isArray(row)) {
|
|
713
|
+
return Object.fromEntries(
|
|
714
|
+
row.map((cell, index) => [
|
|
715
|
+
`column-${index + 1}`,
|
|
716
|
+
normalizePreviewScalar(cell),
|
|
717
|
+
]),
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
if (!isRecord(row)) return { value: normalizePreviewScalar(row) };
|
|
721
|
+
if (Array.isArray(row.cells)) {
|
|
722
|
+
return Object.fromEntries(
|
|
723
|
+
row.cells.map((cell, index) => [
|
|
724
|
+
`column-${index + 1}`,
|
|
725
|
+
normalizePreviewScalar(cell),
|
|
726
|
+
]),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return Object.fromEntries(
|
|
730
|
+
Object.entries(row)
|
|
731
|
+
.slice(0, 12)
|
|
732
|
+
.map(([key, value]) => [key, normalizePreviewScalar(value)]),
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
const columnIds = Array.from(
|
|
736
|
+
new Set(records.flatMap((record) => Object.keys(record))),
|
|
737
|
+
).slice(0, 12);
|
|
738
|
+
return {
|
|
739
|
+
columns: columnIds.map((id, index) => ({
|
|
740
|
+
id,
|
|
741
|
+
label: id.startsWith("column-") ? `Column ${index + 1}` : id,
|
|
742
|
+
})),
|
|
743
|
+
rows: records.map((record) =>
|
|
744
|
+
Object.fromEntries(columnIds.map((id) => [id, record[id] ?? ""])),
|
|
745
|
+
),
|
|
746
|
+
truncated: rows.length > 6 || columnIds.length > 12,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function tablePreviewFromOptions(
|
|
751
|
+
options: Record<string, unknown>,
|
|
752
|
+
): SourceComponentPreviewTable | undefined {
|
|
753
|
+
const materialTable = materialTablePreviewFromOptions(options);
|
|
754
|
+
if (materialTable) return materialTable;
|
|
755
|
+
const candidates = [
|
|
756
|
+
options.rows,
|
|
757
|
+
options.data,
|
|
758
|
+
isRecord(options.table) ? options.table.rows : undefined,
|
|
759
|
+
isRecord(options.tableData) ? options.tableData.rows : undefined,
|
|
760
|
+
];
|
|
761
|
+
for (const candidate of candidates) {
|
|
762
|
+
if (Array.isArray(candidate)) return tablePreviewFromRows(candidate);
|
|
763
|
+
}
|
|
764
|
+
const columns = Array.isArray(options.columns)
|
|
765
|
+
? options.columns.slice(0, 12).map((column, index) => {
|
|
766
|
+
if (typeof column === "string" && column.trim()) {
|
|
767
|
+
return { id: column, label: column };
|
|
768
|
+
}
|
|
769
|
+
if (isRecord(column)) {
|
|
770
|
+
const id =
|
|
771
|
+
stringFromRecord(column, ["id", "key", "name", "field"]) ??
|
|
772
|
+
`column-${index + 1}`;
|
|
773
|
+
const label =
|
|
774
|
+
stringFromRecord(column, ["label", "title", "name"]) ?? id;
|
|
775
|
+
return { id, label };
|
|
776
|
+
}
|
|
777
|
+
return { id: `column-${index + 1}`, label: `Column ${index + 1}` };
|
|
778
|
+
})
|
|
779
|
+
: [];
|
|
780
|
+
return columns.length ? { columns, rows: [], truncated: false } : undefined;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function pluralize(count: number, singular: string) {
|
|
784
|
+
return `${count} ${singular}${count === 1 ? "" : "s"}`;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function builderSourceComponentPreview(
|
|
788
|
+
block: unknown,
|
|
789
|
+
): Pick<
|
|
790
|
+
SourceComponentData,
|
|
791
|
+
"previewKind" | "previewUrl" | "previewItems" | "preview" | "summary"
|
|
792
|
+
> {
|
|
793
|
+
const name = componentName(block);
|
|
794
|
+
const options = componentOptions(block);
|
|
795
|
+
const url = typeof options.url === "string" ? options.url.trim() : "";
|
|
796
|
+
if (name === "Embed") {
|
|
797
|
+
const summary = url ? `Embedded URL: ${url}` : "Embedded Builder content.";
|
|
798
|
+
return {
|
|
799
|
+
previewKind: "embed",
|
|
800
|
+
previewUrl: url || undefined,
|
|
801
|
+
previewItems: url ? [url] : undefined,
|
|
802
|
+
preview: {
|
|
803
|
+
status: url ? "available" : "unavailable",
|
|
804
|
+
kind: "embed",
|
|
805
|
+
label: "Builder Embed",
|
|
806
|
+
summary,
|
|
807
|
+
url: url || undefined,
|
|
808
|
+
},
|
|
809
|
+
summary,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (name === "Image") {
|
|
813
|
+
const imageUrl =
|
|
814
|
+
typeof options.image === "string" ? options.image.trim() : "";
|
|
815
|
+
const altText =
|
|
816
|
+
typeof options.altText === "string" ? options.altText.trim() : "";
|
|
817
|
+
const summary = altText || "Builder image.";
|
|
818
|
+
return {
|
|
819
|
+
previewKind: "component",
|
|
820
|
+
previewUrl: imageUrl || undefined,
|
|
821
|
+
previewItems: altText ? [altText] : undefined,
|
|
822
|
+
preview: {
|
|
823
|
+
status: imageUrl ? "available" : "unavailable",
|
|
824
|
+
kind: "component",
|
|
825
|
+
label: "Builder Image",
|
|
826
|
+
summary,
|
|
827
|
+
url: imageUrl || undefined,
|
|
828
|
+
fields: altText ? [{ label: "Alt", value: altText }] : undefined,
|
|
829
|
+
},
|
|
830
|
+
summary,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
if (name === "Table" || name?.toLowerCase().includes("table")) {
|
|
834
|
+
const shape = tableShapeFromOptions(options);
|
|
835
|
+
const items = shape
|
|
836
|
+
? [pluralize(shape.rows, "row"), pluralize(shape.columns, "column")]
|
|
837
|
+
: undefined;
|
|
838
|
+
const summary = shape
|
|
839
|
+
? `Builder table with ${items?.join(", ")}.`
|
|
840
|
+
: "Builder table component preserved from source.";
|
|
841
|
+
return {
|
|
842
|
+
previewKind: "table",
|
|
843
|
+
previewItems: items,
|
|
844
|
+
preview: {
|
|
845
|
+
status: shape ? "available" : "unavailable",
|
|
846
|
+
kind: "table",
|
|
847
|
+
label: "Builder Table",
|
|
848
|
+
summary,
|
|
849
|
+
fields: items?.map((item) => {
|
|
850
|
+
const [value, ...label] = item.split(" ");
|
|
851
|
+
return { label: label.join(" "), value };
|
|
852
|
+
}),
|
|
853
|
+
table: tablePreviewFromOptions(options),
|
|
854
|
+
},
|
|
855
|
+
summary,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (name === "Symbol") {
|
|
859
|
+
const symbol = isRecord(options.symbol) ? options.symbol : {};
|
|
860
|
+
const entry = typeof symbol.entry === "string" ? symbol.entry : "";
|
|
861
|
+
const model = typeof symbol.model === "string" ? symbol.model : "";
|
|
862
|
+
const fields = [
|
|
863
|
+
model ? { label: "Model", value: model } : null,
|
|
864
|
+
entry ? { label: "Entry", value: entry } : null,
|
|
865
|
+
].filter((field): field is { label: string; value: string } =>
|
|
866
|
+
Boolean(field),
|
|
867
|
+
);
|
|
868
|
+
const summary =
|
|
869
|
+
model || entry
|
|
870
|
+
? `Builder symbol ${[model, entry].filter(Boolean).join(" / ")}.`
|
|
871
|
+
: "Builder symbol reference.";
|
|
872
|
+
return {
|
|
873
|
+
previewKind: "symbol",
|
|
874
|
+
previewItems: [model, entry].filter(Boolean),
|
|
875
|
+
preview: {
|
|
876
|
+
status: fields.length ? "available" : "unavailable",
|
|
877
|
+
kind: "symbol",
|
|
878
|
+
label: "Builder Symbol",
|
|
879
|
+
summary,
|
|
880
|
+
fields: fields.length ? fields : undefined,
|
|
881
|
+
},
|
|
882
|
+
summary,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const summary = blockSummary(block);
|
|
886
|
+
return {
|
|
887
|
+
previewKind: "component",
|
|
888
|
+
preview: {
|
|
889
|
+
status: name ? "available" : "unavailable",
|
|
890
|
+
kind: "component",
|
|
891
|
+
label: name ? `Builder ${name}` : "Builder component",
|
|
892
|
+
summary,
|
|
893
|
+
},
|
|
894
|
+
summary,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function builderBlockStableId(block: unknown, fallback: string) {
|
|
899
|
+
if (isRecord(block)) {
|
|
900
|
+
const id = stringFromRecord(block, ["id"]);
|
|
901
|
+
if (id) return id;
|
|
902
|
+
}
|
|
903
|
+
return fallback;
|
|
904
|
+
}
|
|
905
|
+
|
|
432
906
|
function rawRefData(args: {
|
|
433
907
|
block: unknown;
|
|
434
908
|
rawRef: string;
|
|
@@ -462,6 +936,29 @@ async function builderBlocksToMdxBody(
|
|
|
462
936
|
return mdxBlocks.filter(Boolean).join("\n\n").trim();
|
|
463
937
|
}
|
|
464
938
|
|
|
939
|
+
async function builderBlocksToReadableMdxBody(
|
|
940
|
+
blocks: unknown[],
|
|
941
|
+
ctx: BlocksToMdxContext,
|
|
942
|
+
) {
|
|
943
|
+
const mdxBlocks: string[] = [];
|
|
944
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
945
|
+
const block = blocks[index];
|
|
946
|
+
const rawRef = rawSidecarPath({ rawRoot: ctx.rawRoot, block, index });
|
|
947
|
+
ctx.files[rawRef] = stableJson(block);
|
|
948
|
+
mdxBlocks.push(await builderBlockToReadableMdx(block, rawRef, ctx));
|
|
949
|
+
}
|
|
950
|
+
return mdxBlocks.filter(Boolean).join("\n\n").trim();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
export async function builderBlocksToReadableMarkdown(blocks: unknown[]) {
|
|
954
|
+
return builderBlocksToReadableMdxBody(blocks, {
|
|
955
|
+
rawRoot: `${BUILDER_DOCS_RAW_ROOT}/readable`,
|
|
956
|
+
files: {},
|
|
957
|
+
warnings: [],
|
|
958
|
+
emitReferencedEntry: async (entry) => builderMdxPathForEntry(entry),
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
465
962
|
async function builderBlockToMdx(
|
|
466
963
|
block: unknown,
|
|
467
964
|
rawRef: string,
|
|
@@ -578,6 +1075,12 @@ async function builderBlockToMdx(
|
|
|
578
1075
|
});
|
|
579
1076
|
}
|
|
580
1077
|
|
|
1078
|
+
const children = childBlocks(block);
|
|
1079
|
+
if (children.length) {
|
|
1080
|
+
const childBody = await builderBlocksToMdxBody(children, ctx);
|
|
1081
|
+
if (childBody) return childBody;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
581
1084
|
const data: BuilderRawBlockData = {
|
|
582
1085
|
...raw,
|
|
583
1086
|
summary: blockSummary(block),
|
|
@@ -586,7 +1089,6 @@ async function builderBlockToMdx(
|
|
|
586
1089
|
id,
|
|
587
1090
|
data,
|
|
588
1091
|
});
|
|
589
|
-
const children = childBlocks(block);
|
|
590
1092
|
if (children.length) {
|
|
591
1093
|
ctx.warnings.push(
|
|
592
1094
|
`${name ?? "Unmodeled block"} has child blocks that are preserved only in the raw sidecar.`,
|
|
@@ -595,6 +1097,351 @@ async function builderBlockToMdx(
|
|
|
595
1097
|
return serialized;
|
|
596
1098
|
}
|
|
597
1099
|
|
|
1100
|
+
async function builderBlockToReadableMdx(
|
|
1101
|
+
block: unknown,
|
|
1102
|
+
rawRef: string,
|
|
1103
|
+
ctx: BlocksToMdxContext,
|
|
1104
|
+
) {
|
|
1105
|
+
const name = componentName(block);
|
|
1106
|
+
const options = componentOptions(block);
|
|
1107
|
+
|
|
1108
|
+
if (name === "Text") {
|
|
1109
|
+
return htmlToMarkdown(String(options.text ?? "")).trim();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (name === "Code Block" || name === "Blog Code Block") {
|
|
1113
|
+
const language =
|
|
1114
|
+
typeof options.language === "string" ? options.language.trim() : "";
|
|
1115
|
+
const code = String(options.code ?? "").trimEnd();
|
|
1116
|
+
return code ? `\`\`\`${language}\n${code}\n\`\`\`` : "";
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (name === "Image") {
|
|
1120
|
+
return builderImageMarkdown(options);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (name === "Tabbed Content") {
|
|
1124
|
+
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
1125
|
+
const convertedTabs = await Promise.all(
|
|
1126
|
+
tabs.map(async (tab, tabIndex) => {
|
|
1127
|
+
const tabRecord = isRecord(tab) ? tab : {};
|
|
1128
|
+
const content = Array.isArray(tabRecord.content)
|
|
1129
|
+
? tabRecord.content
|
|
1130
|
+
: [];
|
|
1131
|
+
const label =
|
|
1132
|
+
typeof tabRecord.label === "string" && tabRecord.label.trim()
|
|
1133
|
+
? tabRecord.label.trim()
|
|
1134
|
+
: `Tab ${tabIndex + 1}`;
|
|
1135
|
+
const body = await builderBlocksToReadableMdxBody(content, ctx);
|
|
1136
|
+
return body ? `### ${label}\n\n${body}` : "";
|
|
1137
|
+
}),
|
|
1138
|
+
);
|
|
1139
|
+
return convertedTabs.filter(Boolean).join("\n\n").trim();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const children = childBlocks(block);
|
|
1143
|
+
if (children.length) {
|
|
1144
|
+
const childBody = await builderBlocksToReadableMdxBody(children, ctx);
|
|
1145
|
+
if (childBody) return childBody;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (name) {
|
|
1149
|
+
ctx.warnings.push(
|
|
1150
|
+
`${name} is preserved in the Builder raw sidecar and shown as a read-only source component.`,
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
const data: SourceComponentData = {
|
|
1154
|
+
provider: "builder",
|
|
1155
|
+
componentName: name || "Builder component",
|
|
1156
|
+
rawRef,
|
|
1157
|
+
rawHash: stableHash(block),
|
|
1158
|
+
sourceLabel: "Builder body",
|
|
1159
|
+
previewStatus: name ? "available" : "unavailable",
|
|
1160
|
+
title: name ? `Builder ${name}` : "Builder component",
|
|
1161
|
+
...builderSourceComponentPreview(block),
|
|
1162
|
+
};
|
|
1163
|
+
const id = sourceComponentMarkerIdForBlock(block);
|
|
1164
|
+
return serializeRegistryBlockToMdx("source-component", { id, data });
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function markdownUnits(markdown: string) {
|
|
1168
|
+
const units: string[] = [];
|
|
1169
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
1170
|
+
let current: string[] = [];
|
|
1171
|
+
let fence: string | null = null;
|
|
1172
|
+
const flush = () => {
|
|
1173
|
+
const value = current.join("\n").trim();
|
|
1174
|
+
if (value) units.push(value);
|
|
1175
|
+
current = [];
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
for (const line of lines) {
|
|
1179
|
+
const fenceMatch = line.match(/^(```|~~~)/);
|
|
1180
|
+
if (fenceMatch) {
|
|
1181
|
+
current.push(line);
|
|
1182
|
+
fence = fence ? null : fenceMatch[1];
|
|
1183
|
+
if (!fence) flush();
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (fence) {
|
|
1187
|
+
current.push(line);
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (!line.trim()) {
|
|
1191
|
+
flush();
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
current.push(line);
|
|
1195
|
+
}
|
|
1196
|
+
flush();
|
|
1197
|
+
return units;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function fencedCodeFromMarkdown(markdown: string) {
|
|
1201
|
+
const trimmed = markdown.trim();
|
|
1202
|
+
const match = trimmed.match(/^```[^\n]*\n([\s\S]*?)\n```$/);
|
|
1203
|
+
if (match) return match[1];
|
|
1204
|
+
return trimmed;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
interface ReadableEditableSegment {
|
|
1208
|
+
kind: "text" | "code" | "image" | "tab-label";
|
|
1209
|
+
block: Record<string, unknown>;
|
|
1210
|
+
baseline: string;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function readableCodeBlockMarkdown(options: Record<string, unknown>) {
|
|
1214
|
+
const language =
|
|
1215
|
+
typeof options.language === "string" ? options.language.trim() : "";
|
|
1216
|
+
const code = String(options.code ?? "").trimEnd();
|
|
1217
|
+
return code ? `\`\`\`${language}\n${code}\n\`\`\`` : "";
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function collectReadableEditableSegments(
|
|
1221
|
+
blocks: unknown[],
|
|
1222
|
+
segments: ReadableEditableSegment[] = [],
|
|
1223
|
+
) {
|
|
1224
|
+
for (const block of blocks) {
|
|
1225
|
+
if (!isRecord(block)) continue;
|
|
1226
|
+
const name = componentName(block);
|
|
1227
|
+
const options = componentOptions(block);
|
|
1228
|
+
if (name === "Text") {
|
|
1229
|
+
const baseline = htmlToMarkdown(String(options.text ?? "")).trim();
|
|
1230
|
+
if (baseline) segments.push({ kind: "text", block, baseline });
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (name === "Code Block" || name === "Blog Code Block") {
|
|
1234
|
+
const baseline = readableCodeBlockMarkdown(options);
|
|
1235
|
+
if (baseline) segments.push({ kind: "code", block, baseline });
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (name === "Image") {
|
|
1239
|
+
const baseline = builderImageMarkdown(options);
|
|
1240
|
+
if (baseline) segments.push({ kind: "image", block, baseline });
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (name === "Tabbed Content") {
|
|
1244
|
+
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
1245
|
+
for (let index = 0; index < tabs.length; index += 1) {
|
|
1246
|
+
const tab = tabs[index];
|
|
1247
|
+
const tabRecord = isRecord(tab) ? tab : {};
|
|
1248
|
+
const content = Array.isArray(tabRecord.content)
|
|
1249
|
+
? tabRecord.content
|
|
1250
|
+
: [];
|
|
1251
|
+
if (!content.some(builderBlockHasReadableOutput)) continue;
|
|
1252
|
+
const label =
|
|
1253
|
+
typeof tabRecord.label === "string" && tabRecord.label.trim()
|
|
1254
|
+
? tabRecord.label.trim()
|
|
1255
|
+
: `Tab ${index + 1}`;
|
|
1256
|
+
segments.push({
|
|
1257
|
+
kind: "tab-label",
|
|
1258
|
+
block: tabRecord,
|
|
1259
|
+
baseline: `### ${label}`,
|
|
1260
|
+
});
|
|
1261
|
+
collectReadableEditableSegments(content, segments);
|
|
1262
|
+
}
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
collectReadableEditableSegments(childBlocks(block), segments);
|
|
1266
|
+
}
|
|
1267
|
+
return segments;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function builderBlockHasReadableOutput(block: unknown): boolean {
|
|
1271
|
+
if (!isRecord(block)) return false;
|
|
1272
|
+
const name = componentName(block);
|
|
1273
|
+
const options = componentOptions(block);
|
|
1274
|
+
if (name === "Text") {
|
|
1275
|
+
return htmlToMarkdown(String(options.text ?? "")).trim().length > 0;
|
|
1276
|
+
}
|
|
1277
|
+
if (name === "Code Block" || name === "Blog Code Block") {
|
|
1278
|
+
return readableCodeBlockMarkdown(options).trim().length > 0;
|
|
1279
|
+
}
|
|
1280
|
+
if (name === "Image") {
|
|
1281
|
+
return builderImageMarkdown(options).trim().length > 0;
|
|
1282
|
+
}
|
|
1283
|
+
if (name === "Tabbed Content") {
|
|
1284
|
+
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
1285
|
+
return tabs.some((tab) => {
|
|
1286
|
+
const content =
|
|
1287
|
+
isRecord(tab) && Array.isArray(tab.content) ? tab.content : [];
|
|
1288
|
+
return content.some(builderBlockHasReadableOutput);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
const children = childBlocks(block);
|
|
1292
|
+
return children.length ? children.some(builderBlockHasReadableOutput) : true;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function countExpectedReadableSourceComponentMarkers(
|
|
1296
|
+
blocks: unknown[],
|
|
1297
|
+
): number {
|
|
1298
|
+
let count = 0;
|
|
1299
|
+
for (const block of blocks) {
|
|
1300
|
+
if (!isRecord(block)) continue;
|
|
1301
|
+
const name = componentName(block);
|
|
1302
|
+
if (
|
|
1303
|
+
name === "Text" ||
|
|
1304
|
+
name === "Code Block" ||
|
|
1305
|
+
name === "Blog Code Block" ||
|
|
1306
|
+
name === "Image"
|
|
1307
|
+
) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const options = componentOptions(block);
|
|
1311
|
+
if (name === "Tabbed Content") {
|
|
1312
|
+
const tabs = Array.isArray(options.tabs) ? options.tabs : [];
|
|
1313
|
+
for (const tab of tabs) {
|
|
1314
|
+
const content =
|
|
1315
|
+
isRecord(tab) && Array.isArray(tab.content) ? tab.content : [];
|
|
1316
|
+
count += countExpectedReadableSourceComponentMarkers(content);
|
|
1317
|
+
}
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const children = childBlocks(block);
|
|
1321
|
+
if (children.length && children.some(builderBlockHasReadableOutput)) {
|
|
1322
|
+
count += countExpectedReadableSourceComponentMarkers(children);
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
count += 1;
|
|
1326
|
+
}
|
|
1327
|
+
return count;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
export async function builderReadableBodyToBuilderBlocks(args: {
|
|
1331
|
+
localContent: string;
|
|
1332
|
+
losslessContent: string;
|
|
1333
|
+
sidecars: Record<string, string>;
|
|
1334
|
+
}): Promise<BuilderReadableBodyMergeResult> {
|
|
1335
|
+
const baselineBlocks = await builderMdxBodyToBuilderBlocks(
|
|
1336
|
+
args.losslessContent,
|
|
1337
|
+
args.sidecars,
|
|
1338
|
+
);
|
|
1339
|
+
const segments = collectReadableEditableSegments(baselineBlocks);
|
|
1340
|
+
const expectedSourceMarkerCount =
|
|
1341
|
+
countExpectedReadableSourceComponentMarkers(baselineBlocks);
|
|
1342
|
+
const currentSourceMarkerCount = countReadableSourceComponentMarkers(
|
|
1343
|
+
args.localContent,
|
|
1344
|
+
);
|
|
1345
|
+
if (expectedSourceMarkerCount !== currentSourceMarkerCount) {
|
|
1346
|
+
return {
|
|
1347
|
+
blocks: null,
|
|
1348
|
+
warnings: [
|
|
1349
|
+
`Readable Builder body changed preserved source component markers from ${expectedSourceMarkerCount} to ${currentSourceMarkerCount}; refresh or review in Builder before pushing.`,
|
|
1350
|
+
],
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
const sourceMarkerWarnings = await validateReadableSourceComponentMarkers(
|
|
1354
|
+
args.localContent,
|
|
1355
|
+
args.sidecars,
|
|
1356
|
+
);
|
|
1357
|
+
if (sourceMarkerWarnings.length) {
|
|
1358
|
+
return {
|
|
1359
|
+
blocks: null,
|
|
1360
|
+
warnings: sourceMarkerWarnings,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
const [expectedLayout, currentLayout] = await Promise.all([
|
|
1364
|
+
expectedReadableLayoutFingerprint(baselineBlocks),
|
|
1365
|
+
readableLayoutFingerprint(args.localContent),
|
|
1366
|
+
]);
|
|
1367
|
+
if (expectedLayout.join("\n") !== currentLayout.join("\n")) {
|
|
1368
|
+
return {
|
|
1369
|
+
blocks: null,
|
|
1370
|
+
warnings: [
|
|
1371
|
+
"Readable Builder body changed structure or moved or restructured preserved source component markers; refresh or review in Builder before pushing.",
|
|
1372
|
+
],
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
if (!segments.length) {
|
|
1376
|
+
return { blocks: baselineBlocks, warnings: [] };
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const baselineUnitCounts = segments.map(
|
|
1380
|
+
(segment) => markdownUnits(segment.baseline).length,
|
|
1381
|
+
);
|
|
1382
|
+
const currentUnits = markdownUnits(args.localContent).filter(
|
|
1383
|
+
(unit) => !isReadableUnsupportedBuilderPlaceholder(unit),
|
|
1384
|
+
);
|
|
1385
|
+
const expectedUnitCount = baselineUnitCounts.reduce(
|
|
1386
|
+
(sum, count) => sum + count,
|
|
1387
|
+
0,
|
|
1388
|
+
);
|
|
1389
|
+
if (currentUnits.length !== expectedUnitCount) {
|
|
1390
|
+
return {
|
|
1391
|
+
blocks: null,
|
|
1392
|
+
warnings: [
|
|
1393
|
+
`Readable Builder body changed structure from ${expectedUnitCount} markdown blocks to ${currentUnits.length}; review in Builder before pushing.`,
|
|
1394
|
+
],
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
let offset = 0;
|
|
1399
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
1400
|
+
const segment = segments[index];
|
|
1401
|
+
const count = baselineUnitCounts[index];
|
|
1402
|
+
const nextMarkdown = currentUnits
|
|
1403
|
+
.slice(offset, offset + count)
|
|
1404
|
+
.join("\n\n");
|
|
1405
|
+
offset += count;
|
|
1406
|
+
if (segment.kind === "tab-label") {
|
|
1407
|
+
const match = nextMarkdown.trim().match(/^#{3}\s+(.+)$/);
|
|
1408
|
+
if (!match?.[1]?.trim()) {
|
|
1409
|
+
return {
|
|
1410
|
+
blocks: null,
|
|
1411
|
+
warnings: [
|
|
1412
|
+
"Readable Builder tab heading changed into unsupported markdown; keep it as a level-3 heading before pushing.",
|
|
1413
|
+
],
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
segment.block.label = match[1].trim();
|
|
1417
|
+
} else if (segment.kind === "text") {
|
|
1418
|
+
const options = ensureComponentOptions(segment.block, "Text");
|
|
1419
|
+
options.text = markdownToBuilderTextHtml(nextMarkdown);
|
|
1420
|
+
} else if (segment.kind === "image") {
|
|
1421
|
+
const options = ensureComponentOptions(segment.block, "Image");
|
|
1422
|
+
const image = parseMarkdownImage(nextMarkdown);
|
|
1423
|
+
if (!image?.src) {
|
|
1424
|
+
return {
|
|
1425
|
+
blocks: null,
|
|
1426
|
+
warnings: [
|
|
1427
|
+
"Readable Builder image changed into unsupported markdown; keep it as a standard image block before pushing.",
|
|
1428
|
+
],
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
options.image = image.src;
|
|
1432
|
+
options.altText = image.alt;
|
|
1433
|
+
} else {
|
|
1434
|
+
const options = ensureComponentOptions(
|
|
1435
|
+
segment.block,
|
|
1436
|
+
componentName(segment.block) ?? "Code Block",
|
|
1437
|
+
);
|
|
1438
|
+
options.code = fencedCodeFromMarkdown(nextMarkdown);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
return { blocks: baselineBlocks, warnings: [] };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
598
1445
|
export async function builderEntryToMdxBundle(
|
|
599
1446
|
entry: BuilderContentEntry,
|
|
600
1447
|
): Promise<BuilderMdxBundle> {
|
|
@@ -611,18 +1458,37 @@ export async function builderEntryToMdxBundle(
|
|
|
611
1458
|
return { mdx, files, blocks: builderEntryBlocks(entry) };
|
|
612
1459
|
}
|
|
613
1460
|
|
|
1461
|
+
export async function builderEntryToReadableMdxBundle(
|
|
1462
|
+
entry: BuilderContentEntry,
|
|
1463
|
+
): Promise<BuilderMdxBundle> {
|
|
1464
|
+
const files: Record<string, string> = {};
|
|
1465
|
+
const warnings: string[] = [];
|
|
1466
|
+
const emitted = new Set<string>();
|
|
1467
|
+
const mdx = await emitBuilderEntryToMdxFile({
|
|
1468
|
+
entry,
|
|
1469
|
+
files,
|
|
1470
|
+
warnings,
|
|
1471
|
+
emitted,
|
|
1472
|
+
symbolPath: false,
|
|
1473
|
+
readableBody: true,
|
|
1474
|
+
});
|
|
1475
|
+
return { mdx, files, blocks: builderEntryBlocks(entry) };
|
|
1476
|
+
}
|
|
1477
|
+
|
|
614
1478
|
async function emitBuilderEntryToMdxFile({
|
|
615
1479
|
entry,
|
|
616
1480
|
files,
|
|
617
1481
|
warnings,
|
|
618
1482
|
emitted,
|
|
619
1483
|
symbolPath,
|
|
1484
|
+
readableBody = false,
|
|
620
1485
|
}: {
|
|
621
1486
|
entry: BuilderContentEntry;
|
|
622
1487
|
files: Record<string, string>;
|
|
623
1488
|
warnings: string[];
|
|
624
1489
|
emitted: Set<string>;
|
|
625
1490
|
symbolPath: boolean;
|
|
1491
|
+
readableBody?: boolean;
|
|
626
1492
|
}): Promise<BuilderMdxFile> {
|
|
627
1493
|
const path = symbolPath
|
|
628
1494
|
? builderSymbolMdxPathForEntry(entry)
|
|
@@ -663,11 +1529,14 @@ async function emitBuilderEntryToMdxFile({
|
|
|
663
1529
|
warnings,
|
|
664
1530
|
emitted,
|
|
665
1531
|
symbolPath: true,
|
|
1532
|
+
readableBody,
|
|
666
1533
|
});
|
|
667
1534
|
return nested.path;
|
|
668
1535
|
},
|
|
669
1536
|
};
|
|
670
|
-
const body =
|
|
1537
|
+
const body = readableBody
|
|
1538
|
+
? await builderBlocksToReadableMdxBody(blocks, ctx)
|
|
1539
|
+
: await builderBlocksToMdxBody(blocks, ctx);
|
|
671
1540
|
const blocksHash = builderBlocksHash(blocks);
|
|
672
1541
|
const sourceHash = builderSourceHash(entry);
|
|
673
1542
|
const frontmatter = frontmatterForEntry({
|
|
@@ -829,6 +1698,30 @@ function freshTextBlock(markdown: string): unknown {
|
|
|
829
1698
|
};
|
|
830
1699
|
}
|
|
831
1700
|
|
|
1701
|
+
function freshImageBlock(markdown: string): unknown | null {
|
|
1702
|
+
const image = parseMarkdownImage(markdown);
|
|
1703
|
+
if (!image?.src) return null;
|
|
1704
|
+
return {
|
|
1705
|
+
"@type": "@builder.io/sdk:Element",
|
|
1706
|
+
"@version": 2,
|
|
1707
|
+
id: `builder-mdx-image-${stableHash(markdown).slice(0, 16)}`,
|
|
1708
|
+
component: {
|
|
1709
|
+
name: "Image",
|
|
1710
|
+
options: {
|
|
1711
|
+
image: image.src,
|
|
1712
|
+
altText: image.alt,
|
|
1713
|
+
},
|
|
1714
|
+
},
|
|
1715
|
+
responsiveStyles: {
|
|
1716
|
+
large: {
|
|
1717
|
+
display: "flex",
|
|
1718
|
+
flexDirection: "column",
|
|
1719
|
+
position: "relative",
|
|
1720
|
+
},
|
|
1721
|
+
},
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
|
|
832
1725
|
function rawBlockForData(
|
|
833
1726
|
data: BuilderRawRefData,
|
|
834
1727
|
sidecars: Record<string, string>,
|
|
@@ -948,7 +1841,7 @@ async function applyTabbedContentData(
|
|
|
948
1841
|
return {
|
|
949
1842
|
...rawTab,
|
|
950
1843
|
label: tab.label,
|
|
951
|
-
content: await
|
|
1844
|
+
content: await builderMdxBodyToBuilderBlocks(tab.body, sidecars),
|
|
952
1845
|
};
|
|
953
1846
|
}),
|
|
954
1847
|
);
|
|
@@ -1010,12 +1903,14 @@ async function blockFromMdxComponent(
|
|
|
1010
1903
|
return applySymbolData(parsed.data as BuilderSymbolData, sidecars);
|
|
1011
1904
|
case "builder-raw-block":
|
|
1012
1905
|
return rawBlockForData(parsed.data as BuilderRawBlockData, sidecars);
|
|
1906
|
+
case "source-component":
|
|
1907
|
+
return rawBlockForData(parsed.data as SourceComponentData, sidecars);
|
|
1013
1908
|
default:
|
|
1014
1909
|
return null;
|
|
1015
1910
|
}
|
|
1016
1911
|
}
|
|
1017
1912
|
|
|
1018
|
-
async function
|
|
1913
|
+
export async function builderMdxBodyToBuilderBlocks(
|
|
1019
1914
|
body: string,
|
|
1020
1915
|
sidecars: Record<string, string>,
|
|
1021
1916
|
) {
|
|
@@ -1042,6 +1937,11 @@ async function builderBodyToBlocks(
|
|
|
1042
1937
|
"Unsupported Builder MDX syntax: import/export statements cannot be pushed to Builder.",
|
|
1043
1938
|
);
|
|
1044
1939
|
}
|
|
1940
|
+
const imageBlock = freshImageBlock(raw);
|
|
1941
|
+
if (imageBlock) {
|
|
1942
|
+
blocks.push(imageBlock);
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1045
1945
|
blocks.push(freshTextBlock(raw));
|
|
1046
1946
|
}
|
|
1047
1947
|
return blocks;
|
|
@@ -1053,7 +1953,7 @@ export async function builderMdxToBuilderBlocks(args: {
|
|
|
1053
1953
|
sidecars: Record<string, string>;
|
|
1054
1954
|
}): Promise<BuilderBlocksFromMdxResult> {
|
|
1055
1955
|
const mdx = parseBuilderMdxFile(args.path, args.source);
|
|
1056
|
-
const blocks = await
|
|
1956
|
+
const blocks = await builderMdxBodyToBuilderBlocks(mdx.body, args.sidecars);
|
|
1057
1957
|
const blocksHash = builderBlocksHash(blocks);
|
|
1058
1958
|
const sourceHash = stableHash({
|
|
1059
1959
|
model: mdx.metadata.model,
|