@agent-native/core-corpus 0.213.1 → 0.216.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.
Files changed (1108) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +1 -1
  3. package/corpus/templates/analytics/CHANGELOG.md +1 -0
  4. package/corpus/templates/analytics/changelog/2026-10-21-session-replay-quotas-count-the-compressed-size-analytics-st.md +6 -0
  5. package/corpus/templates/analytics/server/lib/session-replay.ts +40 -9
  6. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +1 -1
  7. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +1 -1
  8. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +1 -1
  9. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +1 -1
  10. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +1 -1
  11. package/corpus/templates/community/qr/package.json +1 -1
  12. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +1 -1
  13. package/corpus/templates/content/.agents/skills/content-product-development/references/verification.md +36 -0
  14. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +10 -0
  15. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +26 -1
  16. package/corpus/templates/content/.agents/skills/notion-integration/SKILL.md +4 -2
  17. package/corpus/templates/content/CHANGELOG.md +51 -0
  18. package/corpus/templates/content/acceptance/_writers.ts +604 -0
  19. package/corpus/templates/content/acceptance/generate-action-registry.ts +10 -0
  20. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/create-document.json +75 -0
  21. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/duplicate-page.json +75 -0
  22. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-find-and-replace-without-a-revision.json +75 -0
  23. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-find-and-replace.json +75 -0
  24. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-initializecontent.json +75 -0
  25. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/import-content.json +75 -0
  26. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-import-content-source-existing-page.json +75 -0
  27. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-import-content-source-new-page.json +75 -0
  28. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-sync-local-folder-source-changed-file.json +75 -0
  29. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-sync-local-folder-source-new-file.json +75 -0
  30. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/restore-document-version.json +75 -0
  31. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/set-document-property-primary-blocks-field.json +75 -0
  32. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/set-image-alt-text.json +75 -0
  33. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/undo-content-import.json +75 -0
  34. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/update-document-browser-full-rewrite.json +75 -0
  35. package/corpus/templates/content/acceptance/known-loss.ts +69 -0
  36. package/corpus/templates/content/acceptance/round-trip-corpus.ts +1397 -0
  37. package/corpus/templates/content/actions/_blocks-field-identity.ts +35 -1
  38. package/corpus/templates/content/actions/_content-database-lifecycle.ts +11 -11
  39. package/corpus/templates/content/actions/_content-database-personal-view.ts +2 -0
  40. package/corpus/templates/content/actions/_content-favorites.ts +37 -0
  41. package/corpus/templates/content/actions/_database-membership-lock.ts +39 -8
  42. package/corpus/templates/content/actions/_database-navigation.ts +19 -21
  43. package/corpus/templates/content/actions/_database-row-batch.ts +41 -18
  44. package/corpus/templates/content/actions/_database-row-mutation.ts +17 -28
  45. package/corpus/templates/content/actions/_database-source-utils.ts +18 -1
  46. package/corpus/templates/content/actions/_database-utils.ts +3 -0
  47. package/corpus/templates/content/actions/_database-view-setup.ts +321 -11
  48. package/corpus/templates/content/actions/_document-edit-mutation.ts +33 -32
  49. package/corpus/templates/content/actions/_document-flush.ts +593 -171
  50. package/corpus/templates/content/actions/_document-save-locks.ts +54 -0
  51. package/corpus/templates/content/actions/_document-save-row.ts +133 -0
  52. package/corpus/templates/content/actions/_github-source.ts +18 -8
  53. package/corpus/templates/content/actions/_move-page-to-space.ts +189 -25
  54. package/corpus/templates/content/actions/_nfm-syntax.ts +2 -0
  55. package/corpus/templates/content/actions/_property-utils.ts +3 -0
  56. package/corpus/templates/content/actions/_row-lock-order.ts +115 -0
  57. package/corpus/templates/content/actions/create-content-database.ts +11 -0
  58. package/corpus/templates/content/actions/create-document.ts +13 -1
  59. package/corpus/templates/content/actions/delete-content-database.ts +17 -14
  60. package/corpus/templates/content/actions/delete-document-property.ts +5 -0
  61. package/corpus/templates/content/actions/delete-document.ts +270 -97
  62. package/corpus/templates/content/actions/edit-document.ts +18 -5
  63. package/corpus/templates/content/actions/export-document.ts +3 -0
  64. package/corpus/templates/content/actions/get-blocks-field-word-count.ts +22 -5
  65. package/corpus/templates/content/actions/import-content-source.ts +170 -54
  66. package/corpus/templates/content/actions/move-document.ts +191 -73
  67. package/corpus/templates/content/actions/pull-document.ts +8 -2
  68. package/corpus/templates/content/actions/remove-database-items.ts +23 -4
  69. package/corpus/templates/content/actions/restore-content-database.ts +76 -55
  70. package/corpus/templates/content/actions/restore-document-version.ts +19 -35
  71. package/corpus/templates/content/actions/restore-document.ts +65 -53
  72. package/corpus/templates/content/actions/rollback-created-slash-document.ts +11 -16
  73. package/corpus/templates/content/actions/set-document-property.ts +15 -1
  74. package/corpus/templates/content/actions/share-local-file-document.ts +18 -14
  75. package/corpus/templates/content/actions/suggest-document-edit.ts +2 -3
  76. package/corpus/templates/content/actions/sync-local-folder-source.ts +35 -1
  77. package/corpus/templates/content/actions/undo-content-import.ts +16 -12
  78. package/corpus/templates/content/actions/update-content-database-view.ts +20 -2
  79. package/corpus/templates/content/actions/update-document.ts +19 -14
  80. package/corpus/templates/content/actions/view-screen.ts +12 -0
  81. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -6
  82. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +49 -3
  83. package/corpus/templates/content/app/components/editor/ContentImportDialog.tsx +2 -0
  84. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +53 -11
  85. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +608 -353
  86. package/corpus/templates/content/app/components/editor/StaticDocumentBody.tsx +765 -0
  87. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +233 -80
  88. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +32 -3
  89. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +88 -23
  90. package/corpus/templates/content/app/components/editor/comment-anchors.ts +19 -0
  91. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +296 -87
  92. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
  93. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
  94. package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
  95. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
  96. package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
  97. package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
  98. package/corpus/templates/content/app/components/editor/document-flush-participant.ts +253 -0
  99. package/corpus/templates/content/app/components/editor/editor-transaction-meta.ts +28 -0
  100. package/corpus/templates/content/app/components/editor/extensions/AudioBlock.tsx +2 -1
  101. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +27 -6
  102. package/corpus/templates/content/app/components/editor/extensions/LocalMdxComponentNode.tsx +0 -1
  103. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +4 -4
  104. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +24 -2
  105. package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +25 -6
  106. package/corpus/templates/content/app/components/editor/pending-document-persistence.ts +59 -1
  107. package/corpus/templates/content/app/components/editor/stored-body-images.ts +27 -0
  108. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +17 -0
  109. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -4
  110. package/corpus/templates/content/app/components/trash/TrashBrowser.tsx +4 -1
  111. package/corpus/templates/content/app/global.css +34 -0
  112. package/corpus/templates/content/app/hooks/use-content-trash.ts +8 -0
  113. package/corpus/templates/content/app/i18n-data.ts +32 -0
  114. package/corpus/templates/content/app/import-messages.ts +25 -0
  115. package/corpus/templates/content/app/trash-messages.ts +16 -0
  116. package/corpus/templates/content/changelog/2026-10-16-a-page-open-in-a-tab-that-shows-it-s-in-the-trash-reopens-wi.md +6 -0
  117. package/corpus/templates/content/changelog/2026-10-16-a-page-restored-from-trash-returns-to-its-place-in-its-colle.md +6 -0
  118. package/corpus/templates/content/changelog/2026-10-16-agent-written-markdown-keeps-nested-lists-indented-with-spac.md +6 -0
  119. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-you-can-edit-no-longer-rebuilds-its-editor-on.md +6 -0
  120. package/corpus/templates/content/changelog/2026-10-16-pages-moved-or-imported-at-the-same-time-can-no-longer-end-u.md +6 -0
  121. package/corpus/templates/content/changelog/2026-10-16-re-importing-a-markdown-export-of-a-page-no-longer-reports-t.md +6 -0
  122. package/corpus/templates/content/changelog/2026-10-16-saving-a-page-or-restoring-one-of-its-versions-no-longer-fai.md +6 -0
  123. package/corpus/templates/content/changelog/2026-10-17-agents-reading-a-page-you-have-open-no-longer-wait-or-fail-w.md +6 -0
  124. package/corpus/templates/content/changelog/2026-10-19-fresh-document-reads-require-confirmation-from-every-open-ed.md +6 -0
  125. package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
  126. package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-20-html-table-cells-preserve-a-trailing-backslash-when-opening-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-20-preserve-empty-paragraph-colors-and-refuse-unsupported-inlin.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-20-the-advanced-filter-and-each-filter-group-inside-it-have-the.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-21-blocks-fields-keep-unconfirmed-saves-visible-and-agent-reads.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-21-preserve-compact-table-row-groups-and-report-invalid-block-colors.md +6 -0
  132. package/corpus/templates/content/changelog/2026-10-21-suggested-edits-show-an-agent-s-whole-reason-open-a-group-to.md +6 -0
  133. package/corpus/templates/content/changelog/2026-10-21-suggestions-keep-refusing-unmatched-emphasis-in-stored-markd.md +6 -0
  134. package/corpus/templates/content/changelog/2026-10-22-accept-remaining-and-reject-remaining-show-the-result-for-ev.md +6 -0
  135. package/corpus/templates/content/changelog/2026-10-22-accepting-a-group-of-suggested-edits-saves-faster-and-adds-o.md +6 -0
  136. package/corpus/templates/content/changelog/2026-10-22-agents-read-saved-page-text-after-failed-icon-change.md +6 -0
  137. package/corpus/templates/content/changelog/2026-10-22-changes-made-from-a-menu-or-popover-such-as-an-image-s-alt-t.md +6 -0
  138. package/corpus/templates/content/changelog/2026-10-22-text-an-agent-writes-inside-a-callout-column-or-synced-block.md +6 -0
  139. package/corpus/templates/content/e2e/auth-files.ts +28 -0
  140. package/corpus/templates/content/e2e/convergence-summary.ts +84 -29
  141. package/corpus/templates/content/e2e/gate-arm.ts +15 -0
  142. package/corpus/templates/content/e2e/global-setup.ts +43 -75
  143. package/corpus/templates/content/e2e/global-teardown.ts +34 -0
  144. package/corpus/templates/content/e2e/helpers.ts +598 -92
  145. package/corpus/templates/content/e2e/live-body-gate.ts +188 -0
  146. package/corpus/templates/content/e2e/playwright.config.ts +12 -3
  147. package/corpus/templates/content/e2e/server-identity.ts +47 -0
  148. package/corpus/templates/content/e2e/stored-form/collab-blocks.ts +123 -0
  149. package/corpus/templates/content/package.json +1 -1
  150. package/corpus/templates/content/scripts/_stored-form-census-kinds.ts +30 -2
  151. package/corpus/templates/content/scripts/historical-icon-apply.ts +9 -1
  152. package/corpus/templates/content/scripts/trace-startup.mjs +460 -133
  153. package/corpus/templates/content/server/db/exec.ts +58 -0
  154. package/corpus/templates/content/server/db/schema.ts +1 -0
  155. package/corpus/templates/content/server/lib/content-trash-purge.ts +208 -144
  156. package/corpus/templates/content/server/lib/document-metadata-updated-at.ts +43 -1
  157. package/corpus/templates/content/server/lib/document-revision.ts +23 -0
  158. package/corpus/templates/content/server/lib/live-body-writer.ts +199 -0
  159. package/corpus/templates/content/server/lib/notion-sync.ts +63 -29
  160. package/corpus/templates/content/server/lib/notion.ts +5 -5
  161. package/corpus/templates/content/server/lib/suggested-edits.ts +357 -230
  162. package/corpus/templates/content/server/plugins/db.ts +5 -0
  163. package/corpus/templates/content/server/routes/api/documents/[id]/move.patch.ts +50 -168
  164. package/corpus/templates/content/shared/api.ts +20 -0
  165. package/corpus/templates/content/shared/blocks-fields-manifest.ts +106 -0
  166. package/corpus/templates/content/shared/database-item-layout.ts +591 -0
  167. package/corpus/templates/content/shared/database-query.ts +192 -30
  168. package/corpus/templates/content/shared/document-export.ts +12 -14
  169. package/corpus/templates/content/shared/document-flush-protocol.ts +88 -0
  170. package/corpus/templates/content/shared/editor-save-state.ts +97 -0
  171. package/corpus/templates/content/shared/feature-flags.ts +10 -0
  172. package/corpus/templates/content/shared/import/markdown.ts +91 -7
  173. package/corpus/templates/content/shared/import/types.ts +2 -0
  174. package/corpus/templates/content/shared/inline-math.ts +6 -4
  175. package/corpus/templates/content/shared/nfm-fidelity.ts +247 -0
  176. package/corpus/templates/content/shared/nfm.ts +1933 -340
  177. package/corpus/templates/content/shared/suggestion-diff.ts +13 -0
  178. package/corpus/templates/content/shared/suggestion-formatting.ts +143 -20
  179. package/corpus/templates/content/shared/suggestion-text.ts +39 -0
  180. package/corpus/templates/content/vitest.config.ts +10 -0
  181. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +1 -1
  182. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +1 -1
  183. package/corpus/templates/design/.agents/skills/native-effects/SKILL.md +9 -2
  184. package/corpus/templates/design/.generated/bridge/shader-fill-preview.generated.ts +3 -249
  185. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +5 -88
  186. package/corpus/templates/design/AGENTS.md +1 -3
  187. package/corpus/templates/design/CHANGELOG.md +32 -0
  188. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -6
  189. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -7
  190. package/corpus/templates/design/actions/_native-local-export-server.ts +44 -31
  191. package/corpus/templates/design/actions/_native-render-contexts.ts +76 -21
  192. package/corpus/templates/design/actions/claim-native-local-export.ts +5 -0
  193. package/corpus/templates/design/actions/finish-native-local-export.ts +13 -2
  194. package/corpus/templates/design/actions/get-native-shader-approvals.ts +35 -0
  195. package/corpus/templates/design/actions/get-shader.ts +134 -219
  196. package/corpus/templates/design/actions/list-design-extensions.ts +3 -58
  197. package/corpus/templates/design/actions/request-native-local-export.ts +32 -6
  198. package/corpus/templates/design/actions/run-design-extension-action.ts +0 -29
  199. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +131 -108
  200. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +0 -218
  201. package/corpus/templates/design/app/components/design/DesignThumbnail.tsx +59 -3
  202. package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -47
  203. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +242 -62
  204. package/corpus/templates/design/app/components/design/NativePreviewErrorBoundary.tsx +53 -0
  205. package/corpus/templates/design/app/components/design/bridge/codegen.ts +18 -3
  206. package/corpus/templates/design/app/components/design/bridge/native-composition-clock.ts +5 -4
  207. package/corpus/templates/design/app/components/design/bridge/native-dom.ts +37 -0
  208. package/corpus/templates/design/app/components/design/bridge/native-intrinsic-image.ts +2 -1
  209. package/corpus/templates/design/app/components/design/bridge/native-mesh-grid.ts +90 -0
  210. package/corpus/templates/design/app/components/design/bridge/native-opaque-public-asset.ts +363 -0
  211. package/corpus/templates/design/app/components/design/bridge/native-opaque-worker-lease.ts +179 -0
  212. package/corpus/templates/design/app/components/design/bridge/native-preview-geometry.ts +132 -7
  213. package/corpus/templates/design/app/components/design/bridge/native-preview-policy.ts +4 -0
  214. package/corpus/templates/design/app/components/design/bridge/native-preview-viewport-transport.ts +378 -0
  215. package/corpus/templates/design/app/components/design/bridge/native-scene-body-transform.ts +62 -0
  216. package/corpus/templates/design/app/components/design/bridge/native-scene-presentation-element.ts +10 -0
  217. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +646 -167
  218. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +246 -192
  219. package/corpus/templates/design/app/components/design/bridge/native-stateful-resource.ts +2 -2
  220. package/corpus/templates/design/app/components/design/bridge/native-stateless-compute.ts +5 -2
  221. package/corpus/templates/design/app/components/design/bridge/native-svg-source.ts +11 -1
  222. package/corpus/templates/design/app/components/design/bridge/native-worker-broker.ts +6 -0
  223. package/corpus/templates/design/app/components/design/bridge/native-worker-entry.ts +761 -303
  224. package/corpus/templates/design/app/components/design/bridge/native-worker-fill.ts +16 -6
  225. package/corpus/templates/design/app/components/design/bridge/native-worker-gpu.ts +854 -91
  226. package/corpus/templates/design/app/components/design/bridge/native-worker-host.ts +15 -1
  227. package/corpus/templates/design/app/components/design/bridge/native-worker-input-assets.ts +200 -0
  228. package/corpus/templates/design/app/components/design/bridge/native-worker-protocol.ts +40 -1
  229. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +425 -126
  230. package/corpus/templates/design/app/components/design/bridge/native-worker-source-snapshot.ts +21 -0
  231. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +5 -371
  232. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +4 -146
  233. package/corpus/templates/design/app/components/design/design-canvas/interact-document.ts +5 -9
  234. package/corpus/templates/design/app/components/design/design-canvas/native-preview-surface.ts +150 -0
  235. package/corpus/templates/design/app/components/design/design-canvas/native-public-asset-preview.ts +409 -0
  236. package/corpus/templates/design/app/components/design/design-canvas/native-public-worker-preview.ts +1086 -0
  237. package/corpus/templates/design/app/components/design/design-canvas/private-replay-screenshot-preview.ts +2 -2
  238. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +15 -26
  239. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +74 -67
  240. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +7 -19
  241. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +39 -30
  242. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -0
  243. package/corpus/templates/design/app/components/design/inspector/NativeShaderColorControls.tsx +3 -7
  244. package/corpus/templates/design/app/components/design/inspector/NativeShaderPaintControls.tsx +12 -5
  245. package/corpus/templates/design/app/components/design/inspector/NativeShaderPanel.tsx +705 -190
  246. package/corpus/templates/design/app/components/design/inspector/fill-field-reading.ts +1 -2
  247. package/corpus/templates/design/app/components/design/inspector/index.ts +0 -6
  248. package/corpus/templates/design/app/components/design/inspector/native-catalog-display.ts +26 -5
  249. package/corpus/templates/design/app/components/design/inspector/native-shader-catalog-fixture.ts +42 -0
  250. package/corpus/templates/design/app/components/design/inspector/native-shader-catalog-preview.ts +51 -33
  251. package/corpus/templates/design/app/components/design/inspector/native-shader-paint.ts +1 -0
  252. package/corpus/templates/design/app/components/design/inspector/native-shader-property-draft.ts +89 -23
  253. package/corpus/templates/design/app/components/design/inspector/saved-glsl-shader.tsx +243 -0
  254. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +54 -5
  255. package/corpus/templates/design/app/components/design/shader-source-coordination.ts +66 -0
  256. package/corpus/templates/design/app/components/design/use-native-shader-approvals.ts +36 -8
  257. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +60 -3
  258. package/corpus/templates/design/app/i18n/en-US.ts +78 -2
  259. package/corpus/templates/design/app/pages/Present.tsx +36 -1
  260. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +1 -1
  261. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +1 -1
  262. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +2 -3
  263. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +1 -1
  264. package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +1 -1
  265. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +1 -1
  266. package/corpus/templates/design/app/pages/design-editor/commands/paste-svg-layer.ts +1 -1
  267. package/corpus/templates/design/app/pages/design-editor/commands/prepare-content-history-replay.ts +1 -1
  268. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1 -1
  269. package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +84 -41
  270. package/corpus/templates/design/app/pages/design-editor/commands/shader-source-edit.ts +1 -1
  271. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +1 -1
  272. package/corpus/templates/design/app/pages/design-editor/design-editor-view.tsx +0 -2
  273. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-content-and-components.ts +4 -33
  274. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-export-and-handoff.ts +2 -2
  275. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-history.ts +0 -11
  276. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-layer-models.ts +2 -55
  277. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +0 -3
  278. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-selection-and-styles.ts +0 -24
  279. package/corpus/templates/design/app/pages/design-editor/text-edit-utils.ts +0 -16
  280. package/corpus/templates/design/app/pages/design-editor/use-native-local-export-requests.ts +70 -11
  281. package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +0 -2
  282. package/corpus/templates/design/changelog/2026-10-20-folded-mesh-adds-an-animated-sculpted-fill-with-editable-col.md +6 -0
  283. package/corpus/templates/design/changelog/2026-10-20-read-only-design-views-render-native-shader-previews-in-nest.md +6 -0
  284. package/corpus/templates/design/changelog/2026-10-20-selected-screens-with-shader-effects-can-be-exported-togethe.md +6 -0
  285. package/corpus/templates/design/changelog/2026-10-20-shader-previews-reuse-validated-scene-data-while-panning-inc.md +6 -0
  286. package/corpus/templates/design/changelog/2026-10-20-shader-previews-stay-aligned-while-you-zoom.md +6 -0
  287. package/corpus/templates/design/changelog/2026-10-20-signed-out-sessions-no-longer-watch-for-private-png-export-r.md +6 -0
  288. package/corpus/templates/design/changelog/2026-10-21-effect-thumbnails-show-richer-previews.md +6 -0
  289. package/corpus/templates/design/changelog/2026-10-21-native-effects-render-across-previews.md +6 -0
  290. package/corpus/templates/design/changelog/2026-10-21-native-shader-previews-use-less-gpu-memory-when-many-mesh-ef.md +6 -0
  291. package/corpus/templates/design/changelog/2026-10-21-opening-design-no-longer-shows-png-export-errors-when-no-exp.md +6 -0
  292. package/corpus/templates/design/changelog/2026-10-21-pulsing-border-controls-grouped.md +6 -0
  293. package/corpus/templates/design/changelog/2026-10-21-pulsing-border-follows-element-outlines.md +6 -0
  294. package/corpus/templates/design/changelog/2026-10-21-remove-obsolete-shader-fills.md +6 -0
  295. package/corpus/templates/design/changelog/2026-10-21-shader-previews-recover-from-temporary-gpu-and-image-failure.md +6 -0
  296. package/corpus/templates/design/changelog/2026-10-22-shader-fills-and-effects-render-correctly-on-canvas-elements.md +6 -0
  297. package/corpus/templates/design/package.json +0 -1
  298. package/corpus/templates/design/playwright.config.ts +1 -1
  299. package/corpus/templates/design/scripts/runtime-budget-zoom.ts +9 -21
  300. package/corpus/templates/design/scripts/runtime-budget.ts +10 -24
  301. package/corpus/templates/design/server/plugins/agent-chat.ts +0 -2
  302. package/corpus/templates/design/shared/effect-graph.ts +46 -1
  303. package/corpus/templates/design/shared/generation-output-receipt.ts +0 -1
  304. package/corpus/templates/design/shared/native-effect-animation-capability.ts +6 -0
  305. package/corpus/templates/design/shared/native-effect-folded-mesh-v1.ts +268 -0
  306. package/corpus/templates/design/shared/native-effect-folded-mesh-v2.ts +310 -0
  307. package/corpus/templates/design/shared/native-effect-folded-mesh.ts +489 -0
  308. package/corpus/templates/design/shared/native-effect-gallery-additions.ts +2 -2
  309. package/corpus/templates/design/shared/native-effect-paint.ts +8 -0
  310. package/corpus/templates/design/shared/native-effect-parameter-constraints.ts +8 -1
  311. package/corpus/templates/design/shared/native-effect-presets.ts +18 -1
  312. package/corpus/templates/design/shared/native-effect-property-presentation.ts +81 -0
  313. package/corpus/templates/design/shared/native-effect-pulsing-border-v3.ts +281 -0
  314. package/corpus/templates/design/shared/native-effect-pulsing-border-v4.ts +284 -0
  315. package/corpus/templates/design/shared/native-effect-pulsing-border.ts +300 -0
  316. package/corpus/templates/design/shared/native-effect-trust.ts +6 -0
  317. package/corpus/templates/design/shared/native-effect-wgsl.ts +12 -0
  318. package/corpus/templates/design/shared/native-effects.ts +528 -15
  319. package/corpus/templates/design/shared/native-input-resources.ts +36 -0
  320. package/corpus/templates/design/shared/native-local-export.ts +14 -0
  321. package/corpus/templates/design/shared/native-parameter-animation.ts +1 -0
  322. package/corpus/templates/design/shared/native-pointer-input.ts +9 -11
  323. package/corpus/templates/design/shared/shader-fills.ts +0 -513
  324. package/corpus/templates/design/vitest.setup.ts +7 -1
  325. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +1 -1
  326. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +20 -18
  327. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +1 -1
  328. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +36 -80
  329. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +54 -16
  330. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +50 -73
  331. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +20 -18
  332. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +1 -1
  333. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +1 -1
  334. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +1 -1
  335. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +1 -1
  336. package/corpus/templates/slides-2/.agents/skills/actions/SKILL.md +199 -0
  337. package/corpus/templates/slides-2/.agents/skills/actions/references/action-fields.md +145 -0
  338. package/corpus/templates/slides-2/.agents/skills/actions/references/examples.md +96 -0
  339. package/corpus/templates/slides-2/.agents/skills/actions/references/provider-apis.md +95 -0
  340. package/corpus/templates/slides-2/.agents/skills/adding-a-feature/SKILL.md +235 -0
  341. package/corpus/templates/slides-2/.agents/skills/agent-native-docs/SKILL.md +161 -0
  342. package/corpus/templates/slides-2/.agents/skills/agent-native-toolkit/SKILL.md +314 -0
  343. package/corpus/templates/slides-2/.agents/skills/analytics-data-for-decks/SKILL.md +44 -0
  344. package/corpus/templates/slides-2/.agents/skills/apply-slide-comments/SKILL.md +63 -0
  345. package/corpus/templates/slides-2/.agents/skills/client-side-routing/SKILL.md +87 -0
  346. package/corpus/templates/slides-2/.agents/skills/context-awareness/SKILL.md +282 -0
  347. package/corpus/templates/slides-2/.agents/skills/create-deck/SKILL.md +263 -0
  348. package/corpus/templates/slides-2/.agents/skills/create-deck/references/reference-inputs.md +72 -0
  349. package/corpus/templates/slides-2/.agents/skills/create-deck/references/slide-templates.md +93 -0
  350. package/corpus/templates/slides-2/.agents/skills/create-deck/references/source-material.md +16 -0
  351. package/corpus/templates/slides-2/.agents/skills/creative-context/SKILL.md +134 -0
  352. package/corpus/templates/slides-2/.agents/skills/customizing-agent-native/SKILL.md +219 -0
  353. package/corpus/templates/slides-2/.agents/skills/deck-management/SKILL.md +182 -0
  354. package/corpus/templates/slides-2/.agents/skills/delegate-to-agent/SKILL.md +62 -0
  355. package/corpus/templates/slides-2/.agents/skills/design-systems/SKILL.md +233 -0
  356. package/corpus/templates/slides-2/.agents/skills/external-agents/SKILL.md +549 -0
  357. package/corpus/templates/slides-2/.agents/skills/external-agents/references/mcp-apps-embedding.md +169 -0
  358. package/corpus/templates/slides-2/.agents/skills/frontend-design/SKILL.md +102 -0
  359. package/corpus/templates/slides-2/.agents/skills/frontend-design/references/visual-direction.md +102 -0
  360. package/corpus/templates/slides-2/.agents/skills/image-generation-via-a2a/SKILL.md +134 -0
  361. package/corpus/templates/slides-2/.agents/skills/performance/SKILL.md +79 -0
  362. package/corpus/templates/slides-2/.agents/skills/portability/SKILL.md +108 -0
  363. package/corpus/templates/slides-2/.agents/skills/real-time-sync/SKILL.md +146 -0
  364. package/corpus/templates/slides-2/.agents/skills/reliable-mutations/SKILL.md +72 -0
  365. package/corpus/templates/slides-2/.agents/skills/secrets/SKILL.md +82 -0
  366. package/corpus/templates/slides-2/.agents/skills/security/SKILL.md +80 -0
  367. package/corpus/templates/slides-2/.agents/skills/self-modifying-code/SKILL.md +123 -0
  368. package/corpus/templates/slides-2/.agents/skills/shadcn-ui/SKILL.md +71 -0
  369. package/corpus/templates/slides-2/.agents/skills/sharing/SKILL.md +286 -0
  370. package/corpus/templates/slides-2/.agents/skills/slide-design/SKILL.md +144 -0
  371. package/corpus/templates/slides-2/.agents/skills/slide-editing/SKILL.md +284 -0
  372. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/animations.md +33 -0
  373. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/freeform-objects.md +22 -0
  374. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/restyling.md +69 -0
  375. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/transitions.md +26 -0
  376. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/video.md +45 -0
  377. package/corpus/templates/slides-2/.agents/skills/slide-images/SKILL.md +136 -0
  378. package/corpus/templates/slides-2/.agents/skills/storing-data/SKILL.md +234 -0
  379. package/corpus/templates/slides-2/.agents/skills/turn-into-skill/SKILL.md +112 -0
  380. package/corpus/templates/slides-2/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  381. package/corpus/templates/slides-2/.agents/skills/workspace-conventions/SKILL.md +103 -0
  382. package/corpus/templates/slides-2/.builder/rules/deploy-app.mdc +22 -0
  383. package/corpus/templates/slides-2/.builder/rules/organize-ui.mdc +9 -0
  384. package/corpus/templates/slides-2/.dockerignore +61 -0
  385. package/corpus/templates/slides-2/.env.example +23 -0
  386. package/corpus/templates/slides-2/.ignore +3 -0
  387. package/corpus/templates/slides-2/.mcp.json +8 -0
  388. package/corpus/templates/slides-2/.oxfmtrc.json +8 -0
  389. package/corpus/templates/slides-2/AGENTS.md +66 -0
  390. package/corpus/templates/slides-2/CHANGELOG.md +76 -0
  391. package/corpus/templates/slides-2/DEVELOPING.md +157 -0
  392. package/corpus/templates/slides-2/README.md +58 -0
  393. package/corpus/templates/slides-2/THIRD_PARTY_NOTICES.md +33 -0
  394. package/corpus/templates/slides-2/_gitignore +38 -0
  395. package/corpus/templates/slides-2/actions/_agent-presence.ts +29 -0
  396. package/corpus/templates/slides-2/actions/_app-url.ts +37 -0
  397. package/corpus/templates/slides-2/actions/_deck-app-scope.ts +30 -0
  398. package/corpus/templates/slides-2/actions/_deck-tracking.ts +260 -0
  399. package/corpus/templates/slides-2/actions/_deck-write.ts +206 -0
  400. package/corpus/templates/slides-2/actions/_imported-slide-source.ts +162 -0
  401. package/corpus/templates/slides-2/actions/_inline-image-storage.ts +1013 -0
  402. package/corpus/templates/slides-2/actions/_linked-design-system.ts +83 -0
  403. package/corpus/templates/slides-2/actions/_presenter-notes.ts +83 -0
  404. package/corpus/templates/slides-2/actions/_render-artifacts.ts +111 -0
  405. package/corpus/templates/slides-2/actions/_slide-source.ts +344 -0
  406. package/corpus/templates/slides-2/actions/_tab-state.ts +51 -0
  407. package/corpus/templates/slides-2/actions/_test-brandfetch.ts +31 -0
  408. package/corpus/templates/slides-2/actions/_uploaded-files.ts +114 -0
  409. package/corpus/templates/slides-2/actions/add-deck.ts +123 -0
  410. package/corpus/templates/slides-2/actions/add-slide-comment.ts +267 -0
  411. package/corpus/templates/slides-2/actions/add-slide.ts +888 -0
  412. package/corpus/templates/slides-2/actions/analyze-brand-assets.ts +66 -0
  413. package/corpus/templates/slides-2/actions/apply-design-system.ts +111 -0
  414. package/corpus/templates/slides-2/actions/approve-deck-access-request.ts +287 -0
  415. package/corpus/templates/slides-2/actions/audit-contrast.ts +143 -0
  416. package/corpus/templates/slides-2/actions/check-image-gen.ts +17 -0
  417. package/corpus/templates/slides-2/actions/clone-context-slide.ts +340 -0
  418. package/corpus/templates/slides-2/actions/clone-creative-context-deck.ts +143 -0
  419. package/corpus/templates/slides-2/actions/control-deck-history.ts +186 -0
  420. package/corpus/templates/slides-2/actions/control-presenter.ts +117 -0
  421. package/corpus/templates/slides-2/actions/control-slide-editor.ts +267 -0
  422. package/corpus/templates/slides-2/actions/create-deck-from-template.ts +274 -0
  423. package/corpus/templates/slides-2/actions/create-deck.ts +1243 -0
  424. package/corpus/templates/slides-2/actions/create-design-system.ts +131 -0
  425. package/corpus/templates/slides-2/actions/create-share-link.ts +127 -0
  426. package/corpus/templates/slides-2/actions/db-health.ts +22 -0
  427. package/corpus/templates/slides-2/actions/delete-asset.ts +154 -0
  428. package/corpus/templates/slides-2/actions/delete-deck.ts +427 -0
  429. package/corpus/templates/slides-2/actions/delete-design-system.ts +167 -0
  430. package/corpus/templates/slides-2/actions/delete-slide-comment.ts +143 -0
  431. package/corpus/templates/slides-2/actions/delete-staged-dataset.ts +30 -0
  432. package/corpus/templates/slides-2/actions/duplicate-deck.ts +299 -0
  433. package/corpus/templates/slides-2/actions/export-google-slides.ts +41 -0
  434. package/corpus/templates/slides-2/actions/export-html.ts +770 -0
  435. package/corpus/templates/slides-2/actions/export-pptx.ts +2203 -0
  436. package/corpus/templates/slides-2/actions/extract-pdf.ts +49 -0
  437. package/corpus/templates/slides-2/actions/generate-image-api.ts +671 -0
  438. package/corpus/templates/slides-2/actions/generate-image.ts +492 -0
  439. package/corpus/templates/slides-2/actions/get-agent-access-deck.ts +52 -0
  440. package/corpus/templates/slides-2/actions/get-deck-access-status.ts +130 -0
  441. package/corpus/templates/slides-2/actions/get-deck-design-tokens.ts +62 -0
  442. package/corpus/templates/slides-2/actions/get-deck-reference-context.ts +232 -0
  443. package/corpus/templates/slides-2/actions/get-deck-template.ts +38 -0
  444. package/corpus/templates/slides-2/actions/get-deck-version.ts +85 -0
  445. package/corpus/templates/slides-2/actions/get-deck.ts +562 -0
  446. package/corpus/templates/slides-2/actions/get-design-system-decode-job-status.ts +57 -0
  447. package/corpus/templates/slides-2/actions/get-design-system-tier-limit.ts +12 -0
  448. package/corpus/templates/slides-2/actions/get-design-system.ts +453 -0
  449. package/corpus/templates/slides-2/actions/get-google-docs-status.ts +106 -0
  450. package/corpus/templates/slides-2/actions/get-google-picker-token.ts +84 -0
  451. package/corpus/templates/slides-2/actions/get-home-suggestions.ts +52 -0
  452. package/corpus/templates/slides-2/actions/get-layout-overflows.ts +192 -0
  453. package/corpus/templates/slides-2/actions/get-slide-render.ts +125 -0
  454. package/corpus/templates/slides-2/actions/get-slide-source.ts +115 -0
  455. package/corpus/templates/slides-2/actions/get-slide-theme-preview.ts +64 -0
  456. package/corpus/templates/slides-2/actions/get-slide-theme.ts +44 -0
  457. package/corpus/templates/slides-2/actions/get-slides-2-upload-storage-status.ts +17 -0
  458. package/corpus/templates/slides-2/actions/get-slides-notification-prefs.ts +37 -0
  459. package/corpus/templates/slides-2/actions/get-workspace-defaults.ts +51 -0
  460. package/corpus/templates/slides-2/actions/image-gen-status.ts +35 -0
  461. package/corpus/templates/slides-2/actions/import-code.ts +26 -0
  462. package/corpus/templates/slides-2/actions/import-design-project.ts +49 -0
  463. package/corpus/templates/slides-2/actions/import-document.ts +125 -0
  464. package/corpus/templates/slides-2/actions/import-docx.ts +57 -0
  465. package/corpus/templates/slides-2/actions/import-file.ts +1273 -0
  466. package/corpus/templates/slides-2/actions/import-from-url.ts +25 -0
  467. package/corpus/templates/slides-2/actions/import-google-doc.ts +208 -0
  468. package/corpus/templates/slides-2/actions/import-google-font.ts +110 -0
  469. package/corpus/templates/slides-2/actions/import-google-slides-reference.ts +667 -0
  470. package/corpus/templates/slides-2/actions/import-pptx.ts +564 -0
  471. package/corpus/templates/slides-2/actions/import-svgl-logo.ts +115 -0
  472. package/corpus/templates/slides-2/actions/index-design-system-with-builder.ts +181 -0
  473. package/corpus/templates/slides-2/actions/index-uploaded-design-system-files.ts +77 -0
  474. package/corpus/templates/slides-2/actions/list-assets.ts +286 -0
  475. package/corpus/templates/slides-2/actions/list-deck-templates.ts +83 -0
  476. package/corpus/templates/slides-2/actions/list-deck-versions.ts +185 -0
  477. package/corpus/templates/slides-2/actions/list-decks.ts +591 -0
  478. package/corpus/templates/slides-2/actions/list-design-systems.ts +180 -0
  479. package/corpus/templates/slides-2/actions/list-slide-comments.ts +128 -0
  480. package/corpus/templates/slides-2/actions/list-slide-themes.ts +17 -0
  481. package/corpus/templates/slides-2/actions/list-staged-datasets.ts +14 -0
  482. package/corpus/templates/slides-2/actions/logo-config.ts +13 -0
  483. package/corpus/templates/slides-2/actions/navigate.ts +158 -0
  484. package/corpus/templates/slides-2/actions/patch-deck.ts +2378 -0
  485. package/corpus/templates/slides-2/actions/provider-api-catalog.ts +25 -0
  486. package/corpus/templates/slides-2/actions/provider-api-docs.ts +83 -0
  487. package/corpus/templates/slides-2/actions/provider-api-request.ts +222 -0
  488. package/corpus/templates/slides-2/actions/query-staged-dataset.ts +83 -0
  489. package/corpus/templates/slides-2/actions/read-composer-source.ts +108 -0
  490. package/corpus/templates/slides-2/actions/request-deck-access.ts +803 -0
  491. package/corpus/templates/slides-2/actions/restore-deck-version.ts +203 -0
  492. package/corpus/templates/slides-2/actions/run.ts +2 -0
  493. package/corpus/templates/slides-2/actions/save-deck.ts +313 -0
  494. package/corpus/templates/slides-2/actions/search-google-fonts.ts +61 -0
  495. package/corpus/templates/slides-2/actions/search-images.ts +30 -0
  496. package/corpus/templates/slides-2/actions/search-logos.ts +23 -0
  497. package/corpus/templates/slides-2/actions/search-svgl-logos.ts +42 -0
  498. package/corpus/templates/slides-2/actions/set-default-design-system.ts +88 -0
  499. package/corpus/templates/slides-2/actions/set-workspace-defaults.ts +61 -0
  500. package/corpus/templates/slides-2/actions/start-design-system-upload.ts +80 -0
  501. package/corpus/templates/slides-2/actions/sync-design-system-with-builder.ts +84 -0
  502. package/corpus/templates/slides-2/actions/toggle-slide-comment-reaction.ts +150 -0
  503. package/corpus/templates/slides-2/actions/transform-slide-objects.ts +1528 -0
  504. package/corpus/templates/slides-2/actions/update-asset.ts +499 -0
  505. package/corpus/templates/slides-2/actions/update-deck-aspect-ratio.ts +95 -0
  506. package/corpus/templates/slides-2/actions/update-deck-design-tokens.ts +175 -0
  507. package/corpus/templates/slides-2/actions/update-design-system.ts +95 -0
  508. package/corpus/templates/slides-2/actions/update-slide-comment.ts +213 -0
  509. package/corpus/templates/slides-2/actions/update-slide-source.ts +353 -0
  510. package/corpus/templates/slides-2/actions/update-slide.ts +850 -0
  511. package/corpus/templates/slides-2/actions/update-slides-notification-prefs.ts +55 -0
  512. package/corpus/templates/slides-2/actions/view-screen.ts +1098 -0
  513. package/corpus/templates/slides-2/agent-native.app-skill.json +165 -0
  514. package/corpus/templates/slides-2/agent-native.config.ts +20 -0
  515. package/corpus/templates/slides-2/agent-native.json +18 -0
  516. package/corpus/templates/slides-2/app/components/ThemeToggle.tsx +47 -0
  517. package/corpus/templates/slides-2/app/components/comments/SlideCommentPins.tsx +781 -0
  518. package/corpus/templates/slides-2/app/components/comments/SlideCommentsPanel.tsx +1075 -0
  519. package/corpus/templates/slides-2/app/components/deck/DeckCard.tsx +304 -0
  520. package/corpus/templates/slides-2/app/components/deck/DeckFilterMenu.tsx +83 -0
  521. package/corpus/templates/slides-2/app/components/deck/ExcalidrawSlide.tsx +183 -0
  522. package/corpus/templates/slides-2/app/components/deck/MermaidRenderer.tsx +126 -0
  523. package/corpus/templates/slides-2/app/components/deck/SlideRenderer.tsx +1553 -0
  524. package/corpus/templates/slides-2/app/components/design-system/DesignSystemCard.tsx +235 -0
  525. package/corpus/templates/slides-2/app/components/design-system/DesignSystemSetup.tsx +2021 -0
  526. package/corpus/templates/slides-2/app/components/design-system/builder-design-system-upload.ts +200 -0
  527. package/corpus/templates/slides-2/app/components/design-system/builder-index-response.ts +104 -0
  528. package/corpus/templates/slides-2/app/components/editor/AddSlidePopover.tsx +461 -0
  529. package/corpus/templates/slides-2/app/components/editor/AiEditingMarker.tsx +24 -0
  530. package/corpus/templates/slides-2/app/components/editor/AlignmentGuides.tsx +82 -0
  531. package/corpus/templates/slides-2/app/components/editor/AnimationsPanel.tsx +698 -0
  532. package/corpus/templates/slides-2/app/components/editor/AppMenu.tsx +356 -0
  533. package/corpus/templates/slides-2/app/components/editor/AssetLibraryPanel.tsx +508 -0
  534. package/corpus/templates/slides-2/app/components/editor/BlockBubbleMenu.tsx +548 -0
  535. package/corpus/templates/slides-2/app/components/editor/DeckAccessDeniedPage.tsx +173 -0
  536. package/corpus/templates/slides-2/app/components/editor/DeckEditorSkeleton.tsx +35 -0
  537. package/corpus/templates/slides-2/app/components/editor/DeferredPopoverFallback.tsx +148 -0
  538. package/corpus/templates/slides-2/app/components/editor/EditorActionCluster.tsx +217 -0
  539. package/corpus/templates/slides-2/app/components/editor/EditorSidebar.tsx +1047 -0
  540. package/corpus/templates/slides-2/app/components/editor/EditorToolbar.tsx +1285 -0
  541. package/corpus/templates/slides-2/app/components/editor/ExportMenu.tsx +579 -0
  542. package/corpus/templates/slides-2/app/components/editor/GeneratingOverlay.tsx +21 -0
  543. package/corpus/templates/slides-2/app/components/editor/GeneratingSlidePreview.tsx +73 -0
  544. package/corpus/templates/slides-2/app/components/editor/GoogleDocImportHint.tsx +378 -0
  545. package/corpus/templates/slides-2/app/components/editor/GoogleDriveConnectionCta.tsx +154 -0
  546. package/corpus/templates/slides-2/app/components/editor/GoogleSlidesReferenceImport.tsx +299 -0
  547. package/corpus/templates/slides-2/app/components/editor/HistoryPanel.tsx +354 -0
  548. package/corpus/templates/slides-2/app/components/editor/HomeQuickStartDialog.tsx +202 -0
  549. package/corpus/templates/slides-2/app/components/editor/ImageCropOverlay.tsx +820 -0
  550. package/corpus/templates/slides-2/app/components/editor/ImageDropPromptPopover.tsx +280 -0
  551. package/corpus/templates/slides-2/app/components/editor/ImageGenPanel.tsx +244 -0
  552. package/corpus/templates/slides-2/app/components/editor/ImageOverlay.tsx +280 -0
  553. package/corpus/templates/slides-2/app/components/editor/ImageSearchPanel.tsx +183 -0
  554. package/corpus/templates/slides-2/app/components/editor/ImportDeckButton.tsx +247 -0
  555. package/corpus/templates/slides-2/app/components/editor/LogoSearchPanel.tsx +366 -0
  556. package/corpus/templates/slides-2/app/components/editor/MermaidToExcalidrawPanel.tsx +99 -0
  557. package/corpus/templates/slides-2/app/components/editor/MissingDeckAccessPane.tsx +219 -0
  558. package/corpus/templates/slides-2/app/components/editor/NewDeckReferenceStep.tsx +812 -0
  559. package/corpus/templates/slides-2/app/components/editor/PromptDialog.tsx +963 -0
  560. package/corpus/templates/slides-2/app/components/editor/QuestionFlow.tsx +119 -0
  561. package/corpus/templates/slides-2/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  562. package/corpus/templates/slides-2/app/components/editor/ShareDialog.tsx +126 -0
  563. package/corpus/templates/slides-2/app/components/editor/SlideContextToolbar.tsx +1471 -0
  564. package/corpus/templates/slides-2/app/components/editor/SlideEditor.tsx +11183 -0
  565. package/corpus/templates/slides-2/app/components/editor/SlideOverflowWarning.tsx +82 -0
  566. package/corpus/templates/slides-2/app/components/editor/SlideScrollStack.tsx +307 -0
  567. package/corpus/templates/slides-2/app/components/editor/SlideSlashCommandMenu.tsx +692 -0
  568. package/corpus/templates/slides-2/app/components/editor/SlideThumbnailContextMenu.tsx +52 -0
  569. package/corpus/templates/slides-2/app/components/editor/SlideTransitionControl.tsx +184 -0
  570. package/corpus/templates/slides-2/app/components/editor/SlidesComposerContext.tsx +789 -0
  571. package/corpus/templates/slides-2/app/components/editor/SlidesComposerContextProvider.tsx +74 -0
  572. package/corpus/templates/slides-2/app/components/editor/SlidesLayersPanel.tsx +377 -0
  573. package/corpus/templates/slides-2/app/components/editor/SpeakerNotesPanel.tsx +119 -0
  574. package/corpus/templates/slides-2/app/components/editor/TweaksPanel.tsx +77 -0
  575. package/corpus/templates/slides-2/app/components/editor/UploadStorageGate.tsx +37 -0
  576. package/corpus/templates/slides-2/app/components/editor/bullet-editing.ts +880 -0
  577. package/corpus/templates/slides-2/app/components/editor/canvas-interactions/index.ts +12 -0
  578. package/corpus/templates/slides-2/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +119 -0
  579. package/corpus/templates/slides-2/app/components/editor/commit-active-edit.ts +14 -0
  580. package/corpus/templates/slides-2/app/components/editor/editor-command-model.ts +35 -0
  581. package/corpus/templates/slides-2/app/components/editor/fit-text-object.ts +31 -0
  582. package/corpus/templates/slides-2/app/components/editor/in-place-text-session.browser-cases.json +983 -0
  583. package/corpus/templates/slides-2/app/components/editor/in-place-text-session.browser-host.ts +747 -0
  584. package/corpus/templates/slides-2/app/components/editor/in-place-text-session.ts +6891 -0
  585. package/corpus/templates/slides-2/app/components/editor/inline-edit-session.ts +26 -0
  586. package/corpus/templates/slides-2/app/components/editor/list-editing.ts +501 -0
  587. package/corpus/templates/slides-2/app/components/editor/rich-text-selection.ts +975 -0
  588. package/corpus/templates/slides-2/app/components/editor/rte-test-utils.ts +265 -0
  589. package/corpus/templates/slides-2/app/components/editor/selection-overlay-measurement.ts +191 -0
  590. package/corpus/templates/slides-2/app/components/editor/slide-escape-arbiter.ts +34 -0
  591. package/corpus/templates/slides-2/app/components/editor/slide-object-interactions.ts +6362 -0
  592. package/corpus/templates/slides-2/app/components/editor/slide-object-transform.browser-host.ts +19 -0
  593. package/corpus/templates/slides-2/app/components/editor/slide-pointer-target.ts +432 -0
  594. package/corpus/templates/slides-2/app/components/editor/slide-presence.ts +13 -0
  595. package/corpus/templates/slides-2/app/components/editor/slide-style.ts +243 -0
  596. package/corpus/templates/slides-2/app/components/editor/slide-text-targets.ts +540 -0
  597. package/corpus/templates/slides-2/app/components/editor/style-clipboard.ts +102 -0
  598. package/corpus/templates/slides-2/app/components/editor/use-prompt-import.ts +159 -0
  599. package/corpus/templates/slides-2/app/components/home/SlidesHomeLibrary.tsx +79 -0
  600. package/corpus/templates/slides-2/app/components/layout/AgentWorkIndicator.tsx +193 -0
  601. package/corpus/templates/slides-2/app/components/layout/Header.tsx +110 -0
  602. package/corpus/templates/slides-2/app/components/layout/Layout.tsx +492 -0
  603. package/corpus/templates/slides-2/app/components/layout/Sidebar.tsx +109 -0
  604. package/corpus/templates/slides-2/app/components/layout/layout-route-policy.ts +28 -0
  605. package/corpus/templates/slides-2/app/components/presentation/PresentationView.tsx +1116 -0
  606. package/corpus/templates/slides-2/app/components/presentation/PresenterView.tsx +526 -0
  607. package/corpus/templates/slides-2/app/components/presentation/SlideTransitionStage.tsx +297 -0
  608. package/corpus/templates/slides-2/app/components/presentation/present-channel.ts +31 -0
  609. package/corpus/templates/slides-2/app/components/presentation/slide-build-steps.ts +231 -0
  610. package/corpus/templates/slides-2/app/components/presentation/slide-navigation.ts +273 -0
  611. package/corpus/templates/slides-2/app/components/presentation/slide-transition-motion.ts +258 -0
  612. package/corpus/templates/slides-2/app/components/presentation/voice-navigation.ts +61 -0
  613. package/corpus/templates/slides-2/app/components/settings/notification-settings.tsx +93 -0
  614. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2AssetBrowser.tsx +726 -0
  615. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2CommandMenu.tsx +453 -0
  616. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Comments.tsx +571 -0
  617. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2DesignTokens.tsx +345 -0
  618. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Editor.tsx +4804 -0
  619. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2ExportMenu.tsx +671 -0
  620. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2FontBrowser.tsx +342 -0
  621. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2ImageCropControls.tsx +487 -0
  622. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Inspector.tsx +950 -0
  623. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2SvglLogoSearchDialog.tsx +332 -0
  624. package/corpus/templates/slides-2/app/components/slides-2-editor/cross-slide-edit-history.ts +151 -0
  625. package/corpus/templates/slides-2/app/components/slides-2-editor/design-token-presets.ts +140 -0
  626. package/corpus/templates/slides-2/app/components/slides-2-editor/editor-action-errors.ts +47 -0
  627. package/corpus/templates/slides-2/app/components/slides-2-editor/editor-export-errors.ts +60 -0
  628. package/corpus/templates/slides-2/app/components/slides-2-editor/geometry.ts +275 -0
  629. package/corpus/templates/slides-2/app/components/slides-2-editor/image-crop-style.ts +82 -0
  630. package/corpus/templates/slides-2/app/components/slides-2-editor/inline-text-editor.tsx +631 -0
  631. package/corpus/templates/slides-2/app/components/slides-2-editor/keyboard-shortcuts.ts +104 -0
  632. package/corpus/templates/slides-2/app/components/slides-2-editor/object-insertion.ts +13 -0
  633. package/corpus/templates/slides-2/app/components/slides-2-editor/preview-patches.ts +54 -0
  634. package/corpus/templates/slides-2/app/components/slides-2-editor/sandbox-render-errors.ts +23 -0
  635. package/corpus/templates/slides-2/app/components/slides-2-editor/source-structure.ts +634 -0
  636. package/corpus/templates/slides-2/app/components/slides-2-editor/types.ts +85 -0
  637. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2BlackoutOverlay.tsx +13 -0
  638. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2ControlBar.tsx +285 -0
  639. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2HelpDialog.tsx +72 -0
  640. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2JumpInput.tsx +38 -0
  641. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2LaserPointer.tsx +67 -0
  642. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2Overview.tsx +161 -0
  643. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2Player.tsx +839 -0
  644. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2ProgressBar.tsx +23 -0
  645. package/corpus/templates/slides-2/app/components/slides-2-presenter/keyboard.ts +36 -0
  646. package/corpus/templates/slides-2/app/components/slides-2-presenter/presenter-channel.ts +131 -0
  647. package/corpus/templates/slides-2/app/components/slides-2-presenter/use-presenter-action-bridge.ts +296 -0
  648. package/corpus/templates/slides-2/app/components/slides-2-presenter/useSlides2TouchSwipe.ts +80 -0
  649. package/corpus/templates/slides-2/app/components/slides-2-runtime/SandboxedSlide.tsx +724 -0
  650. package/corpus/templates/slides-2/app/components/slides-2-runtime/SlideContentRenderer.tsx +137 -0
  651. package/corpus/templates/slides-2/app/components/slides-2-runtime/Slides2Canvas.tsx +99 -0
  652. package/corpus/templates/slides-2/app/components/slides-2-runtime/constants.ts +4 -0
  653. package/corpus/templates/slides-2/app/components/slides-2-runtime/protocol.ts +1400 -0
  654. package/corpus/templates/slides-2/app/components/slides-2-runtime/transition-frame.ts +439 -0
  655. package/corpus/templates/slides-2/app/components/templates/DeckTemplateLibrary.tsx +314 -0
  656. package/corpus/templates/slides-2/app/components/templates/DeckTemplatePreview.tsx +60 -0
  657. package/corpus/templates/slides-2/app/components/templates/DeckTemplateStage.tsx +45 -0
  658. package/corpus/templates/slides-2/app/components/ui/alert-dialog.tsx +1 -0
  659. package/corpus/templates/slides-2/app/components/ui/avatar.tsx +1 -0
  660. package/corpus/templates/slides-2/app/components/ui/button-group.tsx +1 -0
  661. package/corpus/templates/slides-2/app/components/ui/button.tsx +1 -0
  662. package/corpus/templates/slides-2/app/components/ui/card.tsx +1 -0
  663. package/corpus/templates/slides-2/app/components/ui/command.tsx +1 -0
  664. package/corpus/templates/slides-2/app/components/ui/context-menu.tsx +32 -0
  665. package/corpus/templates/slides-2/app/components/ui/dialog.tsx +1 -0
  666. package/corpus/templates/slides-2/app/components/ui/dropdown-menu.tsx +1 -0
  667. package/corpus/templates/slides-2/app/components/ui/filter-trigger.tsx +1 -0
  668. package/corpus/templates/slides-2/app/components/ui/input.tsx +1 -0
  669. package/corpus/templates/slides-2/app/components/ui/label.tsx +1 -0
  670. package/corpus/templates/slides-2/app/components/ui/popover.tsx +1 -0
  671. package/corpus/templates/slides-2/app/components/ui/progress.tsx +1 -0
  672. package/corpus/templates/slides-2/app/components/ui/scroll-area.tsx +1 -0
  673. package/corpus/templates/slides-2/app/components/ui/select.tsx +1 -0
  674. package/corpus/templates/slides-2/app/components/ui/separator.tsx +1 -0
  675. package/corpus/templates/slides-2/app/components/ui/sheet.tsx +1 -0
  676. package/corpus/templates/slides-2/app/components/ui/skeleton.tsx +1 -0
  677. package/corpus/templates/slides-2/app/components/ui/slider.tsx +1 -0
  678. package/corpus/templates/slides-2/app/components/ui/spinner.tsx +1 -0
  679. package/corpus/templates/slides-2/app/components/ui/switch.tsx +1 -0
  680. package/corpus/templates/slides-2/app/components/ui/tabs.tsx +59 -0
  681. package/corpus/templates/slides-2/app/components/ui/textarea.tsx +1 -0
  682. package/corpus/templates/slides-2/app/components/ui/toggle-group.tsx +1 -0
  683. package/corpus/templates/slides-2/app/components/ui/toolkit-provider.tsx +17 -0
  684. package/corpus/templates/slides-2/app/components/ui/tooltip.tsx +1 -0
  685. package/corpus/templates/slides-2/app/components/visual-editor/CanvasCommentPins.tsx +46 -0
  686. package/corpus/templates/slides-2/app/components/visual-editor/DrawOverlay.tsx +27 -0
  687. package/corpus/templates/slides-2/app/components/visual-editor/MultiSelectChip.tsx +97 -0
  688. package/corpus/templates/slides-2/app/components/visual-editor/SaveStatusIndicator.tsx +273 -0
  689. package/corpus/templates/slides-2/app/components/visual-editor/index.ts +4 -0
  690. package/corpus/templates/slides-2/app/context/DeckContext.tsx +7486 -0
  691. package/corpus/templates/slides-2/app/design-system.ts +3 -0
  692. package/corpus/templates/slides-2/app/entry.client.tsx +10 -0
  693. package/corpus/templates/slides-2/app/entry.server.tsx +10 -0
  694. package/corpus/templates/slides-2/app/global.css +1626 -0
  695. package/corpus/templates/slides-2/app/hooks/use-agent-generating.ts +517 -0
  696. package/corpus/templates/slides-2/app/hooks/use-compact-editor-layout.ts +26 -0
  697. package/corpus/templates/slides-2/app/hooks/use-contrast-audit-bridge.ts +49 -0
  698. package/corpus/templates/slides-2/app/hooks/use-deck-access-reload.ts +70 -0
  699. package/corpus/templates/slides-2/app/hooks/use-deck-access.ts +50 -0
  700. package/corpus/templates/slides-2/app/hooks/use-deck-design-system.ts +146 -0
  701. package/corpus/templates/slides-2/app/hooks/use-deck-history-action-bridge.ts +266 -0
  702. package/corpus/templates/slides-2/app/hooks/use-deck-presence.ts +89 -0
  703. package/corpus/templates/slides-2/app/hooks/use-deck-role.ts +46 -0
  704. package/corpus/templates/slides-2/app/hooks/use-deck-versions.ts +43 -0
  705. package/corpus/templates/slides-2/app/hooks/use-design-system-workflows.ts +11 -0
  706. package/corpus/templates/slides-2/app/hooks/use-design-systems.ts +31 -0
  707. package/corpus/templates/slides-2/app/hooks/use-generating-slide-preview.ts +115 -0
  708. package/corpus/templates/slides-2/app/hooks/use-navigation-state.ts +229 -0
  709. package/corpus/templates/slides-2/app/hooks/use-new-deck-generation-signal.ts +46 -0
  710. package/corpus/templates/slides-2/app/hooks/use-new-deck-generation.ts +710 -0
  711. package/corpus/templates/slides-2/app/hooks/use-sidebar-collapsed.ts +95 -0
  712. package/corpus/templates/slides-2/app/hooks/use-slide-comments.ts +202 -0
  713. package/corpus/templates/slides-2/app/hooks/use-slide-file-storage-status.ts +55 -0
  714. package/corpus/templates/slides-2/app/hooks/use-undo-selection.ts +66 -0
  715. package/corpus/templates/slides-2/app/hooks/use-workspace-defaults.ts +31 -0
  716. package/corpus/templates/slides-2/app/i18n/en-US.ts +1507 -0
  717. package/corpus/templates/slides-2/app/i18n/index.ts +19 -0
  718. package/corpus/templates/slides-2/app/lib/agent-visible-message.ts +21 -0
  719. package/corpus/templates/slides-2/app/lib/aspect-ratios.ts +7 -0
  720. package/corpus/templates/slides-2/app/lib/asset-error-copy.ts +59 -0
  721. package/corpus/templates/slides-2/app/lib/canvas-zoom.ts +47 -0
  722. package/corpus/templates/slides-2/app/lib/composer-context.ts +152 -0
  723. package/corpus/templates/slides-2/app/lib/composer-draft.ts +34 -0
  724. package/corpus/templates/slides-2/app/lib/contrast-audit.ts +390 -0
  725. package/corpus/templates/slides-2/app/lib/contrast-resolve.ts +240 -0
  726. package/corpus/templates/slides-2/app/lib/create-deck-generation.ts +938 -0
  727. package/corpus/templates/slides-2/app/lib/deck-access-lost.ts +4 -0
  728. package/corpus/templates/slides-2/app/lib/deck-backup.ts +186 -0
  729. package/corpus/templates/slides-2/app/lib/deck-editor-loading.ts +91 -0
  730. package/corpus/templates/slides-2/app/lib/deck-export-file-client.ts +77 -0
  731. package/corpus/templates/slides-2/app/lib/deck-filter.ts +52 -0
  732. package/corpus/templates/slides-2/app/lib/deck-list-loading.ts +16 -0
  733. package/corpus/templates/slides-2/app/lib/deck-preview-frame.ts +15 -0
  734. package/corpus/templates/slides-2/app/lib/deck-share-links.ts +9 -0
  735. package/corpus/templates/slides-2/app/lib/deck-sorting.ts +19 -0
  736. package/corpus/templates/slides-2/app/lib/deferred-editor-surfaces.ts +24 -0
  737. package/corpus/templates/slides-2/app/lib/design-system-selection.ts +11 -0
  738. package/corpus/templates/slides-2/app/lib/design-systems.ts +231 -0
  739. package/corpus/templates/slides-2/app/lib/dynamic-import.ts +50 -0
  740. package/corpus/templates/slides-2/app/lib/editor-agent-handoff.ts +92 -0
  741. package/corpus/templates/slides-2/app/lib/editor-shortcuts.ts +153 -0
  742. package/corpus/templates/slides-2/app/lib/export-google-slides-client.ts +168 -0
  743. package/corpus/templates/slides-2/app/lib/export-pdf-client.ts +684 -0
  744. package/corpus/templates/slides-2/app/lib/export-pptx-client.ts +2420 -0
  745. package/corpus/templates/slides-2/app/lib/generation-lifecycle.ts +24 -0
  746. package/corpus/templates/slides-2/app/lib/generation-state.ts +247 -0
  747. package/corpus/templates/slides-2/app/lib/google-docs-client.ts +110 -0
  748. package/corpus/templates/slides-2/app/lib/google-slides-export-availability-client.ts +53 -0
  749. package/corpus/templates/slides-2/app/lib/google-slides-reference-source.ts +18 -0
  750. package/corpus/templates/slides-2/app/lib/image-download.ts +85 -0
  751. package/corpus/templates/slides-2/app/lib/image-drop-to-agent.ts +210 -0
  752. package/corpus/templates/slides-2/app/lib/import-uploaded-deck.ts +132 -0
  753. package/corpus/templates/slides-2/app/lib/inline-edit-remote.ts +55 -0
  754. package/corpus/templates/slides-2/app/lib/localize-slides-changelog.ts +47 -0
  755. package/corpus/templates/slides-2/app/lib/mermaid-blocks.ts +18 -0
  756. package/corpus/templates/slides-2/app/lib/new-deck-reference-selection.ts +174 -0
  757. package/corpus/templates/slides-2/app/lib/normalize-slide-padding.ts +74 -0
  758. package/corpus/templates/slides-2/app/lib/pending-deck-changes.ts +33 -0
  759. package/corpus/templates/slides-2/app/lib/portaled-layer.ts +7 -0
  760. package/corpus/templates/slides-2/app/lib/pptx-google-slides.ts +204 -0
  761. package/corpus/templates/slides-2/app/lib/prompt-attachment-adapter.ts +37 -0
  762. package/corpus/templates/slides-2/app/lib/prompt-file-uploads.ts +557 -0
  763. package/corpus/templates/slides-2/app/lib/recent-references.ts +106 -0
  764. package/corpus/templates/slides-2/app/lib/reference-document-hydration.ts +406 -0
  765. package/corpus/templates/slides-2/app/lib/register-chat-renderers.tsx +645 -0
  766. package/corpus/templates/slides-2/app/lib/reveal-after-delete.ts +55 -0
  767. package/corpus/templates/slides-2/app/lib/sanitize-slide-html.ts +752 -0
  768. package/corpus/templates/slides-2/app/lib/slide-agent-context.ts +148 -0
  769. package/corpus/templates/slides-2/app/lib/slide-animation-elements.ts +900 -0
  770. package/corpus/templates/slides-2/app/lib/slide-clipboard.ts +534 -0
  771. package/corpus/templates/slides-2/app/lib/slide-comment-anchor.ts +286 -0
  772. package/corpus/templates/slides-2/app/lib/slide-content-merge.ts +295 -0
  773. package/corpus/templates/slides-2/app/lib/slide-image-replacement.ts +1919 -0
  774. package/corpus/templates/slides-2/app/lib/slide-live-remote.ts +215 -0
  775. package/corpus/templates/slides-2/app/lib/slide-scroll.ts +56 -0
  776. package/corpus/templates/slides-2/app/lib/slide-source-map.ts +1099 -0
  777. package/corpus/templates/slides-2/app/lib/slide-video.ts +390 -0
  778. package/corpus/templates/slides-2/app/lib/slides-relay-tracking.ts +114 -0
  779. package/corpus/templates/slides-2/app/lib/streaming-slide-html.ts +77 -0
  780. package/corpus/templates/slides-2/app/lib/tab-id.ts +3 -0
  781. package/corpus/templates/slides-2/app/lib/template-navigation.ts +15 -0
  782. package/corpus/templates/slides-2/app/lib/text-tool-shortcut.ts +75 -0
  783. package/corpus/templates/slides-2/app/lib/undo-reveal.ts +332 -0
  784. package/corpus/templates/slides-2/app/lib/upload-response.ts +107 -0
  785. package/corpus/templates/slides-2/app/lib/utils.ts +38 -0
  786. package/corpus/templates/slides-2/app/lib/validate-native-slide-html.ts +177 -0
  787. package/corpus/templates/slides-2/app/lib/video-upload.ts +354 -0
  788. package/corpus/templates/slides-2/app/pages/DeckEditor.tsx +6158 -0
  789. package/corpus/templates/slides-2/app/pages/DesignSystems.tsx +363 -0
  790. package/corpus/templates/slides-2/app/pages/Index.tsx +3254 -0
  791. package/corpus/templates/slides-2/app/pages/NotFound.tsx +33 -0
  792. package/corpus/templates/slides-2/app/pages/Presentation.tsx +120 -0
  793. package/corpus/templates/slides-2/app/pages/SharedPresentation.tsx +174 -0
  794. package/corpus/templates/slides-2/app/pages/SlideThemeDetail.tsx +227 -0
  795. package/corpus/templates/slides-2/app/pages/SlideThemes.tsx +155 -0
  796. package/corpus/templates/slides-2/app/pages/Slides2EditorPage.tsx +1618 -0
  797. package/corpus/templates/slides-2/app/pages/Slides2PresenterConsolePage.tsx +937 -0
  798. package/corpus/templates/slides-2/app/pages/Slides2PresenterPage.tsx +979 -0
  799. package/corpus/templates/slides-2/app/pages/Templates.tsx +32 -0
  800. package/corpus/templates/slides-2/app/root.tsx +391 -0
  801. package/corpus/templates/slides-2/app/routes/$.tsx +10 -0
  802. package/corpus/templates/slides-2/app/routes/_index.tsx +1 -0
  803. package/corpus/templates/slides-2/app/routes/access-request.approve.tsx +276 -0
  804. package/corpus/templates/slides-2/app/routes/agent.tsx +19 -0
  805. package/corpus/templates/slides-2/app/routes/chat.$threadId.tsx +1 -0
  806. package/corpus/templates/slides-2/app/routes/chat.tsx +190 -0
  807. package/corpus/templates/slides-2/app/routes/deck.$id.tsx +10 -0
  808. package/corpus/templates/slides-2/app/routes/deck.$id_.present.tsx +10 -0
  809. package/corpus/templates/slides-2/app/routes/design-systems.tsx +10 -0
  810. package/corpus/templates/slides-2/app/routes/extensions.$id.$slug.tsx +2 -0
  811. package/corpus/templates/slides-2/app/routes/extensions.$id.tsx +5 -0
  812. package/corpus/templates/slides-2/app/routes/extensions._index.tsx +5 -0
  813. package/corpus/templates/slides-2/app/routes/extensions.tsx +5 -0
  814. package/corpus/templates/slides-2/app/routes/home-layout.tsx +31 -0
  815. package/corpus/templates/slides-2/app/routes/home.tsx +22 -0
  816. package/corpus/templates/slides-2/app/routes/p.$id.tsx +195 -0
  817. package/corpus/templates/slides-2/app/routes/settings.$.tsx +1 -0
  818. package/corpus/templates/slides-2/app/routes/settings.tsx +131 -0
  819. package/corpus/templates/slides-2/app/routes/share.$token.tsx +137 -0
  820. package/corpus/templates/slides-2/app/routes/slide.tsx +209 -0
  821. package/corpus/templates/slides-2/app/routes/team.tsx +11 -0
  822. package/corpus/templates/slides-2/app/routes/templates.tsx +1 -0
  823. package/corpus/templates/slides-2/app/routes/themes.$id.tsx +1 -0
  824. package/corpus/templates/slides-2/app/routes/themes.tsx +1 -0
  825. package/corpus/templates/slides-2/app/routes.ts +24 -0
  826. package/corpus/templates/slides-2/app/vite-env.d.ts +24 -0
  827. package/corpus/templates/slides-2/changelog/2026-10-17-find-and-switch-decks-from-the-presenter-console.md +6 -0
  828. package/corpus/templates/slides-2/changelog/2026-10-17-meet-slides-2-create-and-refine-editable-slide-decks-with-an.md +6 -0
  829. package/corpus/templates/slides-2/changelog/2026-10-17-pptx-image-export.md +6 -0
  830. package/corpus/templates/slides-2/changelog/2026-10-17-visual-editor.md +6 -0
  831. package/corpus/templates/slides-2/changelog/2026-10-18-apply-an-accessible-design-system-to-an-existing-deck-before.md +6 -0
  832. package/corpus/templates/slides-2/changelog/2026-10-18-choose-a-template-or-start-a-new-presentation-from-the-slide.md +6 -0
  833. package/corpus/templates/slides-2/changelog/2026-10-18-generate-decks-from-a-prompt-then-edit-slide-elements-direct.md +6 -0
  834. package/corpus/templates/slides-2/changelog/2026-10-18-generate-presentation-decks-from-a-prompt-and-edit-every-sli.md +6 -0
  835. package/corpus/templates/slides-2/changelog/2026-10-18-new-presentation-drafts-save-before-generation-starts-so-you.md +6 -0
  836. package/corpus/templates/slides-2/changelog/2026-10-18-the-assistant-can-undo-redo-and-save-pending-edits-in-the-vi.md +6 -0
  837. package/corpus/templates/slides-2/changelog/2026-10-19-agents-can-insert-duplicate-and-delete-canvas-objects-in-sou.md +6 -0
  838. package/corpus/templates/slides-2/changelog/2026-10-19-browse-slide-themes.md +6 -0
  839. package/corpus/templates/slides-2/changelog/2026-10-19-clicking-a-slide-preview-in-overview-selects-that-slide.md +6 -0
  840. package/corpus/templates/slides-2/changelog/2026-10-19-create-decks-from-a-prompt-and-edit-slides-directly-on-the-c.md +6 -0
  841. package/corpus/templates/slides-2/changelog/2026-10-19-creating-and-editing-decks-now-starts-with-the-tools-most-re.md +6 -0
  842. package/corpus/templates/slides-2/changelog/2026-10-19-deleting-decks-revokes-public-links.md +7 -0
  843. package/corpus/templates/slides-2/changelog/2026-10-19-escape-cancels-an-in-progress-slide-drag-without-losing-earl.md +6 -0
  844. package/corpus/templates/slides-2/changelog/2026-10-19-import-failures-now-show-clear-localized-descriptions-instea.md +6 -0
  845. package/corpus/templates/slides-2/changelog/2026-10-19-presenter-controls-stay-responsive-when-moving-over-the-slid.md +6 -0
  846. package/corpus/templates/slides-2/changelog/2026-10-19-presenter-notes-stay-private-from-viewers-and-commenters.md +6 -0
  847. package/corpus/templates/slides-2/changelog/2026-10-19-preview-generated-slides-in-chat.md +6 -0
  848. package/corpus/templates/slides-2/changelog/2026-10-19-remote-image-previews-accept-supported-raster-formats.md +6 -0
  849. package/corpus/templates/slides-2/changelog/2026-10-19-search-the-svgl-catalog-and-add-svg-logos-to-a-deck-or-the-g.md +6 -0
  850. package/corpus/templates/slides-2/changelog/2026-10-19-slide-generation-explains-when-react-effect-hooks-cannot-run.md +6 -0
  851. package/corpus/templates/slides-2/changelog/2026-10-19-the-editor-highlights-hovered-objects-and-keeps-image-replac.md +6 -0
  852. package/corpus/templates/slides-2/changelog/2026-10-19-the-slide-editor-uses-the-shared-agent-native-controls.md +6 -0
  853. package/corpus/templates/slides-2/changelog/2026-10-20-deck-editing-controls-match-the-workspace.md +6 -0
  854. package/corpus/templates/slides-2/changelog/2026-10-20-google-slides-imports-now-report-missing-image-data-instead-.md +6 -0
  855. package/corpus/templates/slides-2/changelog/2026-10-20-imports-and-previews-handle-large-content-safely.md +6 -0
  856. package/corpus/templates/slides-2/changelog/2026-10-20-public-deck-pages-can-export-source-backed-slides-to-pdf.md +6 -0
  857. package/corpus/templates/slides-2/changelog/2026-10-20-saved-slide-images-stay-available.md +6 -0
  858. package/corpus/templates/slides-2/changelog/2026-10-20-scope-global-media-picks-to-assets-the-current-deck-can-reso.md +6 -0
  859. package/corpus/templates/slides-2/components.json +20 -0
  860. package/corpus/templates/slides-2/drizzle.config.ts +3 -0
  861. package/corpus/templates/slides-2/learnings.defaults.md +5 -0
  862. package/corpus/templates/slides-2/netlify.toml +24 -0
  863. package/corpus/templates/slides-2/package.json +147 -0
  864. package/corpus/templates/slides-2/public/agent-native-icon-dark.svg +10 -0
  865. package/corpus/templates/slides-2/public/agent-native-icon-light.svg +10 -0
  866. package/corpus/templates/slides-2/public/agent-native-logo-dark.svg +21 -0
  867. package/corpus/templates/slides-2/public/agent-native-logo-light.svg +21 -0
  868. package/corpus/templates/slides-2/public/assets/builder-logo-white.svg +15 -0
  869. package/corpus/templates/slides-2/public/assets/generated/preview.html +61 -0
  870. package/corpus/templates/slides-2/public/assets/intuit-logo-white.svg +6 -0
  871. package/corpus/templates/slides-2/public/assets/walmart-logo-white.svg +6 -0
  872. package/corpus/templates/slides-2/public/chatgpt-plugin-icon.svg +7 -0
  873. package/corpus/templates/slides-2/public/favicon.svg +1 -0
  874. package/corpus/templates/slides-2/public/icon-180.svg +1 -0
  875. package/corpus/templates/slides-2/public/icon-192.svg +1 -0
  876. package/corpus/templates/slides-2/public/icon-512.svg +1 -0
  877. package/corpus/templates/slides-2/public/manifest.json +20 -0
  878. package/corpus/templates/slides-2/public/placeholder.svg +1 -0
  879. package/corpus/templates/slides-2/public/robots.txt +14 -0
  880. package/corpus/templates/slides-2/public/visual-edit-structure-proof.html +99 -0
  881. package/corpus/templates/slides-2/react-router.config.ts +7 -0
  882. package/corpus/templates/slides-2/scripts/edit-fidelity/README.md +499 -0
  883. package/corpus/templates/slides-2/scripts/edit-fidelity/authoring-fuzz.ts +9517 -0
  884. package/corpus/templates/slides-2/scripts/edit-fidelity/baseline.json +16427 -0
  885. package/corpus/templates/slides-2/scripts/edit-fidelity/dev-server-url.ts +10 -0
  886. package/corpus/templates/slides-2/scripts/edit-fidelity/lib/in-page.ts +1975 -0
  887. package/corpus/templates/slides-2/scripts/edit-fidelity/lib/metrics.ts +1401 -0
  888. package/corpus/templates/slides-2/scripts/edit-fidelity/retry-infra.ts +7 -0
  889. package/corpus/templates/slides-2/scripts/edit-fidelity/run-options.ts +21 -0
  890. package/corpus/templates/slides-2/scripts/edit-fidelity/run-outcomes.ts +127 -0
  891. package/corpus/templates/slides-2/scripts/edit-fidelity/run.ts +8451 -0
  892. package/corpus/templates/slides-2/scripts/export-fidelity/README.md +116 -0
  893. package/corpus/templates/slides-2/scripts/export-fidelity/compare-layout-lib.ts +241 -0
  894. package/corpus/templates/slides-2/scripts/export-fidelity/compare-layout.ts +30 -0
  895. package/corpus/templates/slides-2/scripts/export-fidelity/diff.ts +222 -0
  896. package/corpus/templates/slides-2/scripts/export-fidelity/export.ts +765 -0
  897. package/corpus/templates/slides-2/scripts/export-fidelity/fixtures/agent-dark-deck.json +48 -0
  898. package/corpus/templates/slides-2/scripts/export-fidelity/generate-calibration.ts +765 -0
  899. package/corpus/templates/slides-2/scripts/export-fidelity/google-export.ts +137 -0
  900. package/corpus/templates/slides-2/scripts/export-fidelity/google-layout.ts +82 -0
  901. package/corpus/templates/slides-2/scripts/export-fidelity/resolve-pkg.ts +29 -0
  902. package/corpus/templates/slides-2/scripts/migrate-production.ts +22 -0
  903. package/corpus/templates/slides-2/server/db/index.ts +73 -0
  904. package/corpus/templates/slides-2/server/db/schema.ts +189 -0
  905. package/corpus/templates/slides-2/server/handlers/assets-url.ts +13 -0
  906. package/corpus/templates/slides-2/server/handlers/assets.ts +3217 -0
  907. package/corpus/templates/slides-2/server/handlers/decks.ts +185 -0
  908. package/corpus/templates/slides-2/server/handlers/demo.ts +9 -0
  909. package/corpus/templates/slides-2/server/handlers/design-systems.ts +219 -0
  910. package/corpus/templates/slides-2/server/handlers/google-docs-auth.ts +165 -0
  911. package/corpus/templates/slides-2/server/handlers/image-gen.ts +153 -0
  912. package/corpus/templates/slides-2/server/handlers/image-providers/gemini.ts +209 -0
  913. package/corpus/templates/slides-2/server/handlers/image-providers/index.ts +61 -0
  914. package/corpus/templates/slides-2/server/handlers/image-providers/openai.ts +288 -0
  915. package/corpus/templates/slides-2/server/handlers/image-providers/types.ts +35 -0
  916. package/corpus/templates/slides-2/server/handlers/import/docx-parser.ts +31 -0
  917. package/corpus/templates/slides-2/server/handlers/import/html-converter.ts +1534 -0
  918. package/corpus/templates/slides-2/server/handlers/import/pdf-fidelity-parser.ts +1039 -0
  919. package/corpus/templates/slides-2/server/handlers/import/pdf-sidecar-reader.ts +81 -0
  920. package/corpus/templates/slides-2/server/handlers/import/pdf-style-digest.ts +187 -0
  921. package/corpus/templates/slides-2/server/handlers/import/pptx-assets.ts +155 -0
  922. package/corpus/templates/slides-2/server/handlers/import/pptx-parser.ts +57 -0
  923. package/corpus/templates/slides-2/server/handlers/request-auth-context.ts +108 -0
  924. package/corpus/templates/slides-2/server/handlers/share.ts +52 -0
  925. package/corpus/templates/slides-2/server/handlers/uploads-chunked.ts +1066 -0
  926. package/corpus/templates/slides-2/server/handlers/uploads.ts +457 -0
  927. package/corpus/templates/slides-2/server/lib/access-request-email.ts +66 -0
  928. package/corpus/templates/slides-2/server/lib/assets-image-delegation.ts +331 -0
  929. package/corpus/templates/slides-2/server/lib/bounded-multipart.ts +49 -0
  930. package/corpus/templates/slides-2/server/lib/builder-design-system-proxy.ts +122 -0
  931. package/corpus/templates/slides-2/server/lib/chat-attachments.ts +425 -0
  932. package/corpus/templates/slides-2/server/lib/chatgpt-directory-tools.ts +246 -0
  933. package/corpus/templates/slides-2/server/lib/chunked-upload-session.ts +195 -0
  934. package/corpus/templates/slides-2/server/lib/comment-notifications.ts +184 -0
  935. package/corpus/templates/slides-2/server/lib/deck-access-requests.ts +102 -0
  936. package/corpus/templates/slides-2/server/lib/deck-asset-scope.ts +150 -0
  937. package/corpus/templates/slides-2/server/lib/deck-export-tracking.ts +119 -0
  938. package/corpus/templates/slides-2/server/lib/deck-templates/a-case-study.ts +154 -0
  939. package/corpus/templates/slides-2/server/lib/deck-templates/a-company.ts +160 -0
  940. package/corpus/templates/slides-2/server/lib/deck-templates/a-pitch.ts +151 -0
  941. package/corpus/templates/slides-2/server/lib/deck-templates/a-quarterly.ts +264 -0
  942. package/corpus/templates/slides-2/server/lib/deck-templates/a-shared.ts +35 -0
  943. package/corpus/templates/slides-2/server/lib/deck-templates/a-update.ts +188 -0
  944. package/corpus/templates/slides-2/server/lib/deck-templates/a-workshop.ts +196 -0
  945. package/corpus/templates/slides-2/server/lib/deck-templates/b-keynote.ts +131 -0
  946. package/corpus/templates/slides-2/server/lib/deck-templates/b-lesson.ts +209 -0
  947. package/corpus/templates/slides-2/server/lib/deck-templates/b-manifesto.ts +149 -0
  948. package/corpus/templates/slides-2/server/lib/deck-templates/b-portfolio.ts +144 -0
  949. package/corpus/templates/slides-2/server/lib/deck-templates/b-research.ts +221 -0
  950. package/corpus/templates/slides-2/server/lib/deck-templates/b-roadmap.ts +264 -0
  951. package/corpus/templates/slides-2/server/lib/deck-templates/b-shared.ts +44 -0
  952. package/corpus/templates/slides-2/server/lib/deck-templates/group-a.ts +16 -0
  953. package/corpus/templates/slides-2/server/lib/deck-templates/group-b.ts +16 -0
  954. package/corpus/templates/slides-2/server/lib/deck-templates/legacy.ts +567 -0
  955. package/corpus/templates/slides-2/server/lib/deck-templates.ts +60 -0
  956. package/corpus/templates/slides-2/server/lib/deck-versions.ts +233 -0
  957. package/corpus/templates/slides-2/server/lib/design-system-workflows.ts +22 -0
  958. package/corpus/templates/slides-2/server/lib/emails.ts +84 -0
  959. package/corpus/templates/slides-2/server/lib/env-config.ts +15 -0
  960. package/corpus/templates/slides-2/server/lib/fetch-remote-image.ts +245 -0
  961. package/corpus/templates/slides-2/server/lib/generation-completion.ts +278 -0
  962. package/corpus/templates/slides-2/server/lib/google-docs-access.ts +51 -0
  963. package/corpus/templates/slides-2/server/lib/google-docs-error.ts +54 -0
  964. package/corpus/templates/slides-2/server/lib/google-docs-oauth.ts +422 -0
  965. package/corpus/templates/slides-2/server/lib/google-fonts.ts +509 -0
  966. package/corpus/templates/slides-2/server/lib/google-oauth-preflight.ts +119 -0
  967. package/corpus/templates/slides-2/server/lib/google-slides-export-availability.ts +55 -0
  968. package/corpus/templates/slides-2/server/lib/image-proxy-response.ts +16 -0
  969. package/corpus/templates/slides-2/server/lib/image-proxy-url.ts +155 -0
  970. package/corpus/templates/slides-2/server/lib/import-asset-storage.ts +214 -0
  971. package/corpus/templates/slides-2/server/lib/imported-asset-batch.ts +218 -0
  972. package/corpus/templates/slides-2/server/lib/legacy-slide-source.ts +692 -0
  973. package/corpus/templates/slides-2/server/lib/media-search-providers.ts +216 -0
  974. package/corpus/templates/slides-2/server/lib/native-creative-context.ts +295 -0
  975. package/corpus/templates/slides-2/server/lib/pdf-parse-setup.ts +115 -0
  976. package/corpus/templates/slides-2/server/lib/provider-api.ts +57 -0
  977. package/corpus/templates/slides-2/server/lib/request-wait-until.ts +30 -0
  978. package/corpus/templates/slides-2/server/lib/shared-deck.ts +29 -0
  979. package/corpus/templates/slides-2/server/lib/slide-asset-references.ts +309 -0
  980. package/corpus/templates/slides-2/server/lib/slide-content-patch.ts +1004 -0
  981. package/corpus/templates/slides-2/server/lib/slide-hygiene.ts +731 -0
  982. package/corpus/templates/slides-2/server/lib/slide-image-insertion.ts +136 -0
  983. package/corpus/templates/slides-2/server/lib/slide-reveal-issues.ts +41 -0
  984. package/corpus/templates/slides-2/server/lib/slide-source-editing/babel-walk.ts +117 -0
  985. package/corpus/templates/slides-2/server/lib/slide-source-editing/batch-edit.ts +152 -0
  986. package/corpus/templates/slides-2/server/lib/slide-source-editing/edit-ops.ts +1478 -0
  987. package/corpus/templates/slides-2/server/lib/slide-source-editing/image-target.ts +128 -0
  988. package/corpus/templates/slides-2/server/lib/slide-source-editing/text-diff.ts +23 -0
  989. package/corpus/templates/slides-2/server/lib/slide-source-limits.ts +9 -0
  990. package/corpus/templates/slides-2/server/lib/slide-source.ts +1786 -0
  991. package/corpus/templates/slides-2/server/lib/slide-theme-demo-sources.ts +2106 -0
  992. package/corpus/templates/slides-2/server/lib/slide-themes.ts +2174 -0
  993. package/corpus/templates/slides-2/server/lib/slide-worker-runtime.ts +1995 -0
  994. package/corpus/templates/slides-2/server/lib/slides-tracking.ts +73 -0
  995. package/corpus/templates/slides-2/server/lib/source-import.ts +213 -0
  996. package/corpus/templates/slides-2/server/lib/svgl.ts +364 -0
  997. package/corpus/templates/slides-2/server/lib/tenant-files.ts +179 -0
  998. package/corpus/templates/slides-2/server/lib/upload-storage-status.ts +9 -0
  999. package/corpus/templates/slides-2/server/lib/uploaded-asset-cleanup-outbox.ts +521 -0
  1000. package/corpus/templates/slides-2/server/lib/uploaded-asset-object-lifecycle.ts +404 -0
  1001. package/corpus/templates/slides-2/server/lib/uploaded-fonts.ts +696 -0
  1002. package/corpus/templates/slides-2/server/lib/uploaded-reference-storage.ts +77 -0
  1003. package/corpus/templates/slides-2/server/lib/validate-slide-animations.ts +341 -0
  1004. package/corpus/templates/slides-2/server/middleware/auth.ts +6 -0
  1005. package/corpus/templates/slides-2/server/plugins/_dev-upload-stub.ts +6 -0
  1006. package/corpus/templates/slides-2/server/plugins/agent-chat.ts +402 -0
  1007. package/corpus/templates/slides-2/server/plugins/auth.ts +31 -0
  1008. package/corpus/templates/slides-2/server/plugins/collab.ts +30 -0
  1009. package/corpus/templates/slides-2/server/plugins/config.ts +6 -0
  1010. package/corpus/templates/slides-2/server/plugins/core-routes.ts +30 -0
  1011. package/corpus/templates/slides-2/server/plugins/creative-context.ts +187 -0
  1012. package/corpus/templates/slides-2/server/plugins/db.ts +462 -0
  1013. package/corpus/templates/slides-2/server/plugins/feature-flags.ts +5 -0
  1014. package/corpus/templates/slides-2/server/plugins/labs.ts +8 -0
  1015. package/corpus/templates/slides-2/server/plugins/orphaned-asset-cleanup.ts +17 -0
  1016. package/corpus/templates/slides-2/server/plugins/otel.ts +6 -0
  1017. package/corpus/templates/slides-2/server/plugins/transactional-emails.ts +5 -0
  1018. package/corpus/templates/slides-2/server/register-secrets.ts +209 -0
  1019. package/corpus/templates/slides-2/server/routes/[...page].get.ts +5 -0
  1020. package/corpus/templates/slides-2/server/routes/[...page].head.ts +1 -0
  1021. package/corpus/templates/slides-2/server/routes/[base]/_agent-native/google-docs/auth-url.get.ts +1 -0
  1022. package/corpus/templates/slides-2/server/routes/[base]/_agent-native/google-docs/callback.get.ts +1 -0
  1023. package/corpus/templates/slides-2/server/routes/_agent-native/google-docs/auth-url.get.ts +1 -0
  1024. package/corpus/templates/slides-2/server/routes/_agent-native/google-docs/callback.get.ts +1 -0
  1025. package/corpus/templates/slides-2/server/routes/api/assets/upload-video.post.ts +1 -0
  1026. package/corpus/templates/slides-2/server/routes/api/assets/upload.post.ts +1 -0
  1027. package/corpus/templates/slides-2/server/routes/api/assets/video-uploads.delete.ts +1 -0
  1028. package/corpus/templates/slides-2/server/routes/api/deck-agent-context.json.get.ts +105 -0
  1029. package/corpus/templates/slides-2/server/routes/api/decks/events.get.ts +1 -0
  1030. package/corpus/templates/slides-2/server/routes/api/demo.get.ts +1 -0
  1031. package/corpus/templates/slides-2/server/routes/api/exports/[filename].get.ts +221 -0
  1032. package/corpus/templates/slides-2/server/routes/api/exports/google-slides.post.ts +240 -0
  1033. package/corpus/templates/slides-2/server/routes/api/exports/html.post.ts +67 -0
  1034. package/corpus/templates/slides-2/server/routes/api/exports/pptx.post.ts +100 -0
  1035. package/corpus/templates/slides-2/server/routes/api/gen-preview/[...].get.ts +24 -0
  1036. package/corpus/templates/slides-2/server/routes/api/generated/[...].get.ts +24 -0
  1037. package/corpus/templates/slides-2/server/routes/api/image-proxy.get.ts +518 -0
  1038. package/corpus/templates/slides-2/server/routes/api/import-assets/[token].get.ts +70 -0
  1039. package/corpus/templates/slides-2/server/routes/api/preview-images.get.ts +20 -0
  1040. package/corpus/templates/slides-2/server/routes/api/share/[token].get.ts +1 -0
  1041. package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId]/chunk.post.ts +1 -0
  1042. package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId]/status.get.ts +1 -0
  1043. package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId].delete.ts +1 -0
  1044. package/corpus/templates/slides-2/server/routes/api/uploads-chunked/start.post.ts +1 -0
  1045. package/corpus/templates/slides-2/server/routes/api/uploads.delete.ts +1 -0
  1046. package/corpus/templates/slides-2/server/routes/api/uploads.post.ts +1 -0
  1047. package/corpus/templates/slides-2/server/workspace-defaults.ts +196 -0
  1048. package/corpus/templates/slides-2/shared/action-ui.ts +147 -0
  1049. package/corpus/templates/slides-2/shared/agent-readable.ts +2 -0
  1050. package/corpus/templates/slides-2/shared/api.ts +406 -0
  1051. package/corpus/templates/slides-2/shared/app-state-tabs.ts +30 -0
  1052. package/corpus/templates/slides-2/shared/app-storage.ts +9 -0
  1053. package/corpus/templates/slides-2/shared/aspect-ratios.ts +49 -0
  1054. package/corpus/templates/slides-2/shared/blank-slide.ts +179 -0
  1055. package/corpus/templates/slides-2/shared/client-compatibility.ts +2 -0
  1056. package/corpus/templates/slides-2/shared/contrast-audit.ts +255 -0
  1057. package/corpus/templates/slides-2/shared/deck-access.ts +26 -0
  1058. package/corpus/templates/slides-2/shared/deck-app-scope.ts +14 -0
  1059. package/corpus/templates/slides-2/shared/deck-content.ts +37 -0
  1060. package/corpus/templates/slides-2/shared/deck-history-control.ts +79 -0
  1061. package/corpus/templates/slides-2/shared/deck-scope.ts +5 -0
  1062. package/corpus/templates/slides-2/shared/deck-title.ts +199 -0
  1063. package/corpus/templates/slides-2/shared/deck-write.ts +61 -0
  1064. package/corpus/templates/slides-2/shared/design-flags.ts +7 -0
  1065. package/corpus/templates/slides-2/shared/design-system-validation.ts +88 -0
  1066. package/corpus/templates/slides-2/shared/editor-control.ts +120 -0
  1067. package/corpus/templates/slides-2/shared/generation-timing.ts +19 -0
  1068. package/corpus/templates/slides-2/shared/google-docs.ts +69 -0
  1069. package/corpus/templates/slides-2/shared/labs.ts +10 -0
  1070. package/corpus/templates/slides-2/shared/overview-navigation.ts +34 -0
  1071. package/corpus/templates/slides-2/shared/ownership.ts +6 -0
  1072. package/corpus/templates/slides-2/shared/pdf-sidecar.ts +130 -0
  1073. package/corpus/templates/slides-2/shared/presenter-control.ts +111 -0
  1074. package/corpus/templates/slides-2/shared/representative-slide.ts +89 -0
  1075. package/corpus/templates/slides-2/shared/slide-background.ts +30 -0
  1076. package/corpus/templates/slides-2/shared/slide-comment-anchor.ts +117 -0
  1077. package/corpus/templates/slides-2/shared/slide-comment-reactions.ts +47 -0
  1078. package/corpus/templates/slides-2/shared/slide-design-tokens.ts +92 -0
  1079. package/corpus/templates/slides-2/shared/slide-fit.ts +78 -0
  1080. package/corpus/templates/slides-2/shared/slide-ids.ts +133 -0
  1081. package/corpus/templates/slides-2/shared/slide-number.ts +124 -0
  1082. package/corpus/templates/slides-2/shared/slide-object-structure.ts +790 -0
  1083. package/corpus/templates/slides-2/shared/slide-text-range-selection.ts +50 -0
  1084. package/corpus/templates/slides-2/shared/slide-transitions.ts +70 -0
  1085. package/corpus/templates/slides-2/shared/slides-2-transition.ts +194 -0
  1086. package/corpus/templates/slides-2/shared/slides-user-prefs.ts +22 -0
  1087. package/corpus/templates/slides-2/shared/source-import-errors.ts +12 -0
  1088. package/corpus/templates/slides-2/shared/source-runtime-export.ts +76 -0
  1089. package/corpus/templates/slides-2/shared/upload-types.ts +41 -0
  1090. package/corpus/templates/slides-2/ssr-entry.ts +11 -0
  1091. package/corpus/templates/slides-2/tsconfig.json +27 -0
  1092. package/corpus/templates/slides-2/vite.config.ts +77 -0
  1093. package/corpus/templates/slides-2/vitest.config.ts +28 -0
  1094. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +1 -1
  1095. package/package.json +1 -1
  1096. package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +0 -182
  1097. package/corpus/templates/design/actions/apply-shader-fill.ts +0 -450
  1098. package/corpus/templates/design/actions/apply-shader.ts +0 -257
  1099. package/corpus/templates/design/actions/preview-shader-fill.ts +0 -157
  1100. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +0 -1144
  1101. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +0 -450
  1102. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +0 -71
  1103. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +0 -555
  1104. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +0 -105
  1105. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +0 -433
  1106. package/corpus/templates/design/app/components/design/inspector/color-picker-shader-pane.tsx +0 -648
  1107. package/corpus/templates/design/shared/shader-fill.ts +0 -174
  1108. package/corpus/templates/design/shared/shader-presets.ts +0 -1523
@@ -1,4 +1,8 @@
1
- import { matchInlineMathAt } from "./inline-math.js";
1
+ import {
2
+ isEscapedInlineDelimiter,
3
+ matchInlineMathAt,
4
+ MAX_INLINE_MATH_LENGTH,
5
+ } from "./inline-math.js";
2
6
  import { registryBlockSpecByTag } from "./nfm-registry.js";
3
7
 
4
8
  export interface PMMark {
@@ -31,6 +35,23 @@ export interface NfmFidelityReport {
31
35
  error?: string;
32
36
  }
33
37
 
38
+ /** Source the parse reads but cannot keep. */
39
+ export type NfmParseLossKind =
40
+ | "dropped-source"
41
+ | "dropped-attribute"
42
+ | "dropped-formatting";
43
+
44
+ export interface NfmParseLoss {
45
+ kind: NfmParseLossKind;
46
+ sample: string;
47
+ }
48
+
49
+ let activeLossSink: NfmParseLoss[] | null = null;
50
+
51
+ function reportLoss(kind: NfmParseLossKind, sample: string): void {
52
+ activeLossSink?.push({ kind, sample });
53
+ }
54
+
34
55
  const BASE_COLORS = [
35
56
  "gray",
36
57
  "brown",
@@ -51,21 +72,25 @@ function isColor(value: string | null | undefined): value is string {
51
72
  return !!value && NFM_COLORS.has(value) && value !== "default";
52
73
  }
53
74
 
54
- const ESCAPABLE = new Set("\\*~`$[]<>{}|^".split(""));
75
+ const ESCAPABLE = /[\\*~`$[\]<>{}|^]/g;
76
+
77
+ // CommonMark lets a backslash escape any ASCII punctuation, and agents write
78
+ // `\_` or `1\.`. Writing still escapes only `ESCAPABLE`, so the bytes the
79
+ // editor already stored keep their spelling.
80
+ const PARSE_ESCAPABLE = new Set("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".split(""));
55
81
 
56
82
  export function escapeInlineText(text: string): string {
57
- let out = "";
58
- for (const ch of text) {
59
- if (ESCAPABLE.has(ch)) out += "\\" + ch;
60
- else out += ch;
61
- }
62
- return out;
83
+ return text.replace(ESCAPABLE, "\\$&");
63
84
  }
64
85
 
65
86
  function unescapeInlineText(text: string): string {
66
87
  let out = "";
67
88
  for (let i = 0; i < text.length; i++) {
68
- if (text[i] === "\\" && i + 1 < text.length && ESCAPABLE.has(text[i + 1])) {
89
+ if (
90
+ text[i] === "\\" &&
91
+ i + 1 < text.length &&
92
+ PARSE_ESCAPABLE.has(text[i + 1])
93
+ ) {
69
94
  out += text[i + 1];
70
95
  i++;
71
96
  } else {
@@ -79,8 +104,16 @@ const LEADING_BLOCK_MARKER = /^(#{1,6} |[-*+] |\d+[.)] |\[[ xX]\] )/;
79
104
 
80
105
  const DIVIDER_LOOKALIKE = /^(-{3,}|\*{3,}|_{3,})$/;
81
106
 
107
+ // A list marker after spaces nests under the list above it, the way agents
108
+ // indent with spaces, so paragraph text shaped like one is escaped too.
109
+ const SPACED_LIST_MARKER = /^ +(?:[-*+] |\d+[.)] )/;
110
+
82
111
  function escapeLeadingBlockMarker(text: string): string {
83
- if (LEADING_BLOCK_MARKER.test(text) || DIVIDER_LOOKALIKE.test(text.trim())) {
112
+ if (
113
+ LEADING_BLOCK_MARKER.test(text) ||
114
+ SPACED_LIST_MARKER.test(text) ||
115
+ DIVIDER_LOOKALIKE.test(text.trim())
116
+ ) {
84
117
  return "\\" + text;
85
118
  }
86
119
  return text;
@@ -90,6 +123,7 @@ function unescapeLeadingBlockMarker(text: string): string {
90
123
  if (
91
124
  text[0] === "\\" &&
92
125
  (LEADING_BLOCK_MARKER.test(text.slice(1)) ||
126
+ SPACED_LIST_MARKER.test(text.slice(1)) ||
93
127
  DIVIDER_LOOKALIKE.test(text.slice(1).trim()))
94
128
  ) {
95
129
  return text.slice(1);
@@ -121,13 +155,91 @@ function serializeAttrs(
121
155
  );
122
156
  return parts.length ? " " + parts.join(" ") : "";
123
157
  }
158
+ const ATTR_NAME_CHAR = /[\w:-]/;
159
+ const ATTR_NAME_START = /[a-zA-Z_:]/;
160
+ const SPACE_CHAR = /\s/;
161
+
162
+ // Each `name="value"` in `raw`, as /([a-zA-Z_:][\w:-]*)\s*=\s*"([^"]*)"/g
163
+ // finds them. Found by hand: that pattern tries a name from every offset of a
164
+ // word, and rescans the rest of a long word from each one.
165
+ function attrMatches(
166
+ raw: string,
167
+ ): Array<{ from: number; to: number; name: string; value: string }> {
168
+ const found: Array<{
169
+ from: number;
170
+ to: number;
171
+ name: string;
172
+ value: string;
173
+ }> = [];
174
+ let i = 0;
175
+ while (i < raw.length) {
176
+ if (!ATTR_NAME_CHAR.test(raw[i])) {
177
+ i++;
178
+ continue;
179
+ }
180
+ let end = i;
181
+ while (end < raw.length && ATTR_NAME_CHAR.test(raw[end])) end++;
182
+ // A name from any later offset of the word ends where this one does.
183
+ let from = i;
184
+ while (from < end && !ATTR_NAME_START.test(raw[from])) from++;
185
+ let at = end;
186
+ while (at < raw.length && SPACE_CHAR.test(raw[at])) at++;
187
+ if (from < end && raw[at] === "=") {
188
+ at++;
189
+ while (at < raw.length && SPACE_CHAR.test(raw[at])) at++;
190
+ const close = raw[at] === '"' ? raw.indexOf('"', at + 1) : -1;
191
+ if (close !== -1) {
192
+ found.push({
193
+ from,
194
+ to: close + 1,
195
+ name: raw.slice(from, end),
196
+ value: raw.slice(at + 1, close),
197
+ });
198
+ i = close + 1;
199
+ continue;
200
+ }
201
+ }
202
+ i = end;
203
+ }
204
+ return found;
205
+ }
124
206
  function parseAttrs(raw: string): Record<string, string> {
125
207
  const attrs: Record<string, string> = {};
126
- const re = /([a-zA-Z_:][\w:-]*)\s*=\s*"([^"]*)"/g;
127
- let m: RegExpExecArray | null;
128
- while ((m = re.exec(raw))) attrs[m[1]] = unescapeAttr(m[2]);
208
+ for (const { name, value } of attrMatches(raw))
209
+ attrs[name] = unescapeAttr(value);
210
+ return attrs;
211
+ }
212
+ /** Attribute source `parseAttrs` skips: unquoted, single-quoted or bare. */
213
+ function unparsedAttrSource(raw: string): string {
214
+ let rest = "";
215
+ let last = 0;
216
+ for (const { from, to } of attrMatches(raw)) {
217
+ rest += raw.slice(last, from);
218
+ last = to;
219
+ }
220
+ return (rest + raw.slice(last)).replace(/\/\s*$/, "").trim();
221
+ }
222
+
223
+ /**
224
+ * `parseAttrs`, reporting what the node cannot keep: source it cannot read
225
+ * and, when the node keeps only `kept`, any other attribute and a color it
226
+ * does not know.
227
+ */
228
+ function readAttrs(
229
+ tag: string,
230
+ raw: string,
231
+ kept?: ReadonlySet<string>,
232
+ ): Record<string, string> {
233
+ const attrs = parseAttrs(raw);
234
+ for (const [name, value] of Object.entries(attrs)) {
235
+ if (kept && (!kept.has(name) || (name === "color" && !isColor(value))))
236
+ reportLoss("dropped-attribute", `<${tag} ${name}="${value}">`);
237
+ }
238
+ const unparsed = unparsedAttrSource(raw);
239
+ if (unparsed) reportLoss("dropped-attribute", `<${tag} ${unparsed}>`);
129
240
  return attrs;
130
241
  }
242
+
131
243
  function hasUnsupportedJsxProps(raw: string): boolean {
132
244
  const withoutDoubleQuotedStrings = raw.replace(/"[^"]*"/g, '""');
133
245
  return (
@@ -145,45 +257,77 @@ function blockAttrSuffix(opts: {
145
257
  if (isColor(opts.color)) parts.push(`color="${opts.color}"`);
146
258
  return parts.length ? ` {${parts.join(" ")}}` : "";
147
259
  }
260
+ // Counted by hand: /\\+$/ retries from every backslash of a run that does
261
+ // not end the string.
262
+ function trailingBackslashes(s: string): number {
263
+ let run = 0;
264
+ while (run < s.length && s[s.length - 1 - run] === "\\") run++;
265
+ return run;
266
+ }
267
+
148
268
  function oddTrailingBackslashes(s: string): boolean {
149
- return (s.match(/\\+$/)?.[0].length ?? 0) % 2 === 1;
269
+ return trailingBackslashes(s) % 2 === 1;
150
270
  }
151
271
 
152
272
  function escapeTrailingBackslashRun(s: string): string {
153
- const run = s.match(/\\+$/)?.[0];
154
- if (!run) return s;
155
- return s.slice(0, -run.length) + run + run;
273
+ return s + "\\".repeat(trailingBackslashes(s));
156
274
  }
157
275
 
158
276
  function unescapeTrailingBackslashRun(s: string): string {
159
- const run = s.match(/\\+$/)?.[0];
277
+ const run = trailingBackslashes(s);
160
278
  if (!run) return s;
161
- return s.slice(0, -run.length) + "\\".repeat(run.length / 2);
162
- }
163
-
164
- function splitBlockAttrs(line: string): {
279
+ return s.slice(0, -run) + "\\".repeat(run / 2);
280
+ }
281
+
282
+ // /^(.*?)\s*\{([^{}]*)\}\s*$/, found by hand: that pattern retries the spaces
283
+ // before the brace from every offset of a long run of them.
284
+ function matchBlockAttrSuffix(
285
+ line: string,
286
+ ): { text: string; body: string } | null {
287
+ const brace = /\{([^{}]*)\}\s*$/.exec(line);
288
+ if (!brace) return null;
289
+ let end = brace.index;
290
+ while (end > 0 && SPACE_CHAR.test(line[end - 1])) end--;
291
+ const text = line.slice(0, end);
292
+ // The pattern's `.` stops at a line terminator.
293
+ if (/[\n\r\u2028\u2029]/.test(text)) return null;
294
+ return { text, body: brace[1] };
295
+ }
296
+
297
+ function splitBlockAttrs(
298
+ line: string,
299
+ allowToggle = false,
300
+ ): {
165
301
  text: string;
166
302
  toggle: boolean;
167
303
  color: string | null;
168
304
  } {
169
- const m = line.match(/^(.*?)\s*\{([^{}]*)\}\s*$/);
305
+ const m = matchBlockAttrSuffix(line);
170
306
  if (!m) return { text: line, toggle: false, color: null };
171
- const body = m[2];
172
- if (oddTrailingBackslashes(m[1]) || oddTrailingBackslashes(body)) {
307
+ const body = m.body;
308
+ if (oddTrailingBackslashes(m.text) || oddTrailingBackslashes(body)) {
173
309
  return { text: line, toggle: false, color: null };
174
310
  }
175
311
  const toggle = /\btoggle\s*=\s*"true"/.test(body);
176
- const colorMatch = body.match(/\bcolor\s*=\s*"([^"]+)"/);
312
+ const colorMatch = body.match(/\bcolor\s*=\s*"([^"]*)"/);
177
313
  const color = colorMatch && isColor(colorMatch[1]) ? colorMatch[1] : null;
314
+ if (toggle && !allowToggle)
315
+ reportLoss("dropped-attribute", '{toggle="true"}');
316
+ const rest = body
317
+ .replace(/\btoggle\s*=\s*"true"/, "")
318
+ .replace(/\bcolor\s*=\s*"[^"]*"/, "")
319
+ .trim();
320
+ if ((colorMatch && !color) || ((toggle || color) && rest))
321
+ reportLoss("dropped-attribute", `{${body}}`);
178
322
  if (!toggle && !color) return { text: line, toggle: false, color: null };
179
- return { text: m[1], toggle, color };
323
+ return { text: m.text, toggle, color };
180
324
  }
181
325
 
182
326
  function markOf(node: PMNode, type: string): PMMark | undefined {
183
327
  return node.marks?.find((m) => m.type === type);
184
328
  }
185
329
 
186
- function serializeInlineAtom(node: PMNode): string {
330
+ function serializeInlineAtom(node: PMNode, following = ""): string {
187
331
  const tagName = (node.attrs?.tagName as string) || "mention";
188
332
  const label = (node.attrs?.label as string) || "";
189
333
  let attrs: Record<string, string> = {};
@@ -193,7 +337,7 @@ function serializeInlineAtom(node: PMNode): string {
193
337
  attrs = {};
194
338
  }
195
339
  if (tagName === "math") {
196
- return "$" + (label || attrs.latex || "") + "$";
340
+ return serializeInlineMath(label || attrs.latex || "", following);
197
341
  }
198
342
  const attrEntries = Object.entries(attrs).filter(([k]) => k !== "latex");
199
343
  const attrStr = serializeAttrs(attrEntries);
@@ -202,17 +346,81 @@ function serializeInlineAtom(node: PMNode): string {
202
346
  return `<${tagName}${attrStr}>${escapeAttr(label)}</${tagName}>`;
203
347
  }
204
348
 
205
- function serializeInline(nodes: PMNode[] | undefined): string {
349
+ // Plain math does not close before a digit or another `$`, nor next to a
350
+ // space inside it; GitHub's $`…`$ form has none of those rules.
351
+ function serializeInlineMath(latex: string, following: string): string {
352
+ const plain = `$${latex}$`;
353
+ const read = matchInlineMathAt(plain + following, 0);
354
+ if (read?.to === plain.length && read.latex === latex) return plain;
355
+ return latex.includes("`$") ? plain : `$\`${latex}\`$`;
356
+ }
357
+
358
+ function serializeInline(
359
+ nodes: PMNode[] | undefined,
360
+ escapeUnderscore = false,
361
+ ): string {
206
362
  if (!nodes || !nodes.length) return "";
207
- return nodes
208
- .map((node, index) => serializeInlineNode(node, index === 0))
209
- .join("");
363
+ const parts = nodes.map((node, index) =>
364
+ node.type === "notionInlineAtom"
365
+ ? ""
366
+ : serializeInlineNode(node, escapeUnderscore, index === 0),
367
+ );
368
+ let following = "";
369
+ for (let index = parts.length - 1; index >= 0; index--) {
370
+ if (nodes[index]!.type === "notionInlineAtom")
371
+ parts[index] = serializeInlineAtom(nodes[index]!, following);
372
+ else if (
373
+ nodes[index]!.type === "text" &&
374
+ parts[index]!.endsWith("!") &&
375
+ following === "["
376
+ )
377
+ parts[index] = serializeInlineTextNode(
378
+ nodes[index]!,
379
+ false,
380
+ escapeUnderscore,
381
+ true,
382
+ index === 0,
383
+ )!.source;
384
+ following = (parts[index] + following).slice(0, 1);
385
+ }
386
+ const source = parts.join("");
387
+ return escapeUnderscore || !readsUnderscoreEmphasis(source)
388
+ ? source
389
+ : serializeInline(nodes, true);
390
+ }
391
+
392
+ // The writer never spells formatting with underscores, so underscore emphasis
393
+ // in its own output means literal text such as `__init__` would come back
394
+ // bold; escaping that line's underscores keeps it text.
395
+ function readsUnderscoreEmphasis(source: string): boolean {
396
+ if (!source.includes("_")) return false;
397
+ const before = underscoreEmphasisParsed;
398
+ const sink = activeLossSink;
399
+ activeLossSink = null;
400
+ try {
401
+ parseInline(source);
402
+ } finally {
403
+ activeLossSink = sink;
404
+ }
405
+ return underscoreEmphasisParsed !== before;
406
+ }
407
+
408
+ /** The longest match of a global `run` pattern, like /`+/g, in `text`. */
409
+ export function longestRun(text: string, run: RegExp): number {
410
+ // Not Math.max(...matches): a long code block holds more runs than a call
411
+ // can take arguments.
412
+ let longest = 0;
413
+ for (const [match] of text.matchAll(run))
414
+ longest = Math.max(longest, match.length);
415
+ return longest;
210
416
  }
211
417
 
212
418
  function serializeInlineTextNode(
213
419
  node: PMNode,
214
420
  collectOffsets: boolean,
215
- startsLine: boolean,
421
+ escapeUnderscore = false,
422
+ escapeTrailingBang = false,
423
+ startsLine = false,
216
424
  ): {
217
425
  source: string;
218
426
  textOffsets: number[] | null;
@@ -224,12 +432,9 @@ function serializeInlineTextNode(
224
432
  let textOffsets: number[] | null = collectOffsets ? [0] : null;
225
433
  if (code) {
226
434
  const codeText = raw.replace(/\n/g, "<br>");
227
- const isAllSpaces = /^ *$/.test(codeText);
228
- const longestRun = Math.max(
229
- 0,
230
- ...(codeText.match(/`+/g) || []).map((r) => r.length),
231
- );
232
- const delim = "`".repeat(Math.max(1, longestRun + 1));
435
+ // The reader strips padding only around code that is not all whitespace.
436
+ const isAllSpaces = raw.trim() === "";
437
+ const delim = "`".repeat(longestRun(codeText, /`+/g) + 1);
233
438
  const needsPadding =
234
439
  !isAllSpaces &&
235
440
  (codeText.startsWith("`") ||
@@ -247,12 +452,25 @@ function serializeInlineTextNode(
247
452
  }
248
453
  }
249
454
  } else {
250
- out = escapeInlineText(raw);
455
+ if (escapeUnderscore) contextualSpellings?.escapedUnderscores.add(node);
456
+ if (escapeTrailingBang) contextualSpellings?.escapedTrailingBang.add(node);
457
+ const escape = (text: string) => {
458
+ const escaped = escapeInlineText(text);
459
+ return escapeUnderscore ? escaped.replace(/_/g, "\\_") : escaped;
460
+ };
461
+ out = escape(raw);
462
+ if (escapeTrailingBang && raw.endsWith("!")) out = out.slice(0, -1) + "\\!";
251
463
  if (textOffsets) {
252
464
  let contentOffset = 0;
253
465
  textOffsets = [contentOffset];
254
466
  for (let index = 0; index < raw.length; index += 1) {
255
- contentOffset += escapeInlineText(raw[index]!).length;
467
+ contentOffset += escape(raw[index]!).length;
468
+ if (
469
+ escapeTrailingBang &&
470
+ index === raw.length - 1 &&
471
+ raw[index] === "!"
472
+ )
473
+ contentOffset++;
256
474
  textOffsets.push(contentOffset);
257
475
  }
258
476
  }
@@ -315,21 +533,39 @@ function serializeInlineTextNode(
315
533
 
316
534
  export function serializeInlineTextNodeWithOffsets(
317
535
  node: PMNode,
536
+ escapeUnderscore = false,
537
+ escapeTrailingBang = false,
318
538
  startsLine = false,
319
539
  ): {
320
540
  source: string;
321
541
  textOffsets: number[];
322
542
  } | null {
323
- const serialized = serializeInlineTextNode(node, true, startsLine);
543
+ const serialized = serializeInlineTextNode(
544
+ node,
545
+ true,
546
+ escapeUnderscore,
547
+ escapeTrailingBang,
548
+ startsLine,
549
+ );
324
550
  return serialized?.textOffsets
325
551
  ? { source: serialized.source, textOffsets: serialized.textOffsets }
326
552
  : null;
327
553
  }
328
554
 
329
- export function serializeInlineNode(node: PMNode, startsLine = false): string {
555
+ export function serializeInlineNode(
556
+ node: PMNode,
557
+ escapeUnderscore = false,
558
+ startsLine = false,
559
+ ): string {
330
560
  if (node.type === "hardBreak") return "<br>";
331
561
  if (node.type === "notionInlineAtom") return serializeInlineAtom(node);
332
- const textNode = serializeInlineTextNode(node, false, startsLine);
562
+ const textNode = serializeInlineTextNode(
563
+ node,
564
+ false,
565
+ escapeUnderscore,
566
+ false,
567
+ startsLine,
568
+ );
333
569
  if (textNode) return textNode.source;
334
570
  return node.text ? escapeInlineText(node.text) : "";
335
571
  }
@@ -338,12 +574,22 @@ function textNode(text: string, marks: PMMark[]): PMNode {
338
574
  return marks.length ? { type: "text", text, marks } : { type: "text", text };
339
575
  }
340
576
 
577
+ // The live copy keeps formatting only on text: Yjs stores no marks on math or
578
+ // a mention, so formatting around one is a loss.
579
+ function reportAtomFormatting(node: PMNode, markType: string): void {
580
+ if (node.type === "notionInlineAtom")
581
+ reportLoss(
582
+ "dropped-formatting",
583
+ `${markType} on ${serializeInlineAtom(node)}`,
584
+ );
585
+ }
586
+
341
587
  function addMark(nodes: PMNode[], mark: PMMark): void {
342
588
  for (const n of nodes) {
343
589
  if (n.type === "text") {
344
590
  n.marks = n.marks || [];
345
591
  if (!n.marks.some((m) => m.type === mark.type)) n.marks.push(mark);
346
- }
592
+ } else reportAtomFormatting(n, mark.type);
347
593
  }
348
594
  }
349
595
 
@@ -368,33 +614,360 @@ export function notionSpanAttrs(
368
614
  };
369
615
  }
370
616
 
371
- function mergeSpanMark(nodes: PMNode[], attrs: Record<string, string>): void {
617
+ function mergeSpanMark(
618
+ nodes: PMNode[],
619
+ attrs: Record<string, string>,
620
+ rawAttrs: string,
621
+ ): void {
372
622
  const spanAttrs = notionSpanAttrs(attrs);
623
+ for (const [name, value] of Object.entries(attrs)) {
624
+ const kept =
625
+ name === "color"
626
+ ? spanAttrs.color === value || spanAttrs.bgColor === value
627
+ : name === "bg_color"
628
+ ? spanAttrs.bgColor === value
629
+ : name === "underline" && (value === "true" || value === "false");
630
+ if (!kept) reportLoss("dropped-attribute", `<span ${name}="${value}">`);
631
+ }
632
+ const unparsed = unparsedAttrSource(rawAttrs);
633
+ if (unparsed) reportLoss("dropped-attribute", `<span ${unparsed}>`);
634
+ // A bare span is no formatting; the writer would drop it. One whose
635
+ // attributes could not be read keeps an empty mark, so suggestions can tell
636
+ // its source holds formatting they cannot map.
637
+ if (
638
+ !spanAttrs.color &&
639
+ !spanAttrs.bgColor &&
640
+ !spanAttrs.underline &&
641
+ !spanAttrs.href &&
642
+ !rawAttrs.trim()
643
+ )
644
+ return;
373
645
  for (const n of nodes) {
374
646
  if (n.type === "text") {
375
647
  n.marks = n.marks || [];
376
648
  if (!n.marks.some((m) => m.type === "notionSpan")) {
377
649
  n.marks.push({ type: "notionSpan", attrs: spanAttrs });
378
650
  }
379
- }
651
+ } else reportAtomFormatting(n, "span");
380
652
  }
381
653
  }
382
654
 
383
- function findToken(s: string, token: string, from: number): number {
655
+ // Code spans and math bind tighter than emphasis, so a delimiter inside one
656
+ // never closes a run outside it: the editor writes the code `*args` in
657
+ // italics as *`*args`*.
658
+ function findToken(
659
+ s: string,
660
+ token: string,
661
+ from: number,
662
+ codeRuns: CodeRuns,
663
+ ): number {
384
664
  for (let i = from; i <= s.length - token.length; i++) {
385
665
  if (s[i] === "\\") {
386
666
  i++;
387
667
  continue;
388
668
  }
669
+ const skipTo = atomicSpanEnd(s, i, codeRuns);
670
+ if (skipTo !== -1) {
671
+ i = skipTo - 1;
672
+ continue;
673
+ }
389
674
  if (s.startsWith(token, i)) return i;
390
675
  }
391
676
  return -1;
392
677
  }
393
678
 
394
- function parseInline(input: string): PMNode[] {
679
+ // A `**` before a word that the next `**` closes, as in `*Run **npm**
680
+ // install*`, is bold inside the italic when a `*` after it closes the italic.
681
+ // Otherwise the `**` closes the italic, as main reads it. The writer's own `**`
682
+ // inside an italic closes and reopens it around another mark's delimiter
683
+ // (`*a **~~b~~** c*`), so a `**` before punctuation always closes the italic.
684
+ // `chains` holds each bold opener's walk, so a line of them stays linear.
685
+ function findItalicClose(
686
+ s: string,
687
+ from: number,
688
+ codeRuns: CodeRuns,
689
+ chains: Map<number, number>,
690
+ ): number {
691
+ const first = findToken(s, "*", from, codeRuns);
692
+ const walked: number[] = [];
693
+ let close = -1;
694
+ for (let at = first; at !== -1; ) {
695
+ const known = chains.get(at);
696
+ if (known !== undefined) {
697
+ close = known;
698
+ break;
699
+ }
700
+ const bold = boldInsideItalicClose(s, at, codeRuns);
701
+ if (bold === -1) {
702
+ close = at;
703
+ break;
704
+ }
705
+ walked.push(at);
706
+ at = findToken(s, "*", bold + 2, codeRuns);
707
+ }
708
+ for (const at of walked) chains.set(at, close);
709
+ return close === -1 ? first : close;
710
+ }
711
+
712
+ /** Where the bold opened by the `**` at `at` closes, or -1 if it opens none. */
713
+ function boldInsideItalicClose(
714
+ s: string,
715
+ at: number,
716
+ codeRuns: CodeRuns,
717
+ ): number {
718
+ if (s[at - 1] === "*" || delimiterRunLength(s, at, "*") !== 2) return -1;
719
+ const open = delimiterFlanking(s, at, at + 2);
720
+ if (!open.left || open.right || isFlankPunctuation(open.after)) return -1;
721
+ const bold = findToken(s, "**", at + 2, codeRuns);
722
+ if (bold <= at + 2) return -1;
723
+ const run = delimiterRunLength(s, bold, "*");
724
+ const close = delimiterFlanking(s, bold, bold + run);
725
+ return run > 3 || !close.right || close.left ? -1 : bold;
726
+ }
727
+
728
+ /**
729
+ * Where the code span or math starting at `at` ends, or where an unclosed
730
+ * backtick run ends; -1 when neither starts there.
731
+ */
732
+ function atomicSpanEnd(s: string, at: number, codeRuns: CodeRuns): number {
733
+ if (s[at] === "$") return matchInlineMathAt(s, at)?.to ?? -1;
734
+ return codeSpanSkip(s, at, codeRuns);
735
+ }
736
+
737
+ /** `atomicSpanEnd` for code spans alone. */
738
+ function codeSpanSkip(s: string, at: number, codeRuns: CodeRuns): number {
739
+ if (s[at] !== "`") return -1;
740
+ const run = delimiterRunLength(s, at, "`");
741
+ const end = codeSpanEnd(at, run, codeRuns);
742
+ return end === -1 ? at + run : end;
743
+ }
744
+
745
+ // Where each backtick run of one source starts, by run length. Every scanner
746
+ // of that source shares it: searching the rest of the source for a closer
747
+ // from each opener is quadratic in a source of unclosed runs of many lengths.
748
+ type CodeRuns = Map<number, number[]>;
749
+
750
+ function indexCodeRuns(s: string): CodeRuns {
751
+ const runs: CodeRuns = new Map();
752
+ for (let at = s.indexOf("`"); at !== -1; ) {
753
+ const run = delimiterRunLength(s, at, "`");
754
+ const starts = runs.get(run);
755
+ if (starts) starts.push(at);
756
+ else runs.set(run, [at]);
757
+ at = s.indexOf("`", at + run);
758
+ }
759
+ return runs;
760
+ }
761
+
762
+ // A code span closes at the next run of exactly its length; the backticks
763
+ // at `at` may be the end of a longer run an escape split.
764
+ function codeSpanEnd(at: number, run: number, codeRuns: CodeRuns): number {
765
+ const starts = codeRuns.get(run);
766
+ if (!starts) return -1;
767
+ let low = 0;
768
+ let high = starts.length;
769
+ while (low < high) {
770
+ const mid = (low + high) >> 1;
771
+ if (starts[mid] <= at) low = mid + 1;
772
+ else high = mid;
773
+ }
774
+ return low < starts.length ? starts[low] + run : -1;
775
+ }
776
+
777
+ function delimiterRunLength(s: string, from: number, ch: string): number {
778
+ let length = 0;
779
+ while (s[from + length] === ch) length++;
780
+ return length;
781
+ }
782
+
783
+ function characterBefore(s: string, index: number): string | undefined {
784
+ if (index <= 0) return undefined;
785
+ const low = s.charCodeAt(index - 1);
786
+ if (low >= 0xdc00 && low <= 0xdfff && index >= 2) {
787
+ const high = s.charCodeAt(index - 2);
788
+ if (high >= 0xd800 && high <= 0xdbff) return s.slice(index - 2, index);
789
+ }
790
+ return s[index - 1];
791
+ }
792
+
793
+ function characterAt(s: string, index: number): string | undefined {
794
+ if (index >= s.length) return undefined;
795
+ return String.fromCodePoint(s.codePointAt(index)!);
796
+ }
797
+
798
+ function isFlankWhitespace(ch: string | undefined): boolean {
799
+ return ch === undefined || /^\s$/u.test(ch);
800
+ }
801
+
802
+ function isFlankPunctuation(ch: string | undefined): boolean {
803
+ return ch !== undefined && /^[\p{P}\p{S}]$/u.test(ch);
804
+ }
805
+
806
+ // CommonMark's flanking rules, so `snake_case_name` and other underscores
807
+ // inside words stay literal.
808
+ function delimiterFlanking(s: string, from: number, to: number) {
809
+ const before = characterBefore(s, from);
810
+ const after = characterAt(s, to);
811
+ const left =
812
+ !isFlankWhitespace(after) &&
813
+ (!isFlankPunctuation(after) ||
814
+ isFlankWhitespace(before) ||
815
+ isFlankPunctuation(before));
816
+ const right =
817
+ !isFlankWhitespace(before) &&
818
+ (!isFlankPunctuation(before) ||
819
+ isFlankWhitespace(after) ||
820
+ isFlankPunctuation(after));
821
+ return { left, right, before, after };
822
+ }
823
+
824
+ let underscoreEmphasisParsed = 0;
825
+
826
+ // `misses[run]` is the earliest offset a search for a closer of that length
827
+ // already failed from, so a paragraph of unmatched `_x` openers stays linear.
828
+ function matchUnderscoreEmphasis(
829
+ input: string,
830
+ start: number,
831
+ misses: number[],
832
+ codeRuns: CodeRuns,
833
+ ): { from: number; to: number; run: number } | null {
834
+ const run = delimiterRunLength(input, start, "_");
835
+ if (run > 3) return null;
836
+ const open = delimiterFlanking(input, start, start + run);
837
+ if (!open.left || (open.right && !isFlankPunctuation(open.before)))
838
+ return null;
839
+ const from = start + run;
840
+ if (misses[run] !== undefined && from >= misses[run]) return null;
841
+ for (let j = from; j < input.length; j++) {
842
+ if (input[j] === "\\") {
843
+ j++;
844
+ continue;
845
+ }
846
+ const skipTo = atomicSpanEnd(input, j, codeRuns);
847
+ if (skipTo !== -1) {
848
+ j = skipTo - 1;
849
+ continue;
850
+ }
851
+ if (input[j] !== "_") continue;
852
+ const length = delimiterRunLength(input, j, "_");
853
+ const close = delimiterFlanking(input, j, j + length);
854
+ if (
855
+ j > from &&
856
+ length >= run &&
857
+ close.right &&
858
+ (!close.left || isFlankPunctuation(close.after))
859
+ )
860
+ return { from, to: j, run };
861
+ j += length - 1;
862
+ }
863
+ misses[run] = Math.min(misses[run] ?? from, from);
864
+ return null;
865
+ }
866
+
867
+ // Every run of one link or span shares its mark, so each mark is keyed once:
868
+ // keying it per neighbouring pair is quadratic under one long URL.
869
+ const markKeys = new WeakMap<PMMark, string>();
870
+
871
+ function markKey(mark: PMMark): string {
872
+ let key = markKeys.get(mark);
873
+ if (key === undefined) {
874
+ key = `${mark.type}:${JSON.stringify(mark.attrs ?? null)}`;
875
+ markKeys.set(mark, key);
876
+ }
877
+ return key;
878
+ }
879
+
880
+ function sameMarks(a: PMMark[] | undefined, b: PMMark[] | undefined): boolean {
881
+ const left = (a ?? []).map(markKey).sort();
882
+ const right = (b ?? []).map(markKey).sort();
883
+ return (
884
+ left.length === right.length &&
885
+ left.every((key, index) => key === right[index])
886
+ );
887
+ }
888
+
889
+ // The editor joins neighbouring text with the same formatting into one run,
890
+ // so the parse does too; otherwise `**a****b**` keeps a seam the editor never
891
+ // saves.
892
+ function mergeAdjacentText(nodes: PMNode[]): PMNode[] {
893
+ const out: PMNode[] = [];
894
+ for (const node of nodes) {
895
+ const last = out[out.length - 1];
896
+ if (
897
+ node.type === "text" &&
898
+ last?.type === "text" &&
899
+ sameMarks(last.marks, node.marks)
900
+ ) {
901
+ out[out.length - 1] = {
902
+ ...last,
903
+ text: (last.text ?? "") + (node.text ?? ""),
904
+ };
905
+ } else {
906
+ out.push(node);
907
+ }
908
+ }
909
+ return out;
910
+ }
911
+
912
+ // Spans, marks and links read their text by parsing it again, so each level
913
+ // of nesting costs its whole text and a few call frames. Past this depth,
914
+ // which no page needs, the text reads as written and the loss is reported.
915
+ const MAX_INLINE_DEPTH = 32;
916
+
917
+ // Fidelity must name closed math the bounded matcher cannot represent. Index
918
+ // its closers once: searching to the end from every unmatched `$` is quadratic.
919
+ function indexOverLimitMath(input: string): Set<number> {
920
+ const starts = new Set<number>();
921
+ let plainClose = -1;
922
+ let githubClose = -1;
923
+ let lineEnd = input.length;
924
+ for (let at = input.length - 1; at >= 0; at--) {
925
+ const ch = input[at];
926
+ if (ch === "\n" || ch === "\r") lineEnd = at;
927
+ if (ch === "`" && input[at + 1] === "$") githubClose = at;
928
+ if (ch !== "$") continue;
929
+ const escaped = isEscapedInlineDelimiter(input, at);
930
+ if (!escaped) {
931
+ if (input[at + 1] === "`") {
932
+ if (githubClose - at - 2 > MAX_INLINE_MATH_LENGTH) starts.add(at);
933
+ } else if (
934
+ input[at + 1] &&
935
+ input[at + 1] !== "$" &&
936
+ !/\s/.test(input[at + 1]) &&
937
+ plainClose < lineEnd &&
938
+ plainClose - at - 1 > MAX_INLINE_MATH_LENGTH
939
+ ) {
940
+ starts.add(at);
941
+ }
942
+ }
943
+ if (
944
+ !escaped &&
945
+ input[at - 1] !== "$" &&
946
+ input[at + 1] !== "$" &&
947
+ !/\s/.test(input[at - 1] ?? "") &&
948
+ !/\d/.test(input[at + 1] ?? "")
949
+ )
950
+ plainClose = at;
951
+ }
952
+ return starts;
953
+ }
954
+
955
+ function parseInline(input: string, depth = 0): PMNode[] {
956
+ if (depth > MAX_INLINE_DEPTH) {
957
+ // Without a character that opens a construct, text reads as itself.
958
+ if (/[\\$`<*~_[]/.test(input)) reportLoss("dropped-formatting", input);
959
+ return input ? [textNode(input, [])] : [];
960
+ }
395
961
  const out: PMNode[] = [];
396
962
  let buf = "";
397
963
  let i = 0;
964
+ const codeRuns = indexCodeRuns(input);
965
+ const underscoreMisses: number[] = [];
966
+ let italicChains: Map<number, number> | null = null;
967
+ let linkCloses: LinkCloses | null = null;
968
+ let nextIndex: ((needle: string, from: number) => number) | null = null;
969
+ let mentionCloses: MentionCloseTrie | null = null;
970
+ let overLimitMath: Set<number> | null = null;
398
971
  const flush = () => {
399
972
  if (buf) {
400
973
  out.push(textNode(buf, []));
@@ -405,7 +978,11 @@ function parseInline(input: string): PMNode[] {
405
978
  while (i < input.length) {
406
979
  const ch = input[i];
407
980
 
408
- if (ch === "\\" && i + 1 < input.length && ESCAPABLE.has(input[i + 1])) {
981
+ if (
982
+ ch === "\\" &&
983
+ i + 1 < input.length &&
984
+ PARSE_ESCAPABLE.has(input[i + 1])
985
+ ) {
409
986
  buf += input[i + 1];
410
987
  i += 2;
411
988
  continue;
@@ -425,38 +1002,42 @@ function parseInline(input: string): PMNode[] {
425
1002
  i = inlineMath.to;
426
1003
  continue;
427
1004
  }
1005
+ if (
1006
+ ch === "$" &&
1007
+ activeLossSink &&
1008
+ (overLimitMath ??= indexOverLimitMath(input)).has(i)
1009
+ )
1010
+ reportLoss("dropped-formatting", input.slice(i, i + 200));
428
1011
 
429
1012
  if (ch === "`") {
430
- const openRun = /^`+/.exec(input.slice(i))?.[0].length ?? 0;
431
- const delim = "`".repeat(openRun);
432
- let searchFrom = i + openRun;
433
- let close = -1;
434
- while (searchFrom <= input.length - openRun) {
435
- const idx = input.indexOf(delim, searchFrom);
436
- if (idx === -1) break;
437
- const runLen = /^`+/.exec(input.slice(idx))?.[0].length ?? 0;
438
- if (runLen === openRun) {
439
- close = idx;
440
- break;
441
- }
442
- searchFrom = idx + runLen;
443
- }
444
- if (close !== -1) {
445
- flush();
446
- let codeText = input
447
- .slice(i + openRun, close)
448
- .replace(/<br\/?>/g, "\n");
449
- if (
450
- codeText.startsWith(" ") &&
451
- codeText.endsWith(" ") &&
452
- codeText.trim().length > 0
453
- ) {
454
- codeText = codeText.slice(1, -1);
455
- }
456
- out.push(textNode(codeText, [{ type: "code" }]));
457
- i = close + openRun;
1013
+ // A run with no closer stays text, and so does each backtick of it
1014
+ // until what is left of the run finds one: ```a`` is ` then code `a`.
1015
+ // The run is walked once, since measuring it again from each backtick
1016
+ // is quadratic in its length.
1017
+ const runEnd = i + delimiterRunLength(input, i, "`");
1018
+ let at = i;
1019
+ let end = codeSpanEnd(at, runEnd - at, codeRuns);
1020
+ while (end === -1 && ++at < runEnd)
1021
+ end = codeSpanEnd(at, runEnd - at, codeRuns);
1022
+ buf += input.slice(i, at);
1023
+ if (end === -1) {
1024
+ i = runEnd;
458
1025
  continue;
459
1026
  }
1027
+ const openRun = runEnd - at;
1028
+ const close = end - openRun;
1029
+ flush();
1030
+ let codeText = input.slice(at + openRun, close).replace(/<br\/?>/g, "\n");
1031
+ if (
1032
+ codeText.startsWith(" ") &&
1033
+ codeText.endsWith(" ") &&
1034
+ codeText.trim().length > 0
1035
+ ) {
1036
+ codeText = codeText.slice(1, -1);
1037
+ }
1038
+ out.push(textNode(codeText, [{ type: "code" }]));
1039
+ i = end;
1040
+ continue;
460
1041
  }
461
1042
 
462
1043
  if (input.startsWith("<br/>", i) || input.startsWith("<br>", i)) {
@@ -467,13 +1048,14 @@ function parseInline(input: string): PMNode[] {
467
1048
  }
468
1049
 
469
1050
  if (input.startsWith("<span", i)) {
470
- const open = input.indexOf(">", i);
471
- const close = input.indexOf("</span>", open);
1051
+ nextIndex ??= nextIndexFinder(input);
1052
+ const open = nextIndex(">", i);
1053
+ const close = open === -1 ? -1 : nextIndex("</span>", open);
472
1054
  if (open !== -1 && close !== -1) {
473
1055
  flush();
474
- const attrs = parseAttrs(input.slice(i + 5, open));
475
- const inner = parseInline(input.slice(open + 1, close));
476
- mergeSpanMark(inner, attrs);
1056
+ const rawAttrs = input.slice(i + 5, open);
1057
+ const inner = parseInline(input.slice(open + 1, close), depth + 1);
1058
+ mergeSpanMark(inner, parseAttrs(rawAttrs), rawAttrs);
477
1059
  pushAll(out, inner);
478
1060
  i = close + "</span>".length;
479
1061
  continue;
@@ -481,8 +1063,9 @@ function parseInline(input: string): PMNode[] {
481
1063
  }
482
1064
 
483
1065
  if (input.startsWith("<mention-", i)) {
484
- const selfClose = input.indexOf("/>", i);
485
- const open = input.indexOf(">", i);
1066
+ nextIndex ??= nextIndexFinder(input);
1067
+ const selfClose = nextIndex("/>", i);
1068
+ const open = nextIndex(">", i);
486
1069
  if (selfClose !== -1 && (open === -1 || selfClose <= open)) {
487
1070
  flush();
488
1071
  const tagMatch = input.slice(i).match(/^<(mention-[\w-]+)([^>]*?)\/>/);
@@ -492,22 +1075,28 @@ function parseInline(input: string): PMNode[] {
492
1075
  continue;
493
1076
  }
494
1077
  }
495
- const tagMatch = input
496
- .slice(i)
497
- .match(/^<(mention-[\w-]+)([^>]*)>([\s\S]*?)<\/\1>/);
498
- if (tagMatch) {
1078
+ const tag =
1079
+ open === -1
1080
+ ? null
1081
+ : matchMentionTag(
1082
+ input,
1083
+ i,
1084
+ open,
1085
+ (mentionCloses ??= indexMentionCloses(input)),
1086
+ );
1087
+ if (tag) {
499
1088
  flush();
500
- out.push(makeInlineAtom(tagMatch[1], tagMatch[2], tagMatch[3]));
501
- i += tagMatch[0].length;
1089
+ out.push(makeInlineAtom(tag.name, tag.attrs, tag.label));
1090
+ i = tag.end;
502
1091
  continue;
503
1092
  }
504
1093
  }
505
1094
 
506
1095
  if (input.startsWith("***", i)) {
507
- const close = findToken(input, "***", i + 3);
508
- if (close !== -1) {
1096
+ const close = findToken(input, "***", i + 3, codeRuns);
1097
+ if (close > i + 3) {
509
1098
  flush();
510
- const inner = parseInline(input.slice(i + 3, close));
1099
+ const inner = parseInline(input.slice(i + 3, close), depth + 1);
511
1100
  addMark(inner, { type: "bold" });
512
1101
  addMark(inner, { type: "italic" });
513
1102
  pushAll(out, inner);
@@ -517,10 +1106,10 @@ function parseInline(input: string): PMNode[] {
517
1106
  }
518
1107
 
519
1108
  if (input.startsWith("**", i)) {
520
- const close = findToken(input, "**", i + 2);
521
- if (close !== -1) {
1109
+ const close = findToken(input, "**", i + 2, codeRuns);
1110
+ if (close > i + 2) {
522
1111
  flush();
523
- const inner = parseInline(input.slice(i + 2, close));
1112
+ const inner = parseInline(input.slice(i + 2, close), depth + 1);
524
1113
  addMark(inner, { type: "bold" });
525
1114
  pushAll(out, inner);
526
1115
  i = close + 2;
@@ -529,10 +1118,10 @@ function parseInline(input: string): PMNode[] {
529
1118
  }
530
1119
 
531
1120
  if (input.startsWith("~~", i)) {
532
- const close = findToken(input, "~~", i + 2);
533
- if (close !== -1) {
1121
+ const close = findToken(input, "~~", i + 2, codeRuns);
1122
+ if (close > i + 2) {
534
1123
  flush();
535
- const inner = parseInline(input.slice(i + 2, close));
1124
+ const inner = parseInline(input.slice(i + 2, close), depth + 1);
536
1125
  addMark(inner, { type: "strike" });
537
1126
  pushAll(out, inner);
538
1127
  i = close + 2;
@@ -540,11 +1129,42 @@ function parseInline(input: string): PMNode[] {
540
1129
  }
541
1130
  }
542
1131
 
1132
+ if (ch === "_") {
1133
+ const emphasis = matchUnderscoreEmphasis(
1134
+ input,
1135
+ i,
1136
+ underscoreMisses,
1137
+ codeRuns,
1138
+ );
1139
+ if (emphasis) {
1140
+ flush();
1141
+ const inner = parseInline(
1142
+ input.slice(emphasis.from, emphasis.to),
1143
+ depth + 1,
1144
+ );
1145
+ if (emphasis.run >= 2) addMark(inner, { type: "bold" });
1146
+ if (emphasis.run !== 2) addMark(inner, { type: "italic" });
1147
+ pushAll(out, inner);
1148
+ underscoreEmphasisParsed++;
1149
+ i = emphasis.to + emphasis.run;
1150
+ continue;
1151
+ }
1152
+ const run = delimiterRunLength(input, i, "_");
1153
+ buf += input.slice(i, i + run);
1154
+ i += run;
1155
+ continue;
1156
+ }
1157
+
543
1158
  if (ch === "*") {
544
- const close = findToken(input, "*", i + 1);
545
- if (close !== -1) {
1159
+ const close = findItalicClose(
1160
+ input,
1161
+ i + 1,
1162
+ codeRuns,
1163
+ (italicChains ??= new Map()),
1164
+ );
1165
+ if (close > i + 1) {
546
1166
  flush();
547
- const inner = parseInline(input.slice(i + 1, close));
1167
+ const inner = parseInline(input.slice(i + 1, close), depth + 1);
548
1168
  addMark(inner, { type: "italic" });
549
1169
  pushAll(out, inner);
550
1170
  i = close + 1;
@@ -553,10 +1173,16 @@ function parseInline(input: string): PMNode[] {
553
1173
  }
554
1174
 
555
1175
  if (ch === "[") {
556
- const link = matchLink(input, i);
1176
+ linkCloses ??= {
1177
+ brackets: new Int32Array(input.length),
1178
+ parens: new Int32Array(input.length),
1179
+ };
1180
+ const link = matchLink(input, i, linkCloses, codeRuns);
557
1181
  if (link) {
1182
+ if (buf.endsWith("!") && !isEscapedInlineDelimiter(input, i - 1))
1183
+ reportLoss("dropped-formatting", input.slice(i - 1, link.end));
558
1184
  flush();
559
- const inner = parseInline(link.text);
1185
+ const inner = parseInline(link.text, depth + 1);
560
1186
  addMark(inner, { type: "link", attrs: { href: link.href } });
561
1187
  pushAll(out, inner);
562
1188
  i = link.end;
@@ -568,7 +1194,87 @@ function parseInline(input: string): PMNode[] {
568
1194
  i++;
569
1195
  }
570
1196
  flush();
571
- return out;
1197
+ return mergeAdjacentText(out);
1198
+ }
1199
+
1200
+ // The next `needle` at or after `from`. The parse asks with growing offsets,
1201
+ // so the last answer for each needle usually still holds, and a line of
1202
+ // unclosed tags is searched once rather than once per tag.
1203
+ function nextIndexFinder(s: string): (needle: string, from: number) => number {
1204
+ const last = new Map<string, { from: number; at: number }>();
1205
+ return (needle, from) => {
1206
+ const hit = last.get(needle);
1207
+ if (hit && from >= hit.from && (hit.at === -1 || from <= hit.at))
1208
+ return hit.at;
1209
+ const at = s.indexOf(needle, from);
1210
+ last.set(needle, { from, at });
1211
+ return at;
1212
+ };
1213
+ }
1214
+
1215
+ const MENTION_NAME_CHAR = /[\w-]/;
1216
+
1217
+ // The names of a source's `</mention-name>` close tags, as a trie of their
1218
+ // characters holding each name's tag offsets.
1219
+ type MentionCloseTrie = { next: Map<string, MentionCloseTrie>; at: number[] };
1220
+
1221
+ function indexMentionCloses(s: string): MentionCloseTrie {
1222
+ const root: MentionCloseTrie = { next: new Map(), at: [] };
1223
+ const prefix = "</mention-";
1224
+ for (
1225
+ let tag = s.indexOf(prefix);
1226
+ tag !== -1;
1227
+ tag = s.indexOf(prefix, tag + 1)
1228
+ ) {
1229
+ let node = root;
1230
+ let end = tag + prefix.length;
1231
+ for (; end < s.length && MENTION_NAME_CHAR.test(s[end]); end++) {
1232
+ let child = node.next.get(s[end]);
1233
+ if (!child) node.next.set(s[end], (child = { next: new Map(), at: [] }));
1234
+ node = child;
1235
+ }
1236
+ if (node !== root && s[end] === ">") node.at.push(tag);
1237
+ }
1238
+ return root;
1239
+ }
1240
+
1241
+ // The mention at `at` whose open tag ends at `gt`, as
1242
+ // /^<(mention-[\w-]+)([^>]*)>([\s\S]*?)<\/\1>/ reads it: the longest name
1243
+ // with a close tag after the open tag, up to that tag's first occurrence. By
1244
+ // hand, since the pattern searches the rest of the source again for each
1245
+ // shorter name it backs off to.
1246
+ function matchMentionTag(
1247
+ s: string,
1248
+ at: number,
1249
+ gt: number,
1250
+ closes: MentionCloseTrie,
1251
+ ): { name: string; attrs: string; label: string; end: number } | null {
1252
+ const closable: Array<{ node: MentionCloseTrie; end: number }> = [];
1253
+ let node: MentionCloseTrie | undefined = closes;
1254
+ let end = at + "<mention-".length;
1255
+ while (node && end < s.length && MENTION_NAME_CHAR.test(s[end])) {
1256
+ node = node.next.get(s[end++]);
1257
+ if (node?.at.length) closable.push({ node, end });
1258
+ }
1259
+ for (let k = closable.length - 1; k >= 0; k--) {
1260
+ const tags = closable[k].node.at;
1261
+ let low = 0;
1262
+ let high = tags.length;
1263
+ while (low < high) {
1264
+ const mid = (low + high) >> 1;
1265
+ if (tags[mid] <= gt) low = mid + 1;
1266
+ else high = mid;
1267
+ }
1268
+ if (low === tags.length) continue;
1269
+ const name = s.slice(at + 1, closable[k].end);
1270
+ return {
1271
+ name,
1272
+ attrs: s.slice(closable[k].end, gt),
1273
+ label: s.slice(gt + 1, tags[low]),
1274
+ end: tags[low] + name.length + 3,
1275
+ };
1276
+ }
1277
+ return null;
572
1278
  }
573
1279
 
574
1280
  function makeInlineAtom(
@@ -576,46 +1282,79 @@ function makeInlineAtom(
576
1282
  rawAttrs: string,
577
1283
  label: string,
578
1284
  ): PMNode {
579
- const attrs = parseAttrs(rawAttrs);
1285
+ const attrs = readAttrs(tagName, rawAttrs);
1286
+ const text = unescapeAttr(label);
1287
+ const trimmed = text.trim();
1288
+ // The chip shows the spaces; the editor never makes a label that has them.
1289
+ if (trimmed !== text)
1290
+ reportLoss("dropped-source", `<${tagName}>${label}</${tagName}>`);
580
1291
  return {
581
1292
  type: "notionInlineAtom",
582
1293
  attrs: {
583
1294
  tagName,
584
1295
  attrsJson: JSON.stringify(attrs),
585
- label: unescapeAttr(label).trim(),
1296
+ label: trimmed,
586
1297
  },
587
1298
  };
588
1299
  }
589
1300
 
590
- function findMatchingBracketClose(s: string, start: number): number {
591
- let depth = 0;
1301
+ // A scan from one opener passes the same steps a scan from any opener inside
1302
+ // it would, so it records their closers too in `closes`, and a line of
1303
+ // unmatched openers is scanned once instead of once per opener. Each is kept
1304
+ // at its opener's offset as the closer + 2, so 0 is unknown and 1 is none: a
1305
+ // Map of every opener in a long line slows faster than the line grows.
1306
+ type LinkCloses = { brackets: Int32Array; parens: Int32Array };
1307
+
1308
+ // A code span binds tighter than link text, as the editor writes code `]`
1309
+ // under a link: [`]`](u). Math in link text and an image's alt read as main
1310
+ // reads them: the editor writes neither.
1311
+ function findMatchingBracketClose(
1312
+ s: string,
1313
+ start: number,
1314
+ closes: Int32Array = new Int32Array(s.length),
1315
+ codeRuns?: CodeRuns,
1316
+ ): number {
1317
+ if (closes[start]) return closes[start] - 2;
1318
+ const openers: number[] = [];
592
1319
  for (let i = start; i < s.length; i++) {
593
1320
  if (s[i] === "\\") {
594
1321
  i++;
595
1322
  continue;
596
1323
  }
597
- if (s[i] === "[") depth++;
1324
+ const skipTo = codeRuns ? codeSpanSkip(s, i, codeRuns) : -1;
1325
+ if (skipTo !== -1) {
1326
+ i = skipTo - 1;
1327
+ continue;
1328
+ }
1329
+ if (s[i] === "[") openers.push(i);
598
1330
  else if (s[i] === "]") {
599
- depth--;
600
- if (depth === 0) return i;
1331
+ closes[openers.pop()!] = i + 2;
1332
+ if (openers.length === 0) return i;
601
1333
  }
602
1334
  }
1335
+ for (const opener of openers) closes[opener] = 1;
603
1336
  return -1;
604
1337
  }
605
1338
 
606
- function findMatchingParenClose(s: string, openParenIdx: number): number {
607
- let depth = 0;
1339
+ function findMatchingParenClose(
1340
+ s: string,
1341
+ openParenIdx: number,
1342
+ closes: Int32Array = new Int32Array(s.length),
1343
+ ): number {
1344
+ if (closes[openParenIdx]) return closes[openParenIdx] - 2;
1345
+ const openers: number[] = [];
608
1346
  for (let i = openParenIdx; i < s.length; i++) {
609
1347
  if (s[i] === "\\") {
610
1348
  i++;
611
1349
  continue;
612
1350
  }
613
- if (s[i] === "(") depth++;
1351
+ if (s[i] === "(") openers.push(i);
614
1352
  else if (s[i] === ")") {
615
- depth--;
616
- if (depth === 0) return i;
1353
+ closes[openers.pop()!] = i + 2;
1354
+ if (openers.length === 0) return i;
617
1355
  }
618
1356
  }
1357
+ for (const opener of openers) closes[opener] = 1;
619
1358
  return -1;
620
1359
  }
621
1360
 
@@ -643,10 +1382,17 @@ function unescapeUrlParens(url: string): string {
643
1382
  function matchLink(
644
1383
  s: string,
645
1384
  start: number,
1385
+ closes: LinkCloses,
1386
+ codeRuns: CodeRuns,
646
1387
  ): { text: string; href: string; end: number } | null {
647
- const closeBracket = findMatchingBracketClose(s, start);
1388
+ const closeBracket = findMatchingBracketClose(
1389
+ s,
1390
+ start,
1391
+ closes.brackets,
1392
+ codeRuns,
1393
+ );
648
1394
  if (closeBracket === -1 || s[closeBracket + 1] !== "(") return null;
649
- const closeParen = findMatchingParenClose(s, closeBracket + 1);
1395
+ const closeParen = findMatchingParenClose(s, closeBracket + 1, closes.parens);
650
1396
  if (closeParen === -1) return null;
651
1397
  return {
652
1398
  text: s.slice(start + 1, closeBracket),
@@ -665,6 +1411,8 @@ export interface NfmSerializeContext {
665
1411
  blockId: string,
666
1412
  node: PMNode,
667
1413
  ) => string | undefined | null;
1414
+ /** Write the trailing empty paragraph the editor's save drops. */
1415
+ keepTrailingEmptyBlock?: boolean;
668
1416
  }
669
1417
 
670
1418
  /**
@@ -676,7 +1424,45 @@ export function pushAll<T>(target: T[], items: readonly T[]) {
676
1424
  }
677
1425
 
678
1426
  let activeSerializeContext: NfmSerializeContext | null = null;
679
- let suppressTerminalFillerTrim = false;
1427
+
1428
+ /** Text nodes the writer spelled differently because of their neighbors. */
1429
+ export interface NfmContextualSpellings {
1430
+ /** Its line read as underscore emphasis, so every `_` became `\_`. */
1431
+ escapedUnderscores: ReadonlySet<PMNode>;
1432
+ /** It opens a paragraph that looked like block syntax, so a `\` precedes it. */
1433
+ escapedBlockMarker: ReadonlySet<PMNode>;
1434
+ /** Its literal `!` before a link must not become image syntax. */
1435
+ escapedTrailingBang: ReadonlySet<PMNode>;
1436
+ }
1437
+
1438
+ let contextualSpellings: {
1439
+ escapedUnderscores: Set<PMNode>;
1440
+ escapedBlockMarker: Set<PMNode>;
1441
+ escapedTrailingBang: Set<PMNode>;
1442
+ } | null = null;
1443
+
1444
+ /**
1445
+ * Which text nodes `docToNfm(doc)` spells in context. A caller that writes
1446
+ * each text node on its own, like the suggestion mapper, needs these to
1447
+ * reproduce the writer's bytes.
1448
+ */
1449
+ export function nfmContextualSpellings(
1450
+ doc: PMDoc | PMNode,
1451
+ ): NfmContextualSpellings {
1452
+ const spellings = {
1453
+ escapedUnderscores: new Set<PMNode>(),
1454
+ escapedBlockMarker: new Set<PMNode>(),
1455
+ escapedTrailingBang: new Set<PMNode>(),
1456
+ };
1457
+ const previous = contextualSpellings;
1458
+ contextualSpellings = spellings;
1459
+ try {
1460
+ docToNfm(doc);
1461
+ } finally {
1462
+ contextualSpellings = previous;
1463
+ }
1464
+ return spellings;
1465
+ }
680
1466
 
681
1467
  export function docToNfm(
682
1468
  doc: PMDoc | PMNode | null | undefined,
@@ -701,7 +1487,6 @@ function isEmptyParagraphNode(node: PMNode | undefined): boolean {
701
1487
  }
702
1488
 
703
1489
  function trimEditorTerminalFiller(blocks: PMNode[]): PMNode[] {
704
- if (suppressTerminalFillerTrim) return blocks;
705
1490
  if (blocks.length < 2) return blocks;
706
1491
  const last = blocks[blocks.length - 1];
707
1492
  const previous = blocks[blocks.length - 2];
@@ -717,9 +1502,10 @@ function serializeBlocks(
717
1502
  isTopLevel = false,
718
1503
  ): string[] {
719
1504
  const out: string[] = [];
720
- const serializableBlocks = isTopLevel
721
- ? trimEditorTerminalFiller(blocks)
722
- : blocks;
1505
+ const serializableBlocks =
1506
+ isTopLevel && !activeSerializeContext?.keepTrailingEmptyBlock
1507
+ ? trimEditorTerminalFiller(blocks)
1508
+ : blocks;
723
1509
  for (let i = 0; i < serializableBlocks.length; i++) {
724
1510
  const block = serializableBlocks[i];
725
1511
  if (block.type === "bulletList" || block.type === "orderedList") {
@@ -745,12 +1531,20 @@ function serializeBlock(node: PMNode, indent: number): string[] {
745
1531
  switch (node.type) {
746
1532
  case "paragraph": {
747
1533
  const inline = serializeInline(node.content);
748
- if (!inline) return [indentStr(ind) + "<empty-block/>"];
749
- return [
750
- indentStr(ind) +
751
- escapeLeadingBlockMarker(inline) +
752
- blockAttrSuffix({ color: node.attrs?.color }),
753
- ];
1534
+ const suffix = blockAttrSuffix({ color: node.attrs?.color });
1535
+ if (!inline)
1536
+ return [indentStr(ind) + (suffix.trimStart() || "<empty-block/>")];
1537
+ const escaped = escapeLeadingBlockMarker(inline);
1538
+ if (escaped !== inline && node.content?.[0])
1539
+ contextualSpellings?.escapedBlockMarker.add(node.content[0]);
1540
+ // Text one space short of a block marker, like `#`, would become one
1541
+ // with the suffix's space, so the suffix goes on without it.
1542
+ const joined =
1543
+ escaped === inline &&
1544
+ escapeLeadingBlockMarker(`${inline} `) !== `${inline} `
1545
+ ? inline + suffix.trimStart()
1546
+ : escaped + suffix;
1547
+ return [indentStr(ind) + joined];
754
1548
  }
755
1549
  case "heading": {
756
1550
  const level = Math.min(6, Math.max(1, Number(node.attrs?.level) || 1));
@@ -769,12 +1563,19 @@ function serializeBlock(node: PMNode, indent: number): string[] {
769
1563
  const lang = (node.attrs?.language as string) || "";
770
1564
  const text = (node.content || []).map((t) => t.text || "").join("");
771
1565
  const body = text.split("\n").map((l) => indentStr(ind) + l);
772
- const longestRun = Math.max(
773
- 0,
774
- ...(text.match(/`+/g) || []).map((r) => r.length),
1566
+ // A backtick fence's language cannot hold a backtick, so a language
1567
+ // with one takes a tilde fence, spaced off when it starts with a tilde
1568
+ // that would lengthen the fence.
1569
+ const marker = lang.includes("`") ? "~" : "`";
1570
+ const fence = marker.repeat(
1571
+ Math.max(3, longestRun(text, marker === "`" ? /`+/g : /~+/g) + 1),
775
1572
  );
776
- const fence = "`".repeat(Math.max(3, longestRun + 1));
777
- return [indentStr(ind) + fence + lang, ...body, indentStr(ind) + fence];
1573
+ const gap = lang.startsWith(marker) ? " " : "";
1574
+ return [
1575
+ indentStr(ind) + fence + gap + lang,
1576
+ ...body,
1577
+ indentStr(ind) + fence,
1578
+ ];
778
1579
  }
779
1580
  case "blockquote":
780
1581
  return serializeQuote(node, ind);
@@ -1012,27 +1813,36 @@ function serializeTaskList(node: PMNode, indent: number): string[] {
1012
1813
  return out;
1013
1814
  }
1014
1815
 
1015
- function serializeCellInline(cell: PMNode): string {
1816
+ // A cell reads back as one line, so its children share one underscore check.
1817
+ function serializeCellInline(cell: PMNode, escapeUnderscore = false): string {
1016
1818
  const parts: string[] = [];
1017
1819
  for (const child of cell.content || []) {
1018
- const part = serializeCellChildInline(child);
1820
+ const part = serializeCellChildInline(child, escapeUnderscore);
1019
1821
  if (part) parts.push(part);
1020
1822
  }
1021
- return parts.join("<br>");
1823
+ const source = parts.join("<br>");
1824
+ return escapeUnderscore || !readsUnderscoreEmphasis(source)
1825
+ ? source
1826
+ : serializeCellInline(cell, true);
1022
1827
  }
1023
1828
 
1024
- function serializeCellChildInline(node: PMNode): string {
1829
+ function serializeCellChildInline(
1830
+ node: PMNode,
1831
+ escapeUnderscore: boolean,
1832
+ ): string {
1025
1833
  switch (node.type) {
1026
1834
  case "paragraph":
1027
1835
  case "heading":
1028
- return serializeInline(node.content);
1836
+ return serializeInline(node.content, escapeUnderscore);
1029
1837
  case "bulletList":
1030
1838
  case "orderedList":
1031
1839
  case "taskList": {
1032
1840
  const items: string[] = [];
1033
1841
  for (const item of node.content || []) {
1034
1842
  const textPara = firstParagraph(item);
1035
- const inline = textPara ? serializeInline(textPara.content) : "";
1843
+ const inline = textPara
1844
+ ? serializeInline(textPara.content, escapeUnderscore)
1845
+ : "";
1036
1846
  if (inline) items.push(inline);
1037
1847
  }
1038
1848
  return items.join("<br>");
@@ -1040,13 +1850,13 @@ function serializeCellChildInline(node: PMNode): string {
1040
1850
  case "blockquote": {
1041
1851
  const parts: string[] = [];
1042
1852
  for (const child of node.content || []) {
1043
- const part = serializeCellChildInline(child);
1853
+ const part = serializeCellChildInline(child, escapeUnderscore);
1044
1854
  if (part) parts.push(part);
1045
1855
  }
1046
1856
  return parts.join("<br>");
1047
1857
  }
1048
1858
  default:
1049
- return serializeInline(node.content);
1859
+ return serializeInline(node.content, escapeUnderscore);
1050
1860
  }
1051
1861
  }
1052
1862
 
@@ -1105,6 +1915,27 @@ function leadingTabs(line: string): number {
1105
1915
  return n;
1106
1916
  }
1107
1917
 
1918
+ // Code fences open with three or more backticks or tildes, and close with at
1919
+ // least as many of the same character.
1920
+ const FENCE_OPEN = /^(`{3,}|~{3,})(.*)$/;
1921
+ const FENCE_CLOSE = /^(`{3,}|~{3,})\s*$/;
1922
+
1923
+ // A backtick fence's language holds no backtick, so a line that starts with
1924
+ // a long code span, as the editor writes ``` `` ```, stays a paragraph.
1925
+ function matchFenceOpen(line: string): RegExpMatchArray | null {
1926
+ const open = line.match(FENCE_OPEN);
1927
+ return open && open[1][0] === "`" && open[2].includes("`") ? null : open;
1928
+ }
1929
+
1930
+ function closesFence(
1931
+ close: RegExpMatchArray | null,
1932
+ marker: string,
1933
+ ): close is RegExpMatchArray {
1934
+ return (
1935
+ !!close && close[1][0] === marker[0] && close[1].length >= marker.length
1936
+ );
1937
+ }
1938
+
1108
1939
  export function nfmToDoc(nfm: string | null | undefined): PMDoc {
1109
1940
  const lines = (nfm ?? "").replace(/\r\n?/g, "\n").split("\n");
1110
1941
  const { nodes } = parseBlockSequence(lines, 0, 0);
@@ -1114,6 +1945,21 @@ export function nfmToDoc(nfm: string | null | undefined): PMDoc {
1114
1945
  };
1115
1946
  }
1116
1947
 
1948
+ /** `nfmToDoc`, with the source it read but could not keep. */
1949
+ export function parseNfmWithLosses(nfm: string | null | undefined): {
1950
+ doc: PMDoc;
1951
+ losses: NfmParseLoss[];
1952
+ } {
1953
+ const previous = activeLossSink;
1954
+ const losses: NfmParseLoss[] = [];
1955
+ activeLossSink = losses;
1956
+ try {
1957
+ return { doc: nfmToDoc(nfm), losses };
1958
+ } finally {
1959
+ activeLossSink = previous;
1960
+ }
1961
+ }
1962
+
1117
1963
  interface ParseResult {
1118
1964
  nodes: PMNode[];
1119
1965
  end: number;
@@ -1157,7 +2003,12 @@ function parseBlockSequence(
1157
2003
  continue;
1158
2004
  }
1159
2005
 
1160
- const listKind = listKindOf(dedent);
2006
+ // A list here keeps no indent of its own, so a spaced marker one tab in
2007
+ // stays text: as a list it would swallow the indented block after it on
2008
+ // the next read.
2009
+ const listKind =
2010
+ listKindOf(dedent) ??
2011
+ (rel === 0 ? listKindOf(dedent.replace(/^ {1,3}/, "")) : null);
1161
2012
  if (listKind) {
1162
2013
  const res = parseList(lines, i, ind, listKind);
1163
2014
  out.push(res.nodes[0]);
@@ -1181,46 +2032,59 @@ function parseDetailsBody(
1181
2032
  ): PMNode[] {
1182
2033
  const childIndent = parentIndent + 1;
1183
2034
  const nestedContainers: Array<{ tagKey: string; closeTag: string }> = [];
1184
- let fence: { length: number; promoteBy: number } | undefined;
2035
+ let fence: { marker: string; promoteBy: number } | undefined;
1185
2036
  const sourceLines = lines.slice(start, end);
2037
+ // By tag, whether each opener has its close. A scan from one opener passes a
2038
+ // same-tag opener inside it as a scan from that one would, so it answers for
2039
+ // that one too.
2040
+ const matched = new Map<string, Map<number, boolean>>();
1186
2041
  const hasMatchingClose = (from: number, tagKey: string): boolean => {
1187
- const closeTag = CONTAINER_CLOSE[tagKey];
1188
- let depth = 1;
1189
- let fenceLength = 0;
1190
- for (let i = from + 1; i < sourceLines.length; i++) {
1191
- const candidate = sourceLines[i].slice(leadingTabs(sourceLines[i]));
1192
- const fenceMatch = candidate.match(/^(`{3,})(.*)$/);
1193
- if (fenceLength) {
1194
- if (
1195
- fenceMatch &&
1196
- !fenceMatch[2].trim() &&
1197
- fenceMatch[1].length >= fenceLength
1198
- ) {
1199
- fenceLength = 0;
2042
+ let known = matched.get(tagKey);
2043
+ if (!known) matched.set(tagKey, (known = new Map()));
2044
+ if (!known.has(from)) {
2045
+ const closeTag = CONTAINER_CLOSE[tagKey];
2046
+ const openers = [from];
2047
+ let fenceMarker = "";
2048
+ for (let i = from + 1; i < sourceLines.length && openers.length; i++) {
2049
+ const candidate = sourceLines[i].slice(leadingTabs(sourceLines[i]));
2050
+ const fenceMatch = matchFenceOpen(candidate);
2051
+ if (fenceMarker) {
2052
+ if (
2053
+ fenceMatch &&
2054
+ !fenceMatch[2].trim() &&
2055
+ closesFence(fenceMatch, fenceMarker)
2056
+ ) {
2057
+ fenceMarker = "";
2058
+ }
2059
+ continue;
1200
2060
  }
1201
- continue;
1202
- }
1203
- if (fenceMatch) {
1204
- fenceLength = fenceMatch[1].length;
1205
- continue;
2061
+ if (fenceMatch) {
2062
+ fenceMarker = fenceMatch[1];
2063
+ continue;
2064
+ }
2065
+ if (matchContainerOpen(candidate) === tagKey) openers.push(i);
2066
+ else if (candidate === closeTag) known.set(openers.pop()!, true);
1206
2067
  }
1207
- if (matchContainerOpen(candidate) === tagKey) depth++;
1208
- if (candidate === closeTag && --depth === 0) return true;
2068
+ for (const opener of openers) known.set(opener, false);
1209
2069
  }
1210
- return false;
2070
+ return known.get(from)!;
1211
2071
  };
2072
+ // Lines inside a container too deep to read need no more tabs: one more per
2073
+ // level would make each line as long as the nesting is deep.
2074
+ const nestedIndent = (depth: number) =>
2075
+ Math.min(childIndent + depth, MAX_CONTAINER_INDENT + 1);
1212
2076
  const bodyLines = sourceLines.map((line, lineIndex) => {
1213
2077
  const indent = leadingTabs(line);
1214
2078
  const dedented = line.slice(indent);
1215
2079
  if (fence) {
1216
- const requiredIndent = childIndent + nestedContainers.length;
2080
+ const requiredIndent = nestedIndent(nestedContainers.length);
1217
2081
  const promoteBy =
1218
2082
  fence.promoteBy > 0
1219
2083
  ? fence.promoteBy
1220
2084
  : Math.max(0, requiredIndent - indent);
1221
2085
  const promoted = `${"\t".repeat(promoteBy)}${line}`;
1222
- const close = dedented.match(/^(`{3,})\s*$/);
1223
- if (close && close[1].length >= fence.length) fence = undefined;
2086
+ if (closesFence(dedented.match(FENCE_CLOSE), fence.marker))
2087
+ fence = undefined;
1224
2088
  return promoted;
1225
2089
  }
1226
2090
  if (nestedContainers[nestedContainers.length - 1]?.closeTag === dedented) {
@@ -1229,12 +2093,13 @@ function parseDetailsBody(
1229
2093
  const detailsSummary =
1230
2094
  nestedContainers[nestedContainers.length - 1]?.tagKey === "<details" &&
1231
2095
  /^<summary>[\s\S]*<\/summary>\s*$/.test(dedented);
1232
- const requiredIndent =
1233
- childIndent + nestedContainers.length - (detailsSummary ? 1 : 0);
1234
- const open = dedented.match(/^(`{3,})(.*)$/);
2096
+ const requiredIndent = nestedIndent(
2097
+ nestedContainers.length - (detailsSummary ? 1 : 0),
2098
+ );
2099
+ const open = matchFenceOpen(dedented);
1235
2100
  if (open) {
1236
2101
  const promoteBy = Math.max(0, requiredIndent - indent);
1237
- fence = { length: open[1].length, promoteBy };
2102
+ fence = { marker: open[1], promoteBy };
1238
2103
  return `${"\t".repeat(promoteBy)}${line}`;
1239
2104
  }
1240
2105
  if (!line.trim()) return line;
@@ -1250,7 +2115,67 @@ function parseDetailsBody(
1250
2115
  if (indent >= requiredIndent) return line;
1251
2116
  return `${"\t".repeat(requiredIndent - indent)}${line}`;
1252
2117
  });
1253
- return parseBlockSequence(bodyLines, 0, childIndent).nodes;
2118
+ const res = parseBlockSequence(bodyLines, 0, childIndent);
2119
+ // A fence's lines move with its opener, so a line written shallower than
2120
+ // the opener can stay below the body and end the read before the close.
2121
+ for (let i = res.end; i < bodyLines.length; i++) {
2122
+ const text = bodyLines[i].trim();
2123
+ if (text) reportLoss("dropped-source", text);
2124
+ }
2125
+ return res.nodes;
2126
+ }
2127
+
2128
+ // A container's children sit one tab in from its tags, but agents also write
2129
+ // them at the tags' own indent. Where the read stops at such a line, it and
2130
+ // every line after it move in together, so the rest reads as it would from
2131
+ // that line's indent. Moving only the lines at least as deep would split a
2132
+ // block that reads shallower lines as its own, such as code, math or a nested
2133
+ // container.
2134
+ function parseContainerChildren(
2135
+ lines: string[],
2136
+ start: number,
2137
+ end: number,
2138
+ parentIndent: number,
2139
+ ): PMNode[] {
2140
+ const childIndent = parentIndent + 1;
2141
+ const body = lines.slice(start, end);
2142
+ const nodes: PMNode[] = [];
2143
+ for (let i = 0; ; ) {
2144
+ const res = parseBlockSequence(body, i, childIndent);
2145
+ // A list that goes on past the stop is still one list.
2146
+ appendBlocks(nodes, res.nodes);
2147
+ if (res.end >= body.length) return nodes;
2148
+ i = res.end;
2149
+ const shift = TAB.repeat(childIndent - leadingTabs(body[i]));
2150
+ const tabCounts = lineTabCounts.get(body);
2151
+ for (let j = i; j < body.length; j++) {
2152
+ body[j] = shift + body[j];
2153
+ if (tabCounts) tabCounts[j] += shift.length;
2154
+ }
2155
+ // Its closes were found at the old indents.
2156
+ containerCloseIndexes.delete(body);
2157
+ }
2158
+ }
2159
+
2160
+ // Two lists of one kind side by side are written as one list, which reads
2161
+ // back as one and renumbers, so a list appended after one joins it.
2162
+ function appendBlocks(target: PMNode[], blocks: readonly PMNode[]): void {
2163
+ const last = target[target.length - 1];
2164
+ const first = blocks[0];
2165
+ if (last && first && last.type === first.type && isListNode(first)) {
2166
+ pushAll(last.content!, first.content ?? []);
2167
+ pushAll(target, blocks.slice(1));
2168
+ } else {
2169
+ pushAll(target, blocks);
2170
+ }
2171
+ }
2172
+
2173
+ function isListNode(node: PMNode): boolean {
2174
+ return (
2175
+ node.type === "bulletList" ||
2176
+ node.type === "orderedList" ||
2177
+ node.type === "taskList"
2178
+ );
1254
2179
  }
1255
2180
 
1256
2181
  function endsWithUnescapedPipe(value: string): boolean {
@@ -1262,23 +2187,23 @@ function endsWithUnescapedPipe(value: string): boolean {
1262
2187
  return backslashes % 2 === 0;
1263
2188
  }
1264
2189
 
1265
- function hasMatchingBacktickRun(
1266
- value: string,
1267
- start: number,
1268
- expectedLength: number,
1269
- ): boolean {
1270
- for (let i = start; i < value.length; i++) {
2190
+ // Where a pipe row's last backtick run of each length starts, read as the row
2191
+ // is: a backslash takes the next character with it. A run opens code when a
2192
+ // later run of its length closes it, and searching the rest of the row from
2193
+ // every run is quadratic in a row of runs of many lengths.
2194
+ function lastBacktickRuns(value: string): Map<number, number> {
2195
+ const last = new Map<number, number>();
2196
+ for (let i = 0; i < value.length; i++) {
1271
2197
  if (value[i] === "\\") {
1272
2198
  i++;
1273
2199
  continue;
1274
2200
  }
1275
2201
  if (value[i] !== "`") continue;
1276
- let runLength = 1;
1277
- while (value[i + runLength] === "`") runLength++;
1278
- if (runLength === expectedLength) return true;
1279
- i += runLength - 1;
2202
+ const run = delimiterRunLength(value, i, "`");
2203
+ last.set(run, i);
2204
+ i += run - 1;
1280
2205
  }
1281
- return false;
2206
+ return last;
1282
2207
  }
1283
2208
 
1284
2209
  export function splitGfmPipeRow(line: string): string[] | null {
@@ -1289,6 +2214,7 @@ export function splitGfmPipeRow(line: string): string[] | null {
1289
2214
  let cell = "";
1290
2215
  let codeFenceLength = 0;
1291
2216
  let sawSeparator = false;
2217
+ const lastRuns = lastBacktickRuns(value);
1292
2218
 
1293
2219
  for (let i = 0; i < value.length; i++) {
1294
2220
  const char = value[i];
@@ -1299,10 +2225,7 @@ export function splitGfmPipeRow(line: string): string[] | null {
1299
2225
  if (char === "`") {
1300
2226
  let runLength = 1;
1301
2227
  while (value[i + runLength] === "`") runLength++;
1302
- if (
1303
- codeFenceLength === 0 &&
1304
- hasMatchingBacktickRun(value, i + runLength, runLength)
1305
- )
2228
+ if (codeFenceLength === 0 && (lastRuns.get(runLength) ?? -1) > i)
1306
2229
  codeFenceLength = runLength;
1307
2230
  else if (codeFenceLength === runLength) codeFenceLength = 0;
1308
2231
  cell += "`".repeat(runLength);
@@ -1326,11 +2249,11 @@ export function splitGfmPipeRow(line: string): string[] | null {
1326
2249
  }
1327
2250
 
1328
2251
  function isGfmDelimiterCell(value: string): boolean {
1329
- return /^-{3,}$/.test(value.trim());
2252
+ return /^-+$/.test(value.trim());
1330
2253
  }
1331
2254
 
1332
2255
  function isAlignedGfmDelimiterCell(value: string): boolean {
1333
- return /^:?-{3,}:?$/.test(value.trim()) && value.includes(":");
2256
+ return /^:?-+:?$/.test(value.trim()) && value.includes(":");
1334
2257
  }
1335
2258
 
1336
2259
  function isTableAlignment(
@@ -1373,7 +2296,10 @@ function parseGfmPipeTable(
1373
2296
  end++;
1374
2297
  }
1375
2298
 
1376
- const columnCount = Math.max(header.length, ...rows.map((row) => row.length));
2299
+ // Not Math.max(...rows): a long table has more rows than a call can take
2300
+ // arguments.
2301
+ let columnCount = header.length;
2302
+ for (const row of rows) columnCount = Math.max(columnCount, row.length);
1377
2303
  const alignments = delimiter.map((cell) => {
1378
2304
  if (cell.startsWith(":") && cell.endsWith(":")) return "center";
1379
2305
  if (cell.endsWith(":")) return "right";
@@ -1410,12 +2336,286 @@ function listKindOf(dedent: string): ListKind | null {
1410
2336
  return null;
1411
2337
  }
1412
2338
 
2339
+ /** Columns from a list marker to its item's text, as CommonMark counts them. */
2340
+ function listItemContentOffset(item: string): number {
2341
+ const m = item.match(/^([-*+]|\d+[.)])( *)(.?)/);
2342
+ if (!m) return 0;
2343
+ const gap = m[2].length;
2344
+ return m[1].length + (gap > 4 || !m[3] ? 1 : gap);
2345
+ }
2346
+
2347
+ const spacedLineIndexes = new WeakMap<string[], Int32Array>();
2348
+
2349
+ // For each line, the first line at or after it whose tabs are followed by a
2350
+ // space: only those are rewritten, so a run with none ahead has nothing to do.
2351
+ // A rewrite never makes a line spaced that was not, so the index computed for
2352
+ // the array as given stays a safe bound while its lines are rewritten.
2353
+ function spacedLinesFrom(lines: string[]): Int32Array {
2354
+ let next = spacedLineIndexes.get(lines);
2355
+ if (next) return next;
2356
+ next = new Int32Array(lines.length + 1);
2357
+ next[lines.length] = lines.length;
2358
+ for (let i = lines.length - 1; i >= 0; i--)
2359
+ next[i] = /^\t* /.test(lines[i]) ? i : next[i + 1];
2360
+ spacedLineIndexes.set(lines, next);
2361
+ return next;
2362
+ }
2363
+
2364
+ const lineTabCounts = new WeakMap<string[], Int32Array>();
2365
+
2366
+ // Each line's leading tabs, kept in step with the rewrites below. Every list
2367
+ // depth scans the lines nested under it, so measuring a line again at each
2368
+ // depth makes a deeply nested page cubic.
2369
+ function tabCountsOf(lines: string[]): Int32Array {
2370
+ let counts = lineTabCounts.get(lines);
2371
+ if (counts) return counts;
2372
+ counts = new Int32Array(lines.length);
2373
+ for (let i = 0; i < lines.length; i++) counts[i] = leadingTabs(lines[i]);
2374
+ lineTabCounts.set(lines, counts);
2375
+ return counts;
2376
+ }
2377
+
2378
+ type FenceCloses = {
2379
+ // Each line's closing marker length, or 0.
2380
+ length: Int32Array;
2381
+ // The first closing line at or after each line.
2382
+ first: Int32Array;
2383
+ // After each closing line, the next one with a longer marker.
2384
+ longer: Int32Array;
2385
+ };
2386
+
2387
+ // What each line's text says, read without its leading whitespace, which the
2388
+ // rewrites below never change. Every list depth steps over the lines nested
2389
+ // under it, and a blank line or a fence's body costs no indentation, so
2390
+ // reading them again at each depth is quadratic.
2391
+ type LineIndex = {
2392
+ // The first line at or after each line with text.
2393
+ filled: Int32Array;
2394
+ // Each line's opening fence marker, as `matchFenceOpen` reads it.
2395
+ fenceOpens: Array<string | null>;
2396
+ fenceCloses: Record<string, FenceCloses>;
2397
+ };
2398
+
2399
+ const lineIndexes = new WeakMap<string[], LineIndex>();
2400
+
2401
+ function lineIndexOf(lines: string[]): LineIndex {
2402
+ let index = lineIndexes.get(lines);
2403
+ if (index) return index;
2404
+ const filled = new Int32Array(lines.length + 1);
2405
+ const fenceOpens: Array<string | null> = [];
2406
+ const fenceCloses: Record<string, FenceCloses> = {};
2407
+ for (const char of ["`", "~"]) {
2408
+ fenceCloses[char] = {
2409
+ length: new Int32Array(lines.length),
2410
+ first: new Int32Array(lines.length + 1),
2411
+ longer: new Int32Array(lines.length),
2412
+ };
2413
+ }
2414
+ for (let i = 0; i < lines.length; i++) {
2415
+ fenceOpens.push(matchFenceOpen(lines[i].trimStart())?.[1] ?? null);
2416
+ const close = lines[i].trim().match(FENCE_CLOSE);
2417
+ if (close) fenceCloses[close[1][0]].length[i] = close[1].length;
2418
+ }
2419
+ filled[lines.length] = lines.length;
2420
+ for (let i = lines.length - 1; i >= 0; i--)
2421
+ filled[i] = lines[i].trim() ? i : filled[i + 1];
2422
+ for (const { length, first, longer } of Object.values(fenceCloses)) {
2423
+ first[lines.length] = lines.length;
2424
+ const ahead: number[] = [];
2425
+ for (let i = lines.length - 1; i >= 0; i--) {
2426
+ first[i] = length[i] ? i : first[i + 1];
2427
+ if (!length[i]) continue;
2428
+ while (ahead.length && length[ahead[ahead.length - 1]] <= length[i])
2429
+ ahead.pop();
2430
+ longer[i] = ahead.length ? ahead[ahead.length - 1] : lines.length;
2431
+ ahead.push(i);
2432
+ }
2433
+ }
2434
+ index = { filled, fenceOpens, fenceCloses };
2435
+ lineIndexes.set(lines, index);
2436
+ return index;
2437
+ }
2438
+
2439
+ // The first line after `open` that closes a fence of `marker`, or the page's
2440
+ // end. Each step reaches a longer marker, so the steps are fewer than the
2441
+ // marker's length.
2442
+ function fenceCloseAfter(
2443
+ index: LineIndex,
2444
+ open: number,
2445
+ marker: string,
2446
+ ): number {
2447
+ const { length, first, longer } = index.fenceCloses[marker[0]];
2448
+ let at = first[open + 1];
2449
+ while (at < length.length && length[at] < marker.length) at = longer[at];
2450
+ return at;
2451
+ }
2452
+
2453
+ // The parse takes a few call frames per depth, and nesting by spaces would
2454
+ // otherwise overflow the stack on pages main reads. Past this depth, which no
2455
+ // page needs, space-indented lines read as main reads them, and the loss is
2456
+ // reported.
2457
+ const MAX_SPACE_NESTED_DEPTH = 256;
2458
+
2459
+ // Agents nest list items with spaces, and CommonMark puts a line inside the
2460
+ // innermost item whose text column it reaches. The parser nests by tabs, so
2461
+ // a list run's space-indented lines are rewritten as the tab-indented lines
2462
+ // the editor writes. A line without a marker joins an item only after a blank
2463
+ // line, since the editor never writes one and CommonMark would otherwise read
2464
+ // it as more of the item's text. Lines are rewritten in place, and the
2465
+ // returned originals must be put back once the run is parsed: copying the
2466
+ // array for every run makes a long page of short lists quadratic.
2467
+ function normalizeSpaceIndentedListRun(
2468
+ lines: string[],
2469
+ start: number,
2470
+ indent: number,
2471
+ kind: ListKind,
2472
+ ): Map<number, string> {
2473
+ const originals = new Map<number, string>();
2474
+ const write = (index: number, depth: number, text: string) => {
2475
+ if (!originals.has(index)) {
2476
+ if (originals.size === 0) containerCloseLayers(lines).push(new Map());
2477
+ originals.set(index, lines[index]);
2478
+ }
2479
+ lines[index] = "\t".repeat(depth) + text;
2480
+ tabCounts[index] = depth + leadingTabs(text);
2481
+ };
2482
+ const spaced = spacedLinesFrom(lines);
2483
+ if (spaced[start] === lines.length) return originals;
2484
+ const tabCounts = tabCountsOf(lines);
2485
+ const index = lineIndexOf(lines);
2486
+ const open: Array<{ content: number; depth: number }> = [];
2487
+ let blank = false;
2488
+ let continuing = false;
2489
+ for (let i = start; i < lines.length && spaced[i] < lines.length; i++) {
2490
+ if (index.filled[i] !== i) {
2491
+ blank = true;
2492
+ i = index.filled[i] - 1;
2493
+ continue;
2494
+ }
2495
+ const line = lines[i];
2496
+ const tabs = tabCounts[i];
2497
+ if (tabs < indent) break;
2498
+ const afterBlank = blank;
2499
+ blank = false;
2500
+ if (tabs > indent) {
2501
+ while (open.length && open[open.length - 1].depth >= tabs) open.pop();
2502
+ continuing = false;
2503
+ // A marker read without the line's leading whitespace is the one read
2504
+ // after its tabs only when nothing else comes before it.
2505
+ const fence =
2506
+ line[tabs] === "`" || line[tabs] === "~" ? index.fenceOpens[i] : null;
2507
+ if (fence) i = fenceCloseAfter(index, i, fence);
2508
+ continue;
2509
+ }
2510
+ const afterTabs = line.slice(tabs);
2511
+ let spaces = 0;
2512
+ while (afterTabs[spaces] === " ") spaces++;
2513
+ const text = afterTabs.slice(spaces);
2514
+ if (text[0] === "\t") break;
2515
+ const item = listKindOf(text) !== null;
2516
+ if (spaces === 0) {
2517
+ if (listKindOf(text) !== kind) break;
2518
+ open.length = 0;
2519
+ open.push({ content: listItemContentOffset(text), depth: indent });
2520
+ continuing = false;
2521
+ continue;
2522
+ }
2523
+ while (open.length && spaces < open[open.length - 1].content) open.pop();
2524
+ const parent = open[open.length - 1];
2525
+ if (!parent) {
2526
+ if (listKindOf(text) !== kind || spaces > 3) break;
2527
+ write(i, indent, text);
2528
+ open.push({
2529
+ content: spaces + listItemContentOffset(text),
2530
+ depth: indent,
2531
+ });
2532
+ continuing = false;
2533
+ continue;
2534
+ }
2535
+ const depth = parent.depth + 1;
2536
+ if (depth > MAX_SPACE_NESTED_DEPTH) {
2537
+ reportLoss("dropped-formatting", text);
2538
+ break;
2539
+ }
2540
+ if (item) {
2541
+ write(i, depth, text);
2542
+ open.push({ content: spaces + listItemContentOffset(text), depth });
2543
+ continuing = false;
2544
+ continue;
2545
+ }
2546
+ const fence = matchFenceOpen(text);
2547
+ if (fence) {
2548
+ // An unclosed fence runs to the page's end in the tab form, so one
2549
+ // whose item ends first reads as on main and takes nothing with it.
2550
+ if (
2551
+ fenceLeavesItem(
2552
+ lines,
2553
+ tabCounts,
2554
+ index,
2555
+ i,
2556
+ indent,
2557
+ parent.content,
2558
+ fence[1],
2559
+ )
2560
+ )
2561
+ break;
2562
+ write(i, depth, text);
2563
+ for (i++; i < lines.length; i++) {
2564
+ const tabs = tabCounts[i];
2565
+ const closes = closesFence(
2566
+ lines[i].slice(tabs).trim().match(FENCE_CLOSE),
2567
+ fence[1],
2568
+ );
2569
+ const body = lines[i].slice(Math.min(indent, tabs));
2570
+ let strip = 0;
2571
+ while (strip < spaces && body[strip] === " ") strip++;
2572
+ write(i, depth, body.slice(strip));
2573
+ if (closes) break;
2574
+ }
2575
+ continuing = false;
2576
+ continue;
2577
+ }
2578
+ if (text.startsWith(">")) {
2579
+ write(i, depth, text);
2580
+ continuing = false;
2581
+ continue;
2582
+ }
2583
+ if (!afterBlank && !continuing) break;
2584
+ write(i, depth, text);
2585
+ continuing = true;
2586
+ }
2587
+ return originals;
2588
+ }
2589
+
2590
+ function fenceLeavesItem(
2591
+ lines: string[],
2592
+ tabCounts: Int32Array,
2593
+ index: LineIndex,
2594
+ open: number,
2595
+ indent: number,
2596
+ content: number,
2597
+ marker: string,
2598
+ ): boolean {
2599
+ const close = fenceCloseAfter(index, open, marker);
2600
+ for (let i = index.filled[open + 1]; i < close; i = index.filled[i + 1]) {
2601
+ const line = lines[i];
2602
+ const tabs = tabCounts[i];
2603
+ if (tabs < indent) return true;
2604
+ if (tabs > indent) continue;
2605
+ let spaces = 0;
2606
+ while (line[indent + spaces] === " ") spaces++;
2607
+ if (spaces < content) return true;
2608
+ }
2609
+ return false;
2610
+ }
2611
+
1413
2612
  function parseList(
1414
2613
  lines: string[],
1415
2614
  start: number,
1416
2615
  indent: number,
1417
2616
  kind: ListKind,
1418
2617
  ): ParseResult {
2618
+ const originals = normalizeSpaceIndentedListRun(lines, start, indent, kind);
1419
2619
  const items: PMNode[] = [];
1420
2620
  let i = start;
1421
2621
  let orderedStart: number | null = null;
@@ -1463,6 +2663,14 @@ function parseList(
1463
2663
  }
1464
2664
  i = childRes.end;
1465
2665
  }
2666
+ // No try/finally: it grows this recursive frame, and a parse that throws
2667
+ // abandons its lines anyway.
2668
+ const tabCounts = tabCountsOf(lines);
2669
+ for (const [index, line] of originals) {
2670
+ lines[index] = line;
2671
+ tabCounts[index] = leadingTabs(line);
2672
+ }
2673
+ if (originals.size > 0) containerCloseLayers(lines).pop();
1466
2674
 
1467
2675
  if (kind === "task") {
1468
2676
  return { nodes: [{ type: "taskList", content: items }], end: i };
@@ -1489,7 +2697,10 @@ function parseSingleBlock(
1489
2697
  return node;
1490
2698
  };
1491
2699
 
1492
- if (/^<empty-block\s*\/?>/.test(dedent)) {
2700
+ const emptyBlock = dedent.match(/^<empty-block\s*\/?>/);
2701
+ if (emptyBlock) {
2702
+ const rest = dedent.slice(emptyBlock[0].length).trim();
2703
+ if (rest) reportLoss("dropped-source", rest);
1493
2704
  return { nodes: [withIndentAttr({ type: "paragraph" })], end: start + 1 };
1494
2705
  }
1495
2706
 
@@ -1500,28 +2711,27 @@ function parseSingleBlock(
1500
2711
  };
1501
2712
  }
1502
2713
 
1503
- const fenceOpenMatch = dedent.match(/^(`{3,})(.*)$/);
2714
+ const fenceOpenMatch = matchFenceOpen(dedent);
1504
2715
  if (fenceOpenMatch) {
1505
- const fenceLen = fenceOpenMatch[1].length;
2716
+ const marker = fenceOpenMatch[1];
1506
2717
  const lang = fenceOpenMatch[2].trim();
1507
2718
  const body: string[] = [];
1508
2719
  let i = start + 1;
1509
2720
  for (; i < lines.length; i++) {
1510
2721
  const l = lines[i];
1511
2722
  const ld = l.slice(Math.min(indent, leadingTabs(l)));
1512
- const closeMatch = ld.trim().match(/^(`{3,})\s*$/);
1513
2723
  if (
1514
- closeMatch &&
1515
- closeMatch[1].length >= fenceLen &&
2724
+ closesFence(ld.trim().match(FENCE_CLOSE), marker) &&
1516
2725
  leadingTabs(l) >= indent
1517
2726
  )
1518
2727
  break;
1519
2728
  body.push(stripTabs(l, indent));
1520
2729
  }
2730
+ const code = body.join("\n");
1521
2731
  const node: PMNode = {
1522
2732
  type: "codeBlock",
1523
2733
  attrs: { language: lang || null },
1524
- content: body.length ? [{ type: "text", text: body.join("\n") }] : [],
2734
+ content: code ? [{ type: "text", text: code }] : [],
1525
2735
  };
1526
2736
  return { nodes: [withIndentAttr(node)], end: i + 1 };
1527
2737
  }
@@ -1551,7 +2761,7 @@ function parseSingleBlock(
1551
2761
  const headingMatch = dedent.match(/^(#{1,6})\s+(.*)$/);
1552
2762
  if (headingMatch) {
1553
2763
  const level = headingMatch[1].length;
1554
- const { text, toggle, color } = splitBlockAttrs(headingMatch[2]);
2764
+ const { text, toggle, color } = splitBlockAttrs(headingMatch[2], true);
1555
2765
  if (toggle) {
1556
2766
  const childRes = parseBlockSequence(lines, start + 1, indent + 1);
1557
2767
  const node: PMNode = {
@@ -1616,7 +2826,7 @@ function parseSingleBlock(
1616
2826
 
1617
2827
  const containerTag = matchContainerOpen(dedent);
1618
2828
  if (containerTag) {
1619
- return parseContainer(lines, start, indent, rel, containerTag);
2829
+ return parseContainer(lines, start, indent, containerTag, withIndentAttr);
1620
2830
  }
1621
2831
 
1622
2832
  if (dedent.startsWith("![")) {
@@ -1631,6 +2841,9 @@ function parseSingleBlock(
1631
2841
  const rest = dedent.slice(srcCloseParen + 1);
1632
2842
  const suffixMatch = rest.match(/^\s*(\{[^}]*\})?\s*$/);
1633
2843
  if (suffixMatch) {
2844
+ if (suffixMatch[1]) {
2845
+ readAttrs("image", suffixMatch[1].slice(1, -1), IMAGE_ATTRS);
2846
+ }
1634
2847
  const colorMatch = suffixMatch[1]?.match(/color="([^"]+)"/);
1635
2848
  const node: PMNode = {
1636
2849
  type: "image",
@@ -1923,19 +3136,53 @@ function matchContainerOpen(dedent: string): string | null {
1923
3136
  return null;
1924
3137
  }
1925
3138
 
3139
+ const MAX_CONTAINER_INDENT = 32;
3140
+
3141
+ // Where each container opener's closing tag is, or -1 for none, by tag. A
3142
+ // scan from one opener passes a same-tag opener at its indent as a scan from
3143
+ // that one would, so it records that one's close too, and a page of unclosed
3144
+ // containers is scanned once. A list run whose indents are rewritten reads
3145
+ // from a set of its own, dropped when the run is put back.
3146
+ type ContainerCloses = Map<string, Map<number, number>>;
3147
+ const containerCloseIndexes = new WeakMap<string[], ContainerCloses[]>();
3148
+
3149
+ function containerCloseLayers(lines: string[]): ContainerCloses[] {
3150
+ let layers = containerCloseIndexes.get(lines);
3151
+ if (!layers) containerCloseIndexes.set(lines, (layers = [new Map()]));
3152
+ return layers;
3153
+ }
3154
+
3155
+ function containerClosesOf(
3156
+ lines: string[],
3157
+ tagKey: string,
3158
+ ): Map<number, number> {
3159
+ const layers = containerCloseLayers(lines);
3160
+ const byTag = layers[layers.length - 1];
3161
+ let closes = byTag.get(tagKey);
3162
+ if (!closes) byTag.set(tagKey, (closes = new Map()));
3163
+ return closes;
3164
+ }
3165
+
3166
+ function containerOpenerAsText(
3167
+ openLine: string,
3168
+ start: number,
3169
+ withIndentAttr: (node: PMNode) => PMNode,
3170
+ ): ParseResult {
3171
+ const { text, color } = splitBlockAttrs(openLine);
3172
+ const node: PMNode = { type: "paragraph", content: parseInline(text) };
3173
+ if (isColor(color)) node.attrs = { color };
3174
+ return { nodes: [withIndentAttr(node)], end: start + 1 };
3175
+ }
3176
+
1926
3177
  function parseContainer(
1927
3178
  lines: string[],
1928
3179
  start: number,
1929
3180
  indent: number,
1930
- rel: number,
1931
3181
  tagKey: string,
3182
+ withIndentAttr: (node: PMNode) => PMNode,
1932
3183
  ): ParseResult {
1933
3184
  const closeTag = CONTAINER_CLOSE[tagKey];
1934
3185
  const openLine = lines[start].slice(indent);
1935
- const withIndentAttr = (node: PMNode): PMNode => {
1936
- if (rel > 0) node.attrs = { ...(node.attrs || {}), indent: rel };
1937
- return node;
1938
- };
1939
3186
 
1940
3187
  if (tagKey === "<table") {
1941
3188
  return parseTable(lines, start, indent, withIndentAttr);
@@ -1951,47 +3198,54 @@ function parseContainer(
1951
3198
  );
1952
3199
  }
1953
3200
 
1954
- let i = start + 1;
1955
- const childStart = i;
1956
- let depth = 1;
1957
- let fence: { indent: number; length: number } | undefined;
1958
- for (; i < lines.length; i++) {
1959
- const li = leadingTabs(lines[i]);
1960
- const ld = lines[i].slice(li);
1961
- if (fence) {
1962
- const close = ld.match(/^(`{3,})\s*$/);
1963
- if (close && close[1].length >= fence.length) {
1964
- fence = undefined;
3201
+ // A container holds its children one tab in, so its indent bounds how many
3202
+ // containers it is inside. Each level reads its body again, with its whole
3203
+ // subtree, so past this depth the opener reads as text,
3204
+ // without the scan for its close: openers at many indents would each scan
3205
+ // the rest of the nesting.
3206
+ if (indent > MAX_CONTAINER_INDENT) {
3207
+ reportLoss("dropped-formatting", openLine);
3208
+ return containerOpenerAsText(openLine, start, withIndentAttr);
3209
+ }
3210
+ const closes = containerClosesOf(lines, tagKey);
3211
+ if (!closes.has(start)) {
3212
+ const openers = [start];
3213
+ let fence: { indent: number; marker: string } | undefined;
3214
+ for (let i = start + 1; i < lines.length && openers.length > 0; i++) {
3215
+ const li = leadingTabs(lines[i]);
3216
+ const ld = lines[i].slice(li);
3217
+ if (fence) {
3218
+ if (closesFence(ld.match(FENCE_CLOSE), fence.marker)) {
3219
+ fence = undefined;
3220
+ }
3221
+ continue;
1965
3222
  }
1966
- continue;
1967
- }
1968
- const openFence = ld.match(/^(`{3,})(.*)$/);
1969
- if (openFence) {
1970
- fence = { indent: li, length: openFence[1].length };
1971
- continue;
1972
- }
1973
- if (
1974
- li === indent &&
1975
- matchContainerOpen(ld) === tagKey &&
1976
- !ld.startsWith("</")
1977
- ) {
1978
- depth++;
1979
- }
1980
- if (li === indent && ld === closeTag) {
1981
- depth--;
1982
- if (depth === 0) break;
3223
+ const openFence = matchFenceOpen(ld);
3224
+ if (openFence) {
3225
+ fence = { indent: li, marker: openFence[1] };
3226
+ continue;
3227
+ }
3228
+ if (li !== indent) continue;
3229
+ if (matchContainerOpen(ld) === tagKey && !ld.startsWith("</"))
3230
+ openers.push(i);
3231
+ else if (ld === closeTag) closes.set(openers.pop()!, i);
1983
3232
  }
3233
+ for (const opener of openers) closes.set(opener, -1);
1984
3234
  }
1985
- if (depth !== 0) {
1986
- const { text, color } = splitBlockAttrs(openLine);
1987
- const node: PMNode = { type: "paragraph", content: parseInline(text) };
1988
- if (isColor(color)) node.attrs = { color };
1989
- return { nodes: [withIndentAttr(node)], end: start + 1 };
1990
- }
1991
- const closeIdx = i;
3235
+ const closeIdx = closes.get(start)!;
3236
+ if (closeIdx === -1)
3237
+ return containerOpenerAsText(openLine, start, withIndentAttr);
3238
+ const childStart = start + 1;
1992
3239
 
1993
3240
  if (tagKey === "<details") {
1994
- const attrs = parseAttrs(openLine);
3241
+ const attrs = readContainerAttrs(
3242
+ "details",
3243
+ openLine.replace(/\sopen(?=\s|>|$)/, ""),
3244
+ DETAILS_ATTRS,
3245
+ );
3246
+ // HTML reads any value of `open` as open, and the editor writes it bare.
3247
+ if (attrs.open !== undefined && !["", "open", "true"].includes(attrs.open))
3248
+ reportLoss("dropped-attribute", `<details open="${attrs.open}">`);
1995
3249
  let summary = "";
1996
3250
  let bodyStart = childStart;
1997
3251
  const summaryLine = lines[childStart]?.slice(indent) ?? "";
@@ -2015,35 +3269,49 @@ function parseContainer(
2015
3269
  }
2016
3270
 
2017
3271
  if (tagKey === "<callout") {
2018
- const attrs = parseAttrs(openLine);
2019
- const childRes = parseBlockSequence(lines, childStart, indent + 1);
3272
+ const attrs = readContainerAttrs("callout", openLine, CALLOUT_ATTRS);
2020
3273
  const node: PMNode = {
2021
3274
  type: "notionCallout",
2022
3275
  attrs: {
2023
3276
  icon: attrs.icon ?? "",
2024
3277
  color: isColor(attrs.color) ? attrs.color : null,
2025
3278
  },
2026
- content: childRes.nodes,
3279
+ content: parseContainerChildren(lines, childStart, closeIdx, indent),
2027
3280
  };
2028
3281
  return { nodes: [withIndentAttr(node)], end: closeIdx + 1 };
2029
3282
  }
2030
3283
 
2031
3284
  if (tagKey === "<columns>") {
2032
- const childRes = parseBlockSequence(lines, childStart, indent + 1);
2033
- const columns = childRes.nodes.filter((n) => n.type === "notionColumn");
3285
+ // A child outside every column joins the column before it, or a new
3286
+ // column when none comes before. Dropping it would lose text: a block
3287
+ // written straight under `<columns>`, or one left there when a block open
3288
+ // in a column ends at `</columns>`.
3289
+ const columns: PMNode[] = [];
3290
+ for (const child of parseContainerChildren(
3291
+ lines,
3292
+ childStart,
3293
+ closeIdx,
3294
+ indent,
3295
+ )) {
3296
+ const last = columns[columns.length - 1];
3297
+ if (child.type === "notionColumn") columns.push(child);
3298
+ else if (last) appendBlocks((last.content ??= []), [child]);
3299
+ else columns.push({ type: "notionColumn", content: [child] });
3300
+ }
2034
3301
  const node: PMNode = { type: "notionColumns", content: columns };
2035
3302
  return { nodes: [withIndentAttr(node)], end: closeIdx + 1 };
2036
3303
  }
2037
3304
 
2038
3305
  if (tagKey === "<column>") {
2039
- const childRes = parseBlockSequence(lines, childStart, indent + 1);
2040
- const node: PMNode = { type: "notionColumn", content: childRes.nodes };
3306
+ const node: PMNode = {
3307
+ type: "notionColumn",
3308
+ content: parseContainerChildren(lines, childStart, closeIdx, indent),
3309
+ };
2041
3310
  return { nodes: [withIndentAttr(node)], end: closeIdx + 1 };
2042
3311
  }
2043
3312
 
2044
3313
  if (tagKey === "<synced_block" || tagKey === "<synced_block_reference") {
2045
- const attrs = parseAttrs(openLine);
2046
- const childRes = parseBlockSequence(lines, childStart, indent + 1);
3314
+ const attrs = readContainerAttrs(tagKey.slice(1), openLine, SYNCED_ATTRS);
2047
3315
  const node: PMNode = {
2048
3316
  type: "notionSyncedBlock",
2049
3317
  attrs: {
@@ -2051,7 +3319,7 @@ function parseContainer(
2051
3319
  url: attrs.url || null,
2052
3320
  notice: attrs.notice || null,
2053
3321
  },
2054
- content: childRes.nodes,
3322
+ content: parseContainerChildren(lines, childStart, closeIdx, indent),
2055
3323
  };
2056
3324
  return { nodes: [withIndentAttr(node)], end: closeIdx + 1 };
2057
3325
  }
@@ -2066,6 +3334,18 @@ function parseContainer(
2066
3334
  );
2067
3335
  }
2068
3336
 
3337
+ const DETAILS_ATTRS = new Set(["color", "open"]);
3338
+ const CALLOUT_ATTRS = new Set(["icon", "color"]);
3339
+ const SYNCED_ATTRS = new Set(["url", "notice"]);
3340
+
3341
+ function readContainerAttrs(
3342
+ tag: string,
3343
+ openLine: string,
3344
+ kept: ReadonlySet<string>,
3345
+ ): Record<string, string> {
3346
+ return readAttrs(tag, openLine.slice(tag.length + 1).replace(/>$/, ""), kept);
3347
+ }
3348
+
2069
3349
  function parseRawContainer(
2070
3350
  lines: string[],
2071
3351
  start: number,
@@ -2074,23 +3354,28 @@ function parseRawContainer(
2074
3354
  tagName: string,
2075
3355
  withIndentAttr: (n: PMNode) => PMNode,
2076
3356
  ): ParseResult {
2077
- let i = start + 1;
2078
- let closed = false;
2079
- for (; i < lines.length; i++) {
2080
- if (
2081
- lines[i].slice(indent) === closeTag &&
2082
- leadingTabs(lines[i]) >= indent
2083
- ) {
2084
- closed = true;
2085
- break;
2086
- }
2087
- }
2088
- if (!closed) {
2089
- const openLine = lines[start].slice(indent);
2090
- const { text, color } = splitBlockAttrs(openLine);
2091
- const node: PMNode = { type: "paragraph", content: parseInline(text) };
2092
- if (isColor(color)) node.attrs = { color };
2093
- return { nodes: [withIndentAttr(node)], end: start + 1 };
3357
+ // The first line after the opener that is the closing tag at its indent;
3358
+ // every same opener on the way closes there too.
3359
+ const closes = containerClosesOf(lines, closeTag);
3360
+ if (!closes.has(start)) {
3361
+ const close = TAB.repeat(indent) + closeTag;
3362
+ const open = TAB.repeat(indent) + "<" + closeTag.slice(2);
3363
+ const openers = [start];
3364
+ let at = start + 1;
3365
+ while (at < lines.length && lines[at] !== close) {
3366
+ if (lines[at] === open) openers.push(at);
3367
+ at++;
3368
+ }
3369
+ for (const opener of openers)
3370
+ closes.set(opener, at < lines.length ? at : -1);
3371
+ }
3372
+ const i = closes.get(start)!;
3373
+ if (i === -1) {
3374
+ return containerOpenerAsText(
3375
+ lines[start].slice(indent),
3376
+ start,
3377
+ withIndentAttr,
3378
+ );
2094
3379
  }
2095
3380
  const rawLines = lines.slice(start, i + 1).map((l) => stripTabs(l, indent));
2096
3381
  const node: PMNode = {
@@ -2105,78 +3390,202 @@ function parseRawContainer(
2105
3390
  return { nodes: [withIndentAttr(node)], end: i + 1 };
2106
3391
  }
2107
3392
 
3393
+ // For each line of a table, indexed from its open tag, the first line at or
3394
+ // after it that opens or closes a cell or starts or ends a row, or `close`: a
3395
+ // table of unclosed cells would otherwise rescan the rest of itself from every
3396
+ // one.
3397
+ function cellClosesFrom(
3398
+ start: number,
3399
+ close: number,
3400
+ lineAt: (index: number) => string,
3401
+ ): Int32Array {
3402
+ const next = new Int32Array(close - start + 1);
3403
+ next[close - start] = close;
3404
+ for (let index = close - 1; index > start; index--) {
3405
+ const line = lineAt(index);
3406
+ next[index - start] =
3407
+ ROW_EDGE.test(line) || CELL_OPEN.test(line) || CELL_LINE_END.test(line)
3408
+ ? index
3409
+ : next[index - start + 1];
3410
+ }
3411
+ return next;
3412
+ }
3413
+
3414
+ const tableCloseIndexes = new WeakMap<string[], Int32Array>();
3415
+
3416
+ // For each line, the first `</table>` line at or after it. A page of unclosed
3417
+ // tables would otherwise search the rest of itself from every one.
3418
+ function tableClosesFrom(lines: string[]): Int32Array {
3419
+ let next = tableCloseIndexes.get(lines);
3420
+ if (next) return next;
3421
+ next = new Int32Array(lines.length + 1);
3422
+ next[lines.length] = lines.length;
3423
+ for (let i = lines.length - 1; i >= 0; i--)
3424
+ next[i] = lines[i].trim() === "</table>" ? i : next[i + 1];
3425
+ tableCloseIndexes.set(lines, next);
3426
+ return next;
3427
+ }
3428
+
2108
3429
  function parseTable(
2109
3430
  lines: string[],
2110
3431
  start: number,
2111
3432
  indent: number,
2112
3433
  withIndentAttr: (n: PMNode) => PMNode,
2113
3434
  ): ParseResult {
2114
- const openAttrs = parseAttrs(lines[start].slice(indent));
2115
- const headerRow = openAttrs["header-row"] === "true";
2116
- const headerColumn = openAttrs["header-column"] === "true";
2117
- const fitPageWidth = openAttrs["fit-page-width"] === "true";
3435
+ const openLine = lines[start].slice(indent);
3436
+ const lineAt = (index: number) =>
3437
+ lines[index].slice(Math.min(indent, leadingTabs(lines[index]))).trim();
2118
3438
 
2119
- let i = start + 1;
3439
+ // A line's tabs are all leading whitespace, so `lineAt` is its trimmed text.
3440
+ const close = tableClosesFrom(lines)[start + 1];
3441
+ if (close === lines.length) {
3442
+ const { text, color } = splitBlockAttrs(openLine);
3443
+ const node: PMNode = { type: "paragraph", content: parseInline(text) };
3444
+ if (isColor(color)) node.attrs = { color };
3445
+ return { nodes: [withIndentAttr(node)], end: start + 1 };
3446
+ }
3447
+
3448
+ const openAttrSource = openLine.replace(/^<table/, "").replace(/>$/, "");
3449
+ const openAttrs = readAttrs("table", openAttrSource, TABLE_ATTRS);
3450
+ for (const [name, value] of Object.entries(openAttrs)) {
3451
+ if (TABLE_ATTRS.has(name) && value !== "true" && value !== "false")
3452
+ reportLoss("dropped-attribute", `<table ${name}="${value}">`);
3453
+ }
2120
3454
  const colMeta: Array<{ color?: string; width?: string }> = [];
2121
- const rows: PMNode[] = [];
2122
- let closed = false;
3455
+ const rows: Array<{ color: string | null; cells: string[]; head: boolean }> =
3456
+ [];
3457
+ let group: string | null = null;
3458
+ // The editor has no footer rows, so a `<tfoot>` reads as body rows.
3459
+ const enterGroups = (source: string) => {
3460
+ for (const [tag, close, name] of source.matchAll(ROW_GROUP_TAG)) {
3461
+ group = close ? null : name;
3462
+ if (group === "tfoot") reportLoss("dropped-source", tag);
3463
+ }
3464
+ };
3465
+ const openRow = (attrSource: string | undefined) => {
3466
+ const rowAttrs = readAttrs("tr", attrSource ?? "", ROW_ATTRS);
3467
+ rows.push({
3468
+ color: isColor(rowAttrs.color) ? rowAttrs.color : null,
3469
+ cells: [],
3470
+ head: group === "thead",
3471
+ });
3472
+ };
3473
+ const addCells = (source: string) => {
3474
+ for (const part of splitTableCells(source)) {
3475
+ if (typeof part === "string") rows[rows.length - 1].cells.push(part);
3476
+ else {
3477
+ enterGroups(part.gap);
3478
+ if (part.rowAttrs !== null) openRow(part.rowAttrs);
3479
+ }
3480
+ }
3481
+ };
3482
+ let cellCloses: Int32Array | null = null;
2123
3483
 
2124
- for (; i < lines.length; i++) {
2125
- const ld = lines[i].slice(Math.min(indent, leadingTabs(lines[i]))).trim();
2126
- if (ld === "</table>") {
2127
- i++;
2128
- closed = true;
2129
- break;
3484
+ let i = start + 1;
3485
+ while (i < close) {
3486
+ let ld = lineAt(i++);
3487
+ if (!ld) continue;
3488
+ const groupPrefix = ld.match(ROW_GROUP_PREFIX);
3489
+ if (groupPrefix) {
3490
+ enterGroups(groupPrefix[0]);
3491
+ ld = ld.slice(groupPrefix[0].length);
3492
+ if (!ld) continue;
3493
+ }
3494
+ if (TABLE_WRAPPER_LINE.test(ld)) {
3495
+ enterGroups(ld);
3496
+ continue;
2130
3497
  }
2131
- if (ld === "<colgroup>") continue;
2132
- if (ld === "</colgroup>") continue;
2133
- const colMatch = ld.match(/^<col([^>]*)\/?>$/);
3498
+ // Not /^<col(\s[^>]*?)?\s*\/?>$/: its lazy group retries the spaces
3499
+ // before `>` from each offset of a long run of them. The attributes read
3500
+ // the same with the spaces, but not with the slash.
3501
+ const colMatch = ld.match(/^<col(\s[^>]*)?\/?>$/);
2134
3502
  if (colMatch) {
2135
- const a = parseAttrs(colMatch[1]);
3503
+ const attrSource = (colMatch[1] ?? "").replace(/\/$/, "");
3504
+ const a = readAttrs("col", attrSource, COL_ATTRS);
2136
3505
  colMeta.push({ color: a.color, width: a.width });
2137
3506
  continue;
2138
3507
  }
2139
- if (/^<tr/.test(ld)) {
2140
- const rowAttrs = parseAttrs(ld);
2141
- const cells: PMNode[] = [];
2142
- for (i++; i < lines.length; i++) {
2143
- const cd = lines[i]
2144
- .slice(Math.min(indent, leadingTabs(lines[i])))
2145
- .trim();
2146
- if (cd === "</tr>") break;
2147
- const cellMatch = cd.match(/^<t[dh]([^>]*)>([\s\S]*?)<\/t[dh]>$/);
2148
- if (cellMatch) {
2149
- const ca = parseAttrs(cellMatch[1]);
2150
- const isHeader =
2151
- (headerRow && rows.length === 0) ||
2152
- (headerColumn && cells.length === 0);
2153
- cells.push({
2154
- type: isHeader ? "tableHeader" : "tableCell",
2155
- attrs: {
2156
- color: isColor(ca.color) ? ca.color : null,
2157
- textAlign: isTableAlignment(ca.align) ? ca.align : null,
2158
- },
2159
- content: [
2160
- { type: "paragraph", content: parseInline(cellMatch[2]) },
2161
- ],
2162
- });
3508
+ const rowMatch = ld.match(/^<tr(\s[^>]*?)?>(.*)$/);
3509
+ if (!rowMatch) {
3510
+ reportLoss("dropped-source", ld);
3511
+ continue;
3512
+ }
3513
+ openRow(rowMatch[1]);
3514
+ // An agent may start a row's cells, or write the whole row, on its line;
3515
+ // the writer never does.
3516
+ let cd = rowMatch[2].trim();
3517
+ let rowClosed = false;
3518
+ for (let first = true; !rowClosed && (first || i < close); first = false) {
3519
+ if (!first) cd = lineAt(i++);
3520
+ if (cd === "</tr>") break;
3521
+ if (!cd) continue;
3522
+ // HTML reads a cell's line breaks as spaces.
3523
+ if (CELL_OPEN.test(cd) && !CELL_LINE_END.test(cd)) {
3524
+ cellCloses ??= cellClosesFrom(start, close, lineAt);
3525
+ const end = cellCloses[i - start];
3526
+ // A cell left open when its row ends or the next cell opens is
3527
+ // dropped, not read into what follows.
3528
+ if (
3529
+ end < close &&
3530
+ !ROW_EDGE.test(lineAt(end)) &&
3531
+ !CELL_OPEN.test(lineAt(end))
3532
+ ) {
3533
+ for (; i <= end; i++) cd += ` ${lineAt(i)}`;
3534
+ cd = trimJoinedCell(cd);
2163
3535
  }
2164
3536
  }
2165
- rows.push({
2166
- type: "tableRow",
2167
- attrs: { color: isColor(rowAttrs.color) ? rowAttrs.color : null },
2168
- content: cells,
2169
- });
3537
+ rowClosed = ROW_END.test(cd);
3538
+ // The writer puts one cell on a line and escapes `<` outside code and
3539
+ // math, so a close tag outside those before the line's end is an
3540
+ // agent's `<td>a</td><td>b</td>`.
3541
+ addCells(cd);
2170
3542
  }
2171
3543
  }
2172
3544
 
2173
- if (!closed) {
2174
- const openLine = lines[start].slice(indent);
2175
- const { text, color } = splitBlockAttrs(openLine);
2176
- const node: PMNode = { type: "paragraph", content: parseInline(text) };
2177
- if (isColor(color)) node.attrs = { color };
2178
- return { nodes: [withIndentAttr(node)], end: start + 1 };
2179
- }
3545
+ const parsedRows = rows.map((row) => ({
3546
+ color: row.color,
3547
+ cells: row.cells.flatMap((source) => {
3548
+ const cell = source.match(/^<(t[dh])(\s[^>]*?)?>([\s\S]*)<\/t[dh]>$/);
3549
+ if (!cell) {
3550
+ reportLoss("dropped-source", source);
3551
+ return [];
3552
+ }
3553
+ const attrs = readAttrs(cell[1], cell[2] ?? "", CELL_ATTRS);
3554
+ if (attrs.align !== undefined && !isTableAlignment(attrs.align))
3555
+ reportLoss("dropped-attribute", `<${cell[1]} align="${attrs.align}">`);
3556
+ return [
3557
+ {
3558
+ header: cell[1] === "th",
3559
+ attrs: {
3560
+ color: isColor(attrs.color) ? attrs.color : null,
3561
+ textAlign: isTableAlignment(attrs.align) ? attrs.align : null,
3562
+ },
3563
+ inline: cell[3],
3564
+ },
3565
+ ];
3566
+ }),
3567
+ }));
3568
+
3569
+ // The writer spells header cells as attributes, never `<th>` or `<thead>`,
3570
+ // so an agent's `<th>` cells, or a `<thead>` around the first row, name the
3571
+ // header row or column when no attribute does.
3572
+ const allHeaders = (cells: Array<{ header: boolean }>) =>
3573
+ cells.length > 0 && cells.every((cell) => cell.header);
3574
+ const headerRow =
3575
+ openAttrs["header-row"] !== undefined
3576
+ ? openAttrs["header-row"] === "true"
3577
+ : parsedRows.length > 0 &&
3578
+ (rows[0].head || allHeaders(parsedRows[0].cells));
3579
+ // The editor's one header row is all a `<thead>` can become.
3580
+ if (rows.some((row, index) => row.head && !(index === 0 && headerRow)))
3581
+ reportLoss("dropped-source", "<thead>");
3582
+ const bodyRows = headerRow ? parsedRows.slice(1) : parsedRows;
3583
+ const headerColumn =
3584
+ openAttrs["header-column"] !== undefined
3585
+ ? openAttrs["header-column"] === "true"
3586
+ : bodyRows.length > 0 &&
3587
+ bodyRows.every((row) => allHeaders(row.cells.slice(0, 1)));
3588
+ const fitPageWidth = openAttrs["fit-page-width"] === "true";
2180
3589
 
2181
3590
  const node: PMNode = {
2182
3591
  type: "table",
@@ -2186,12 +3595,170 @@ function parseTable(
2186
3595
  fitPageWidth,
2187
3596
  colMeta: colMeta.length ? colMeta : null,
2188
3597
  },
2189
- content: rows,
3598
+ content: parsedRows.map((row, rowIndex) => ({
3599
+ type: "tableRow",
3600
+ attrs: { color: row.color },
3601
+ content: row.cells.map((cell, cellIndex) => {
3602
+ const header =
3603
+ (headerRow && rowIndex === 0) || (headerColumn && cellIndex === 0);
3604
+ if (cell.header && !header) reportLoss("dropped-attribute", "<th>");
3605
+ return {
3606
+ type: header ? "tableHeader" : "tableCell",
3607
+ attrs: cell.attrs,
3608
+ content: [{ type: "paragraph", content: parseInline(cell.inline) }],
3609
+ };
3610
+ }),
3611
+ })),
3612
+ };
3613
+ return { nodes: [withIndentAttr(node)], end: close + 1 };
3614
+ }
3615
+
3616
+ const TABLE_ATTRS = new Set(["header-row", "header-column", "fit-page-width"]);
3617
+ const ROW_ATTRS = new Set(["color"]);
3618
+ const CELL_ATTRS = new Set(["color", "align"]);
3619
+ const COL_ATTRS = new Set(["color", "width"]);
3620
+ const TABLE_WRAPPER_LINE = /^<\/?(?:colgroup|thead|tbody|tfoot)>$/;
3621
+ const ROW_EDGE = /^<\/?tr[\s>]/;
3622
+ const CELL_OPEN = /^<t[dh][\s>]/;
3623
+ // A cell's line may also close its row; the writer never closes one there.
3624
+ const CELL_LINE_END = /<\/t[dh]>(?:\s*<\/tr>)?$/;
3625
+ const ROW_BREAK =
3626
+ /^(?:<\/tr>\s*)?(?:<\/?(?:thead|tbody|tfoot)>\s*)*<tr(\s[^>]*?)?>$/;
3627
+ const ROW_GROUP_TAG = /<(\/?)(thead|tbody|tfoot)>/g;
3628
+ const ROW_GROUP_PREFIX = /^(?:<\/?(?:thead|tbody|tfoot)>\s*)+/;
3629
+ const ROW_END = /<\/tr>(?:\s*<\/?(?:thead|tbody|tfoot)>)*\s*$/;
3630
+ const ROW_GROUP_TAIL = /^<\/tr>\s*(?:<\/?(?:thead|tbody|tfoot)>\s*)*$/;
3631
+
3632
+ // Drops the spaces just inside a joined cell's tags, which HTML ignores. By
3633
+ // hand, since a pattern for the spaces before the close tag retries them from
3634
+ // each offset of a long run.
3635
+ function trimJoinedCell(cell: string): string {
3636
+ const open = /^<t[dh][^>]*>/.exec(cell);
3637
+ let text = cell;
3638
+ if (open) {
3639
+ let from = open[0].length;
3640
+ while (from < text.length && SPACE_CHAR.test(text[from])) from++;
3641
+ text = open[0] + text.slice(from);
3642
+ }
3643
+ const close = CELL_LINE_END.exec(text);
3644
+ if (!close) return text;
3645
+ let to = close.index;
3646
+ while (to > 0 && SPACE_CHAR.test(text[to - 1])) to--;
3647
+ return text.slice(0, to) + text.slice(close.index);
3648
+ }
3649
+
3650
+ // A cell's source, or where the next row opens.
3651
+ type TableLinePart = string | { rowAttrs: string | null; gap: string };
3652
+
3653
+ // Whether a cell's open tag sits between `from` and `to` outside code and
3654
+ // math, where the writer escapes every `<`.
3655
+ function opensCellBefore(
3656
+ s: string,
3657
+ from: number,
3658
+ to: number,
3659
+ codeRuns: CodeRuns,
3660
+ ): boolean {
3661
+ const first = s.indexOf("<t", from);
3662
+ if (first === -1 || first >= to) return false;
3663
+ for (let i = from; i < to; i++) {
3664
+ if (s[i] === "\\") {
3665
+ i++;
3666
+ continue;
3667
+ }
3668
+ const skipTo = atomicSpanEnd(s, i, codeRuns);
3669
+ if (skipTo !== -1) {
3670
+ i = skipTo - 1;
3671
+ continue;
3672
+ }
3673
+ if (s[i] === "<" && /^<t[dh][\s>]/.test(s.slice(i, i + 4))) return true;
3674
+ }
3675
+ return false;
3676
+ }
3677
+
3678
+ function findTableCellClose(
3679
+ source: string,
3680
+ closeTag: string,
3681
+ from: number,
3682
+ codeRuns: CodeRuns,
3683
+ ): number {
3684
+ for (let i = from; i <= source.length - closeTag.length; i++) {
3685
+ // Markdown escapes still shield code/math delimiters, but a literal
3686
+ // backslash cannot escape an HTML cell's closing tag.
3687
+ if (source[i] === "\\" && source[i + 1] !== "<") {
3688
+ i++;
3689
+ continue;
3690
+ }
3691
+ const skipTo = atomicSpanEnd(source, i, codeRuns);
3692
+ if (skipTo !== -1) {
3693
+ i = skipTo - 1;
3694
+ continue;
3695
+ }
3696
+ if (source.startsWith(closeTag, i)) return i;
3697
+ }
3698
+ return -1;
3699
+ }
3700
+
3701
+ function splitTableCells(source: string): TableLinePart[] {
3702
+ const cells: TableLinePart[] = [];
3703
+ // An agent may start the next row between two cells or on a line of its
3704
+ // own, with or without closing this one; HTML closes it there.
3705
+ const readGap = (gap: string) => {
3706
+ const rowBreak = gap.match(ROW_BREAK);
3707
+ if (rowBreak) cells.push({ rowAttrs: rowBreak[1] ?? "", gap });
3708
+ else if (gap && ROW_GROUP_TAIL.test(gap))
3709
+ cells.push({ rowAttrs: null, gap });
3710
+ else if (gap) reportLoss("dropped-source", gap);
3711
+ };
3712
+ // Found by hand, not by one regex: searching from every open tag for a `>`
3713
+ // or a close tag the row lacks rescans the rest of it from each one. A
3714
+ // close tag in code or math is the cell's text.
3715
+ const lastClose = {
3716
+ td: source.lastIndexOf("</td>"),
3717
+ th: source.lastIndexOf("</th>"),
2190
3718
  };
2191
- return { nodes: [withIndentAttr(node)], end: i };
3719
+ const codeRuns = indexCodeRuns(source);
3720
+ const open = /<(t[dh])[\s>]/g;
3721
+ let nextGt = -1;
3722
+ let last = 0;
3723
+ for (let m; (m = open.exec(source)); ) {
3724
+ const tag = m[1] as "td" | "th";
3725
+ let contentFrom = m.index + 4;
3726
+ if (source[m.index + 3] !== ">") {
3727
+ if (nextGt < m.index + 3) {
3728
+ nextGt = source.indexOf(">", m.index + 3);
3729
+ // No later open tag can end either.
3730
+ if (nextGt < 0) break;
3731
+ }
3732
+ contentFrom = nextGt + 1;
3733
+ }
3734
+ const closeTag = `</${tag}>`;
3735
+ const close =
3736
+ lastClose[tag] < contentFrom
3737
+ ? -1
3738
+ : findTableCellClose(source, closeTag, contentFrom, codeRuns);
3739
+ if (close < 0) {
3740
+ // Later open tags of this kind count as unclosed too, so no search
3741
+ // runs to the end of the row twice.
3742
+ lastClose[tag] = -1;
3743
+ open.lastIndex = m.index + 1;
3744
+ continue;
3745
+ }
3746
+ const end = close + closeTag.length;
3747
+ readGap(source.slice(last, m.index).trim());
3748
+ // `<td>a <td>b</td>` reads as main reads it, as one cell holding the
3749
+ // second tag as text; HTML would read two cells.
3750
+ if (opensCellBefore(source, contentFrom, close, codeRuns))
3751
+ reportLoss("dropped-source", source.slice(m.index, end));
3752
+ cells.push(source.slice(m.index, end));
3753
+ last = open.lastIndex = end;
3754
+ }
3755
+ readGap(source.slice(last).trim());
3756
+ return cells;
2192
3757
  }
2193
3758
 
2194
3759
  const MEDIA_TAGS = new Set(["video", "audio"]);
3760
+ const MEDIA_ATTRS = new Set(["src", "color"]);
3761
+ const IMAGE_ATTRS = new Set(["color"]);
2195
3762
  const BLOCK_ATOM_TAGS = new Set([
2196
3763
  "page",
2197
3764
  "database",
@@ -2209,7 +3776,7 @@ function parseLeafTag(
2209
3776
  label: string,
2210
3777
  ): PMNode | null {
2211
3778
  if (MEDIA_TAGS.has(tagName)) {
2212
- const attrs = parseAttrs(rawAttrs);
3779
+ const attrs = readAttrs(tagName, rawAttrs, MEDIA_ATTRS);
2213
3780
  return {
2214
3781
  type: tagName,
2215
3782
  attrs: {
@@ -2220,7 +3787,7 @@ function parseLeafTag(
2220
3787
  };
2221
3788
  }
2222
3789
  if (BLOCK_ATOM_TAGS.has(tagName)) {
2223
- const attrs = parseAttrs(rawAttrs);
3790
+ const attrs = readAttrs(tagName, rawAttrs);
2224
3791
  return {
2225
3792
  type: "notionBlockAtom",
2226
3793
  attrs: {
@@ -2233,14 +3800,38 @@ function parseLeafTag(
2233
3800
  return null;
2234
3801
  }
2235
3802
 
3803
+ // Suggestion presentation canonicalizes the same few Pages once per suggestion
3804
+ // on every render, so recent results are kept.
3805
+ const recentCanonicalNfm = new Map<string, string>();
3806
+
2236
3807
  export function canonicalizeNfm(nfm: string | null | undefined): string {
2237
- const previous = suppressTerminalFillerTrim;
2238
- suppressTerminalFillerTrim = true;
2239
- try {
2240
- return docToNfm(nfmToDoc(nfm ?? ""));
2241
- } finally {
2242
- suppressTerminalFillerTrim = previous;
2243
- }
3808
+ const source = nfm ?? "";
3809
+ const known = recentCanonicalNfm.get(source);
3810
+ if (known !== undefined) return known;
3811
+ const canonical = docToNfm(nfmToDoc(source));
3812
+ if (recentCanonicalNfm.size >= 8)
3813
+ recentCanonicalNfm.delete(recentCanonicalNfm.keys().next().value!);
3814
+ recentCanonicalNfm.set(source, canonical);
3815
+ return canonical;
3816
+ }
3817
+
3818
+ /**
3819
+ * Canonical Markdown that keeps a trailing empty block. A Notion page can end
3820
+ * in an empty paragraph its author made, and its pulls, pushes and sync hashes
3821
+ * must not drop it.
3822
+ */
3823
+ export function canonicalizeNotionNfm(nfm: string | null | undefined): string {
3824
+ return docToNfm(nfmToDoc(nfm ?? ""), { keepTrailingEmptyBlock: true });
3825
+ }
3826
+
3827
+ /**
3828
+ * The document canonical Markdown writes. A trailing empty paragraph after
3829
+ * another kind of block is where the editor lets you type, and its save drops
3830
+ * it.
3831
+ */
3832
+ export function canonicalNfmDoc(doc: PMDoc): PMDoc {
3833
+ const content = trimEditorTerminalFiller(doc.content);
3834
+ return content === doc.content ? doc : { ...doc, content };
2244
3835
  }
2245
3836
 
2246
3837
  function countLocalMdxNodes(
@@ -2283,22 +3874,24 @@ function countGfmPipeTables(
2283
3874
 
2284
3875
  function fencedCodeLineMask(lines: string[]): boolean[] {
2285
3876
  const mask = lines.map(() => false);
2286
- let fence: { indent: number; length: number } | undefined;
3877
+ let fence: { indent: number; marker: string } | undefined;
2287
3878
  for (let index = 0; index < lines.length; index++) {
2288
3879
  const indent = leadingTabs(lines[index]);
2289
3880
  const dedented = lines[index].slice(indent);
2290
3881
  if (fence) {
2291
3882
  mask[index] = true;
2292
- const close = dedented.match(/^(`{3,})\s*$/);
2293
- if (indent >= fence.indent && close && close[1].length >= fence.length) {
3883
+ if (
3884
+ indent >= fence.indent &&
3885
+ closesFence(dedented.match(FENCE_CLOSE), fence.marker)
3886
+ ) {
2294
3887
  fence = undefined;
2295
3888
  }
2296
3889
  continue;
2297
3890
  }
2298
- const open = dedented.match(/^(`{3,})(.*)$/);
3891
+ const open = matchFenceOpen(dedented);
2299
3892
  if (open) {
2300
3893
  mask[index] = true;
2301
- fence = { indent, length: open[1].length };
3894
+ fence = { indent, marker: open[1] };
2302
3895
  }
2303
3896
  }
2304
3897
  return mask;