@agent-native/core 0.101.7 → 0.101.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/deployment.mdx +9 -7
  4. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
  5. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
  6. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
  7. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
  8. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
  9. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
  10. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
  11. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
  12. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
  13. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/client.ts +23 -1
  16. package/corpus/core/src/a2a/handlers.ts +65 -1
  17. package/corpus/core/src/a2a/server.ts +124 -0
  18. package/corpus/core/src/a2a/task-store.ts +253 -0
  19. package/corpus/core/src/a2a/types.ts +26 -0
  20. package/corpus/core/src/agent/production-agent.ts +5 -2
  21. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  22. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  23. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  24. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  25. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  26. package/corpus/core/src/mcp/builtin-tools.ts +25 -1
  27. package/corpus/core/src/scripts/call-agent.ts +22 -3
  28. package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
  29. package/corpus/core/src/server/auth.ts +25 -1
  30. package/corpus/core/src/server/builder-browser.ts +353 -1
  31. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  32. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
  33. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  34. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  35. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  36. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  37. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  38. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  39. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  40. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  41. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
  42. package/corpus/templates/content/AGENTS.md +3 -2
  43. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  44. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  45. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  46. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  47. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  48. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  49. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  50. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  51. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  52. package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
  53. package/corpus/templates/content/actions/_database-utils.ts +89 -12
  54. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  55. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  56. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  57. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  58. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  59. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  60. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  61. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  62. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  63. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  64. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  65. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
  66. package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
  67. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
  68. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  69. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  70. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
  71. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  72. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  73. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
  74. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  75. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  76. package/corpus/templates/content/app/global.css +71 -1
  77. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  78. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  79. package/corpus/templates/content/app/i18n/zh-TW.ts +38 -0
  80. package/corpus/templates/content/app/i18n-data.ts +450 -0
  81. package/corpus/templates/content/app/root.tsx +2 -0
  82. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  83. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  93. package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
  94. package/corpus/templates/content/package.json +1 -0
  95. package/corpus/templates/content/parity/matrix.md +27 -25
  96. package/corpus/templates/content/parity/matrix.ts +50 -2
  97. package/corpus/templates/content/server/db/schema.ts +47 -0
  98. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  99. package/corpus/templates/content/server/plugins/db.ts +53 -0
  100. package/corpus/templates/content/shared/api.ts +70 -1
  101. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  102. package/corpus/templates/content/shared/document-export.ts +296 -25
  103. package/corpus/templates/content/shared/inline-math.ts +128 -0
  104. package/corpus/templates/content/shared/math-rendering.ts +50 -0
  105. package/corpus/templates/content/shared/nfm.ts +20 -17
  106. package/corpus/templates/content/vite.config.ts +0 -1
  107. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  108. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  109. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  110. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  111. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  112. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  113. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  114. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  115. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  116. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  117. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  118. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  119. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  120. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  121. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  122. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  123. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  124. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  125. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  126. package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
  127. package/corpus/templates/mail/AGENTS.md +21 -19
  128. package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
  129. package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
  130. package/corpus/templates/mail/server/handlers/media.ts +84 -61
  131. package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
  132. package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
  133. package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
  134. package/corpus/templates/mail/server/plugins/auth.ts +9 -1
  135. package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
  136. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  137. package/dist/a2a/client.d.ts +5 -1
  138. package/dist/a2a/client.d.ts.map +1 -1
  139. package/dist/a2a/client.js +18 -1
  140. package/dist/a2a/client.js.map +1 -1
  141. package/dist/a2a/handlers.d.ts.map +1 -1
  142. package/dist/a2a/handlers.js +57 -8
  143. package/dist/a2a/handlers.js.map +1 -1
  144. package/dist/a2a/server.d.ts.map +1 -1
  145. package/dist/a2a/server.js +87 -1
  146. package/dist/a2a/server.js.map +1 -1
  147. package/dist/a2a/task-store.d.ts +27 -0
  148. package/dist/a2a/task-store.d.ts.map +1 -1
  149. package/dist/a2a/task-store.js +216 -0
  150. package/dist/a2a/task-store.js.map +1 -1
  151. package/dist/a2a/types.d.ts +24 -0
  152. package/dist/a2a/types.d.ts.map +1 -1
  153. package/dist/a2a/types.js.map +1 -1
  154. package/dist/agent/production-agent.d.ts +1 -0
  155. package/dist/agent/production-agent.d.ts.map +1 -1
  156. package/dist/agent/production-agent.js +5 -2
  157. package/dist/agent/production-agent.js.map +1 -1
  158. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  159. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  160. package/dist/client/review/ReviewCommentComposer.js +3 -3
  161. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  162. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  163. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  164. package/dist/client/review/ReviewThreadPanel.js +6 -4
  165. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  166. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  167. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  168. package/dist/client/settings/SettingsPanel.js +11 -4
  169. package/dist/client/settings/SettingsPanel.js.map +1 -1
  170. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  171. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  172. package/dist/client/settings/useBuilderStatus.js +19 -6
  173. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  174. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  175. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  176. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  177. package/dist/collab/awareness.d.ts +2 -2
  178. package/dist/collab/awareness.d.ts.map +1 -1
  179. package/dist/collab/struct-routes.d.ts +1 -1
  180. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  181. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  182. package/dist/mcp/builtin-tools.js +22 -3
  183. package/dist/mcp/builtin-tools.js.map +1 -1
  184. package/dist/notifications/routes.d.ts +3 -3
  185. package/dist/resources/handlers.d.ts +2 -2
  186. package/dist/scripts/call-agent.d.ts +1 -1
  187. package/dist/scripts/call-agent.d.ts.map +1 -1
  188. package/dist/scripts/call-agent.js +19 -1
  189. package/dist/scripts/call-agent.js.map +1 -1
  190. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  191. package/dist/server/agent-chat-plugin.js +71 -2
  192. package/dist/server/agent-chat-plugin.js.map +1 -1
  193. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  194. package/dist/server/auth.d.ts +1 -0
  195. package/dist/server/auth.d.ts.map +1 -1
  196. package/dist/server/auth.js +19 -2
  197. package/dist/server/auth.js.map +1 -1
  198. package/dist/server/builder-browser.d.ts +78 -0
  199. package/dist/server/builder-browser.d.ts.map +1 -1
  200. package/dist/server/builder-browser.js +246 -1
  201. package/dist/server/builder-browser.js.map +1 -1
  202. package/dist/server/core-routes-plugin.d.ts +41 -0
  203. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  204. package/dist/server/core-routes-plugin.js +260 -14
  205. package/dist/server/core-routes-plugin.js.map +1 -1
  206. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
  207. package/docs/content/deployment.mdx +9 -7
  208. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  209. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  210. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  211. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  212. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  213. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  214. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  215. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  216. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  217. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  218. package/package.json +2 -2
  219. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
@@ -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(blocks);
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
- return htmlToMarkdown(String(options.text ?? "")).trim();
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 baseline = htmlToMarkdown(String(options.text ?? "")).trim();
1290
- if (baseline) segments.push({ kind: "text", block, baseline });
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) return null;
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: image.src,
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|blockquote)[^>]*>/gi, "\n");
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(`<ul>${listItems.join("")}</ul>`);
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 list = trimmed.match(/^[-*]\s+(.+)$/);
2134
- if (list) {
2135
- listItems.push(`<li>${inlineMarkdownToHtml(list[1])}</li>`);
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();