@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +1 -0
- package/corpus/templates/analytics/changelog/2026-10-21-session-replay-quotas-count-the-compressed-size-analytics-st.md +6 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +40 -9
- package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/content/.agents/skills/content-product-development/references/verification.md +36 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +10 -0
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +26 -1
- package/corpus/templates/content/.agents/skills/notion-integration/SKILL.md +4 -2
- package/corpus/templates/content/CHANGELOG.md +51 -0
- package/corpus/templates/content/acceptance/_writers.ts +604 -0
- package/corpus/templates/content/acceptance/generate-action-registry.ts +10 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/create-document.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/duplicate-page.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-find-and-replace-without-a-revision.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-find-and-replace.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-initializecontent.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/import-content.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-import-content-source-existing-page.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-import-content-source-new-page.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-sync-local-folder-source-changed-file.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-sync-local-folder-source-new-file.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/restore-document-version.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/set-document-property-primary-blocks-field.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/set-image-alt-text.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/undo-content-import.json +75 -0
- package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/update-document-browser-full-rewrite.json +75 -0
- package/corpus/templates/content/acceptance/known-loss.ts +69 -0
- package/corpus/templates/content/acceptance/round-trip-corpus.ts +1397 -0
- package/corpus/templates/content/actions/_blocks-field-identity.ts +35 -1
- package/corpus/templates/content/actions/_content-database-lifecycle.ts +11 -11
- package/corpus/templates/content/actions/_content-database-personal-view.ts +2 -0
- package/corpus/templates/content/actions/_content-favorites.ts +37 -0
- package/corpus/templates/content/actions/_database-membership-lock.ts +39 -8
- package/corpus/templates/content/actions/_database-navigation.ts +19 -21
- package/corpus/templates/content/actions/_database-row-batch.ts +41 -18
- package/corpus/templates/content/actions/_database-row-mutation.ts +17 -28
- package/corpus/templates/content/actions/_database-source-utils.ts +18 -1
- package/corpus/templates/content/actions/_database-utils.ts +3 -0
- package/corpus/templates/content/actions/_database-view-setup.ts +321 -11
- package/corpus/templates/content/actions/_document-edit-mutation.ts +33 -32
- package/corpus/templates/content/actions/_document-flush.ts +593 -171
- package/corpus/templates/content/actions/_document-save-locks.ts +54 -0
- package/corpus/templates/content/actions/_document-save-row.ts +133 -0
- package/corpus/templates/content/actions/_github-source.ts +18 -8
- package/corpus/templates/content/actions/_move-page-to-space.ts +189 -25
- package/corpus/templates/content/actions/_nfm-syntax.ts +2 -0
- package/corpus/templates/content/actions/_property-utils.ts +3 -0
- package/corpus/templates/content/actions/_row-lock-order.ts +115 -0
- package/corpus/templates/content/actions/create-content-database.ts +11 -0
- package/corpus/templates/content/actions/create-document.ts +13 -1
- package/corpus/templates/content/actions/delete-content-database.ts +17 -14
- package/corpus/templates/content/actions/delete-document-property.ts +5 -0
- package/corpus/templates/content/actions/delete-document.ts +270 -97
- package/corpus/templates/content/actions/edit-document.ts +18 -5
- package/corpus/templates/content/actions/export-document.ts +3 -0
- package/corpus/templates/content/actions/get-blocks-field-word-count.ts +22 -5
- package/corpus/templates/content/actions/import-content-source.ts +170 -54
- package/corpus/templates/content/actions/move-document.ts +191 -73
- package/corpus/templates/content/actions/pull-document.ts +8 -2
- package/corpus/templates/content/actions/remove-database-items.ts +23 -4
- package/corpus/templates/content/actions/restore-content-database.ts +76 -55
- package/corpus/templates/content/actions/restore-document-version.ts +19 -35
- package/corpus/templates/content/actions/restore-document.ts +65 -53
- package/corpus/templates/content/actions/rollback-created-slash-document.ts +11 -16
- package/corpus/templates/content/actions/set-document-property.ts +15 -1
- package/corpus/templates/content/actions/share-local-file-document.ts +18 -14
- package/corpus/templates/content/actions/suggest-document-edit.ts +2 -3
- package/corpus/templates/content/actions/sync-local-folder-source.ts +35 -1
- package/corpus/templates/content/actions/undo-content-import.ts +16 -12
- package/corpus/templates/content/actions/update-content-database-view.ts +20 -2
- package/corpus/templates/content/actions/update-document.ts +19 -14
- package/corpus/templates/content/actions/view-screen.ts +12 -0
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -6
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +49 -3
- package/corpus/templates/content/app/components/editor/ContentImportDialog.tsx +2 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +53 -11
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +608 -353
- package/corpus/templates/content/app/components/editor/StaticDocumentBody.tsx +765 -0
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +233 -80
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +32 -3
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +88 -23
- package/corpus/templates/content/app/components/editor/comment-anchors.ts +19 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +296 -87
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
- package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
- package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
- package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
- package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
- package/corpus/templates/content/app/components/editor/document-flush-participant.ts +253 -0
- package/corpus/templates/content/app/components/editor/editor-transaction-meta.ts +28 -0
- package/corpus/templates/content/app/components/editor/extensions/AudioBlock.tsx +2 -1
- package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +27 -6
- package/corpus/templates/content/app/components/editor/extensions/LocalMdxComponentNode.tsx +0 -1
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +4 -4
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +24 -2
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +25 -6
- package/corpus/templates/content/app/components/editor/pending-document-persistence.ts +59 -1
- package/corpus/templates/content/app/components/editor/stored-body-images.ts +27 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +17 -0
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -4
- package/corpus/templates/content/app/components/trash/TrashBrowser.tsx +4 -1
- package/corpus/templates/content/app/global.css +34 -0
- package/corpus/templates/content/app/hooks/use-content-trash.ts +8 -0
- package/corpus/templates/content/app/i18n-data.ts +32 -0
- package/corpus/templates/content/app/import-messages.ts +25 -0
- package/corpus/templates/content/app/trash-messages.ts +16 -0
- 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
- package/corpus/templates/content/changelog/2026-10-16-a-page-restored-from-trash-returns-to-its-place-in-its-colle.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-agent-written-markdown-keeps-nested-lists-indented-with-spac.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-opening-a-page-you-can-edit-no-longer-rebuilds-its-editor-on.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-pages-moved-or-imported-at-the-same-time-can-no-longer-end-u.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-re-importing-a-markdown-export-of-a-page-no-longer-reports-t.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-saving-a-page-or-restoring-one-of-its-versions-no-longer-fai.md +6 -0
- package/corpus/templates/content/changelog/2026-10-17-agents-reading-a-page-you-have-open-no-longer-wait-or-fail-w.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-fresh-document-reads-require-confirmation-from-every-open-ed.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-html-table-cells-preserve-a-trailing-backslash-when-opening-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-preserve-empty-paragraph-colors-and-refuse-unsupported-inlin.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-the-advanced-filter-and-each-filter-group-inside-it-have-the.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-blocks-fields-keep-unconfirmed-saves-visible-and-agent-reads.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-preserve-compact-table-row-groups-and-report-invalid-block-colors.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-suggested-edits-show-an-agent-s-whole-reason-open-a-group-to.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-suggestions-keep-refusing-unmatched-emphasis-in-stored-markd.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-accept-remaining-and-reject-remaining-show-the-result-for-ev.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-accepting-a-group-of-suggested-edits-saves-faster-and-adds-o.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-agents-read-saved-page-text-after-failed-icon-change.md +6 -0
- 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
- package/corpus/templates/content/changelog/2026-10-22-text-an-agent-writes-inside-a-callout-column-or-synced-block.md +6 -0
- package/corpus/templates/content/e2e/auth-files.ts +28 -0
- package/corpus/templates/content/e2e/convergence-summary.ts +84 -29
- package/corpus/templates/content/e2e/gate-arm.ts +15 -0
- package/corpus/templates/content/e2e/global-setup.ts +43 -75
- package/corpus/templates/content/e2e/global-teardown.ts +34 -0
- package/corpus/templates/content/e2e/helpers.ts +598 -92
- package/corpus/templates/content/e2e/live-body-gate.ts +188 -0
- package/corpus/templates/content/e2e/playwright.config.ts +12 -3
- package/corpus/templates/content/e2e/server-identity.ts +47 -0
- package/corpus/templates/content/e2e/stored-form/collab-blocks.ts +123 -0
- package/corpus/templates/content/package.json +1 -1
- package/corpus/templates/content/scripts/_stored-form-census-kinds.ts +30 -2
- package/corpus/templates/content/scripts/historical-icon-apply.ts +9 -1
- package/corpus/templates/content/scripts/trace-startup.mjs +460 -133
- package/corpus/templates/content/server/db/exec.ts +58 -0
- package/corpus/templates/content/server/db/schema.ts +1 -0
- package/corpus/templates/content/server/lib/content-trash-purge.ts +208 -144
- package/corpus/templates/content/server/lib/document-metadata-updated-at.ts +43 -1
- package/corpus/templates/content/server/lib/document-revision.ts +23 -0
- package/corpus/templates/content/server/lib/live-body-writer.ts +199 -0
- package/corpus/templates/content/server/lib/notion-sync.ts +63 -29
- package/corpus/templates/content/server/lib/notion.ts +5 -5
- package/corpus/templates/content/server/lib/suggested-edits.ts +357 -230
- package/corpus/templates/content/server/plugins/db.ts +5 -0
- package/corpus/templates/content/server/routes/api/documents/[id]/move.patch.ts +50 -168
- package/corpus/templates/content/shared/api.ts +20 -0
- package/corpus/templates/content/shared/blocks-fields-manifest.ts +106 -0
- package/corpus/templates/content/shared/database-item-layout.ts +591 -0
- package/corpus/templates/content/shared/database-query.ts +192 -30
- package/corpus/templates/content/shared/document-export.ts +12 -14
- package/corpus/templates/content/shared/document-flush-protocol.ts +88 -0
- package/corpus/templates/content/shared/editor-save-state.ts +97 -0
- package/corpus/templates/content/shared/feature-flags.ts +10 -0
- package/corpus/templates/content/shared/import/markdown.ts +91 -7
- package/corpus/templates/content/shared/import/types.ts +2 -0
- package/corpus/templates/content/shared/inline-math.ts +6 -4
- package/corpus/templates/content/shared/nfm-fidelity.ts +247 -0
- package/corpus/templates/content/shared/nfm.ts +1933 -340
- package/corpus/templates/content/shared/suggestion-diff.ts +13 -0
- package/corpus/templates/content/shared/suggestion-formatting.ts +143 -20
- package/corpus/templates/content/shared/suggestion-text.ts +39 -0
- package/corpus/templates/content/vitest.config.ts +10 -0
- package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/design/.agents/skills/native-effects/SKILL.md +9 -2
- package/corpus/templates/design/.generated/bridge/shader-fill-preview.generated.ts +3 -249
- package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +5 -88
- package/corpus/templates/design/AGENTS.md +1 -3
- package/corpus/templates/design/CHANGELOG.md +32 -0
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -6
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -7
- package/corpus/templates/design/actions/_native-local-export-server.ts +44 -31
- package/corpus/templates/design/actions/_native-render-contexts.ts +76 -21
- package/corpus/templates/design/actions/claim-native-local-export.ts +5 -0
- package/corpus/templates/design/actions/finish-native-local-export.ts +13 -2
- package/corpus/templates/design/actions/get-native-shader-approvals.ts +35 -0
- package/corpus/templates/design/actions/get-shader.ts +134 -219
- package/corpus/templates/design/actions/list-design-extensions.ts +3 -58
- package/corpus/templates/design/actions/request-native-local-export.ts +32 -6
- package/corpus/templates/design/actions/run-design-extension-action.ts +0 -29
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +131 -108
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +0 -218
- package/corpus/templates/design/app/components/design/DesignThumbnail.tsx +59 -3
- package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -47
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +242 -62
- package/corpus/templates/design/app/components/design/NativePreviewErrorBoundary.tsx +53 -0
- package/corpus/templates/design/app/components/design/bridge/codegen.ts +18 -3
- package/corpus/templates/design/app/components/design/bridge/native-composition-clock.ts +5 -4
- package/corpus/templates/design/app/components/design/bridge/native-dom.ts +37 -0
- package/corpus/templates/design/app/components/design/bridge/native-intrinsic-image.ts +2 -1
- package/corpus/templates/design/app/components/design/bridge/native-mesh-grid.ts +90 -0
- package/corpus/templates/design/app/components/design/bridge/native-opaque-public-asset.ts +363 -0
- package/corpus/templates/design/app/components/design/bridge/native-opaque-worker-lease.ts +179 -0
- package/corpus/templates/design/app/components/design/bridge/native-preview-geometry.ts +132 -7
- package/corpus/templates/design/app/components/design/bridge/native-preview-policy.ts +4 -0
- package/corpus/templates/design/app/components/design/bridge/native-preview-viewport-transport.ts +378 -0
- package/corpus/templates/design/app/components/design/bridge/native-scene-body-transform.ts +62 -0
- package/corpus/templates/design/app/components/design/bridge/native-scene-presentation-element.ts +10 -0
- package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +646 -167
- package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +246 -192
- package/corpus/templates/design/app/components/design/bridge/native-stateful-resource.ts +2 -2
- package/corpus/templates/design/app/components/design/bridge/native-stateless-compute.ts +5 -2
- package/corpus/templates/design/app/components/design/bridge/native-svg-source.ts +11 -1
- package/corpus/templates/design/app/components/design/bridge/native-worker-broker.ts +6 -0
- package/corpus/templates/design/app/components/design/bridge/native-worker-entry.ts +761 -303
- package/corpus/templates/design/app/components/design/bridge/native-worker-fill.ts +16 -6
- package/corpus/templates/design/app/components/design/bridge/native-worker-gpu.ts +854 -91
- package/corpus/templates/design/app/components/design/bridge/native-worker-host.ts +15 -1
- package/corpus/templates/design/app/components/design/bridge/native-worker-input-assets.ts +200 -0
- package/corpus/templates/design/app/components/design/bridge/native-worker-protocol.ts +40 -1
- package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +425 -126
- package/corpus/templates/design/app/components/design/bridge/native-worker-source-snapshot.ts +21 -0
- package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +5 -371
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +4 -146
- package/corpus/templates/design/app/components/design/design-canvas/interact-document.ts +5 -9
- package/corpus/templates/design/app/components/design/design-canvas/native-preview-surface.ts +150 -0
- package/corpus/templates/design/app/components/design/design-canvas/native-public-asset-preview.ts +409 -0
- package/corpus/templates/design/app/components/design/design-canvas/native-public-worker-preview.ts +1086 -0
- package/corpus/templates/design/app/components/design/design-canvas/private-replay-screenshot-preview.ts +2 -2
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +15 -26
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +74 -67
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +7 -19
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +39 -30
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -0
- package/corpus/templates/design/app/components/design/inspector/NativeShaderColorControls.tsx +3 -7
- package/corpus/templates/design/app/components/design/inspector/NativeShaderPaintControls.tsx +12 -5
- package/corpus/templates/design/app/components/design/inspector/NativeShaderPanel.tsx +705 -190
- package/corpus/templates/design/app/components/design/inspector/fill-field-reading.ts +1 -2
- package/corpus/templates/design/app/components/design/inspector/index.ts +0 -6
- package/corpus/templates/design/app/components/design/inspector/native-catalog-display.ts +26 -5
- package/corpus/templates/design/app/components/design/inspector/native-shader-catalog-fixture.ts +42 -0
- package/corpus/templates/design/app/components/design/inspector/native-shader-catalog-preview.ts +51 -33
- package/corpus/templates/design/app/components/design/inspector/native-shader-paint.ts +1 -0
- package/corpus/templates/design/app/components/design/inspector/native-shader-property-draft.ts +89 -23
- package/corpus/templates/design/app/components/design/inspector/saved-glsl-shader.tsx +243 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +54 -5
- package/corpus/templates/design/app/components/design/shader-source-coordination.ts +66 -0
- package/corpus/templates/design/app/components/design/use-native-shader-approvals.ts +36 -8
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +60 -3
- package/corpus/templates/design/app/i18n/en-US.ts +78 -2
- package/corpus/templates/design/app/pages/Present.tsx +36 -1
- package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +2 -3
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/paste-svg-layer.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/prepare-content-history-replay.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +84 -41
- package/corpus/templates/design/app/pages/design-editor/commands/shader-source-edit.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/design-editor-view.tsx +0 -2
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-content-and-components.ts +4 -33
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-export-and-handoff.ts +2 -2
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-history.ts +0 -11
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-layer-models.ts +2 -55
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +0 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-selection-and-styles.ts +0 -24
- package/corpus/templates/design/app/pages/design-editor/text-edit-utils.ts +0 -16
- package/corpus/templates/design/app/pages/design-editor/use-native-local-export-requests.ts +70 -11
- package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +0 -2
- package/corpus/templates/design/changelog/2026-10-20-folded-mesh-adds-an-animated-sculpted-fill-with-editable-col.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-read-only-design-views-render-native-shader-previews-in-nest.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-selected-screens-with-shader-effects-can-be-exported-togethe.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-shader-previews-reuse-validated-scene-data-while-panning-inc.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-shader-previews-stay-aligned-while-you-zoom.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-signed-out-sessions-no-longer-watch-for-private-png-export-r.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-effect-thumbnails-show-richer-previews.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-native-effects-render-across-previews.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-native-shader-previews-use-less-gpu-memory-when-many-mesh-ef.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-opening-design-no-longer-shows-png-export-errors-when-no-exp.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-pulsing-border-controls-grouped.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-pulsing-border-follows-element-outlines.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-remove-obsolete-shader-fills.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-shader-previews-recover-from-temporary-gpu-and-image-failure.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-shader-fills-and-effects-render-correctly-on-canvas-elements.md +6 -0
- package/corpus/templates/design/package.json +0 -1
- package/corpus/templates/design/playwright.config.ts +1 -1
- package/corpus/templates/design/scripts/runtime-budget-zoom.ts +9 -21
- package/corpus/templates/design/scripts/runtime-budget.ts +10 -24
- package/corpus/templates/design/server/plugins/agent-chat.ts +0 -2
- package/corpus/templates/design/shared/effect-graph.ts +46 -1
- package/corpus/templates/design/shared/generation-output-receipt.ts +0 -1
- package/corpus/templates/design/shared/native-effect-animation-capability.ts +6 -0
- package/corpus/templates/design/shared/native-effect-folded-mesh-v1.ts +268 -0
- package/corpus/templates/design/shared/native-effect-folded-mesh-v2.ts +310 -0
- package/corpus/templates/design/shared/native-effect-folded-mesh.ts +489 -0
- package/corpus/templates/design/shared/native-effect-gallery-additions.ts +2 -2
- package/corpus/templates/design/shared/native-effect-paint.ts +8 -0
- package/corpus/templates/design/shared/native-effect-parameter-constraints.ts +8 -1
- package/corpus/templates/design/shared/native-effect-presets.ts +18 -1
- package/corpus/templates/design/shared/native-effect-property-presentation.ts +81 -0
- package/corpus/templates/design/shared/native-effect-pulsing-border-v3.ts +281 -0
- package/corpus/templates/design/shared/native-effect-pulsing-border-v4.ts +284 -0
- package/corpus/templates/design/shared/native-effect-pulsing-border.ts +300 -0
- package/corpus/templates/design/shared/native-effect-trust.ts +6 -0
- package/corpus/templates/design/shared/native-effect-wgsl.ts +12 -0
- package/corpus/templates/design/shared/native-effects.ts +528 -15
- package/corpus/templates/design/shared/native-input-resources.ts +36 -0
- package/corpus/templates/design/shared/native-local-export.ts +14 -0
- package/corpus/templates/design/shared/native-parameter-animation.ts +1 -0
- package/corpus/templates/design/shared/native-pointer-input.ts +9 -11
- package/corpus/templates/design/shared/shader-fills.ts +0 -513
- package/corpus/templates/design/vitest.setup.ts +7 -1
- package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +20 -18
- package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +36 -80
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +54 -16
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +50 -73
- package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +20 -18
- package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +1 -1
- package/corpus/templates/slides-2/.agents/skills/actions/SKILL.md +199 -0
- package/corpus/templates/slides-2/.agents/skills/actions/references/action-fields.md +145 -0
- package/corpus/templates/slides-2/.agents/skills/actions/references/examples.md +96 -0
- package/corpus/templates/slides-2/.agents/skills/actions/references/provider-apis.md +95 -0
- package/corpus/templates/slides-2/.agents/skills/adding-a-feature/SKILL.md +235 -0
- package/corpus/templates/slides-2/.agents/skills/agent-native-docs/SKILL.md +161 -0
- package/corpus/templates/slides-2/.agents/skills/agent-native-toolkit/SKILL.md +314 -0
- package/corpus/templates/slides-2/.agents/skills/analytics-data-for-decks/SKILL.md +44 -0
- package/corpus/templates/slides-2/.agents/skills/apply-slide-comments/SKILL.md +63 -0
- package/corpus/templates/slides-2/.agents/skills/client-side-routing/SKILL.md +87 -0
- package/corpus/templates/slides-2/.agents/skills/context-awareness/SKILL.md +282 -0
- package/corpus/templates/slides-2/.agents/skills/create-deck/SKILL.md +263 -0
- package/corpus/templates/slides-2/.agents/skills/create-deck/references/reference-inputs.md +72 -0
- package/corpus/templates/slides-2/.agents/skills/create-deck/references/slide-templates.md +93 -0
- package/corpus/templates/slides-2/.agents/skills/create-deck/references/source-material.md +16 -0
- package/corpus/templates/slides-2/.agents/skills/creative-context/SKILL.md +134 -0
- package/corpus/templates/slides-2/.agents/skills/customizing-agent-native/SKILL.md +219 -0
- package/corpus/templates/slides-2/.agents/skills/deck-management/SKILL.md +182 -0
- package/corpus/templates/slides-2/.agents/skills/delegate-to-agent/SKILL.md +62 -0
- package/corpus/templates/slides-2/.agents/skills/design-systems/SKILL.md +233 -0
- package/corpus/templates/slides-2/.agents/skills/external-agents/SKILL.md +549 -0
- package/corpus/templates/slides-2/.agents/skills/external-agents/references/mcp-apps-embedding.md +169 -0
- package/corpus/templates/slides-2/.agents/skills/frontend-design/SKILL.md +102 -0
- package/corpus/templates/slides-2/.agents/skills/frontend-design/references/visual-direction.md +102 -0
- package/corpus/templates/slides-2/.agents/skills/image-generation-via-a2a/SKILL.md +134 -0
- package/corpus/templates/slides-2/.agents/skills/performance/SKILL.md +79 -0
- package/corpus/templates/slides-2/.agents/skills/portability/SKILL.md +108 -0
- package/corpus/templates/slides-2/.agents/skills/real-time-sync/SKILL.md +146 -0
- package/corpus/templates/slides-2/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/slides-2/.agents/skills/secrets/SKILL.md +82 -0
- package/corpus/templates/slides-2/.agents/skills/security/SKILL.md +80 -0
- package/corpus/templates/slides-2/.agents/skills/self-modifying-code/SKILL.md +123 -0
- package/corpus/templates/slides-2/.agents/skills/shadcn-ui/SKILL.md +71 -0
- package/corpus/templates/slides-2/.agents/skills/sharing/SKILL.md +286 -0
- package/corpus/templates/slides-2/.agents/skills/slide-design/SKILL.md +144 -0
- package/corpus/templates/slides-2/.agents/skills/slide-editing/SKILL.md +284 -0
- package/corpus/templates/slides-2/.agents/skills/slide-editing/references/animations.md +33 -0
- package/corpus/templates/slides-2/.agents/skills/slide-editing/references/freeform-objects.md +22 -0
- package/corpus/templates/slides-2/.agents/skills/slide-editing/references/restyling.md +69 -0
- package/corpus/templates/slides-2/.agents/skills/slide-editing/references/transitions.md +26 -0
- package/corpus/templates/slides-2/.agents/skills/slide-editing/references/video.md +45 -0
- package/corpus/templates/slides-2/.agents/skills/slide-images/SKILL.md +136 -0
- package/corpus/templates/slides-2/.agents/skills/storing-data/SKILL.md +234 -0
- package/corpus/templates/slides-2/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/slides-2/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/slides-2/.agents/skills/workspace-conventions/SKILL.md +103 -0
- package/corpus/templates/slides-2/.builder/rules/deploy-app.mdc +22 -0
- package/corpus/templates/slides-2/.builder/rules/organize-ui.mdc +9 -0
- package/corpus/templates/slides-2/.dockerignore +61 -0
- package/corpus/templates/slides-2/.env.example +23 -0
- package/corpus/templates/slides-2/.ignore +3 -0
- package/corpus/templates/slides-2/.mcp.json +8 -0
- package/corpus/templates/slides-2/.oxfmtrc.json +8 -0
- package/corpus/templates/slides-2/AGENTS.md +66 -0
- package/corpus/templates/slides-2/CHANGELOG.md +76 -0
- package/corpus/templates/slides-2/DEVELOPING.md +157 -0
- package/corpus/templates/slides-2/README.md +58 -0
- package/corpus/templates/slides-2/THIRD_PARTY_NOTICES.md +33 -0
- package/corpus/templates/slides-2/_gitignore +38 -0
- package/corpus/templates/slides-2/actions/_agent-presence.ts +29 -0
- package/corpus/templates/slides-2/actions/_app-url.ts +37 -0
- package/corpus/templates/slides-2/actions/_deck-app-scope.ts +30 -0
- package/corpus/templates/slides-2/actions/_deck-tracking.ts +260 -0
- package/corpus/templates/slides-2/actions/_deck-write.ts +206 -0
- package/corpus/templates/slides-2/actions/_imported-slide-source.ts +162 -0
- package/corpus/templates/slides-2/actions/_inline-image-storage.ts +1013 -0
- package/corpus/templates/slides-2/actions/_linked-design-system.ts +83 -0
- package/corpus/templates/slides-2/actions/_presenter-notes.ts +83 -0
- package/corpus/templates/slides-2/actions/_render-artifacts.ts +111 -0
- package/corpus/templates/slides-2/actions/_slide-source.ts +344 -0
- package/corpus/templates/slides-2/actions/_tab-state.ts +51 -0
- package/corpus/templates/slides-2/actions/_test-brandfetch.ts +31 -0
- package/corpus/templates/slides-2/actions/_uploaded-files.ts +114 -0
- package/corpus/templates/slides-2/actions/add-deck.ts +123 -0
- package/corpus/templates/slides-2/actions/add-slide-comment.ts +267 -0
- package/corpus/templates/slides-2/actions/add-slide.ts +888 -0
- package/corpus/templates/slides-2/actions/analyze-brand-assets.ts +66 -0
- package/corpus/templates/slides-2/actions/apply-design-system.ts +111 -0
- package/corpus/templates/slides-2/actions/approve-deck-access-request.ts +287 -0
- package/corpus/templates/slides-2/actions/audit-contrast.ts +143 -0
- package/corpus/templates/slides-2/actions/check-image-gen.ts +17 -0
- package/corpus/templates/slides-2/actions/clone-context-slide.ts +340 -0
- package/corpus/templates/slides-2/actions/clone-creative-context-deck.ts +143 -0
- package/corpus/templates/slides-2/actions/control-deck-history.ts +186 -0
- package/corpus/templates/slides-2/actions/control-presenter.ts +117 -0
- package/corpus/templates/slides-2/actions/control-slide-editor.ts +267 -0
- package/corpus/templates/slides-2/actions/create-deck-from-template.ts +274 -0
- package/corpus/templates/slides-2/actions/create-deck.ts +1243 -0
- package/corpus/templates/slides-2/actions/create-design-system.ts +131 -0
- package/corpus/templates/slides-2/actions/create-share-link.ts +127 -0
- package/corpus/templates/slides-2/actions/db-health.ts +22 -0
- package/corpus/templates/slides-2/actions/delete-asset.ts +154 -0
- package/corpus/templates/slides-2/actions/delete-deck.ts +427 -0
- package/corpus/templates/slides-2/actions/delete-design-system.ts +167 -0
- package/corpus/templates/slides-2/actions/delete-slide-comment.ts +143 -0
- package/corpus/templates/slides-2/actions/delete-staged-dataset.ts +30 -0
- package/corpus/templates/slides-2/actions/duplicate-deck.ts +299 -0
- package/corpus/templates/slides-2/actions/export-google-slides.ts +41 -0
- package/corpus/templates/slides-2/actions/export-html.ts +770 -0
- package/corpus/templates/slides-2/actions/export-pptx.ts +2203 -0
- package/corpus/templates/slides-2/actions/extract-pdf.ts +49 -0
- package/corpus/templates/slides-2/actions/generate-image-api.ts +671 -0
- package/corpus/templates/slides-2/actions/generate-image.ts +492 -0
- package/corpus/templates/slides-2/actions/get-agent-access-deck.ts +52 -0
- package/corpus/templates/slides-2/actions/get-deck-access-status.ts +130 -0
- package/corpus/templates/slides-2/actions/get-deck-design-tokens.ts +62 -0
- package/corpus/templates/slides-2/actions/get-deck-reference-context.ts +232 -0
- package/corpus/templates/slides-2/actions/get-deck-template.ts +38 -0
- package/corpus/templates/slides-2/actions/get-deck-version.ts +85 -0
- package/corpus/templates/slides-2/actions/get-deck.ts +562 -0
- package/corpus/templates/slides-2/actions/get-design-system-decode-job-status.ts +57 -0
- package/corpus/templates/slides-2/actions/get-design-system-tier-limit.ts +12 -0
- package/corpus/templates/slides-2/actions/get-design-system.ts +453 -0
- package/corpus/templates/slides-2/actions/get-google-docs-status.ts +106 -0
- package/corpus/templates/slides-2/actions/get-google-picker-token.ts +84 -0
- package/corpus/templates/slides-2/actions/get-home-suggestions.ts +52 -0
- package/corpus/templates/slides-2/actions/get-layout-overflows.ts +192 -0
- package/corpus/templates/slides-2/actions/get-slide-render.ts +125 -0
- package/corpus/templates/slides-2/actions/get-slide-source.ts +115 -0
- package/corpus/templates/slides-2/actions/get-slide-theme-preview.ts +64 -0
- package/corpus/templates/slides-2/actions/get-slide-theme.ts +44 -0
- package/corpus/templates/slides-2/actions/get-slides-2-upload-storage-status.ts +17 -0
- package/corpus/templates/slides-2/actions/get-slides-notification-prefs.ts +37 -0
- package/corpus/templates/slides-2/actions/get-workspace-defaults.ts +51 -0
- package/corpus/templates/slides-2/actions/image-gen-status.ts +35 -0
- package/corpus/templates/slides-2/actions/import-code.ts +26 -0
- package/corpus/templates/slides-2/actions/import-design-project.ts +49 -0
- package/corpus/templates/slides-2/actions/import-document.ts +125 -0
- package/corpus/templates/slides-2/actions/import-docx.ts +57 -0
- package/corpus/templates/slides-2/actions/import-file.ts +1273 -0
- package/corpus/templates/slides-2/actions/import-from-url.ts +25 -0
- package/corpus/templates/slides-2/actions/import-google-doc.ts +208 -0
- package/corpus/templates/slides-2/actions/import-google-font.ts +110 -0
- package/corpus/templates/slides-2/actions/import-google-slides-reference.ts +667 -0
- package/corpus/templates/slides-2/actions/import-pptx.ts +564 -0
- package/corpus/templates/slides-2/actions/import-svgl-logo.ts +115 -0
- package/corpus/templates/slides-2/actions/index-design-system-with-builder.ts +181 -0
- package/corpus/templates/slides-2/actions/index-uploaded-design-system-files.ts +77 -0
- package/corpus/templates/slides-2/actions/list-assets.ts +286 -0
- package/corpus/templates/slides-2/actions/list-deck-templates.ts +83 -0
- package/corpus/templates/slides-2/actions/list-deck-versions.ts +185 -0
- package/corpus/templates/slides-2/actions/list-decks.ts +591 -0
- package/corpus/templates/slides-2/actions/list-design-systems.ts +180 -0
- package/corpus/templates/slides-2/actions/list-slide-comments.ts +128 -0
- package/corpus/templates/slides-2/actions/list-slide-themes.ts +17 -0
- package/corpus/templates/slides-2/actions/list-staged-datasets.ts +14 -0
- package/corpus/templates/slides-2/actions/logo-config.ts +13 -0
- package/corpus/templates/slides-2/actions/navigate.ts +158 -0
- package/corpus/templates/slides-2/actions/patch-deck.ts +2378 -0
- package/corpus/templates/slides-2/actions/provider-api-catalog.ts +25 -0
- package/corpus/templates/slides-2/actions/provider-api-docs.ts +83 -0
- package/corpus/templates/slides-2/actions/provider-api-request.ts +222 -0
- package/corpus/templates/slides-2/actions/query-staged-dataset.ts +83 -0
- package/corpus/templates/slides-2/actions/read-composer-source.ts +108 -0
- package/corpus/templates/slides-2/actions/request-deck-access.ts +803 -0
- package/corpus/templates/slides-2/actions/restore-deck-version.ts +203 -0
- package/corpus/templates/slides-2/actions/run.ts +2 -0
- package/corpus/templates/slides-2/actions/save-deck.ts +313 -0
- package/corpus/templates/slides-2/actions/search-google-fonts.ts +61 -0
- package/corpus/templates/slides-2/actions/search-images.ts +30 -0
- package/corpus/templates/slides-2/actions/search-logos.ts +23 -0
- package/corpus/templates/slides-2/actions/search-svgl-logos.ts +42 -0
- package/corpus/templates/slides-2/actions/set-default-design-system.ts +88 -0
- package/corpus/templates/slides-2/actions/set-workspace-defaults.ts +61 -0
- package/corpus/templates/slides-2/actions/start-design-system-upload.ts +80 -0
- package/corpus/templates/slides-2/actions/sync-design-system-with-builder.ts +84 -0
- package/corpus/templates/slides-2/actions/toggle-slide-comment-reaction.ts +150 -0
- package/corpus/templates/slides-2/actions/transform-slide-objects.ts +1528 -0
- package/corpus/templates/slides-2/actions/update-asset.ts +499 -0
- package/corpus/templates/slides-2/actions/update-deck-aspect-ratio.ts +95 -0
- package/corpus/templates/slides-2/actions/update-deck-design-tokens.ts +175 -0
- package/corpus/templates/slides-2/actions/update-design-system.ts +95 -0
- package/corpus/templates/slides-2/actions/update-slide-comment.ts +213 -0
- package/corpus/templates/slides-2/actions/update-slide-source.ts +353 -0
- package/corpus/templates/slides-2/actions/update-slide.ts +850 -0
- package/corpus/templates/slides-2/actions/update-slides-notification-prefs.ts +55 -0
- package/corpus/templates/slides-2/actions/view-screen.ts +1098 -0
- package/corpus/templates/slides-2/agent-native.app-skill.json +165 -0
- package/corpus/templates/slides-2/agent-native.config.ts +20 -0
- package/corpus/templates/slides-2/agent-native.json +18 -0
- package/corpus/templates/slides-2/app/components/ThemeToggle.tsx +47 -0
- package/corpus/templates/slides-2/app/components/comments/SlideCommentPins.tsx +781 -0
- package/corpus/templates/slides-2/app/components/comments/SlideCommentsPanel.tsx +1075 -0
- package/corpus/templates/slides-2/app/components/deck/DeckCard.tsx +304 -0
- package/corpus/templates/slides-2/app/components/deck/DeckFilterMenu.tsx +83 -0
- package/corpus/templates/slides-2/app/components/deck/ExcalidrawSlide.tsx +183 -0
- package/corpus/templates/slides-2/app/components/deck/MermaidRenderer.tsx +126 -0
- package/corpus/templates/slides-2/app/components/deck/SlideRenderer.tsx +1553 -0
- package/corpus/templates/slides-2/app/components/design-system/DesignSystemCard.tsx +235 -0
- package/corpus/templates/slides-2/app/components/design-system/DesignSystemSetup.tsx +2021 -0
- package/corpus/templates/slides-2/app/components/design-system/builder-design-system-upload.ts +200 -0
- package/corpus/templates/slides-2/app/components/design-system/builder-index-response.ts +104 -0
- package/corpus/templates/slides-2/app/components/editor/AddSlidePopover.tsx +461 -0
- package/corpus/templates/slides-2/app/components/editor/AiEditingMarker.tsx +24 -0
- package/corpus/templates/slides-2/app/components/editor/AlignmentGuides.tsx +82 -0
- package/corpus/templates/slides-2/app/components/editor/AnimationsPanel.tsx +698 -0
- package/corpus/templates/slides-2/app/components/editor/AppMenu.tsx +356 -0
- package/corpus/templates/slides-2/app/components/editor/AssetLibraryPanel.tsx +508 -0
- package/corpus/templates/slides-2/app/components/editor/BlockBubbleMenu.tsx +548 -0
- package/corpus/templates/slides-2/app/components/editor/DeckAccessDeniedPage.tsx +173 -0
- package/corpus/templates/slides-2/app/components/editor/DeckEditorSkeleton.tsx +35 -0
- package/corpus/templates/slides-2/app/components/editor/DeferredPopoverFallback.tsx +148 -0
- package/corpus/templates/slides-2/app/components/editor/EditorActionCluster.tsx +217 -0
- package/corpus/templates/slides-2/app/components/editor/EditorSidebar.tsx +1047 -0
- package/corpus/templates/slides-2/app/components/editor/EditorToolbar.tsx +1285 -0
- package/corpus/templates/slides-2/app/components/editor/ExportMenu.tsx +579 -0
- package/corpus/templates/slides-2/app/components/editor/GeneratingOverlay.tsx +21 -0
- package/corpus/templates/slides-2/app/components/editor/GeneratingSlidePreview.tsx +73 -0
- package/corpus/templates/slides-2/app/components/editor/GoogleDocImportHint.tsx +378 -0
- package/corpus/templates/slides-2/app/components/editor/GoogleDriveConnectionCta.tsx +154 -0
- package/corpus/templates/slides-2/app/components/editor/GoogleSlidesReferenceImport.tsx +299 -0
- package/corpus/templates/slides-2/app/components/editor/HistoryPanel.tsx +354 -0
- package/corpus/templates/slides-2/app/components/editor/HomeQuickStartDialog.tsx +202 -0
- package/corpus/templates/slides-2/app/components/editor/ImageCropOverlay.tsx +820 -0
- package/corpus/templates/slides-2/app/components/editor/ImageDropPromptPopover.tsx +280 -0
- package/corpus/templates/slides-2/app/components/editor/ImageGenPanel.tsx +244 -0
- package/corpus/templates/slides-2/app/components/editor/ImageOverlay.tsx +280 -0
- package/corpus/templates/slides-2/app/components/editor/ImageSearchPanel.tsx +183 -0
- package/corpus/templates/slides-2/app/components/editor/ImportDeckButton.tsx +247 -0
- package/corpus/templates/slides-2/app/components/editor/LogoSearchPanel.tsx +366 -0
- package/corpus/templates/slides-2/app/components/editor/MermaidToExcalidrawPanel.tsx +99 -0
- package/corpus/templates/slides-2/app/components/editor/MissingDeckAccessPane.tsx +219 -0
- package/corpus/templates/slides-2/app/components/editor/NewDeckReferenceStep.tsx +812 -0
- package/corpus/templates/slides-2/app/components/editor/PromptDialog.tsx +963 -0
- package/corpus/templates/slides-2/app/components/editor/QuestionFlow.tsx +119 -0
- package/corpus/templates/slides-2/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
- package/corpus/templates/slides-2/app/components/editor/ShareDialog.tsx +126 -0
- package/corpus/templates/slides-2/app/components/editor/SlideContextToolbar.tsx +1471 -0
- package/corpus/templates/slides-2/app/components/editor/SlideEditor.tsx +11183 -0
- package/corpus/templates/slides-2/app/components/editor/SlideOverflowWarning.tsx +82 -0
- package/corpus/templates/slides-2/app/components/editor/SlideScrollStack.tsx +307 -0
- package/corpus/templates/slides-2/app/components/editor/SlideSlashCommandMenu.tsx +692 -0
- package/corpus/templates/slides-2/app/components/editor/SlideThumbnailContextMenu.tsx +52 -0
- package/corpus/templates/slides-2/app/components/editor/SlideTransitionControl.tsx +184 -0
- package/corpus/templates/slides-2/app/components/editor/SlidesComposerContext.tsx +789 -0
- package/corpus/templates/slides-2/app/components/editor/SlidesComposerContextProvider.tsx +74 -0
- package/corpus/templates/slides-2/app/components/editor/SlidesLayersPanel.tsx +377 -0
- package/corpus/templates/slides-2/app/components/editor/SpeakerNotesPanel.tsx +119 -0
- package/corpus/templates/slides-2/app/components/editor/TweaksPanel.tsx +77 -0
- package/corpus/templates/slides-2/app/components/editor/UploadStorageGate.tsx +37 -0
- package/corpus/templates/slides-2/app/components/editor/bullet-editing.ts +880 -0
- package/corpus/templates/slides-2/app/components/editor/canvas-interactions/index.ts +12 -0
- package/corpus/templates/slides-2/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +119 -0
- package/corpus/templates/slides-2/app/components/editor/commit-active-edit.ts +14 -0
- package/corpus/templates/slides-2/app/components/editor/editor-command-model.ts +35 -0
- package/corpus/templates/slides-2/app/components/editor/fit-text-object.ts +31 -0
- package/corpus/templates/slides-2/app/components/editor/in-place-text-session.browser-cases.json +983 -0
- package/corpus/templates/slides-2/app/components/editor/in-place-text-session.browser-host.ts +747 -0
- package/corpus/templates/slides-2/app/components/editor/in-place-text-session.ts +6891 -0
- package/corpus/templates/slides-2/app/components/editor/inline-edit-session.ts +26 -0
- package/corpus/templates/slides-2/app/components/editor/list-editing.ts +501 -0
- package/corpus/templates/slides-2/app/components/editor/rich-text-selection.ts +975 -0
- package/corpus/templates/slides-2/app/components/editor/rte-test-utils.ts +265 -0
- package/corpus/templates/slides-2/app/components/editor/selection-overlay-measurement.ts +191 -0
- package/corpus/templates/slides-2/app/components/editor/slide-escape-arbiter.ts +34 -0
- package/corpus/templates/slides-2/app/components/editor/slide-object-interactions.ts +6362 -0
- package/corpus/templates/slides-2/app/components/editor/slide-object-transform.browser-host.ts +19 -0
- package/corpus/templates/slides-2/app/components/editor/slide-pointer-target.ts +432 -0
- package/corpus/templates/slides-2/app/components/editor/slide-presence.ts +13 -0
- package/corpus/templates/slides-2/app/components/editor/slide-style.ts +243 -0
- package/corpus/templates/slides-2/app/components/editor/slide-text-targets.ts +540 -0
- package/corpus/templates/slides-2/app/components/editor/style-clipboard.ts +102 -0
- package/corpus/templates/slides-2/app/components/editor/use-prompt-import.ts +159 -0
- package/corpus/templates/slides-2/app/components/home/SlidesHomeLibrary.tsx +79 -0
- package/corpus/templates/slides-2/app/components/layout/AgentWorkIndicator.tsx +193 -0
- package/corpus/templates/slides-2/app/components/layout/Header.tsx +110 -0
- package/corpus/templates/slides-2/app/components/layout/Layout.tsx +492 -0
- package/corpus/templates/slides-2/app/components/layout/Sidebar.tsx +109 -0
- package/corpus/templates/slides-2/app/components/layout/layout-route-policy.ts +28 -0
- package/corpus/templates/slides-2/app/components/presentation/PresentationView.tsx +1116 -0
- package/corpus/templates/slides-2/app/components/presentation/PresenterView.tsx +526 -0
- package/corpus/templates/slides-2/app/components/presentation/SlideTransitionStage.tsx +297 -0
- package/corpus/templates/slides-2/app/components/presentation/present-channel.ts +31 -0
- package/corpus/templates/slides-2/app/components/presentation/slide-build-steps.ts +231 -0
- package/corpus/templates/slides-2/app/components/presentation/slide-navigation.ts +273 -0
- package/corpus/templates/slides-2/app/components/presentation/slide-transition-motion.ts +258 -0
- package/corpus/templates/slides-2/app/components/presentation/voice-navigation.ts +61 -0
- package/corpus/templates/slides-2/app/components/settings/notification-settings.tsx +93 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2AssetBrowser.tsx +726 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2CommandMenu.tsx +453 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Comments.tsx +571 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2DesignTokens.tsx +345 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Editor.tsx +4804 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2ExportMenu.tsx +671 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2FontBrowser.tsx +342 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2ImageCropControls.tsx +487 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Inspector.tsx +950 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2SvglLogoSearchDialog.tsx +332 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/cross-slide-edit-history.ts +151 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/design-token-presets.ts +140 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/editor-action-errors.ts +47 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/editor-export-errors.ts +60 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/geometry.ts +275 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/image-crop-style.ts +82 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/inline-text-editor.tsx +631 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/keyboard-shortcuts.ts +104 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/object-insertion.ts +13 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/preview-patches.ts +54 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/sandbox-render-errors.ts +23 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/source-structure.ts +634 -0
- package/corpus/templates/slides-2/app/components/slides-2-editor/types.ts +85 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2BlackoutOverlay.tsx +13 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2ControlBar.tsx +285 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2HelpDialog.tsx +72 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2JumpInput.tsx +38 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2LaserPointer.tsx +67 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2Overview.tsx +161 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2Player.tsx +839 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2ProgressBar.tsx +23 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/keyboard.ts +36 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/presenter-channel.ts +131 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/use-presenter-action-bridge.ts +296 -0
- package/corpus/templates/slides-2/app/components/slides-2-presenter/useSlides2TouchSwipe.ts +80 -0
- package/corpus/templates/slides-2/app/components/slides-2-runtime/SandboxedSlide.tsx +724 -0
- package/corpus/templates/slides-2/app/components/slides-2-runtime/SlideContentRenderer.tsx +137 -0
- package/corpus/templates/slides-2/app/components/slides-2-runtime/Slides2Canvas.tsx +99 -0
- package/corpus/templates/slides-2/app/components/slides-2-runtime/constants.ts +4 -0
- package/corpus/templates/slides-2/app/components/slides-2-runtime/protocol.ts +1400 -0
- package/corpus/templates/slides-2/app/components/slides-2-runtime/transition-frame.ts +439 -0
- package/corpus/templates/slides-2/app/components/templates/DeckTemplateLibrary.tsx +314 -0
- package/corpus/templates/slides-2/app/components/templates/DeckTemplatePreview.tsx +60 -0
- package/corpus/templates/slides-2/app/components/templates/DeckTemplateStage.tsx +45 -0
- package/corpus/templates/slides-2/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/button-group.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/button.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/card.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/command.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/context-menu.tsx +32 -0
- package/corpus/templates/slides-2/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/filter-trigger.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/input.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/label.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/progress.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/scroll-area.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/select.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/slider.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/tabs.tsx +59 -0
- package/corpus/templates/slides-2/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/toggle-group.tsx +1 -0
- package/corpus/templates/slides-2/app/components/ui/toolkit-provider.tsx +17 -0
- package/corpus/templates/slides-2/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/slides-2/app/components/visual-editor/CanvasCommentPins.tsx +46 -0
- package/corpus/templates/slides-2/app/components/visual-editor/DrawOverlay.tsx +27 -0
- package/corpus/templates/slides-2/app/components/visual-editor/MultiSelectChip.tsx +97 -0
- package/corpus/templates/slides-2/app/components/visual-editor/SaveStatusIndicator.tsx +273 -0
- package/corpus/templates/slides-2/app/components/visual-editor/index.ts +4 -0
- package/corpus/templates/slides-2/app/context/DeckContext.tsx +7486 -0
- package/corpus/templates/slides-2/app/design-system.ts +3 -0
- package/corpus/templates/slides-2/app/entry.client.tsx +10 -0
- package/corpus/templates/slides-2/app/entry.server.tsx +10 -0
- package/corpus/templates/slides-2/app/global.css +1626 -0
- package/corpus/templates/slides-2/app/hooks/use-agent-generating.ts +517 -0
- package/corpus/templates/slides-2/app/hooks/use-compact-editor-layout.ts +26 -0
- package/corpus/templates/slides-2/app/hooks/use-contrast-audit-bridge.ts +49 -0
- package/corpus/templates/slides-2/app/hooks/use-deck-access-reload.ts +70 -0
- package/corpus/templates/slides-2/app/hooks/use-deck-access.ts +50 -0
- package/corpus/templates/slides-2/app/hooks/use-deck-design-system.ts +146 -0
- package/corpus/templates/slides-2/app/hooks/use-deck-history-action-bridge.ts +266 -0
- package/corpus/templates/slides-2/app/hooks/use-deck-presence.ts +89 -0
- package/corpus/templates/slides-2/app/hooks/use-deck-role.ts +46 -0
- package/corpus/templates/slides-2/app/hooks/use-deck-versions.ts +43 -0
- package/corpus/templates/slides-2/app/hooks/use-design-system-workflows.ts +11 -0
- package/corpus/templates/slides-2/app/hooks/use-design-systems.ts +31 -0
- package/corpus/templates/slides-2/app/hooks/use-generating-slide-preview.ts +115 -0
- package/corpus/templates/slides-2/app/hooks/use-navigation-state.ts +229 -0
- package/corpus/templates/slides-2/app/hooks/use-new-deck-generation-signal.ts +46 -0
- package/corpus/templates/slides-2/app/hooks/use-new-deck-generation.ts +710 -0
- package/corpus/templates/slides-2/app/hooks/use-sidebar-collapsed.ts +95 -0
- package/corpus/templates/slides-2/app/hooks/use-slide-comments.ts +202 -0
- package/corpus/templates/slides-2/app/hooks/use-slide-file-storage-status.ts +55 -0
- package/corpus/templates/slides-2/app/hooks/use-undo-selection.ts +66 -0
- package/corpus/templates/slides-2/app/hooks/use-workspace-defaults.ts +31 -0
- package/corpus/templates/slides-2/app/i18n/en-US.ts +1507 -0
- package/corpus/templates/slides-2/app/i18n/index.ts +19 -0
- package/corpus/templates/slides-2/app/lib/agent-visible-message.ts +21 -0
- package/corpus/templates/slides-2/app/lib/aspect-ratios.ts +7 -0
- package/corpus/templates/slides-2/app/lib/asset-error-copy.ts +59 -0
- package/corpus/templates/slides-2/app/lib/canvas-zoom.ts +47 -0
- package/corpus/templates/slides-2/app/lib/composer-context.ts +152 -0
- package/corpus/templates/slides-2/app/lib/composer-draft.ts +34 -0
- package/corpus/templates/slides-2/app/lib/contrast-audit.ts +390 -0
- package/corpus/templates/slides-2/app/lib/contrast-resolve.ts +240 -0
- package/corpus/templates/slides-2/app/lib/create-deck-generation.ts +938 -0
- package/corpus/templates/slides-2/app/lib/deck-access-lost.ts +4 -0
- package/corpus/templates/slides-2/app/lib/deck-backup.ts +186 -0
- package/corpus/templates/slides-2/app/lib/deck-editor-loading.ts +91 -0
- package/corpus/templates/slides-2/app/lib/deck-export-file-client.ts +77 -0
- package/corpus/templates/slides-2/app/lib/deck-filter.ts +52 -0
- package/corpus/templates/slides-2/app/lib/deck-list-loading.ts +16 -0
- package/corpus/templates/slides-2/app/lib/deck-preview-frame.ts +15 -0
- package/corpus/templates/slides-2/app/lib/deck-share-links.ts +9 -0
- package/corpus/templates/slides-2/app/lib/deck-sorting.ts +19 -0
- package/corpus/templates/slides-2/app/lib/deferred-editor-surfaces.ts +24 -0
- package/corpus/templates/slides-2/app/lib/design-system-selection.ts +11 -0
- package/corpus/templates/slides-2/app/lib/design-systems.ts +231 -0
- package/corpus/templates/slides-2/app/lib/dynamic-import.ts +50 -0
- package/corpus/templates/slides-2/app/lib/editor-agent-handoff.ts +92 -0
- package/corpus/templates/slides-2/app/lib/editor-shortcuts.ts +153 -0
- package/corpus/templates/slides-2/app/lib/export-google-slides-client.ts +168 -0
- package/corpus/templates/slides-2/app/lib/export-pdf-client.ts +684 -0
- package/corpus/templates/slides-2/app/lib/export-pptx-client.ts +2420 -0
- package/corpus/templates/slides-2/app/lib/generation-lifecycle.ts +24 -0
- package/corpus/templates/slides-2/app/lib/generation-state.ts +247 -0
- package/corpus/templates/slides-2/app/lib/google-docs-client.ts +110 -0
- package/corpus/templates/slides-2/app/lib/google-slides-export-availability-client.ts +53 -0
- package/corpus/templates/slides-2/app/lib/google-slides-reference-source.ts +18 -0
- package/corpus/templates/slides-2/app/lib/image-download.ts +85 -0
- package/corpus/templates/slides-2/app/lib/image-drop-to-agent.ts +210 -0
- package/corpus/templates/slides-2/app/lib/import-uploaded-deck.ts +132 -0
- package/corpus/templates/slides-2/app/lib/inline-edit-remote.ts +55 -0
- package/corpus/templates/slides-2/app/lib/localize-slides-changelog.ts +47 -0
- package/corpus/templates/slides-2/app/lib/mermaid-blocks.ts +18 -0
- package/corpus/templates/slides-2/app/lib/new-deck-reference-selection.ts +174 -0
- package/corpus/templates/slides-2/app/lib/normalize-slide-padding.ts +74 -0
- package/corpus/templates/slides-2/app/lib/pending-deck-changes.ts +33 -0
- package/corpus/templates/slides-2/app/lib/portaled-layer.ts +7 -0
- package/corpus/templates/slides-2/app/lib/pptx-google-slides.ts +204 -0
- package/corpus/templates/slides-2/app/lib/prompt-attachment-adapter.ts +37 -0
- package/corpus/templates/slides-2/app/lib/prompt-file-uploads.ts +557 -0
- package/corpus/templates/slides-2/app/lib/recent-references.ts +106 -0
- package/corpus/templates/slides-2/app/lib/reference-document-hydration.ts +406 -0
- package/corpus/templates/slides-2/app/lib/register-chat-renderers.tsx +645 -0
- package/corpus/templates/slides-2/app/lib/reveal-after-delete.ts +55 -0
- package/corpus/templates/slides-2/app/lib/sanitize-slide-html.ts +752 -0
- package/corpus/templates/slides-2/app/lib/slide-agent-context.ts +148 -0
- package/corpus/templates/slides-2/app/lib/slide-animation-elements.ts +900 -0
- package/corpus/templates/slides-2/app/lib/slide-clipboard.ts +534 -0
- package/corpus/templates/slides-2/app/lib/slide-comment-anchor.ts +286 -0
- package/corpus/templates/slides-2/app/lib/slide-content-merge.ts +295 -0
- package/corpus/templates/slides-2/app/lib/slide-image-replacement.ts +1919 -0
- package/corpus/templates/slides-2/app/lib/slide-live-remote.ts +215 -0
- package/corpus/templates/slides-2/app/lib/slide-scroll.ts +56 -0
- package/corpus/templates/slides-2/app/lib/slide-source-map.ts +1099 -0
- package/corpus/templates/slides-2/app/lib/slide-video.ts +390 -0
- package/corpus/templates/slides-2/app/lib/slides-relay-tracking.ts +114 -0
- package/corpus/templates/slides-2/app/lib/streaming-slide-html.ts +77 -0
- package/corpus/templates/slides-2/app/lib/tab-id.ts +3 -0
- package/corpus/templates/slides-2/app/lib/template-navigation.ts +15 -0
- package/corpus/templates/slides-2/app/lib/text-tool-shortcut.ts +75 -0
- package/corpus/templates/slides-2/app/lib/undo-reveal.ts +332 -0
- package/corpus/templates/slides-2/app/lib/upload-response.ts +107 -0
- package/corpus/templates/slides-2/app/lib/utils.ts +38 -0
- package/corpus/templates/slides-2/app/lib/validate-native-slide-html.ts +177 -0
- package/corpus/templates/slides-2/app/lib/video-upload.ts +354 -0
- package/corpus/templates/slides-2/app/pages/DeckEditor.tsx +6158 -0
- package/corpus/templates/slides-2/app/pages/DesignSystems.tsx +363 -0
- package/corpus/templates/slides-2/app/pages/Index.tsx +3254 -0
- package/corpus/templates/slides-2/app/pages/NotFound.tsx +33 -0
- package/corpus/templates/slides-2/app/pages/Presentation.tsx +120 -0
- package/corpus/templates/slides-2/app/pages/SharedPresentation.tsx +174 -0
- package/corpus/templates/slides-2/app/pages/SlideThemeDetail.tsx +227 -0
- package/corpus/templates/slides-2/app/pages/SlideThemes.tsx +155 -0
- package/corpus/templates/slides-2/app/pages/Slides2EditorPage.tsx +1618 -0
- package/corpus/templates/slides-2/app/pages/Slides2PresenterConsolePage.tsx +937 -0
- package/corpus/templates/slides-2/app/pages/Slides2PresenterPage.tsx +979 -0
- package/corpus/templates/slides-2/app/pages/Templates.tsx +32 -0
- package/corpus/templates/slides-2/app/root.tsx +391 -0
- package/corpus/templates/slides-2/app/routes/$.tsx +10 -0
- package/corpus/templates/slides-2/app/routes/_index.tsx +1 -0
- package/corpus/templates/slides-2/app/routes/access-request.approve.tsx +276 -0
- package/corpus/templates/slides-2/app/routes/agent.tsx +19 -0
- package/corpus/templates/slides-2/app/routes/chat.$threadId.tsx +1 -0
- package/corpus/templates/slides-2/app/routes/chat.tsx +190 -0
- package/corpus/templates/slides-2/app/routes/deck.$id.tsx +10 -0
- package/corpus/templates/slides-2/app/routes/deck.$id_.present.tsx +10 -0
- package/corpus/templates/slides-2/app/routes/design-systems.tsx +10 -0
- package/corpus/templates/slides-2/app/routes/extensions.$id.$slug.tsx +2 -0
- package/corpus/templates/slides-2/app/routes/extensions.$id.tsx +5 -0
- package/corpus/templates/slides-2/app/routes/extensions._index.tsx +5 -0
- package/corpus/templates/slides-2/app/routes/extensions.tsx +5 -0
- package/corpus/templates/slides-2/app/routes/home-layout.tsx +31 -0
- package/corpus/templates/slides-2/app/routes/home.tsx +22 -0
- package/corpus/templates/slides-2/app/routes/p.$id.tsx +195 -0
- package/corpus/templates/slides-2/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/slides-2/app/routes/settings.tsx +131 -0
- package/corpus/templates/slides-2/app/routes/share.$token.tsx +137 -0
- package/corpus/templates/slides-2/app/routes/slide.tsx +209 -0
- package/corpus/templates/slides-2/app/routes/team.tsx +11 -0
- package/corpus/templates/slides-2/app/routes/templates.tsx +1 -0
- package/corpus/templates/slides-2/app/routes/themes.$id.tsx +1 -0
- package/corpus/templates/slides-2/app/routes/themes.tsx +1 -0
- package/corpus/templates/slides-2/app/routes.ts +24 -0
- package/corpus/templates/slides-2/app/vite-env.d.ts +24 -0
- package/corpus/templates/slides-2/changelog/2026-10-17-find-and-switch-decks-from-the-presenter-console.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-17-meet-slides-2-create-and-refine-editable-slide-decks-with-an.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-17-pptx-image-export.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-17-visual-editor.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-18-apply-an-accessible-design-system-to-an-existing-deck-before.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-18-choose-a-template-or-start-a-new-presentation-from-the-slide.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-18-generate-decks-from-a-prompt-then-edit-slide-elements-direct.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-18-generate-presentation-decks-from-a-prompt-and-edit-every-sli.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-18-new-presentation-drafts-save-before-generation-starts-so-you.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-18-the-assistant-can-undo-redo-and-save-pending-edits-in-the-vi.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-agents-can-insert-duplicate-and-delete-canvas-objects-in-sou.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-browse-slide-themes.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-clicking-a-slide-preview-in-overview-selects-that-slide.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-create-decks-from-a-prompt-and-edit-slides-directly-on-the-c.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-creating-and-editing-decks-now-starts-with-the-tools-most-re.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-deleting-decks-revokes-public-links.md +7 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-escape-cancels-an-in-progress-slide-drag-without-losing-earl.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-import-failures-now-show-clear-localized-descriptions-instea.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-presenter-controls-stay-responsive-when-moving-over-the-slid.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-presenter-notes-stay-private-from-viewers-and-commenters.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-preview-generated-slides-in-chat.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-remote-image-previews-accept-supported-raster-formats.md +6 -0
- 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
- package/corpus/templates/slides-2/changelog/2026-10-19-slide-generation-explains-when-react-effect-hooks-cannot-run.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-the-editor-highlights-hovered-objects-and-keeps-image-replac.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-19-the-slide-editor-uses-the-shared-agent-native-controls.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-20-deck-editing-controls-match-the-workspace.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-20-google-slides-imports-now-report-missing-image-data-instead-.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-20-imports-and-previews-handle-large-content-safely.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-20-public-deck-pages-can-export-source-backed-slides-to-pdf.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-20-saved-slide-images-stay-available.md +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-20-scope-global-media-picks-to-assets-the-current-deck-can-reso.md +6 -0
- package/corpus/templates/slides-2/components.json +20 -0
- package/corpus/templates/slides-2/drizzle.config.ts +3 -0
- package/corpus/templates/slides-2/learnings.defaults.md +5 -0
- package/corpus/templates/slides-2/netlify.toml +24 -0
- package/corpus/templates/slides-2/package.json +147 -0
- package/corpus/templates/slides-2/public/agent-native-icon-dark.svg +10 -0
- package/corpus/templates/slides-2/public/agent-native-icon-light.svg +10 -0
- package/corpus/templates/slides-2/public/agent-native-logo-dark.svg +21 -0
- package/corpus/templates/slides-2/public/agent-native-logo-light.svg +21 -0
- package/corpus/templates/slides-2/public/assets/builder-logo-white.svg +15 -0
- package/corpus/templates/slides-2/public/assets/generated/preview.html +61 -0
- package/corpus/templates/slides-2/public/assets/intuit-logo-white.svg +6 -0
- package/corpus/templates/slides-2/public/assets/walmart-logo-white.svg +6 -0
- package/corpus/templates/slides-2/public/chatgpt-plugin-icon.svg +7 -0
- package/corpus/templates/slides-2/public/favicon.svg +1 -0
- package/corpus/templates/slides-2/public/icon-180.svg +1 -0
- package/corpus/templates/slides-2/public/icon-192.svg +1 -0
- package/corpus/templates/slides-2/public/icon-512.svg +1 -0
- package/corpus/templates/slides-2/public/manifest.json +20 -0
- package/corpus/templates/slides-2/public/placeholder.svg +1 -0
- package/corpus/templates/slides-2/public/robots.txt +14 -0
- package/corpus/templates/slides-2/public/visual-edit-structure-proof.html +99 -0
- package/corpus/templates/slides-2/react-router.config.ts +7 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/README.md +499 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/authoring-fuzz.ts +9517 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/baseline.json +16427 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/dev-server-url.ts +10 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/lib/in-page.ts +1975 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/lib/metrics.ts +1401 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/retry-infra.ts +7 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/run-options.ts +21 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/run-outcomes.ts +127 -0
- package/corpus/templates/slides-2/scripts/edit-fidelity/run.ts +8451 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/README.md +116 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/compare-layout-lib.ts +241 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/compare-layout.ts +30 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/diff.ts +222 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/export.ts +765 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/fixtures/agent-dark-deck.json +48 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/generate-calibration.ts +765 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/google-export.ts +137 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/google-layout.ts +82 -0
- package/corpus/templates/slides-2/scripts/export-fidelity/resolve-pkg.ts +29 -0
- package/corpus/templates/slides-2/scripts/migrate-production.ts +22 -0
- package/corpus/templates/slides-2/server/db/index.ts +73 -0
- package/corpus/templates/slides-2/server/db/schema.ts +189 -0
- package/corpus/templates/slides-2/server/handlers/assets-url.ts +13 -0
- package/corpus/templates/slides-2/server/handlers/assets.ts +3217 -0
- package/corpus/templates/slides-2/server/handlers/decks.ts +185 -0
- package/corpus/templates/slides-2/server/handlers/demo.ts +9 -0
- package/corpus/templates/slides-2/server/handlers/design-systems.ts +219 -0
- package/corpus/templates/slides-2/server/handlers/google-docs-auth.ts +165 -0
- package/corpus/templates/slides-2/server/handlers/image-gen.ts +153 -0
- package/corpus/templates/slides-2/server/handlers/image-providers/gemini.ts +209 -0
- package/corpus/templates/slides-2/server/handlers/image-providers/index.ts +61 -0
- package/corpus/templates/slides-2/server/handlers/image-providers/openai.ts +288 -0
- package/corpus/templates/slides-2/server/handlers/image-providers/types.ts +35 -0
- package/corpus/templates/slides-2/server/handlers/import/docx-parser.ts +31 -0
- package/corpus/templates/slides-2/server/handlers/import/html-converter.ts +1534 -0
- package/corpus/templates/slides-2/server/handlers/import/pdf-fidelity-parser.ts +1039 -0
- package/corpus/templates/slides-2/server/handlers/import/pdf-sidecar-reader.ts +81 -0
- package/corpus/templates/slides-2/server/handlers/import/pdf-style-digest.ts +187 -0
- package/corpus/templates/slides-2/server/handlers/import/pptx-assets.ts +155 -0
- package/corpus/templates/slides-2/server/handlers/import/pptx-parser.ts +57 -0
- package/corpus/templates/slides-2/server/handlers/request-auth-context.ts +108 -0
- package/corpus/templates/slides-2/server/handlers/share.ts +52 -0
- package/corpus/templates/slides-2/server/handlers/uploads-chunked.ts +1066 -0
- package/corpus/templates/slides-2/server/handlers/uploads.ts +457 -0
- package/corpus/templates/slides-2/server/lib/access-request-email.ts +66 -0
- package/corpus/templates/slides-2/server/lib/assets-image-delegation.ts +331 -0
- package/corpus/templates/slides-2/server/lib/bounded-multipart.ts +49 -0
- package/corpus/templates/slides-2/server/lib/builder-design-system-proxy.ts +122 -0
- package/corpus/templates/slides-2/server/lib/chat-attachments.ts +425 -0
- package/corpus/templates/slides-2/server/lib/chatgpt-directory-tools.ts +246 -0
- package/corpus/templates/slides-2/server/lib/chunked-upload-session.ts +195 -0
- package/corpus/templates/slides-2/server/lib/comment-notifications.ts +184 -0
- package/corpus/templates/slides-2/server/lib/deck-access-requests.ts +102 -0
- package/corpus/templates/slides-2/server/lib/deck-asset-scope.ts +150 -0
- package/corpus/templates/slides-2/server/lib/deck-export-tracking.ts +119 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/a-case-study.ts +154 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/a-company.ts +160 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/a-pitch.ts +151 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/a-quarterly.ts +264 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/a-shared.ts +35 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/a-update.ts +188 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/a-workshop.ts +196 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/b-keynote.ts +131 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/b-lesson.ts +209 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/b-manifesto.ts +149 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/b-portfolio.ts +144 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/b-research.ts +221 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/b-roadmap.ts +264 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/b-shared.ts +44 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/group-a.ts +16 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/group-b.ts +16 -0
- package/corpus/templates/slides-2/server/lib/deck-templates/legacy.ts +567 -0
- package/corpus/templates/slides-2/server/lib/deck-templates.ts +60 -0
- package/corpus/templates/slides-2/server/lib/deck-versions.ts +233 -0
- package/corpus/templates/slides-2/server/lib/design-system-workflows.ts +22 -0
- package/corpus/templates/slides-2/server/lib/emails.ts +84 -0
- package/corpus/templates/slides-2/server/lib/env-config.ts +15 -0
- package/corpus/templates/slides-2/server/lib/fetch-remote-image.ts +245 -0
- package/corpus/templates/slides-2/server/lib/generation-completion.ts +278 -0
- package/corpus/templates/slides-2/server/lib/google-docs-access.ts +51 -0
- package/corpus/templates/slides-2/server/lib/google-docs-error.ts +54 -0
- package/corpus/templates/slides-2/server/lib/google-docs-oauth.ts +422 -0
- package/corpus/templates/slides-2/server/lib/google-fonts.ts +509 -0
- package/corpus/templates/slides-2/server/lib/google-oauth-preflight.ts +119 -0
- package/corpus/templates/slides-2/server/lib/google-slides-export-availability.ts +55 -0
- package/corpus/templates/slides-2/server/lib/image-proxy-response.ts +16 -0
- package/corpus/templates/slides-2/server/lib/image-proxy-url.ts +155 -0
- package/corpus/templates/slides-2/server/lib/import-asset-storage.ts +214 -0
- package/corpus/templates/slides-2/server/lib/imported-asset-batch.ts +218 -0
- package/corpus/templates/slides-2/server/lib/legacy-slide-source.ts +692 -0
- package/corpus/templates/slides-2/server/lib/media-search-providers.ts +216 -0
- package/corpus/templates/slides-2/server/lib/native-creative-context.ts +295 -0
- package/corpus/templates/slides-2/server/lib/pdf-parse-setup.ts +115 -0
- package/corpus/templates/slides-2/server/lib/provider-api.ts +57 -0
- package/corpus/templates/slides-2/server/lib/request-wait-until.ts +30 -0
- package/corpus/templates/slides-2/server/lib/shared-deck.ts +29 -0
- package/corpus/templates/slides-2/server/lib/slide-asset-references.ts +309 -0
- package/corpus/templates/slides-2/server/lib/slide-content-patch.ts +1004 -0
- package/corpus/templates/slides-2/server/lib/slide-hygiene.ts +731 -0
- package/corpus/templates/slides-2/server/lib/slide-image-insertion.ts +136 -0
- package/corpus/templates/slides-2/server/lib/slide-reveal-issues.ts +41 -0
- package/corpus/templates/slides-2/server/lib/slide-source-editing/babel-walk.ts +117 -0
- package/corpus/templates/slides-2/server/lib/slide-source-editing/batch-edit.ts +152 -0
- package/corpus/templates/slides-2/server/lib/slide-source-editing/edit-ops.ts +1478 -0
- package/corpus/templates/slides-2/server/lib/slide-source-editing/image-target.ts +128 -0
- package/corpus/templates/slides-2/server/lib/slide-source-editing/text-diff.ts +23 -0
- package/corpus/templates/slides-2/server/lib/slide-source-limits.ts +9 -0
- package/corpus/templates/slides-2/server/lib/slide-source.ts +1786 -0
- package/corpus/templates/slides-2/server/lib/slide-theme-demo-sources.ts +2106 -0
- package/corpus/templates/slides-2/server/lib/slide-themes.ts +2174 -0
- package/corpus/templates/slides-2/server/lib/slide-worker-runtime.ts +1995 -0
- package/corpus/templates/slides-2/server/lib/slides-tracking.ts +73 -0
- package/corpus/templates/slides-2/server/lib/source-import.ts +213 -0
- package/corpus/templates/slides-2/server/lib/svgl.ts +364 -0
- package/corpus/templates/slides-2/server/lib/tenant-files.ts +179 -0
- package/corpus/templates/slides-2/server/lib/upload-storage-status.ts +9 -0
- package/corpus/templates/slides-2/server/lib/uploaded-asset-cleanup-outbox.ts +521 -0
- package/corpus/templates/slides-2/server/lib/uploaded-asset-object-lifecycle.ts +404 -0
- package/corpus/templates/slides-2/server/lib/uploaded-fonts.ts +696 -0
- package/corpus/templates/slides-2/server/lib/uploaded-reference-storage.ts +77 -0
- package/corpus/templates/slides-2/server/lib/validate-slide-animations.ts +341 -0
- package/corpus/templates/slides-2/server/middleware/auth.ts +6 -0
- package/corpus/templates/slides-2/server/plugins/_dev-upload-stub.ts +6 -0
- package/corpus/templates/slides-2/server/plugins/agent-chat.ts +402 -0
- package/corpus/templates/slides-2/server/plugins/auth.ts +31 -0
- package/corpus/templates/slides-2/server/plugins/collab.ts +30 -0
- package/corpus/templates/slides-2/server/plugins/config.ts +6 -0
- package/corpus/templates/slides-2/server/plugins/core-routes.ts +30 -0
- package/corpus/templates/slides-2/server/plugins/creative-context.ts +187 -0
- package/corpus/templates/slides-2/server/plugins/db.ts +462 -0
- package/corpus/templates/slides-2/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/slides-2/server/plugins/labs.ts +8 -0
- package/corpus/templates/slides-2/server/plugins/orphaned-asset-cleanup.ts +17 -0
- package/corpus/templates/slides-2/server/plugins/otel.ts +6 -0
- package/corpus/templates/slides-2/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/slides-2/server/register-secrets.ts +209 -0
- package/corpus/templates/slides-2/server/routes/[...page].get.ts +5 -0
- package/corpus/templates/slides-2/server/routes/[...page].head.ts +1 -0
- package/corpus/templates/slides-2/server/routes/[base]/_agent-native/google-docs/auth-url.get.ts +1 -0
- package/corpus/templates/slides-2/server/routes/[base]/_agent-native/google-docs/callback.get.ts +1 -0
- package/corpus/templates/slides-2/server/routes/_agent-native/google-docs/auth-url.get.ts +1 -0
- package/corpus/templates/slides-2/server/routes/_agent-native/google-docs/callback.get.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/assets/upload-video.post.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/assets/upload.post.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/assets/video-uploads.delete.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/deck-agent-context.json.get.ts +105 -0
- package/corpus/templates/slides-2/server/routes/api/decks/events.get.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/demo.get.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/exports/[filename].get.ts +221 -0
- package/corpus/templates/slides-2/server/routes/api/exports/google-slides.post.ts +240 -0
- package/corpus/templates/slides-2/server/routes/api/exports/html.post.ts +67 -0
- package/corpus/templates/slides-2/server/routes/api/exports/pptx.post.ts +100 -0
- package/corpus/templates/slides-2/server/routes/api/gen-preview/[...].get.ts +24 -0
- package/corpus/templates/slides-2/server/routes/api/generated/[...].get.ts +24 -0
- package/corpus/templates/slides-2/server/routes/api/image-proxy.get.ts +518 -0
- package/corpus/templates/slides-2/server/routes/api/import-assets/[token].get.ts +70 -0
- package/corpus/templates/slides-2/server/routes/api/preview-images.get.ts +20 -0
- package/corpus/templates/slides-2/server/routes/api/share/[token].get.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId]/chunk.post.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId]/status.get.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId].delete.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/uploads-chunked/start.post.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/uploads.delete.ts +1 -0
- package/corpus/templates/slides-2/server/routes/api/uploads.post.ts +1 -0
- package/corpus/templates/slides-2/server/workspace-defaults.ts +196 -0
- package/corpus/templates/slides-2/shared/action-ui.ts +147 -0
- package/corpus/templates/slides-2/shared/agent-readable.ts +2 -0
- package/corpus/templates/slides-2/shared/api.ts +406 -0
- package/corpus/templates/slides-2/shared/app-state-tabs.ts +30 -0
- package/corpus/templates/slides-2/shared/app-storage.ts +9 -0
- package/corpus/templates/slides-2/shared/aspect-ratios.ts +49 -0
- package/corpus/templates/slides-2/shared/blank-slide.ts +179 -0
- package/corpus/templates/slides-2/shared/client-compatibility.ts +2 -0
- package/corpus/templates/slides-2/shared/contrast-audit.ts +255 -0
- package/corpus/templates/slides-2/shared/deck-access.ts +26 -0
- package/corpus/templates/slides-2/shared/deck-app-scope.ts +14 -0
- package/corpus/templates/slides-2/shared/deck-content.ts +37 -0
- package/corpus/templates/slides-2/shared/deck-history-control.ts +79 -0
- package/corpus/templates/slides-2/shared/deck-scope.ts +5 -0
- package/corpus/templates/slides-2/shared/deck-title.ts +199 -0
- package/corpus/templates/slides-2/shared/deck-write.ts +61 -0
- package/corpus/templates/slides-2/shared/design-flags.ts +7 -0
- package/corpus/templates/slides-2/shared/design-system-validation.ts +88 -0
- package/corpus/templates/slides-2/shared/editor-control.ts +120 -0
- package/corpus/templates/slides-2/shared/generation-timing.ts +19 -0
- package/corpus/templates/slides-2/shared/google-docs.ts +69 -0
- package/corpus/templates/slides-2/shared/labs.ts +10 -0
- package/corpus/templates/slides-2/shared/overview-navigation.ts +34 -0
- package/corpus/templates/slides-2/shared/ownership.ts +6 -0
- package/corpus/templates/slides-2/shared/pdf-sidecar.ts +130 -0
- package/corpus/templates/slides-2/shared/presenter-control.ts +111 -0
- package/corpus/templates/slides-2/shared/representative-slide.ts +89 -0
- package/corpus/templates/slides-2/shared/slide-background.ts +30 -0
- package/corpus/templates/slides-2/shared/slide-comment-anchor.ts +117 -0
- package/corpus/templates/slides-2/shared/slide-comment-reactions.ts +47 -0
- package/corpus/templates/slides-2/shared/slide-design-tokens.ts +92 -0
- package/corpus/templates/slides-2/shared/slide-fit.ts +78 -0
- package/corpus/templates/slides-2/shared/slide-ids.ts +133 -0
- package/corpus/templates/slides-2/shared/slide-number.ts +124 -0
- package/corpus/templates/slides-2/shared/slide-object-structure.ts +790 -0
- package/corpus/templates/slides-2/shared/slide-text-range-selection.ts +50 -0
- package/corpus/templates/slides-2/shared/slide-transitions.ts +70 -0
- package/corpus/templates/slides-2/shared/slides-2-transition.ts +194 -0
- package/corpus/templates/slides-2/shared/slides-user-prefs.ts +22 -0
- package/corpus/templates/slides-2/shared/source-import-errors.ts +12 -0
- package/corpus/templates/slides-2/shared/source-runtime-export.ts +76 -0
- package/corpus/templates/slides-2/shared/upload-types.ts +41 -0
- package/corpus/templates/slides-2/ssr-entry.ts +11 -0
- package/corpus/templates/slides-2/tsconfig.json +27 -0
- package/corpus/templates/slides-2/vite.config.ts +77 -0
- package/corpus/templates/slides-2/vitest.config.ts +28 -0
- package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +1 -1
- package/package.json +1 -1
- package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +0 -182
- package/corpus/templates/design/actions/apply-shader-fill.ts +0 -450
- package/corpus/templates/design/actions/apply-shader.ts +0 -257
- package/corpus/templates/design/actions/preview-shader-fill.ts +0 -157
- package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +0 -1144
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +0 -450
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +0 -71
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +0 -555
- package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +0 -105
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +0 -433
- package/corpus/templates/design/app/components/design/inspector/color-picker-shader-pane.tsx +0 -648
- package/corpus/templates/design/shared/shader-fill.ts +0 -174
- package/corpus/templates/design/shared/shader-presets.ts +0 -1523
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
269
|
+
return trailingBackslashes(s) % 2 === 1;
|
|
150
270
|
}
|
|
151
271
|
|
|
152
272
|
function escapeTrailingBackslashRun(s: string): string {
|
|
153
|
-
|
|
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
|
|
277
|
+
const run = trailingBackslashes(s);
|
|
160
278
|
if (!run) return s;
|
|
161
|
-
return s.slice(0, -run
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
305
|
+
const m = matchBlockAttrSuffix(line);
|
|
170
306
|
if (!m) return { text: line, toggle: false, color: null };
|
|
171
|
-
const body = m
|
|
172
|
-
if (oddTrailingBackslashes(m
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
.
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
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
|
-
|
|
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 +=
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
471
|
-
const
|
|
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
|
|
475
|
-
const inner = parseInline(input.slice(open + 1, close));
|
|
476
|
-
mergeSpanMark(inner,
|
|
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
|
-
|
|
485
|
-
const
|
|
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
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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(
|
|
501
|
-
i
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
545
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
1296
|
+
label: trimmed,
|
|
586
1297
|
},
|
|
587
1298
|
};
|
|
588
1299
|
}
|
|
589
1300
|
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
-
if (
|
|
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(
|
|
607
|
-
|
|
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] === "(")
|
|
1351
|
+
if (s[i] === "(") openers.push(i);
|
|
614
1352
|
else if (s[i] === ")") {
|
|
615
|
-
|
|
616
|
-
if (
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
indentStr(ind) +
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
777
|
-
return [
|
|
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
|
-
|
|
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
|
-
|
|
1823
|
+
const source = parts.join("<br>");
|
|
1824
|
+
return escapeUnderscore || !readsUnderscoreEmphasis(source)
|
|
1825
|
+
? source
|
|
1826
|
+
: serializeCellInline(cell, true);
|
|
1022
1827
|
}
|
|
1023
1828
|
|
|
1024
|
-
function serializeCellChildInline(
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1208
|
-
if (candidate === closeTag && --depth === 0) return true;
|
|
2068
|
+
for (const opener of openers) known.set(opener, false);
|
|
1209
2069
|
}
|
|
1210
|
-
return
|
|
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 =
|
|
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
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
):
|
|
1270
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
i += runLength - 1;
|
|
2202
|
+
const run = delimiterRunLength(value, i, "`");
|
|
2203
|
+
last.set(run, i);
|
|
2204
|
+
i += run - 1;
|
|
1280
2205
|
}
|
|
1281
|
-
return
|
|
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
|
|
2252
|
+
return /^-+$/.test(value.trim());
|
|
1330
2253
|
}
|
|
1331
2254
|
|
|
1332
2255
|
function isAlignedGfmDelimiterCell(value: string): boolean {
|
|
1333
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2714
|
+
const fenceOpenMatch = matchFenceOpen(dedent);
|
|
1504
2715
|
if (fenceOpenMatch) {
|
|
1505
|
-
const
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
continue;
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
2033
|
-
|
|
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
|
|
2040
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
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
|
|
2115
|
-
const
|
|
2116
|
-
|
|
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
|
-
|
|
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:
|
|
2122
|
-
|
|
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
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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
|
|
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
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
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
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
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
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
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;
|
|
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
|
-
|
|
2293
|
-
|
|
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
|
|
3891
|
+
const open = matchFenceOpen(dedented);
|
|
2299
3892
|
if (open) {
|
|
2300
3893
|
mask[index] = true;
|
|
2301
|
-
fence = { indent,
|
|
3894
|
+
fence = { indent, marker: open[1] };
|
|
2302
3895
|
}
|
|
2303
3896
|
}
|
|
2304
3897
|
return mask;
|