@ecsplico/editor 2.1.2 → 3.1.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/CHANGELOG.md +78 -1
- package/README.md +39 -156
- package/dist/adapters/comments/drizzle.d.ts +1 -1
- package/dist/adapters/comments/record.d.ts +15 -0
- package/dist/adapters/comments/{client.js → rest-model.js} +3 -3
- package/dist/adapters/comments/rest-model.js.map +1 -0
- package/dist/adapters/comments/server.d.ts +2 -1
- package/dist/adapters/comments/server.js +2 -1
- package/dist/adapters/comments/server.js.map +1 -1
- package/dist/adapters/comments/types.d.ts +2 -2
- package/dist/adapters/document-state.d.ts +23 -0
- package/dist/adapters/document-state.js +66 -0
- package/dist/adapters/document-state.js.map +1 -0
- package/dist/adapters/documents.js +14 -11
- package/dist/adapters/documents.js.map +1 -1
- package/dist/adapters/drizzle.d.ts +2 -2
- package/dist/adapters/drizzle.js +97 -88
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/forge.d.ts +12 -7
- package/dist/adapters/forge.js +38 -45
- package/dist/adapters/forge.js.map +1 -1
- package/dist/adapters/http.d.ts +20 -0
- package/dist/adapters/http.js +68 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -5
- package/dist/adapters/json.d.ts +3 -3
- package/dist/adapters/json.js +58 -49
- package/dist/adapters/json.js.map +1 -1
- package/dist/adapters/memory.d.ts +21 -0
- package/dist/adapters/memory.js +53 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/nitro.d.ts +27 -20
- package/dist/adapters/nitro.js +59 -163
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/react.d.ts +55 -0
- package/dist/adapters/react.js +209 -0
- package/dist/adapters/react.js.map +1 -0
- package/dist/adapters/shared.d.ts +15 -21
- package/dist/adapters/shared.js +10 -8
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/vue.d.ts +15 -30
- package/dist/adapters/vue.js +36 -87
- package/dist/adapters/vue.js.map +1 -1
- package/dist/code/CodeSurface.vue.d.ts +2 -2
- package/dist/config/index.d.ts +105 -0
- package/dist/config/index.js +95 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/abilities.js +1 -0
- package/dist/core/abilities.js.map +1 -1
- package/dist/{adapters/routes.js → core/api-routes.js} +5 -3
- package/dist/core/api-routes.js.map +1 -0
- package/dist/core/backend-capability.d.ts +7 -0
- package/dist/core/backend-capability.js +17 -0
- package/dist/core/backend-capability.js.map +1 -0
- package/dist/core/backend-types.d.ts +245 -0
- package/dist/core/backend-types.js +17 -0
- package/dist/core/backend-types.js.map +1 -0
- package/dist/core/backend.d.ts +108 -0
- package/dist/core/backend.js +14 -0
- package/dist/core/backend.js.map +1 -0
- package/dist/core/block-convert.d.ts +7 -5
- package/dist/core/block-convert.js +28 -5
- package/dist/core/block-convert.js.map +1 -1
- package/dist/core/command-types.d.ts +96 -0
- package/dist/core/commands.d.ts +40 -0
- package/dist/core/commands.js +151 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/context.d.ts +17 -20
- package/dist/core/context.js +24 -21
- package/dist/core/context.js.map +1 -1
- package/dist/core/core-feature.d.ts +22 -7
- package/dist/core/core-feature.js +51 -18
- package/dist/core/core-feature.js.map +1 -1
- package/dist/core/define-editor.d.ts +4 -3
- package/dist/core/define-editor.js +14 -7
- package/dist/core/define-editor.js.map +1 -1
- package/dist/core/document-markdown.js +12 -3
- package/dist/core/document-markdown.js.map +1 -1
- package/dist/core/editor-ui.d.ts +8 -1
- package/dist/core/editor-ui.js +9 -8
- package/dist/core/editor-ui.js.map +1 -1
- package/dist/core/extension-points.d.ts +3 -15
- package/dist/core/feature-context.d.ts +8 -7
- package/dist/core/feature-context.js +12 -11
- package/dist/core/feature-context.js.map +1 -1
- package/dist/core/feature.d.ts +41 -19
- package/dist/core/feature.js.map +1 -1
- package/dist/core/fences.d.ts +2 -1
- package/dist/core/fences.js +3 -2
- package/dist/core/fences.js.map +1 -1
- package/dist/core/fetch-error.d.ts +1 -1
- package/dist/core/fetch-error.js +3 -1
- package/dist/core/fetch-error.js.map +1 -1
- package/dist/core/hash.d.ts +5 -0
- package/dist/core/hash.js +11 -0
- package/dist/core/hash.js.map +1 -0
- package/dist/core/http-backend.d.ts +11 -0
- package/dist/core/http-backend.js +195 -0
- package/dist/core/http-backend.js.map +1 -0
- package/dist/core/index.d.ts +15 -9
- package/dist/core/index.js +13 -9
- package/dist/core/palette.d.ts +48 -0
- package/dist/core/palette.js +95 -0
- package/dist/core/palette.js.map +1 -0
- package/dist/core/schema/abilities.d.ts +2 -1
- package/dist/core/schema/abilities.js +5 -1
- package/dist/core/schema/abilities.js.map +1 -1
- package/dist/core/schema/index.d.ts +11 -5
- package/dist/core/schema/index.js +13 -8
- package/dist/core/schema/index.js.map +1 -1
- package/dist/core/schema/mark-flags.d.ts +8 -1
- package/dist/core/schema/of-features.d.ts +13 -0
- package/dist/core/schema/of-features.js +14 -0
- package/dist/core/schema/of-features.js.map +1 -0
- package/dist/core/slash.d.ts +8 -6
- package/dist/core/slash.js +24 -16
- package/dist/core/slash.js.map +1 -1
- package/dist/core/surface-session.js +15 -5
- package/dist/core/surface-session.js.map +1 -1
- package/dist/core/toolbar-items.d.ts +10 -4
- package/dist/core/toolbar-items.js +269 -195
- package/dist/core/toolbar-items.js.map +1 -1
- package/dist/core/transaction-guards.d.ts +21 -0
- package/dist/core/transaction-guards.js +49 -0
- package/dist/core/transaction-guards.js.map +1 -0
- package/dist/core/util.js +11 -0
- package/dist/core/util.js.map +1 -0
- package/dist/document/DocumentSurface.d.ts +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +2 -2
- package/dist/features/ai/agent-op-review.d.ts +1 -1
- package/dist/features/ai/agent-op-review.js +1 -1
- package/dist/features/ai/agent-ops.js +1 -1
- package/dist/features/ai/ai-apply.d.ts +2 -2
- package/dist/features/ai/ai-apply.js +1 -1
- package/dist/features/ai/ai-review.js +1 -1
- package/dist/features/ai/ai-sse.d.ts +6 -6
- package/dist/features/ai/ai-sse.js +20 -63
- package/dist/features/ai/ai-sse.js.map +1 -1
- package/dist/features/ai/assistant.d.ts +8 -7
- package/dist/features/ai/assistant.js +3 -3
- package/dist/features/ai/assistant.js.map +1 -1
- package/dist/features/ai/block-actions.d.ts +3 -2
- package/dist/features/ai/block-actions.js +3 -2
- package/dist/features/ai/block-actions.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.js +1 -1
- package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/components/DocumentChatOps.vue.d.ts +1 -1
- package/dist/features/ai/components/SettingsModelProfiles.js +8 -8
- package/dist/features/ai/components/SettingsModelProfiles.js.map +1 -1
- package/dist/features/ai/document-chat-model.d.ts +8 -7
- package/dist/features/ai/document-chat-model.js +5 -5
- package/dist/features/ai/document-chat-model.js.map +1 -1
- package/dist/features/ai/document-chat.js +1 -1
- package/dist/features/ai/feature.js +27 -14
- package/dist/features/ai/feature.js.map +1 -1
- package/dist/features/ai/ghost-autocomplete.d.ts +7 -6
- package/dist/features/ai/ghost-autocomplete.js +6 -9
- package/dist/features/ai/ghost-autocomplete.js.map +1 -1
- package/dist/features/ai/index.d.ts +4 -3
- package/dist/features/ai/index.js +10 -10
- package/dist/features/ai/model-profiles.d.ts +5 -4
- package/dist/features/ai/model-profiles.js +2 -2
- package/dist/features/ai/model-profiles.js.map +1 -1
- package/dist/features/ai/protocol/block-lines.js +1 -1
- package/dist/features/ai/protocol/block-lines.js.map +1 -1
- package/dist/features/ai/protocol/chat.js +6 -2
- package/dist/features/ai/protocol/chat.js.map +1 -1
- package/dist/features/ai/protocol/index.d.ts +3 -2
- package/dist/features/ai/protocol/models.d.ts +2 -16
- package/dist/features/ai/protocol/models.js.map +1 -1
- package/dist/features/ai/rate-limit.d.ts +1 -1
- package/dist/features/ai/rate-limit.js +3 -1
- package/dist/features/ai/rate-limit.js.map +1 -1
- package/dist/features/ai/react/AiSelectionMenu.js +2 -2
- package/dist/features/ai/react/AiSidebar.js +4 -4
- package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/react/DocumentChatOps.d.ts +1 -1
- package/dist/features/ai/react/DocumentChatOps.js +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.js +8 -10
- package/dist/features/ai/react/SettingsModelProfiles.js.map +1 -1
- package/dist/features/ai/react/use-ai-assistant.d.ts +1 -1
- package/dist/features/ai/react/use-ai-assistant.js +3 -3
- package/dist/features/ai/react/use-ai-assistant.js.map +1 -1
- package/dist/features/ai/react/use-document-chat.js +2 -2
- package/dist/features/ai/react/use-document-chat.js.map +1 -1
- package/dist/features/ai/react/use-model-profiles.d.ts +2 -1
- package/dist/features/ai/selection-flow.d.ts +1 -1
- package/dist/features/ai/selection-flow.js +1 -1
- package/dist/features/ai/sidebar-flow.d.ts +1 -1
- package/dist/features/ai/sidebar-flow.js +1 -1
- package/dist/features/ai/use-ai-assistant.d.ts +1 -1
- package/dist/features/ai/use-ai-assistant.js +3 -3
- package/dist/features/ai/use-ai-assistant.js.map +1 -1
- package/dist/features/ai/use-document-chat.js +2 -2
- package/dist/features/ai/use-document-chat.js.map +1 -1
- package/dist/features/ai/use-model-profiles.d.ts +2 -1
- package/dist/features/authors/attribution.d.ts +1 -1
- package/dist/features/authors/feature.js +12 -11
- package/dist/features/authors/feature.js.map +1 -1
- package/dist/features/block-ui/block-actions.d.ts +1 -1
- package/dist/features/block-ui/block-actions.js +1 -1
- package/dist/features/block-ui/block-actions.js.map +1 -1
- package/dist/features/block-ui/components/BlockUi.js +1 -1
- package/dist/features/block-ui/feature.js +1 -0
- package/dist/features/block-ui/feature.js.map +1 -1
- package/dist/features/block-ui/react/BlockUi.js +1 -1
- package/dist/features/collab/anchor-resolver.js +1 -1
- package/dist/features/collab/feature.js +2 -0
- package/dist/features/collab/feature.js.map +1 -1
- package/dist/features/comments/components/CommentThreadView.vue.d.ts +2 -1
- package/dist/features/comments/feature.js +12 -11
- package/dist/features/comments/feature.js.map +1 -1
- package/dist/features/comments/index.d.ts +3 -2
- package/dist/features/comments/index.js +1 -1
- package/dist/features/comments/margin-layout.d.ts +3 -2
- package/dist/features/comments/model.d.ts +3 -67
- package/dist/features/comments/model.js +2 -2
- package/dist/features/comments/model.js.map +1 -1
- package/dist/features/comments/react/CommentThreadView.d.ts +3 -2
- package/dist/features/comments/react/use-comments.d.ts +2 -1
- package/dist/features/comments/react.d.ts +1 -1
- package/dist/features/comments/schema.d.ts +2 -52
- package/dist/features/comments/schema.js.map +1 -1
- package/dist/features/comments/snapshot-equality.js.map +1 -1
- package/dist/features/comments/use-comments.d.ts +3 -2
- package/dist/features/comments/vue.d.ts +1 -1
- package/dist/features/composites/accepted-children.d.ts +1 -0
- package/dist/features/composites/accepted-children.js +16 -1
- package/dist/features/composites/accepted-children.js.map +1 -1
- package/dist/features/composites/ai-brief.js +39 -0
- package/dist/features/composites/ai-brief.js.map +1 -1
- package/dist/features/composites/bibliography-command.d.ts +11 -0
- package/dist/features/composites/bibliography-form.js +90 -0
- package/dist/features/composites/bibliography-form.js.map +1 -0
- package/dist/features/composites/bibliography.d.ts +33 -0
- package/dist/features/composites/bibliography.js +120 -0
- package/dist/features/composites/bibliography.js.map +1 -0
- package/dist/features/composites/components/BibliographyEditor.js +261 -0
- package/dist/features/composites/components/BibliographyEditor.js.map +1 -0
- package/dist/features/composites/components/CompositeChrome.js +78 -29
- package/dist/features/composites/components/CompositeChrome.js.map +1 -1
- package/dist/features/composites/components/CompositeChrome.vue.d.ts +1 -1
- package/dist/features/composites/components/CompositeDetails.js +46 -11
- package/dist/features/composites/components/CompositeDetails.js.map +1 -1
- package/dist/features/composites/components/CompositeDetails.vue.d.ts +2 -1
- package/dist/features/composites/components/CompositeFieldEditor.js +64 -5
- package/dist/features/composites/components/CompositeFieldEditor.js.map +1 -1
- package/dist/features/composites/components/CompositeInspector.js +20 -7
- package/dist/features/composites/components/CompositeInspector.js.map +1 -1
- package/dist/features/composites/components/CompositeTable.js +13 -4
- package/dist/features/composites/components/CompositeTable.js.map +1 -1
- package/dist/features/composites/components/CompositeTagsEditor.js +1 -1
- package/dist/features/composites/components/CompositeView.js +11 -1
- package/dist/features/composites/components/CompositeView.js.map +1 -1
- package/dist/features/composites/components/DerivationBadge.js +103 -0
- package/dist/features/composites/components/DerivationBadge.js.map +1 -0
- package/dist/features/composites/components/ReferenceFieldSelect.js +60 -0
- package/dist/features/composites/components/ReferenceFieldSelect.js.map +1 -0
- package/dist/features/composites/components/ReferenceView.js +122 -0
- package/dist/features/composites/components/ReferenceView.js.map +1 -0
- package/dist/features/composites/components/ReferenceView.vue.d.ts +96 -0
- package/dist/features/composites/components/SettingsEditingTab.js +1 -1
- package/dist/features/composites/convert.js +2 -2
- package/dist/features/composites/convert.js.map +1 -1
- package/dist/features/composites/derivation.d.ts +61 -0
- package/dist/features/composites/derivation.js +98 -0
- package/dist/features/composites/derivation.js.map +1 -0
- package/dist/features/composites/derived-chrome.js +69 -0
- package/dist/features/composites/derived-chrome.js.map +1 -0
- package/dist/features/composites/feature.js +159 -109
- package/dist/features/composites/feature.js.map +1 -1
- package/dist/features/composites/fences.d.ts +4 -3
- package/dist/features/composites/fences.js +51 -7
- package/dist/features/composites/fences.js.map +1 -1
- package/dist/features/composites/index.d.ts +12 -3
- package/dist/features/composites/index.js +15 -6
- package/dist/features/composites/inspector-model.d.ts +4 -1
- package/dist/features/composites/inspector-model.js +8 -4
- package/dist/features/composites/inspector-model.js.map +1 -1
- package/dist/features/composites/kinds.d.ts +16 -2
- package/dist/features/composites/kinds.js +145 -6
- package/dist/features/composites/kinds.js.map +1 -1
- package/dist/features/composites/legacy.d.ts +4 -1
- package/dist/features/composites/legacy.js +56 -1
- package/dist/features/composites/legacy.js.map +1 -1
- package/dist/features/composites/meta-display.js +45 -2
- package/dist/features/composites/meta-display.js.map +1 -1
- package/dist/features/composites/meta-keys.d.ts +7 -0
- package/dist/features/composites/meta-keys.js +21 -0
- package/dist/features/composites/meta-keys.js.map +1 -0
- package/dist/features/composites/node-views.d.ts +6 -3
- package/dist/features/composites/node-views.js +7 -3
- package/dist/features/composites/node-views.js.map +1 -1
- package/dist/features/composites/nodes.d.ts +6 -1
- package/dist/features/composites/nodes.js +33 -7
- package/dist/features/composites/nodes.js.map +1 -1
- package/dist/features/composites/react/BibliographyEditor.js +229 -0
- package/dist/features/composites/react/BibliographyEditor.js.map +1 -0
- package/dist/features/composites/react/CompositeChrome.js +67 -19
- package/dist/features/composites/react/CompositeChrome.js.map +1 -1
- package/dist/features/composites/react/CompositeDetails.d.ts +2 -1
- package/dist/features/composites/react/CompositeDetails.js +45 -6
- package/dist/features/composites/react/CompositeDetails.js.map +1 -1
- package/dist/features/composites/react/CompositeFieldEditor.d.ts +1 -1
- package/dist/features/composites/react/CompositeFieldEditor.js +43 -1
- package/dist/features/composites/react/CompositeFieldEditor.js.map +1 -1
- package/dist/features/composites/react/CompositeInspector.js +12 -6
- package/dist/features/composites/react/CompositeInspector.js.map +1 -1
- package/dist/features/composites/react/CompositeStatusMenu.js +1 -1
- package/dist/features/composites/react/CompositeSummaryEditor.js +1 -1
- package/dist/features/composites/react/CompositeTable.js +16 -2
- package/dist/features/composites/react/CompositeTable.js.map +1 -1
- package/dist/features/composites/react/CompositeTagsEditor.js +2 -2
- package/dist/features/composites/react/CompositeView.js +6 -1
- package/dist/features/composites/react/CompositeView.js.map +1 -1
- package/dist/features/composites/react/DerivationBadge.js +116 -0
- package/dist/features/composites/react/DerivationBadge.js.map +1 -0
- package/dist/features/composites/react/ReferenceFieldSelect.js +52 -0
- package/dist/features/composites/react/ReferenceFieldSelect.js.map +1 -0
- package/dist/features/composites/react/ReferenceView.d.ts +11 -0
- package/dist/features/composites/react/ReferenceView.js +115 -0
- package/dist/features/composites/react/ReferenceView.js.map +1 -0
- package/dist/features/composites/react/SettingsEditingTab.js +2 -2
- package/dist/features/composites/react.d.ts +2 -1
- package/dist/features/composites/react.js +7 -3
- package/dist/features/composites/react.js.map +1 -1
- package/dist/features/composites/reference-ai.js +16 -0
- package/dist/features/composites/reference-ai.js.map +1 -0
- package/dist/features/composites/reference-label.d.ts +19 -0
- package/dist/features/composites/reference-label.js +58 -0
- package/dist/features/composites/reference-label.js.map +1 -0
- package/dist/features/composites/reference-suggestion.d.ts +10 -0
- package/dist/features/composites/reference-suggestion.js +41 -0
- package/dist/features/composites/reference-suggestion.js.map +1 -0
- package/dist/features/composites/reference-text.d.ts +18 -0
- package/dist/features/composites/reference-text.js +89 -0
- package/dist/features/composites/reference-text.js.map +1 -0
- package/dist/features/composites/reference.d.ts +44 -0
- package/dist/features/composites/reference.js +148 -0
- package/dist/features/composites/reference.js.map +1 -0
- package/dist/features/composites/reveal-source.d.ts +32 -0
- package/dist/features/composites/reveal-source.js +89 -0
- package/dist/features/composites/reveal-source.js.map +1 -0
- package/dist/features/composites/rows.d.ts +25 -3
- package/dist/features/composites/rows.js +72 -10
- package/dist/features/composites/rows.js.map +1 -1
- package/dist/features/composites/schema.d.ts +9 -3
- package/dist/features/composites/schema.js +11 -4
- package/dist/features/composites/schema.js.map +1 -1
- package/dist/features/composites/table-model.js +12 -21
- package/dist/features/composites/table-model.js.map +1 -1
- package/dist/features/composites/vue.d.ts +3 -2
- package/dist/features/composites/vue.js +8 -4
- package/dist/features/composites/vue.js.map +1 -1
- package/dist/features/media/assets.d.ts +2 -0
- package/dist/features/media/assets.js +3 -2
- package/dist/features/media/assets.js.map +1 -1
- package/dist/features/media/components/FileView.js +13 -4
- package/dist/features/media/components/FileView.js.map +1 -1
- package/dist/features/media/feature.d.ts +3 -3
- package/dist/features/media/feature.js +21 -28
- package/dist/features/media/feature.js.map +1 -1
- package/dist/features/media/file-import.js +2 -2
- package/dist/features/media/file-import.js.map +1 -1
- package/dist/features/media/index.d.ts +2 -2
- package/dist/features/media/index.js +3 -3
- package/dist/features/media/react/AudioRecorderModal.js +1 -1
- package/dist/features/media/react/FileView.js +19 -2
- package/dist/features/media/react/FileView.js.map +1 -1
- package/dist/features/media/react.js +1 -1
- package/dist/features/media/schema.d.ts +8 -0
- package/dist/features/media/schema.js +126 -11
- package/dist/features/media/schema.js.map +1 -1
- package/dist/features/media/upload.d.ts +16 -14
- package/dist/features/media/upload.js +13 -19
- package/dist/features/media/upload.js.map +1 -1
- package/dist/features/mentions/index.d.ts +4 -3
- package/dist/features/mentions/index.js +4 -1
- package/dist/features/mentions/index.js.map +1 -1
- package/dist/features/mentions/mention-text.d.ts +1 -1
- package/dist/features/mentions/mention-text.js.map +1 -1
- package/dist/features/mentions/mention.d.ts +0 -6
- package/dist/features/mentions/mention.js.map +1 -1
- package/dist/features/mentions/metadata-defaults.d.ts +2 -2
- package/dist/features/mentions/metadata-defaults.js +3 -9
- package/dist/features/mentions/metadata-defaults.js.map +1 -1
- package/dist/features/mentions/schema.d.ts +2 -1
- package/dist/features/mentions/schema.js.map +1 -1
- package/dist/features/tables/feature.d.ts +2 -1
- package/dist/features/tables/feature.js +90 -70
- package/dist/features/tables/feature.js.map +1 -1
- package/dist/features/themes/components/SettingsAppearanceTab.js +6 -9
- package/dist/features/themes/components/SettingsAppearanceTab.js.map +1 -1
- package/dist/features/themes/feature.js +2 -0
- package/dist/features/themes/feature.js.map +1 -1
- package/dist/features/themes/index.d.ts +2 -1
- package/dist/features/themes/react/SettingsAppearanceTab.js +7 -10
- package/dist/features/themes/react/SettingsAppearanceTab.js.map +1 -1
- package/dist/features/themes/schema.d.ts +2 -6
- package/dist/features/themes/schema.js.map +1 -1
- package/dist/features/themes/theme-runtime.d.ts +1 -1
- package/dist/features/themes/theme-runtime.js +4 -2
- package/dist/features/themes/theme-runtime.js.map +1 -1
- package/dist/features/transcript/TranscriptSegmentView.js +48 -28
- package/dist/features/transcript/TranscriptSegmentView.js.map +1 -1
- package/dist/features/transcript/feature.js +21 -19
- package/dist/features/transcript/feature.js.map +1 -1
- package/dist/features/transcript/react/TranscriptSegmentView.js +55 -36
- package/dist/features/transcript/react/TranscriptSegmentView.js.map +1 -1
- package/dist/features/transcript/segment-node.d.ts +2 -0
- package/dist/features/transcript/segment-node.js +35 -7
- package/dist/features/transcript/segment-node.js.map +1 -1
- package/dist/i18n/de/composites.js +91 -1
- package/dist/i18n/de/composites.js.map +1 -1
- package/dist/i18n/de/integrations.js +49 -8
- package/dist/i18n/de/integrations.js.map +1 -1
- package/dist/i18n/de/media.js +0 -1
- package/dist/i18n/de/media.js.map +1 -1
- package/dist/i18n/de/palette.js +22 -0
- package/dist/i18n/de/palette.js.map +1 -0
- package/dist/i18n/de/tables.js +0 -1
- package/dist/i18n/de/tables.js.map +1 -1
- package/dist/i18n/de/transcript.js +2 -1
- package/dist/i18n/de/transcript.js.map +1 -1
- package/dist/i18n/de/tree.js +3 -0
- package/dist/i18n/de/tree.js.map +1 -1
- package/dist/i18n/de.d.ts +157 -11
- package/dist/i18n/de.js +3 -1
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en/composites.js +89 -1
- package/dist/i18n/en/composites.js.map +1 -1
- package/dist/i18n/en/integrations.js +49 -8
- package/dist/i18n/en/integrations.js.map +1 -1
- package/dist/i18n/en/media.js +0 -1
- package/dist/i18n/en/media.js.map +1 -1
- package/dist/i18n/en/palette.js +22 -0
- package/dist/i18n/en/palette.js.map +1 -0
- package/dist/i18n/en/tables.js +0 -1
- package/dist/i18n/en/tables.js.map +1 -1
- package/dist/i18n/en/transcript.js +2 -1
- package/dist/i18n/en/transcript.js.map +1 -1
- package/dist/i18n/en/tree.js +3 -0
- package/dist/i18n/en/tree.js.map +1 -1
- package/dist/i18n/en.js +3 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/integrations/composite-ai/bibliography-fetch.d.ts +31 -0
- package/dist/integrations/composite-ai/bibliography-fetch.js +159 -0
- package/dist/integrations/composite-ai/bibliography-fetch.js.map +1 -0
- package/dist/integrations/composite-ai/composite-fill.d.ts +30 -8
- package/dist/integrations/composite-ai/composite-fill.js +97 -56
- package/dist/integrations/composite-ai/composite-fill.js.map +1 -1
- package/dist/integrations/composite-ai/composite-summary.d.ts +4 -12
- package/dist/integrations/composite-ai/composite-summary.js +7 -26
- package/dist/integrations/composite-ai/composite-summary.js.map +1 -1
- package/dist/integrations/composite-ai/derive.d.ts +29 -0
- package/dist/integrations/composite-ai/derive.js +173 -0
- package/dist/integrations/composite-ai/derive.js.map +1 -0
- package/dist/integrations/composite-ai/index.d.ts +11 -7
- package/dist/integrations/composite-ai/index.js +72 -14
- package/dist/integrations/composite-ai/index.js.map +1 -1
- package/dist/integrations/composite-ai/rendition.d.ts +45 -0
- package/dist/integrations/composite-ai/rendition.js +314 -0
- package/dist/integrations/composite-ai/rendition.js.map +1 -0
- package/dist/integrations/composite-media/index.d.ts +7 -5
- package/dist/integrations/composite-media/index.js +86 -13
- package/dist/integrations/composite-media/index.js.map +1 -1
- package/dist/integrations/transcript-ai/index.d.ts +1 -1
- package/dist/integrations/transcript-ai/index.js +5 -2
- package/dist/integrations/transcript-ai/index.js.map +1 -1
- package/dist/integrations/transcript-ai/protocol.d.ts +2 -12
- package/dist/integrations/transcript-ai/protocol.js +0 -2
- package/dist/integrations/transcript-ai/protocol.js.map +1 -1
- package/dist/integrations/transcript-ai/transcript-cleanup.d.ts +5 -5
- package/dist/integrations/transcript-ai/transcript-cleanup.js +7 -10
- package/dist/integrations/transcript-ai/transcript-cleanup.js.map +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +12 -11
- package/dist/markdown/MarkdownSurface2.js +3 -6
- package/dist/markdown/MarkdownSurface2.js.map +1 -1
- package/dist/markdown/document.js +1 -1
- package/dist/markdown/source-mode.d.ts +1 -1
- package/dist/nuxt/module.d.ts +1402 -54
- package/dist/nuxt/module.js +6 -49
- package/dist/nuxt/module.js.map +1 -1
- package/dist/nuxt/runtime/install-context.d.ts +7 -3
- package/dist/nuxt/runtime/install-context.js +14 -8
- package/dist/nuxt/runtime/install-context.js.map +1 -1
- package/dist/nuxt/runtime/plugins/editor-context-nuxt-ui.client.ts +2 -2
- package/dist/nuxt/runtime/server/plugins/editor-server.js +20 -12
- package/dist/nuxt/runtime/server/plugins/editor-server.js.map +1 -1
- package/dist/presets/compose.d.ts +94 -0
- package/dist/presets/compose.js +164 -0
- package/dist/presets/compose.js.map +1 -0
- package/dist/react/AdvancedEditor.d.ts +15 -15
- package/dist/react/AdvancedEditor.js +15 -13
- package/dist/react/AdvancedEditor.js.map +1 -1
- package/dist/react/EditorCommandPalette.d.ts +13 -0
- package/dist/react/EditorCommandPalette.js +118 -0
- package/dist/react/EditorCommandPalette.js.map +1 -0
- package/dist/react/EditorSettingsModal.d.ts +5 -5
- package/dist/react/EditorSettingsModal.js +7 -4
- package/dist/react/EditorSettingsModal.js.map +1 -1
- package/dist/react/EditorSettingsPanel.d.ts +7 -7
- package/dist/react/EditorSettingsPanel.js +8 -5
- package/dist/react/EditorSettingsPanel.js.map +1 -1
- package/dist/react/EditorSurface.d.ts +7 -7
- package/dist/react/EditorSurface.js +10 -6
- package/dist/react/EditorSurface.js.map +1 -1
- package/dist/react/EditorToolbar.d.ts +4 -6
- package/dist/react/EditorToolbar.js +29 -37
- package/dist/react/EditorToolbar.js.map +1 -1
- package/dist/react/SimpleEditor.d.ts +22 -23
- package/dist/react/SimpleEditor.js +21 -7
- package/dist/react/SimpleEditor.js.map +1 -1
- package/dist/react/advanced.d.ts +4 -3
- package/dist/react/advanced.js +2 -2
- package/dist/react/compose.d.ts +20 -0
- package/dist/react/compose.js +49 -0
- package/dist/react/compose.js.map +1 -0
- package/dist/react/composed-surface.d.ts +39 -0
- package/dist/react/composed-surface.js +71 -0
- package/dist/react/composed-surface.js.map +1 -0
- package/dist/react/context.js +6 -2
- package/dist/react/context.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/key-labels.js +15 -0
- package/dist/react/key-labels.js.map +1 -0
- package/dist/react/settings.d.ts +1 -1
- package/dist/react/simple.d.ts +2 -2
- package/dist/react/ui/command.js +36 -1
- package/dist/react/ui/command.js.map +1 -1
- package/dist/schema/advanced.d.ts +6 -6
- package/dist/schema/advanced.js +26 -29
- package/dist/schema/advanced.js.map +1 -1
- package/dist/server/ai.d.ts +5 -16
- package/dist/server/ai.js.map +1 -1
- package/dist/server/auth.d.ts +9 -0
- package/dist/server/collab-state.d.ts +1 -1
- package/dist/server/collab-state.js.map +1 -1
- package/dist/server/collab.d.ts +13 -13
- package/dist/server/collab.js +34 -18
- package/dist/server/collab.js.map +1 -1
- package/dist/server/crossref.js +78 -0
- package/dist/server/crossref.js.map +1 -0
- package/dist/server/errors.d.ts +1 -1
- package/dist/server/errors.js +1 -1
- package/dist/server/errors.js.map +1 -1
- package/dist/server/extract.js +308 -0
- package/dist/server/extract.js.map +1 -0
- package/dist/server/h3.d.ts +8 -0
- package/dist/{nuxt/runtime/server/utils/editor-route.js → server/h3.js} +8 -16
- package/dist/server/h3.js.map +1 -0
- package/dist/server/handlers/ai.d.ts +32 -10
- package/dist/server/handlers/ai.js +172 -37
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/context.d.ts +12 -5
- package/dist/server/handlers/context.js +10 -4
- package/dist/server/handlers/context.js.map +1 -1
- package/dist/server/handlers/editor.d.ts +19 -13
- package/dist/server/handlers/editor.js +49 -38
- package/dist/server/handlers/editor.js.map +1 -1
- package/dist/server/handlers/index.d.ts +4 -4
- package/dist/server/handlers/index.js +4 -4
- package/dist/server/host.d.ts +16 -25
- package/dist/server/host.js +58 -41
- package/dist/server/host.js.map +1 -1
- package/dist/server/index.d.ts +6 -4
- package/dist/server/index.js +3 -2
- package/dist/server/pdf.js +97 -0
- package/dist/server/pdf.js.map +1 -0
- package/dist/server/{adapter.d.ts → ports.d.ts} +81 -30
- package/dist/server/ports.js +29 -0
- package/dist/server/ports.js.map +1 -0
- package/dist/server/routes.d.ts +22 -0
- package/dist/server/routes.js +144 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.js.map +1 -1
- package/dist/styles/composites.css +25 -0
- package/dist/tree/DocumentTree.js +56 -29
- package/dist/tree/DocumentTree.js.map +1 -1
- package/dist/tree/react/DocumentTree.js +23 -0
- package/dist/tree/react/DocumentTree.js.map +1 -1
- package/dist/tree/tree-host.js +2 -2
- package/dist/tree/tree-items.js +1 -1
- package/dist/vue/AdvancedEditor.js +30 -9
- package/dist/vue/AdvancedEditor.js.map +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +32 -12
- package/dist/vue/SimpleEditor.js +62 -16
- package/dist/vue/SimpleEditor.js.map +1 -1
- package/dist/vue/SimpleEditor.vue.d.ts +39 -9
- package/dist/vue/advanced.d.ts +3 -2
- package/dist/vue/advanced.js +2 -2
- package/dist/vue/components/EditorCommandPalette.js +91 -0
- package/dist/vue/components/EditorCommandPalette.js.map +1 -0
- package/dist/vue/components/EditorCommandPalette.vue.d.ts +15 -0
- package/dist/vue/components/EditorSettingsModal.js +10 -6
- package/dist/vue/components/EditorSettingsModal.js.map +1 -1
- package/dist/vue/components/EditorSettingsModal.vue.d.ts +4 -4
- package/dist/vue/components/EditorSettingsPanel.js +10 -6
- package/dist/vue/components/EditorSettingsPanel.js.map +1 -1
- package/dist/vue/components/EditorSettingsPanel.vue.d.ts +6 -6
- package/dist/vue/components/EditorSurface.js +11 -6
- package/dist/vue/components/EditorSurface.js.map +1 -1
- package/dist/vue/components/EditorSurface.vue.d.ts +11 -11
- package/dist/vue/components/EditorToolbar.js +41 -49
- package/dist/vue/components/EditorToolbar.js.map +1 -1
- package/dist/vue/components/EditorToolbar.vue.d.ts +2 -4
- package/dist/vue/compose.d.ts +17 -0
- package/dist/vue/compose.js +51 -0
- package/dist/vue/compose.js.map +1 -0
- package/dist/vue/context-key.d.ts +6 -0
- package/dist/vue/context-key.js +6 -0
- package/dist/vue/context-key.js.map +1 -0
- package/dist/vue/context.d.ts +8 -6
- package/dist/vue/context.js +6 -5
- package/dist/vue/context.js.map +1 -1
- package/dist/vue/index.d.ts +10 -8
- package/dist/vue/index.js +10 -8
- package/dist/vue/kit/index.d.ts +2 -2
- package/dist/vue/kit/nuxt-ui.js +23 -0
- package/dist/vue/kit/nuxt-ui.js.map +1 -1
- package/dist/vue/kit/types.d.ts +40 -1
- package/package.json +30 -16
- package/dist/adapters/comments/client.d.ts +0 -64
- package/dist/adapters/comments/client.js.map +0 -1
- package/dist/adapters/routes.js.map +0 -1
- package/dist/core/toolbar.d.ts +0 -74
- package/dist/core/toolbar.js +0 -45
- package/dist/core/toolbar.js.map +0 -1
- package/dist/integrations/composite-ai/document-transcribe.d.ts +0 -14
- package/dist/integrations/composite-ai/document-transcribe.js +0 -89
- package/dist/integrations/composite-ai/document-transcribe.js.map +0 -1
- package/dist/nuxt/runtime/server/api/ai/chat.post.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/ai/chat.post.js +0 -8
- package/dist/nuxt/runtime/server/api/ai/chat.post.js.map +0 -1
- package/dist/nuxt/runtime/server/api/ai/complete.post.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/ai/complete.post.js +0 -8
- package/dist/nuxt/runtime/server/api/ai/complete.post.js.map +0 -1
- package/dist/nuxt/runtime/server/api/ai/generate.post.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/ai/generate.post.js +0 -8
- package/dist/nuxt/runtime/server/api/ai/generate.post.js.map +0 -1
- package/dist/nuxt/runtime/server/api/ai/models.get.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/ai/models.get.js +0 -8
- package/dist/nuxt/runtime/server/api/ai/models.get.js.map +0 -1
- package/dist/nuxt/runtime/server/api/ai/transcribe.post.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/ai/transcribe.post.js +0 -8
- package/dist/nuxt/runtime/server/api/ai/transcribe.post.js.map +0 -1
- package/dist/nuxt/runtime/server/api/ai/transcript-cleanup.post.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/ai/transcript-cleanup.post.js +0 -8
- package/dist/nuxt/runtime/server/api/ai/transcript-cleanup.post.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/assets/[id]/[name].get.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/assets/[id]/[name].get.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/assets/[id]/[name].get.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/assets/[id].post.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/assets/[id].post.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/assets/[id].post.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/settings.get.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/settings.get.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/settings.get.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/settings.put.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/settings.put.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/settings.put.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/tags.get.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/tags.get.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/tags.get.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/themes/[id].delete.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/themes/[id].delete.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/themes/[id].delete.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/themes/[id].get.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/themes/[id].get.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/themes/[id].get.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/themes/index.get.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/themes/index.get.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/themes/index.get.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/themes/index.post.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/themes/index.post.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/themes/index.post.js.map +0 -1
- package/dist/nuxt/runtime/server/api/editor/users.get.d.ts +0 -5
- package/dist/nuxt/runtime/server/api/editor/users.get.js +0 -8
- package/dist/nuxt/runtime/server/api/editor/users.get.js.map +0 -1
- package/dist/nuxt/runtime/server/plugins/collab.d.ts +0 -5
- package/dist/nuxt/runtime/server/plugins/collab.js +0 -10
- package/dist/nuxt/runtime/server/plugins/collab.js.map +0 -1
- package/dist/nuxt/runtime/server/routes/collab/[id].d.ts +0 -5
- package/dist/nuxt/runtime/server/routes/collab/[id].js +0 -35
- package/dist/nuxt/runtime/server/routes/collab/[id].js.map +0 -1
- package/dist/nuxt/runtime/server/utils/editor-route.d.ts +0 -11
- package/dist/nuxt/runtime/server/utils/editor-route.js.map +0 -1
- package/dist/presets/advanced.d.ts +0 -33
- package/dist/presets/advanced.js +0 -50
- package/dist/presets/advanced.js.map +0 -1
- package/dist/presets/simple.d.ts +0 -9
- package/dist/presets/simple.js +0 -12
- package/dist/presets/simple.js.map +0 -1
- package/dist/react/advanced-preset.d.ts +0 -11
- package/dist/react/advanced-preset.js +0 -31
- package/dist/react/advanced-preset.js.map +0 -1
- package/dist/vue/advanced-preset.d.ts +0 -13
- package/dist/vue/advanced-preset.js +0 -33
- package/dist/vue/advanced-preset.js.map +0 -1
- package/docs/extending.md +0 -538
- package/docs/integration.md +0 -632
- package/docs/llms.md +0 -383
- package/docs/usage.md +0 -1076
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collab.js","names":[],"sources":["../../src/server/collab.ts"],"sourcesContent":["import { type Configuration, Hocuspocus, type WebSocketLike } from '@hocuspocus/server'\nimport { type AnyExtension, getSchema, type JSONContent } from '@tiptap/core'\nimport type { Schema } from '@tiptap/pm/model'\nimport { prosemirrorToYDoc, prosemirrorToYXmlFragment, yDocToProsemirrorJSON, yXmlFragmentToProsemirror } from '@tiptap/y-tiptap'\nimport * as Y from 'yjs'\nimport { assignBlockIds, EMPTY_EDITOR_CONTENT } from '../core/schema'\nimport { LEGACY_COMPOSITE_NODE, migrateLegacyContent } from '../features/composites/schema'\nimport { advancedSchemaExtensions } from '../schema/advanced'\nimport type { EditorAdapter } from './adapter'\nimport { decodeCollabState, encodeCollabState, isStateStale } from './collab-state'\n\n/** Y.XmlFragment the TipTap Collaboration extension binds to (its default `field`). */\nexport const COLLAB_FIELD = 'default'\n\n/** Close code of a connection whose client could not be authenticated (Hocuspocus' `Unauthorized`, HTTP 401 in websocket terms). */\nconst UNAUTHORIZED = { code: 4401, reason: 'Unauthorized' }\n\n/** Context key under which `handleConnection` hands the connection's socket closer to the `onAuthenticate` hook. */\nconst REJECT = Symbol('collab.reject')\n\nexport interface CollabServerOptions {\n /** Storage and identity of the editor backend. */\n adapter: EditorAdapter\n /** Extensions of host-defined nodes and marks appended to `advancedSchemaExtensions()`; defaults to `adapter.schemaExtensions`. */\n schemaExtensions?: () => AnyExtension[]\n /** Per-document access and content versioning; each defaults to the adapter's method of the same name. */\n hooks?: {\n authorize?: NonNullable<EditorAdapter['authorize']>\n contentVersion?: NonNullable<EditorAdapter['contentVersion']>\n }\n /** Further Hocuspocus configuration (`debounce`, `timeout`, `quiet`, …); the editor's hooks win over equally named ones. */\n configuration?: Partial<Configuration>\n}\n\n/** One served websocket connection: feed it the socket's messages and its close event. */\nexport interface CollabConnection {\n handleMessage(data: Uint8Array): void\n handleClose(event?: CloseEvent): void\n}\n\nexport interface CollabServer {\n /**\n * The Hocuspocus instance of this server. The Yjs state (via the adapter's `loadState`/`storeState`) is the source of\n * truth for content, the host's JSON content (via the adapter) is a mirror kept up to date on every store.\n */\n hocuspocus: Hocuspocus\n /** Schema of the collab server: `advancedSchemaExtensions()` plus the host's extensions, built on first use. */\n schema(): Schema\n /**\n * Serves one websocket connection (any transport: crossws, `ws`, Bun, Deno). The user is authenticated in Hocuspocus'\n * `onAuthenticate` from the upgrade request's headers and the client's token, unless the transport already knows it\n * and passes it as `context.user`. Wire `handleMessage` and `handleClose` of the returned connection to the socket's events.\n */\n handleConnection(socket: WebSocketLike, request: Request, context?: { user?: object }): CollabConnection\n /** Tell connected clients the document is gone, disconnect them and drop the Yjs state. Call before removing the JSON. */\n forgetDocument(id: string): Promise<void>\n /** Current content of a document that is open in Hocuspocus (unsaved edits included), `null` when nobody has it open. */\n liveContent(id: string): JSONContent | null\n /**\n * The host rewrote the content behind the editor's back (an agent wrote the file): merge it into the open document now,\n * as a diff-update connected clients see as an ordinary edit. Without this the merge waits for the document's next store.\n * No-op when nobody has the document open or the content version is unchanged; needs the adapter's `contentVersion`.\n */\n contentChanged(id: string): Promise<void>\n /** Open collaboration connections and loaded documents of this server. */\n stats(): { connections: number, documents: number }\n /** Disconnect every client, write pending stores and wait until all documents are unloaded. The server accepts no connection afterwards. */\n close(): Promise<void>\n}\n\n/** ProseMirror document of stored JSON content: legacy nodes migrated, empty content replaced by the empty document, block IDs assigned. */\nfunction contentNode(schema: Schema, loaded: JSONContent) {\n const content = migrateLegacyContent(loaded)\n const node = schema.nodeFromJSON(content.type === 'doc' && content.content?.length ? content : EMPTY_EDITOR_CONTENT)\n return assignBlockIds(node) ?? node\n}\n\n/** Hocuspocus answers a thrown error's `reason` as permission-denied. */\nconst denied = (message: string, reason: string) => Object.assign(new Error(message), { reason })\n\n/**\n * The editor's collaboration server, independent of any web framework: Hocuspocus with the editor's hooks (seeding from\n * the adapter's JSON, versioned state, block-ID backfill, debounced `storeContent`/`storeState`) over `adapter`.\n * `/nuxt` mounts one in Nitro; any Node/Bun/Deno process can serve `handleConnection` on its own websocket transport.\n */\nexport function createCollabServer(options: CollabServerOptions): CollabServer {\n const { adapter } = options\n const authorize = (user: object, id: string) => options.hooks?.authorize ? options.hooks.authorize(user, id) : adapter.authorize?.(user, id)\n const contentVersion = (id: string) => options.hooks?.contentVersion ? options.hooks.contentVersion(id) : adapter.contentVersion?.(id)\n\n let schema: Schema | undefined\n const collabSchema = () => schema ??= getSchema([...advancedSchemaExtensions(), ...((options.schemaExtensions ?? adapter.schemaExtensions?.bind(adapter))?.() ?? [])])\n\n /** Loaded documents deleted via `forgetDocument`; a store still pending for them must not write their Yjs state back. */\n const forgotten = new Set<string>()\n /** `contentVersion` the loaded document's content mirror had when it was last read or written; only tracked for adapters with `contentVersion`. */\n const loadedVersions = new Map<string, string | null>()\n /** Resolvers of `close()` waiting for the last document to unload. */\n const idle: (() => void)[] = []\n let closed = false\n\n /** Merges content rewritten behind the editor's back into the open document and stores its state against `version`. */\n async function mergeRewrite(id: string, document: Y.Doc, version: string | null): Promise<void> {\n const rewritten = await adapter.loadContent(id)\n if (!rewritten) return // removed meanwhile: nothing to merge, nothing to mirror\n // A diff-update: unchanged blocks keep their Yjs items, so connected clients see an ordinary edit and need no reload.\n prosemirrorToYXmlFragment(contentNode(collabSchema(), rewritten), document.getXmlFragment(COLLAB_FIELD))\n loadedVersions.set(id, version)\n await adapter.storeState(id, encodeCollabState({ version, update: Y.encodeStateAsUpdate(document) }))\n }\n\n /** Stored state of a document, `null` when there is none usable: it was written against another content version and is removed. */\n async function loadCurrentState(id: string, liveVersion: string | null | undefined): Promise<Uint8Array | null> {\n const payload = await adapter.loadState(id)\n if (!payload) return null\n const { version, update } = decodeCollabState(payload)\n if (isStateStale(version, liveVersion)) {\n await adapter.removeState(id)\n return null\n }\n return update\n }\n\n const hocuspocus = new Hocuspocus({\n ...options.configuration,\n async onAuthenticate({ context, token, requestHeaders, documentName, connectionConfig }) {\n // The transport may know the user already; otherwise it comes from the upgrade request's headers or the client's token.\n let user = context.user as object | null | undefined\n if (!user) user = await adapter.authenticate({ headers: requestHeaders, ...(token && { token }) })\n if (!user) {\n // After the permission-denied answer Hocuspocus sends: close the socket, so a client without credentials does not linger.\n const socketCloser = context[REJECT] as (() => void) | undefined\n socketCloser?.()\n throw denied('Unauthorized', 'unauthorized')\n }\n const access = await authorize(user, documentName)\n // `undefined`: no authorize hook, every authenticated user may edit. `null`: rejected; the thrown error's `reason` is answered as permission-denied.\n if (access === null) throw denied('Forbidden', 'forbidden')\n if (access?.readOnly) connectionConfig.readOnly = true\n return { user }\n },\n async onLoadDocument({ documentName }) {\n const currentSchema = collabSchema()\n // Read before the content: a change in between leaves an older version on the state, which only costs a re-seed.\n const version = await contentVersion(documentName)\n const stored = await loadCurrentState(documentName, version)\n if (version !== undefined) loadedVersions.set(documentName, version)\n const persist = (doc: Y.Doc) => adapter.storeState(documentName, encodeCollabState({ version: version ?? null, update: Y.encodeStateAsUpdate(doc) }))\n if (stored) {\n // Decoded into a scratch doc: Hocuspocus only gets it once it is known to be current.\n const doc = new Y.Doc()\n Y.applyUpdate(doc, stored)\n const fragment = doc.getXmlFragment(COLLAB_FIELD)\n // State from before the composite family holds node names the schema no longer knows (any depth): drop it and\n // reseed below from the migrated JSON mirror, which also rewrites the stored state.\n const legacy = !fragment.createTreeWalker(node => node instanceof Y.XmlElement && node.nodeName === LEGACY_COMPOSITE_NODE).next().done\n if (!legacy) {\n // Blocks without an ID (state from before block IDs): assign them here, before any client syncs, so clients\n // never backfill concurrently. The diff-update only sets attributes; the content keeps its Yjs items.\n const withIds = assignBlockIds(yXmlFragmentToProsemirror(currentSchema, fragment))\n if (withIds) {\n prosemirrorToYXmlFragment(withIds, fragment)\n // Persist right away: a backfill after a restart would assign other IDs than clients already hold.\n await persist(doc)\n }\n return doc\n }\n }\n const loaded = await adapter.loadContent(documentName)\n if (!loaded) throw new Error('Dokument nicht gefunden') // Hocuspocus closes the connection\n // No usable Yjs state (new, legacy, stale or restored document): seed it from the stored JSON, with block IDs.\n const seeded = prosemirrorToYDoc(contentNode(currentSchema, loaded), COLLAB_FIELD)\n // Persist right away: re-seeding later would create new Yjs items that duplicate what clients still hold.\n await persist(seeded)\n return seeded\n },\n async onStoreDocument({ documentName, document }) {\n if (forgotten.has(documentName)) return\n const tracked = loadedVersions.get(documentName)\n const current = tracked === undefined ? undefined : await contentVersion(documentName)\n if (current !== undefined && current !== tracked) {\n // The content was rewritten behind the editor's back: writing the Yjs content back would clobber the rewrite.\n // Merge the rewrite into the open document instead and store the state against the new version.\n await mergeRewrite(documentName, document, current)\n return\n }\n await adapter.storeContent(documentName, yDocToProsemirrorJSON(document, COLLAB_FIELD))\n // After the content: the write itself moves the version, and the state must carry the one it matches.\n const version = await contentVersion(documentName)\n if (version !== undefined) loadedVersions.set(documentName, version)\n await adapter.storeState(documentName, encodeCollabState({ version: version ?? null, update: Y.encodeStateAsUpdate(document) }))\n },\n async afterUnloadDocument({ documentName }) {\n forgotten.delete(documentName)\n loadedVersions.delete(documentName)\n if (hocuspocus.getDocumentsCount() === 0) idle.splice(0).forEach(resolve => resolve())\n },\n })\n\n return {\n hocuspocus,\n schema: collabSchema,\n handleConnection(socket, request, context = {}) {\n if (closed) throw new Error('tiptap-editor: der Collab-Server ist geschlossen')\n // Deferred: Hocuspocus answers permission-denied first, then the socket closes.\n const reject = () => void setTimeout(() => socket.close(UNAUTHORIZED.code, UNAUTHORIZED.reason), 0)\n return hocuspocus.handleConnection(socket, request, { ...context, [REJECT]: reject })\n },\n async forgetDocument(id) {\n const doc = hocuspocus.documents.get(id)\n if (doc) {\n forgotten.add(id)\n doc.broadcastStateless('deleted')\n hocuspocus.closeConnections(id)\n }\n await adapter.removeState(id)\n },\n liveContent(id) {\n const doc = hocuspocus.documents.get(id)\n return doc ? yDocToProsemirrorJSON(doc, COLLAB_FIELD) as JSONContent : null\n },\n async contentChanged(id) {\n const doc = hocuspocus.documents.get(id)\n if (!doc || forgotten.has(id)) return\n const tracked = loadedVersions.get(id)\n if (tracked === undefined) return // no `contentVersion`: a rewrite cannot be told from the server's own writes\n const current = await contentVersion(id)\n if (current === undefined || current === tracked) return\n await mergeRewrite(id, doc, current)\n },\n stats: () => ({ connections: hocuspocus.getConnectionsCount(), documents: hocuspocus.getDocumentsCount() }),\n async close() {\n closed = true\n if (hocuspocus.getDocumentsCount() === 0) return\n await new Promise<void>((resolve) => {\n idle.push(resolve)\n hocuspocus.closeConnections()\n hocuspocus.flushPendingStores()\n })\n },\n }\n}\n\nexport type { WebSocketLike }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collab.js","names":[],"sources":["../../src/server/collab.ts"],"sourcesContent":["import { type Configuration, Hocuspocus, type WebSocketLike } from '@hocuspocus/server'\nimport { type AnyExtension, getSchema, type JSONContent } from '@tiptap/core'\nimport type { Schema } from '@tiptap/pm/model'\nimport { prosemirrorToYDoc, prosemirrorToYXmlFragment, yDocToProsemirrorJSON, yXmlFragmentToProsemirror } from '@tiptap/y-tiptap'\nimport * as Y from 'yjs'\nimport { assignBlockIds, EMPTY_EDITOR_CONTENT } from '../core/schema'\nimport { LEGACY_COMPOSITE_NODE, migrateLegacyContent } from '../features/composites/schema'\nimport { advancedSchemaExtensions } from '../schema/advanced'\nimport { decodeCollabState, encodeCollabState, isStateStale } from './collab-state'\nimport type { CollabStateStore, DocumentStore, EditorPorts, Identity } from './ports'\n\n/** Y.XmlFragment the TipTap Collaboration extension binds to (its default `field`). */\nexport const COLLAB_FIELD = 'default'\n\n/** Close code of a connection whose client could not be authenticated (Hocuspocus' `Unauthorized`, HTTP 401 in websocket terms). */\nconst UNAUTHORIZED = { code: 4401, reason: 'Unauthorized' }\n\n/** Context key under which `handleConnection` hands the connection's socket closer to the `onAuthenticate` hook. */\nconst REJECT = Symbol('collab.reject')\n\nexport interface CollabServerOptions {\n /** Identity, documents and Yjs state of the editor backend; `collabState` is required. */\n ports: EditorPorts\n /** TipTap extensions of the collaboration schema; default `advancedSchemaExtensions()`. Host-defined nodes and marks: `() => [...advancedSchemaExtensions(), HostNode]` (Vue-free node files only, no node views). */\n schema?: () => AnyExtension[]\n /** Per-document access and content versioning; each defaults to the port's method of the same name. */\n hooks?: {\n authorize?: NonNullable<Identity['authorize']>\n contentVersion?: NonNullable<DocumentStore['contentVersion']>\n }\n /** Further Hocuspocus configuration (`debounce`, `timeout`, `quiet`, …); the editor's hooks win over equally named ones. */\n configuration?: Partial<Configuration>\n}\n\n/** One served websocket connection: feed it the socket's messages and its close event. */\nexport interface CollabConnection {\n handleMessage(data: Uint8Array): void\n handleClose(event?: CloseEvent): void\n}\n\nexport interface CollabServer {\n /**\n * The Hocuspocus instance of this server. The Yjs state (via the `collabState` port's `loadState`/`storeState`) is the\n * source of truth for content, the host's JSON content (via the `documents` port) is a mirror kept up to date on every store.\n */\n hocuspocus: Hocuspocus\n /** Schema of the collab server: `options.schema()` (default `advancedSchemaExtensions()`), built on first use. */\n schema(): Schema\n /**\n * Serves one websocket connection (any transport: crossws, `ws`, Bun, Deno). The user is authenticated in Hocuspocus'\n * `onAuthenticate` from the upgrade request's headers and the client's token, unless the transport already knows it\n * and passes it as `context.user`. Wire `handleMessage` and `handleClose` of the returned connection to the socket's events.\n */\n handleConnection(socket: WebSocketLike, request: Request, context?: { user?: object }): CollabConnection\n /** Tell connected clients the document is gone, disconnect them and drop the Yjs state. Call before removing the JSON. */\n forgetDocument(id: string): Promise<void>\n /** Current content of a document that is open in Hocuspocus (unsaved edits included), `null` when nobody has it open. */\n liveContent(id: string): JSONContent | null\n /**\n * The host rewrote the content behind the editor's back (an agent wrote the file): merge it into the open document now,\n * as a diff-update connected clients see as an ordinary edit. Without this the merge waits for the document's next store.\n * No-op when nobody has the document open or the content version is unchanged; needs the documents port's `contentVersion`.\n */\n contentChanged(id: string): Promise<void>\n /** Open collaboration connections and loaded documents of this server. */\n stats(): { connections: number, documents: number }\n /** Disconnect every client, write pending stores and wait until all documents are unloaded. The server accepts no connection afterwards. */\n close(): Promise<void>\n}\n\n/** ProseMirror document of stored JSON content: legacy nodes migrated, empty content replaced by the empty document, block IDs assigned. */\nfunction contentNode(schema: Schema, loaded: JSONContent) {\n const content = migrateLegacyContent(loaded)\n const node = schema.nodeFromJSON(content.type === 'doc' && content.content?.length ? content : EMPTY_EDITOR_CONTENT)\n return assignBlockIds(node) ?? node\n}\n\n/** Whether a `document` Baustein of the stored Yjs content still carries the flat `meta.transcript` that `migrateLegacyContent` turns into a `rendition` child. */\nfunction hasFlatTranscript(fragment: Y.XmlFragment): boolean {\n return !fragment.createTreeWalker((node) => {\n if (!(node instanceof Y.XmlElement) || node.nodeName !== 'composite' || node.getAttribute('kind') !== 'document') return false\n const meta: unknown = node.getAttribute('meta')\n return typeof meta === 'object' && meta !== null && 'transcript' in meta && typeof meta.transcript === 'string'\n }).next().done\n}\n\n/** Hocuspocus answers a thrown error's `reason` as permission-denied. */\nconst denied = (message: string, reason: string) => Object.assign(new Error(message), { reason })\n\n/** The `collabState` port; the collaboration server cannot run without it. */\nfunction collabStateOf(ports: EditorPorts): CollabStateStore {\n if (!ports.collabState) throw new Error('@ecsplico/editor: collaboration needs the collabState port')\n return ports.collabState\n}\n\n/**\n * The editor's collaboration server, independent of any web framework: Hocuspocus with the editor's hooks (seeding from\n * the stored JSON, versioned state, block-ID backfill, debounced `storeContent`/`storeState`) over `options.ports`.\n * `/nuxt` mounts one in Nitro; any Node/Bun/Deno process can serve `handleConnection` on its own websocket transport.\n */\nexport function createCollabServer(options: CollabServerOptions): CollabServer {\n const { identity, documents } = options.ports\n const state = collabStateOf(options.ports)\n const authorize = (user: object, id: string) => options.hooks?.authorize ? options.hooks.authorize(user, id) : identity.authorize?.(user, id)\n const contentVersion = (id: string) => options.hooks?.contentVersion ? options.hooks.contentVersion(id) : documents.contentVersion?.(id)\n\n let schema: Schema | undefined\n const collabSchema = () => schema ??= getSchema((options.schema ?? advancedSchemaExtensions)())\n\n /** Loaded documents deleted via `forgetDocument`; a store still pending for them must not write their Yjs state back. */\n const forgotten = new Set<string>()\n /** `contentVersion` the loaded document's content mirror had when it was last read or written; only tracked for documents ports with `contentVersion`. */\n const loadedVersions = new Map<string, string | null>()\n /** Resolvers of `close()` waiting for the last document to unload. */\n const idle: (() => void)[] = []\n let closed = false\n\n /** Merges content rewritten behind the editor's back into the open document and stores its state against `version`. */\n async function mergeRewrite(id: string, document: Y.Doc, version: string | null): Promise<void> {\n const rewritten = await documents.loadContent(id)\n if (!rewritten) return // removed meanwhile: nothing to merge, nothing to mirror\n // A diff-update: unchanged blocks keep their Yjs items, so connected clients see an ordinary edit and need no reload.\n prosemirrorToYXmlFragment(contentNode(collabSchema(), rewritten), document.getXmlFragment(COLLAB_FIELD))\n loadedVersions.set(id, version)\n await state.storeState(id, encodeCollabState({ version, update: Y.encodeStateAsUpdate(document) }))\n }\n\n /** Stored state of a document, `null` when there is none usable: it was written against another content version and is removed. */\n async function loadCurrentState(id: string, liveVersion: string | null | undefined): Promise<Uint8Array | null> {\n const payload = await state.loadState(id)\n if (!payload) return null\n const { version, update } = decodeCollabState(payload)\n if (isStateStale(version, liveVersion)) {\n await state.removeState(id)\n return null\n }\n return update\n }\n\n const hocuspocus = new Hocuspocus({\n ...options.configuration,\n async onAuthenticate({ context, token, requestHeaders, documentName, connectionConfig }) {\n // The transport may know the user already; otherwise it comes from the upgrade request's headers or the client's token.\n let user = context.user as object | null | undefined\n if (!user) user = await identity.authenticate({ headers: requestHeaders, ...(token && { token }) })\n if (!user) {\n // After the permission-denied answer Hocuspocus sends: close the socket, so a client without credentials does not linger.\n const socketCloser = context[REJECT] as (() => void) | undefined\n socketCloser?.()\n throw denied('Unauthorized', 'unauthorized')\n }\n const access = await authorize(user, documentName)\n // `undefined`: no authorize hook, every authenticated user may edit. `null`: rejected; the thrown error's `reason` is answered as permission-denied.\n if (access === null) throw denied('Forbidden', 'forbidden')\n if (access?.readOnly) connectionConfig.readOnly = true\n return { user }\n },\n async onLoadDocument({ documentName }) {\n const currentSchema = collabSchema()\n // Read before the content: a change in between leaves an older version on the state, which only costs a re-seed.\n const version = await contentVersion(documentName)\n const stored = await loadCurrentState(documentName, version)\n if (version !== undefined) loadedVersions.set(documentName, version)\n const persist = (doc: Y.Doc) => state.storeState(documentName, encodeCollabState({ version: version ?? null, update: Y.encodeStateAsUpdate(doc) }))\n if (stored) {\n // Decoded into a scratch doc: Hocuspocus only gets it once it is known to be current.\n const doc = new Y.Doc()\n Y.applyUpdate(doc, stored)\n const fragment = doc.getXmlFragment(COLLAB_FIELD)\n // State from before the composite family holds node names the schema no longer knows (any depth): drop it and\n // reseed below from the migrated JSON mirror, which also rewrites the stored state.\n const legacy = !fragment.createTreeWalker(node => node instanceof Y.XmlElement && node.nodeName === LEGACY_COMPOSITE_NODE).next().done\n if (!legacy) {\n // Blocks without an ID (state from before block IDs) and a `document` Baustein's flat `meta.transcript` (it\n // became a `rendition` child): migrate here, before any client syncs, so clients never backfill concurrently.\n // The diff-update keeps the Yjs items of everything that did not change.\n const current = yXmlFragmentToProsemirror(currentSchema, fragment)\n const migrated = hasFlatTranscript(fragment) ? currentSchema.nodeFromJSON(migrateLegacyContent(current.toJSON())) : current\n const withIds = assignBlockIds(migrated) ?? (migrated === current ? null : migrated)\n if (withIds) {\n prosemirrorToYXmlFragment(withIds, fragment)\n // Persist right away: a backfill after a restart would assign other IDs than clients already hold.\n await persist(doc)\n }\n return doc\n }\n }\n const loaded = await documents.loadContent(documentName)\n if (!loaded) throw new Error('Dokument nicht gefunden') // Hocuspocus closes the connection\n // No usable Yjs state (new, legacy, stale or restored document): seed it from the stored JSON, with block IDs.\n const seeded = prosemirrorToYDoc(contentNode(currentSchema, loaded), COLLAB_FIELD)\n // Persist right away: re-seeding later would create new Yjs items that duplicate what clients still hold.\n await persist(seeded)\n return seeded\n },\n async onStoreDocument({ documentName, document }) {\n if (forgotten.has(documentName)) return\n const tracked = loadedVersions.get(documentName)\n const current = tracked === undefined ? undefined : await contentVersion(documentName)\n if (current !== undefined && current !== tracked) {\n // The content was rewritten behind the editor's back: writing the Yjs content back would clobber the rewrite.\n // Merge the rewrite into the open document instead and store the state against the new version.\n await mergeRewrite(documentName, document, current)\n return\n }\n await documents.storeContent(documentName, yDocToProsemirrorJSON(document, COLLAB_FIELD))\n // After the content: the write itself moves the version, and the state must carry the one it matches.\n const version = await contentVersion(documentName)\n if (version !== undefined) loadedVersions.set(documentName, version)\n await state.storeState(documentName, encodeCollabState({ version: version ?? null, update: Y.encodeStateAsUpdate(document) }))\n },\n async afterUnloadDocument({ documentName }) {\n forgotten.delete(documentName)\n loadedVersions.delete(documentName)\n if (hocuspocus.getDocumentsCount() === 0) idle.splice(0).forEach(resolve => resolve())\n },\n })\n\n return {\n hocuspocus,\n schema: collabSchema,\n handleConnection(socket, request, context = {}) {\n if (closed) throw new Error('tiptap-editor: der Collab-Server ist geschlossen')\n // Deferred: Hocuspocus answers permission-denied first, then the socket closes.\n const reject = () => void setTimeout(() => socket.close(UNAUTHORIZED.code, UNAUTHORIZED.reason), 0)\n return hocuspocus.handleConnection(socket, request, { ...context, [REJECT]: reject })\n },\n async forgetDocument(id) {\n const doc = hocuspocus.documents.get(id)\n if (doc) {\n forgotten.add(id)\n doc.broadcastStateless('deleted')\n hocuspocus.closeConnections(id)\n }\n await state.removeState(id)\n },\n liveContent(id) {\n const doc = hocuspocus.documents.get(id)\n return doc ? yDocToProsemirrorJSON(doc, COLLAB_FIELD) as JSONContent : null\n },\n async contentChanged(id) {\n const doc = hocuspocus.documents.get(id)\n if (!doc || forgotten.has(id)) return\n const tracked = loadedVersions.get(id)\n if (tracked === undefined) return // no `contentVersion`: a rewrite cannot be told from the server's own writes\n const current = await contentVersion(id)\n if (current === undefined || current === tracked) return\n await mergeRewrite(id, doc, current)\n },\n stats: () => ({ connections: hocuspocus.getConnectionsCount(), documents: hocuspocus.getDocumentsCount() }),\n async close() {\n closed = true\n if (hocuspocus.getDocumentsCount() === 0) return\n await new Promise<void>((resolve) => {\n idle.push(resolve)\n hocuspocus.closeConnections()\n hocuspocus.flushPendingStores()\n })\n },\n }\n}\n\nexport type { WebSocketLike }\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,eAAe;;AAG5B,MAAM,eAAe;CAAE,MAAM;CAAM,QAAQ;AAAe;;AAG1D,MAAM,SAAS,OAAO,eAAe;;AAqDrC,SAAS,YAAY,QAAgB,QAAqB;CACxD,MAAM,UAAU,qBAAqB,MAAM;CAC3C,MAAM,OAAO,OAAO,aAAa,QAAQ,SAAS,SAAS,QAAQ,SAAS,SAAS,UAAU,oBAAoB;CACnH,OAAO,eAAe,IAAI,KAAK;AACjC;;AAGA,SAAS,kBAAkB,UAAkC;CAC3D,OAAO,CAAC,SAAS,kBAAkB,SAAS;EAC1C,IAAI,EAAE,gBAAgB,EAAE,eAAe,KAAK,aAAa,eAAe,KAAK,aAAa,MAAM,MAAM,YAAY,OAAO;EACzH,MAAM,OAAgB,KAAK,aAAa,MAAM;EAC9C,OAAO,OAAO,SAAS,YAAY,SAAS,QAAQ,gBAAgB,QAAQ,OAAO,KAAK,eAAe;CACzG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACZ;;AAGA,MAAM,UAAU,SAAiB,WAAmB,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,EAAE,OAAO,CAAC;;AAGhG,SAAS,cAAc,OAAsC;CAC3D,IAAI,CAAC,MAAM,aAAa,MAAM,IAAI,MAAM,4DAA4D;CACpG,OAAO,MAAM;AACf;;;;;;AAOA,SAAgB,mBAAmB,SAA4C;CAC7E,MAAM,EAAE,UAAU,cAAc,QAAQ;CACxC,MAAM,QAAQ,cAAc,QAAQ,KAAK;CACzC,MAAM,aAAa,MAAc,OAAe,QAAQ,OAAO,YAAY,QAAQ,MAAM,UAAU,MAAM,EAAE,IAAI,SAAS,YAAY,MAAM,EAAE;CAC5I,MAAM,kBAAkB,OAAe,QAAQ,OAAO,iBAAiB,QAAQ,MAAM,eAAe,EAAE,IAAI,UAAU,iBAAiB,EAAE;CAEvI,IAAI;CACJ,MAAM,qBAAqB,WAAW,WAAW,QAAQ,UAAU,yBAAA,CAA0B,CAAC;;CAG9F,MAAM,4BAAY,IAAI,IAAY;;CAElC,MAAM,iCAAiB,IAAI,IAA2B;;CAEtD,MAAM,OAAuB,CAAC;CAC9B,IAAI,SAAS;;CAGb,eAAe,aAAa,IAAY,UAAiB,SAAuC;EAC9F,MAAM,YAAY,MAAM,UAAU,YAAY,EAAE;EAChD,IAAI,CAAC,WAAW;EAEhB,0BAA0B,YAAY,aAAa,GAAG,SAAS,GAAG,SAAS,eAAe,YAAY,CAAC;EACvG,eAAe,IAAI,IAAI,OAAO;EAC9B,MAAM,MAAM,WAAW,IAAI,kBAAkB;GAAE;GAAS,QAAQ,EAAE,oBAAoB,QAAQ;EAAE,CAAC,CAAC;CACpG;;CAGA,eAAe,iBAAiB,IAAY,aAAoE;EAC9G,MAAM,UAAU,MAAM,MAAM,UAAU,EAAE;EACxC,IAAI,CAAC,SAAS,OAAO;EACrB,MAAM,EAAE,SAAS,WAAW,kBAAkB,OAAO;EACrD,IAAI,aAAa,SAAS,WAAW,GAAG;GACtC,MAAM,MAAM,YAAY,EAAE;GAC1B,OAAO;EACT;EACA,OAAO;CACT;CAEA,MAAM,aAAa,IAAI,WAAW;EAChC,GAAG,QAAQ;EACX,MAAM,eAAe,EAAE,SAAS,OAAO,gBAAgB,cAAc,oBAAoB;GAEvF,IAAI,OAAO,QAAQ;GACnB,IAAI,CAAC,MAAM,OAAO,MAAM,SAAS,aAAa;IAAE,SAAS;IAAgB,GAAI,SAAS,EAAE,MAAM;GAAG,CAAC;GAClG,IAAI,CAAC,MAAM;IAET,MAAM,eAAe,QAAQ;IAC7B,eAAe;IACf,MAAM,OAAO,gBAAgB,cAAc;GAC7C;GACA,MAAM,SAAS,MAAM,UAAU,MAAM,YAAY;GAEjD,IAAI,WAAW,MAAM,MAAM,OAAO,aAAa,WAAW;GAC1D,IAAI,QAAQ,UAAU,iBAAiB,WAAW;GAClD,OAAO,EAAE,KAAK;EAChB;EACA,MAAM,eAAe,EAAE,gBAAgB;GACrC,MAAM,gBAAgB,aAAa;GAEnC,MAAM,UAAU,MAAM,eAAe,YAAY;GACjD,MAAM,SAAS,MAAM,iBAAiB,cAAc,OAAO;GAC3D,IAAI,YAAY,KAAA,GAAW,eAAe,IAAI,cAAc,OAAO;GACnE,MAAM,WAAW,QAAe,MAAM,WAAW,cAAc,kBAAkB;IAAE,SAAS,WAAW;IAAM,QAAQ,EAAE,oBAAoB,GAAG;GAAE,CAAC,CAAC;GAClJ,IAAI,QAAQ;IAEV,MAAM,MAAM,IAAI,EAAE,IAAI;IACtB,EAAE,YAAY,KAAK,MAAM;IACzB,MAAM,WAAW,IAAI,eAAe,YAAY;IAIhD,IAAI,CAAC,CADW,SAAS,kBAAiB,SAAQ,gBAAgB,EAAE,cAAc,KAAK,aAAA,SAAkC,CAAC,CAAC,KAAK,CAAC,CAAC,MACrH;KAIX,MAAM,UAAU,0BAA0B,eAAe,QAAQ;KACjE,MAAM,WAAW,kBAAkB,QAAQ,IAAI,cAAc,aAAa,qBAAqB,QAAQ,OAAO,CAAC,CAAC,IAAI;KACpH,MAAM,UAAU,eAAe,QAAQ,MAAM,aAAa,UAAU,OAAO;KAC3E,IAAI,SAAS;MACX,0BAA0B,SAAS,QAAQ;MAE3C,MAAM,QAAQ,GAAG;KACnB;KACA,OAAO;IACT;GACF;GACA,MAAM,SAAS,MAAM,UAAU,YAAY,YAAY;GACvD,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,yBAAyB;GAEtD,MAAM,SAAS,kBAAkB,YAAY,eAAe,MAAM,GAAG,YAAY;GAEjF,MAAM,QAAQ,MAAM;GACpB,OAAO;EACT;EACA,MAAM,gBAAgB,EAAE,cAAc,YAAY;GAChD,IAAI,UAAU,IAAI,YAAY,GAAG;GACjC,MAAM,UAAU,eAAe,IAAI,YAAY;GAC/C,MAAM,UAAU,YAAY,KAAA,IAAY,KAAA,IAAY,MAAM,eAAe,YAAY;GACrF,IAAI,YAAY,KAAA,KAAa,YAAY,SAAS;IAGhD,MAAM,aAAa,cAAc,UAAU,OAAO;IAClD;GACF;GACA,MAAM,UAAU,aAAa,cAAc,sBAAsB,UAAU,YAAY,CAAC;GAExF,MAAM,UAAU,MAAM,eAAe,YAAY;GACjD,IAAI,YAAY,KAAA,GAAW,eAAe,IAAI,cAAc,OAAO;GACnE,MAAM,MAAM,WAAW,cAAc,kBAAkB;IAAE,SAAS,WAAW;IAAM,QAAQ,EAAE,oBAAoB,QAAQ;GAAE,CAAC,CAAC;EAC/H;EACA,MAAM,oBAAoB,EAAE,gBAAgB;GAC1C,UAAU,OAAO,YAAY;GAC7B,eAAe,OAAO,YAAY;GAClC,IAAI,WAAW,kBAAkB,MAAM,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,SAAQ,YAAW,QAAQ,CAAC;EACvF;CACF,CAAC;CAED,OAAO;EACL;EACA,QAAQ;EACR,iBAAiB,QAAQ,SAAS,UAAU,CAAC,GAAG;GAC9C,IAAI,QAAQ,MAAM,IAAI,MAAM,kDAAkD;GAE9E,MAAM,eAAe,KAAK,iBAAiB,OAAO,MAAM,aAAa,MAAM,aAAa,MAAM,GAAG,CAAC;GAClG,OAAO,WAAW,iBAAiB,QAAQ,SAAS;IAAE,GAAG;KAAU,SAAS;GAAO,CAAC;EACtF;EACA,MAAM,eAAe,IAAI;GACvB,MAAM,MAAM,WAAW,UAAU,IAAI,EAAE;GACvC,IAAI,KAAK;IACP,UAAU,IAAI,EAAE;IAChB,IAAI,mBAAmB,SAAS;IAChC,WAAW,iBAAiB,EAAE;GAChC;GACA,MAAM,MAAM,YAAY,EAAE;EAC5B;EACA,YAAY,IAAI;GACd,MAAM,MAAM,WAAW,UAAU,IAAI,EAAE;GACvC,OAAO,MAAM,sBAAsB,KAAK,YAAY,IAAmB;EACzE;EACA,MAAM,eAAe,IAAI;GACvB,MAAM,MAAM,WAAW,UAAU,IAAI,EAAE;GACvC,IAAI,CAAC,OAAO,UAAU,IAAI,EAAE,GAAG;GAC/B,MAAM,UAAU,eAAe,IAAI,EAAE;GACrC,IAAI,YAAY,KAAA,GAAW;GAC3B,MAAM,UAAU,MAAM,eAAe,EAAE;GACvC,IAAI,YAAY,KAAA,KAAa,YAAY,SAAS;GAClD,MAAM,aAAa,IAAI,KAAK,OAAO;EACrC;EACA,cAAc;GAAE,aAAa,WAAW,oBAAoB;GAAG,WAAW,WAAW,kBAAkB;EAAE;EACzG,MAAM,QAAQ;GACZ,SAAS;GACT,IAAI,WAAW,kBAAkB,MAAM,GAAG;GAC1C,MAAM,IAAI,SAAe,YAAY;IACnC,KAAK,KAAK,OAAO;IACjB,WAAW,iBAAiB;IAC5B,WAAW,mBAAmB;GAChC,CAAC;EACH;CACF;AACF"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/server/crossref.ts
|
|
2
|
+
/** A DOI as the body of the extract route may carry it. */
|
|
3
|
+
const DOI_PATTERN = /^10\.\d{4,9}\/\S+$/;
|
|
4
|
+
const DOI_IN_TEXT = /10\.\d{4,9}\/[^\s"<>]+/;
|
|
5
|
+
/** The first DOI in `text`, without the punctuation that ends a sentence or closes a bracket; `null` when there is none. */
|
|
6
|
+
function doiIn(text) {
|
|
7
|
+
const match = DOI_IN_TEXT.exec(text);
|
|
8
|
+
return match ? match[0].replace(/[.,;:)\]}]+$/, "") : null;
|
|
9
|
+
}
|
|
10
|
+
const CSL_TYPE_OF_CROSSREF = {
|
|
11
|
+
"journal-article": "article-journal",
|
|
12
|
+
"book": "book",
|
|
13
|
+
"monograph": "book",
|
|
14
|
+
"edited-book": "book",
|
|
15
|
+
"book-chapter": "chapter",
|
|
16
|
+
"proceedings-article": "paper-conference",
|
|
17
|
+
"dissertation": "thesis",
|
|
18
|
+
"report": "report",
|
|
19
|
+
"dataset": "dataset"
|
|
20
|
+
};
|
|
21
|
+
const namesOf = (people) => {
|
|
22
|
+
const names = (people ?? []).flatMap((person) => {
|
|
23
|
+
if (person.family || person.given) return [{
|
|
24
|
+
...person.family && { family: person.family },
|
|
25
|
+
...person.given && { given: person.given }
|
|
26
|
+
}];
|
|
27
|
+
return person.name ? [{ literal: person.name }] : [];
|
|
28
|
+
});
|
|
29
|
+
return names.length ? names : void 0;
|
|
30
|
+
};
|
|
31
|
+
/** The CSL-JSON item of a Crossref `message`, plus the work's title. Absent or empty fields are left out. */
|
|
32
|
+
function cslFromCrossref(work) {
|
|
33
|
+
const year = work.issued?.["date-parts"]?.[0]?.[0];
|
|
34
|
+
const abstract = work.abstract?.replace(/<[^>]+>/g, "").trim();
|
|
35
|
+
return {
|
|
36
|
+
type: CSL_TYPE_OF_CROSSREF[work.type ?? ""] ?? "article",
|
|
37
|
+
...work.title?.[0] && { title: work.title[0] },
|
|
38
|
+
...namesOf(work.author) && { author: namesOf(work.author) },
|
|
39
|
+
...namesOf(work.editor) && { editor: namesOf(work.editor) },
|
|
40
|
+
...typeof year === "number" && { issued: { "date-parts": [[year]] } },
|
|
41
|
+
...work["container-title"]?.[0] && { "container-title": work["container-title"][0] },
|
|
42
|
+
...work.volume && { volume: work.volume },
|
|
43
|
+
...work.issue && { issue: work.issue },
|
|
44
|
+
...work.page && { page: work.page },
|
|
45
|
+
...work.publisher && { publisher: work.publisher },
|
|
46
|
+
...work["publisher-location"] && { "publisher-place": work["publisher-location"] },
|
|
47
|
+
...work.DOI && { DOI: work.DOI },
|
|
48
|
+
...work.ISBN?.[0] && { ISBN: work.ISBN[0] },
|
|
49
|
+
...work.URL && { URL: work.URL },
|
|
50
|
+
...abstract && { abstract },
|
|
51
|
+
...work.language && { language: work.language }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const TIMEOUT_MS = 1e4;
|
|
55
|
+
/**
|
|
56
|
+
* The Crossref record of `doi`; `null` when Crossref does not know it, answers with an error or cannot be reached
|
|
57
|
+
* (offline, timeout): the caller falls back to model extraction.
|
|
58
|
+
*/
|
|
59
|
+
async function fetchCrossref(doi, signal) {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`https://api.crossref.org/works/${encodeURIComponent(doi)}`, {
|
|
62
|
+
headers: {
|
|
63
|
+
"User-Agent": "ecsplico-editor",
|
|
64
|
+
"Accept": "application/json"
|
|
65
|
+
},
|
|
66
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(TIMEOUT_MS)]) : AbortSignal.timeout(TIMEOUT_MS)
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) return null;
|
|
69
|
+
const body = await response.json();
|
|
70
|
+
return body.message ? cslFromCrossref(body.message) : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { DOI_PATTERN, cslFromCrossref, doiIn, fetchCrossref };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=crossref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossref.js","names":[],"sources":["../../src/server/crossref.ts"],"sourcesContent":["import type { CslItem, CslName, CslType } from '../core/backend-types'\n\n// Bibliographic data by DOI from the Crossref REST API (`https://api.crossref.org/works/<doi>`), as a CSL-JSON item.\n\n/** A DOI as the body of the extract route may carry it. */\nexport const DOI_PATTERN = /^10\\.\\d{4,9}\\/\\S+$/\n\nconst DOI_IN_TEXT = /10\\.\\d{4,9}\\/[^\\s\"<>]+/\n\n/** The first DOI in `text`, without the punctuation that ends a sentence or closes a bracket; `null` when there is none. */\nexport function doiIn(text: string): string | null {\n const match = DOI_IN_TEXT.exec(text)\n return match ? match[0].replace(/[.,;:)\\]}]+$/, '') : null\n}\n\nconst CSL_TYPE_OF_CROSSREF: Record<string, CslType> = {\n 'journal-article': 'article-journal',\n 'book': 'book',\n 'monograph': 'book',\n 'edited-book': 'book',\n 'book-chapter': 'chapter',\n 'proceedings-article': 'paper-conference',\n 'dissertation': 'thesis',\n 'report': 'report',\n 'dataset': 'dataset',\n}\n\ninterface CrossrefPerson { family?: string, given?: string, name?: string }\ninterface CrossrefWork {\n 'type'?: string\n 'title'?: string[]\n 'container-title'?: string[]\n 'author'?: CrossrefPerson[]\n 'editor'?: CrossrefPerson[]\n 'issued'?: { 'date-parts'?: (number | null)[][] }\n 'volume'?: string\n 'issue'?: string\n 'page'?: string\n 'publisher'?: string\n 'publisher-location'?: string\n 'DOI'?: string\n 'ISBN'?: string[]\n 'URL'?: string\n 'abstract'?: string\n 'language'?: string\n}\n\nconst namesOf = (people: CrossrefPerson[] | undefined): CslName[] | undefined => {\n const names = (people ?? []).flatMap((person): CslName[] => {\n if (person.family || person.given) return [{ ...(person.family && { family: person.family }), ...(person.given && { given: person.given }) }]\n return person.name ? [{ literal: person.name }] : []\n })\n return names.length ? names : undefined\n}\n\n/** The CSL-JSON item of a Crossref `message`, plus the work's title. Absent or empty fields are left out. */\nexport function cslFromCrossref(work: CrossrefWork): CslItem & { title?: string } {\n const year = work.issued?.['date-parts']?.[0]?.[0]\n const abstract = work.abstract?.replace(/<[^>]+>/g, '').trim()\n const item: CslItem & { title?: string } = {\n type: CSL_TYPE_OF_CROSSREF[work.type ?? ''] ?? 'article',\n ...(work.title?.[0] && { title: work.title[0] }),\n ...(namesOf(work.author) && { author: namesOf(work.author) }),\n ...(namesOf(work.editor) && { editor: namesOf(work.editor) }),\n ...(typeof year === 'number' && { issued: { 'date-parts': [[year]] } }),\n ...(work['container-title']?.[0] && { 'container-title': work['container-title'][0] }),\n ...(work.volume && { volume: work.volume }),\n ...(work.issue && { issue: work.issue }),\n ...(work.page && { page: work.page }),\n ...(work.publisher && { publisher: work.publisher }),\n ...(work['publisher-location'] && { 'publisher-place': work['publisher-location'] }),\n ...(work.DOI && { DOI: work.DOI }),\n ...(work.ISBN?.[0] && { ISBN: work.ISBN[0] }),\n ...(work.URL && { URL: work.URL }),\n ...(abstract && { abstract }),\n ...(work.language && { language: work.language }),\n }\n return item\n}\n\nconst TIMEOUT_MS = 10_000\n\n/**\n * The Crossref record of `doi`; `null` when Crossref does not know it, answers with an error or cannot be reached\n * (offline, timeout): the caller falls back to model extraction.\n */\nexport async function fetchCrossref(doi: string, signal?: AbortSignal): Promise<(CslItem & { title?: string }) | null> {\n try {\n const response = await fetch(`https://api.crossref.org/works/${encodeURIComponent(doi)}`, {\n headers: { 'User-Agent': 'ecsplico-editor', 'Accept': 'application/json' },\n signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(TIMEOUT_MS)]) : AbortSignal.timeout(TIMEOUT_MS),\n })\n if (!response.ok) return null\n const body = await response.json() as { message?: CrossrefWork }\n return body.message ? cslFromCrossref(body.message) : null\n }\n catch {\n return null\n }\n}\n"],"mappings":";;AAKA,MAAa,cAAc;AAE3B,MAAM,cAAc;;AAGpB,SAAgB,MAAM,MAA6B;CACjD,MAAM,QAAQ,YAAY,KAAK,IAAI;CACnC,OAAO,QAAQ,MAAM,EAAE,CAAC,QAAQ,gBAAgB,EAAE,IAAI;AACxD;AAEA,MAAM,uBAAgD;CACpD,mBAAmB;CACnB,QAAQ;CACR,aAAa;CACb,eAAe;CACf,gBAAgB;CAChB,uBAAuB;CACvB,gBAAgB;CAChB,UAAU;CACV,WAAW;AACb;AAsBA,MAAM,WAAW,WAAgE;CAC/E,MAAM,SAAS,UAAU,CAAC,EAAA,CAAG,SAAS,WAAsB;EAC1D,IAAI,OAAO,UAAU,OAAO,OAAO,OAAO,CAAC;GAAE,GAAI,OAAO,UAAU,EAAE,QAAQ,OAAO,OAAO;GAAI,GAAI,OAAO,SAAS,EAAE,OAAO,OAAO,MAAM;EAAG,CAAC;EAC5I,OAAO,OAAO,OAAO,CAAC,EAAE,SAAS,OAAO,KAAK,CAAC,IAAI,CAAC;CACrD,CAAC;CACD,OAAO,MAAM,SAAS,QAAQ,KAAA;AAChC;;AAGA,SAAgB,gBAAgB,MAAkD;CAChF,MAAM,OAAO,KAAK,SAAS,aAAa,GAAG,EAAE,GAAG;CAChD,MAAM,WAAW,KAAK,UAAU,QAAQ,YAAY,EAAE,CAAC,CAAC,KAAK;CAmB7D,OAAO;EAjBL,MAAM,qBAAqB,KAAK,QAAQ,OAAO;EAC/C,GAAI,KAAK,QAAQ,MAAM,EAAE,OAAO,KAAK,MAAM,GAAG;EAC9C,GAAI,QAAQ,KAAK,MAAM,KAAK,EAAE,QAAQ,QAAQ,KAAK,MAAM,EAAE;EAC3D,GAAI,QAAQ,KAAK,MAAM,KAAK,EAAE,QAAQ,QAAQ,KAAK,MAAM,EAAE;EAC3D,GAAI,OAAO,SAAS,YAAY,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE;EACrE,GAAI,KAAK,kBAAkB,GAAG,MAAM,EAAE,mBAAmB,KAAK,kBAAkB,CAAC,GAAG;EACpF,GAAI,KAAK,UAAU,EAAE,QAAQ,KAAK,OAAO;EACzC,GAAI,KAAK,SAAS,EAAE,OAAO,KAAK,MAAM;EACtC,GAAI,KAAK,QAAQ,EAAE,MAAM,KAAK,KAAK;EACnC,GAAI,KAAK,aAAa,EAAE,WAAW,KAAK,UAAU;EAClD,GAAI,KAAK,yBAAyB,EAAE,mBAAmB,KAAK,sBAAsB;EAClF,GAAI,KAAK,OAAO,EAAE,KAAK,KAAK,IAAI;EAChC,GAAI,KAAK,OAAO,MAAM,EAAE,MAAM,KAAK,KAAK,GAAG;EAC3C,GAAI,KAAK,OAAO,EAAE,KAAK,KAAK,IAAI;EAChC,GAAI,YAAY,EAAE,SAAS;EAC3B,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;CAEvC;AACZ;AAEA,MAAM,aAAa;;;;;AAMnB,eAAsB,cAAc,KAAa,QAAsE;CACrH,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,kCAAkC,mBAAmB,GAAG,KAAK;GACxF,SAAS;IAAE,cAAc;IAAmB,UAAU;GAAmB;GACzE,QAAQ,SAAS,YAAY,IAAI,CAAC,QAAQ,YAAY,QAAQ,UAAU,CAAC,CAAC,IAAI,YAAY,QAAQ,UAAU;EAC9G,CAAC;EACD,IAAI,CAAC,SAAS,IAAI,OAAO;EACzB,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,OAAO,KAAK,UAAU,gBAAgB,KAAK,OAAO,IAAI;CACxD,QACM;EACJ,OAAO;CACT;AACF"}
|
package/dist/server/errors.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* A request the server answers with an HTTP status: the handlers and the model utilities throw it instead of a
|
|
4
4
|
* framework error, so they run in any Node server. A transport maps `statusCode`/`statusMessage` (and `headers`, e.g.
|
|
5
|
-
* `Retry-After` of a 429) onto its response;
|
|
5
|
+
* `Retry-After` of a 429) onto its response; `server/h3.ts` hands it to Nitro's `createError`.
|
|
6
6
|
*/
|
|
7
7
|
export declare class HttpError extends Error {
|
|
8
8
|
readonly statusCode: number;
|
package/dist/server/errors.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* A request the server answers with an HTTP status: the handlers and the model utilities throw it instead of a
|
|
4
4
|
* framework error, so they run in any Node server. A transport maps `statusCode`/`statusMessage` (and `headers`, e.g.
|
|
5
|
-
* `Retry-After` of a 429) onto its response;
|
|
5
|
+
* `Retry-After` of a 429) onto its response; `server/h3.ts` hands it to Nitro's `createError`.
|
|
6
6
|
*/
|
|
7
7
|
var HttpError = class extends Error {
|
|
8
8
|
statusCode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../../src/server/errors.ts"],"sourcesContent":["/**\n * A request the server answers with an HTTP status: the handlers and the model utilities throw it instead of a\n * framework error, so they run in any Node server. A transport maps `statusCode`/`statusMessage` (and `headers`, e.g.\n * `Retry-After` of a 429) onto its response;
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../src/server/errors.ts"],"sourcesContent":["/**\n * A request the server answers with an HTTP status: the handlers and the model utilities throw it instead of a\n * framework error, so they run in any Node server. A transport maps `statusCode`/`statusMessage` (and `headers`, e.g.\n * `Retry-After` of a 429) onto its response; `server/h3.ts` hands it to Nitro's `createError`.\n */\nexport class HttpError extends Error {\n readonly statusCode: number\n readonly statusMessage: string\n /** Response headers the answer needs (`Retry-After`). */\n readonly headers: Readonly<Record<string, string>>\n /** Machine-readable detail (the validation issues of a 400). */\n readonly data: unknown\n\n constructor(statusCode: number, statusMessage: string, headers: Record<string, string> = {}, data?: unknown) {\n super(statusMessage)\n this.name = 'HttpError'\n this.statusCode = statusCode\n this.statusMessage = statusMessage\n this.headers = headers\n this.data = data\n }\n}\n"],"mappings":";;;;;;AAKA,IAAa,YAAb,cAA+B,MAAM;CACnC;CACA;;CAEA;;CAEA;CAEA,YAAY,YAAoB,eAAuB,UAAkC,CAAC,GAAG,MAAgB;EAC3G,MAAM,aAAa;EACnB,KAAK,OAAO;EACZ,KAAK,aAAa;EAClB,KAAK,gBAAgB;EACrB,KAAK,UAAU;EACf,KAAK,OAAO;CACd;AACF"}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { HttpError } from "./errors.js";
|
|
2
|
+
import { upstreamError } from "./ai.js";
|
|
3
|
+
import { CSL_TYPES } from "../core/backend-types.js";
|
|
4
|
+
import { doiIn, fetchCrossref } from "./crossref.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { Output, generateText } from "ai";
|
|
7
|
+
//#region src/server/extract.ts
|
|
8
|
+
/** Largest asset sent to speech-to-text (the limit of OpenAI's `/audio/transcriptions`; applied to every provider, since the asset is held in memory). */
|
|
9
|
+
const MAX_TRANSCRIBE_BYTES = 26214400;
|
|
10
|
+
/** Resolution of the page images sent to the OCR model. */
|
|
11
|
+
const OCR_SCALE = 1.5;
|
|
12
|
+
const OCR_CONCURRENCY = 2;
|
|
13
|
+
const BIBLIOGRAPHY_TEXT_LIMIT = 12e3;
|
|
14
|
+
const OCR_PROMPT = "Lies den gesamten Text dieser Seite aus dem Bild und gib ihn als Markdown wieder: Überschriften als Markdown-Überschriften, Absätze durch Leerzeilen getrennt, Listen als Aufzählung, Tabellen als Markdown-Tabelle. Behalte die Lesereihenfolge bei, lass Kopf-, Fußzeilen und Seitenzahlen weg und füge nichts hinzu. Antworte nur mit dem Text der Seite, ohne Einleitung und ohne Codeblock. Enthält die Seite keinen Text, antworte mit einer leeren Antwort.";
|
|
15
|
+
const DESCRIBE_PROMPT = "Beschreibe dieses Bild sachlich in 1 bis 3 Absätzen auf Deutsch: was zu sehen ist (Motiv, Aufbau, Diagramme mit ihren Werten), jeden lesbaren Text im Bild wörtlich und den erkennbaren Zusammenhang. Erfinde nichts, was nicht zu sehen ist. Antworte nur mit der Beschreibung.";
|
|
16
|
+
const BIB_PROMPT = "Ermittle die bibliographischen Angaben des Werks aus dem folgenden Text seiner ersten Seiten (Titelseite, Kopfzeile, Impressum, Zitierhinweis). Gib nur Angaben an, die im Text stehen; lass alles Unbekannte weg. Namen als Familienname und Vorname getrennt, `year` ist das Erscheinungsjahr, `type` die passende CSL-Art (Zeitschriftenartikel: article-journal, Buch: book, Beitrag in Sammelband: chapter, Konferenzbeitrag: paper-conference, Abschlussarbeit: thesis, Bericht: report, sonst article).";
|
|
17
|
+
/** A page of the PDF could not be rendered to an image (no `@napi-rs/canvas`, or rendering failed): OCR is unavailable. */
|
|
18
|
+
var RasterError = class extends Error {};
|
|
19
|
+
/** `text` with a wrapping Markdown code fence removed (models often fence their answer). */
|
|
20
|
+
function unfenced(text) {
|
|
21
|
+
const match = /^```(?:markdown|md)?\s*\n([\s\S]*?)\n?```\s*$/i.exec(text.trim());
|
|
22
|
+
return (match ? match[1] : text).trim();
|
|
23
|
+
}
|
|
24
|
+
/** Runs a model call: its failures become the 502 of `upstreamError`; an abort of `signal` passes through unchanged. */
|
|
25
|
+
async function modelCall(signal, run) {
|
|
26
|
+
try {
|
|
27
|
+
return await run();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof HttpError || signal?.aborted) throw error;
|
|
30
|
+
throw upstreamError(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The pages as one Markdown text: `<!-- page N -->` marks the start of every page after the first. */
|
|
34
|
+
function joinPages(pages) {
|
|
35
|
+
return pages.map((text, index) => index === 0 ? text : `<!-- page ${index + 1} -->\n\n${text}`).join("\n\n");
|
|
36
|
+
}
|
|
37
|
+
/** Timed segments out of a provider's list: entries without text or numeric `start`/`end` are skipped. */
|
|
38
|
+
function timedSegments(list, textKey) {
|
|
39
|
+
if (!Array.isArray(list)) return [];
|
|
40
|
+
return list.flatMap((entry) => {
|
|
41
|
+
if (!entry || typeof entry !== "object") return [];
|
|
42
|
+
const { start, end } = entry;
|
|
43
|
+
const raw = entry[textKey];
|
|
44
|
+
const text = typeof raw === "string" ? raw.trim() : "";
|
|
45
|
+
return text && typeof start === "number" && typeof end === "number" ? [{
|
|
46
|
+
start,
|
|
47
|
+
end,
|
|
48
|
+
text
|
|
49
|
+
}] : [];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function transcriptionResult(segments, text) {
|
|
53
|
+
if (segments.length) return {
|
|
54
|
+
markdown: segments.map((segment) => segment.text).join("\n\n"),
|
|
55
|
+
segments
|
|
56
|
+
};
|
|
57
|
+
return { markdown: typeof text === "string" ? text.trim() : "" };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The provider's request: OpenAI-compatible `<endpoint>/audio/transcriptions` as multipart with segment timestamps
|
|
61
|
+
* (`verbose_json`), or Deepgram's `<endpoint>/listen` with the raw bytes and utterances.
|
|
62
|
+
*/
|
|
63
|
+
function transcriptionRequest(config, asset) {
|
|
64
|
+
const endpoint = config.endpoint.replace(/\/+$/, "");
|
|
65
|
+
if (config.provider === "deepgram") {
|
|
66
|
+
const query = new URLSearchParams({
|
|
67
|
+
model: config.model,
|
|
68
|
+
smart_format: "true",
|
|
69
|
+
utterances: "true"
|
|
70
|
+
});
|
|
71
|
+
if (config.language) query.set("language", config.language);
|
|
72
|
+
else query.set("detect_language", "true");
|
|
73
|
+
return {
|
|
74
|
+
url: `${endpoint}/listen?${query}`,
|
|
75
|
+
init: {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
"Content-Type": asset.record.mimeType,
|
|
79
|
+
...config.apiKey && { Authorization: `Token ${config.apiKey}` }
|
|
80
|
+
},
|
|
81
|
+
body: new Uint8Array(asset.data)
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const form = new FormData();
|
|
86
|
+
form.append("file", new Blob([new Uint8Array(asset.data)], { type: asset.record.mimeType }), asset.record.filename);
|
|
87
|
+
form.append("model", config.model);
|
|
88
|
+
form.append("response_format", "verbose_json");
|
|
89
|
+
form.append("timestamp_granularities[]", "segment");
|
|
90
|
+
if (config.language) form.append("language", config.language);
|
|
91
|
+
return {
|
|
92
|
+
url: `${endpoint}/audio/transcriptions`,
|
|
93
|
+
init: {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : void 0,
|
|
96
|
+
body: form
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** The provider's answer as text plus timed segments; an OpenAI-compatible endpoint may answer in plain text. */
|
|
101
|
+
function transcriptionResponse(config, raw) {
|
|
102
|
+
let body = null;
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if (parsed && typeof parsed === "object") body = parsed;
|
|
106
|
+
} catch {}
|
|
107
|
+
if (config.provider === "deepgram") {
|
|
108
|
+
const results = body?.results ?? {};
|
|
109
|
+
return transcriptionResult(timedSegments(results.utterances, "transcript"), results.channels?.[0]?.alternatives?.[0]?.transcript);
|
|
110
|
+
}
|
|
111
|
+
if (!body) return { markdown: raw };
|
|
112
|
+
return transcriptionResult(timedSegments(body.segments, "text"), body.text);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Speech-to-text through the configured provider (`TranscriptionConfig.provider`) with segment timestamps. An
|
|
116
|
+
* answer without segments yields the text only. Unreachable endpoint and non-2xx answers are 502.
|
|
117
|
+
*/
|
|
118
|
+
async function transcribe(config, asset, signal) {
|
|
119
|
+
const { url, init } = transcriptionRequest(config, asset);
|
|
120
|
+
let response;
|
|
121
|
+
try {
|
|
122
|
+
response = await fetch(url, {
|
|
123
|
+
...init,
|
|
124
|
+
signal
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (signal?.aborted) throw err;
|
|
128
|
+
throw new HttpError(502, `Transkriptions-Endpunkt: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
}
|
|
130
|
+
if (!response.ok) throw new HttpError(502, `Transkriptions-Endpunkt: ${response.status} ${response.statusText}`.trim());
|
|
131
|
+
return transcriptionResponse(config, (await response.text()).trim());
|
|
132
|
+
}
|
|
133
|
+
/** The text layer of the first `count` pages, one string per page. An abort of `signal` stops between pages. */
|
|
134
|
+
async function pdfPagesText(pdf, count, signal) {
|
|
135
|
+
const pages = [];
|
|
136
|
+
for (let page = 1; page <= count; page++) {
|
|
137
|
+
signal?.throwIfAborted();
|
|
138
|
+
pages.push(await pdf.pageText(page));
|
|
139
|
+
}
|
|
140
|
+
return pages;
|
|
141
|
+
}
|
|
142
|
+
/** True when the pages' text layer is too thin to be a real one: fewer than `MIN_TEXT_CHARS_PER_PAGE` characters per page on average. */
|
|
143
|
+
function isScanned(pages) {
|
|
144
|
+
return pages.reduce((sum, text) => sum + text.length, 0) / Math.max(1, pages.length) < 50;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* OCR of the first `count` pages through a vision-capable model: each page is rendered to a PNG and sent as an image
|
|
148
|
+
* part; two pages run at a time. `resolveModel` is called after page 1 rendered, so a server that cannot render never
|
|
149
|
+
* needs a model. A rendering failure throws `RasterError`, a model failure the 502 of `upstreamError`.
|
|
150
|
+
*/
|
|
151
|
+
async function ocrPages(pdf, count, resolveModel, signal) {
|
|
152
|
+
const render = async (page) => {
|
|
153
|
+
try {
|
|
154
|
+
return await pdf.pagePng(page, OCR_SCALE);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new RasterError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const first = await render(1);
|
|
160
|
+
const model = await resolveModel();
|
|
161
|
+
const pages = Array.from({ length: count }, () => "");
|
|
162
|
+
let next = 0;
|
|
163
|
+
let failed = false;
|
|
164
|
+
const worker = async () => {
|
|
165
|
+
while (!failed && next < count) {
|
|
166
|
+
signal?.throwIfAborted();
|
|
167
|
+
const index = next++;
|
|
168
|
+
try {
|
|
169
|
+
const png = index === 0 ? first : await render(index + 1);
|
|
170
|
+
const result = await modelCall(signal, () => generateText({
|
|
171
|
+
model,
|
|
172
|
+
abortSignal: signal,
|
|
173
|
+
messages: [{
|
|
174
|
+
role: "user",
|
|
175
|
+
content: [{
|
|
176
|
+
type: "text",
|
|
177
|
+
text: OCR_PROMPT
|
|
178
|
+
}, {
|
|
179
|
+
type: "file",
|
|
180
|
+
data: png,
|
|
181
|
+
mediaType: "image/png"
|
|
182
|
+
}]
|
|
183
|
+
}]
|
|
184
|
+
}));
|
|
185
|
+
pages[index] = unfenced(result.text);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
failed = true;
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
await Promise.all(Array.from({ length: Math.min(OCR_CONCURRENCY, count) }, worker));
|
|
193
|
+
return pages;
|
|
194
|
+
}
|
|
195
|
+
/** A German description of the image `data` (content, text in the image, context) through a vision-capable model. */
|
|
196
|
+
async function describeImage(model, data, mimeType, signal) {
|
|
197
|
+
return unfenced((await modelCall(signal, () => generateText({
|
|
198
|
+
model,
|
|
199
|
+
abortSignal: signal,
|
|
200
|
+
messages: [{
|
|
201
|
+
role: "user",
|
|
202
|
+
content: [{
|
|
203
|
+
type: "text",
|
|
204
|
+
text: DESCRIBE_PROMPT
|
|
205
|
+
}, {
|
|
206
|
+
type: "file",
|
|
207
|
+
data,
|
|
208
|
+
mediaType: mimeType
|
|
209
|
+
}]
|
|
210
|
+
}]
|
|
211
|
+
}))).text);
|
|
212
|
+
}
|
|
213
|
+
const nameSchema = z.object({
|
|
214
|
+
family: z.string().optional(),
|
|
215
|
+
given: z.string().optional(),
|
|
216
|
+
literal: z.string().optional()
|
|
217
|
+
});
|
|
218
|
+
const bibliographySchema = z.object({
|
|
219
|
+
title: z.string().optional(),
|
|
220
|
+
type: z.enum(CSL_TYPES),
|
|
221
|
+
author: z.array(nameSchema).optional(),
|
|
222
|
+
year: z.number().int().optional(),
|
|
223
|
+
"container-title": z.string().optional(),
|
|
224
|
+
volume: z.string().optional(),
|
|
225
|
+
issue: z.string().optional(),
|
|
226
|
+
page: z.string().optional(),
|
|
227
|
+
publisher: z.string().optional(),
|
|
228
|
+
"publisher-place": z.string().optional(),
|
|
229
|
+
DOI: z.string().optional(),
|
|
230
|
+
ISBN: z.string().optional(),
|
|
231
|
+
URL: z.string().optional(),
|
|
232
|
+
abstract: z.string().optional(),
|
|
233
|
+
language: z.string().optional()
|
|
234
|
+
});
|
|
235
|
+
const STRING_FIELDS = [
|
|
236
|
+
"container-title",
|
|
237
|
+
"volume",
|
|
238
|
+
"issue",
|
|
239
|
+
"page",
|
|
240
|
+
"publisher",
|
|
241
|
+
"publisher-place",
|
|
242
|
+
"DOI",
|
|
243
|
+
"ISBN",
|
|
244
|
+
"URL",
|
|
245
|
+
"abstract",
|
|
246
|
+
"language"
|
|
247
|
+
];
|
|
248
|
+
/** The CSL item of the model's answer: empty strings and nameless authors dropped, `year` as `issued`. */
|
|
249
|
+
function cslFromModel(answer) {
|
|
250
|
+
const author = (answer.author ?? []).flatMap((name) => {
|
|
251
|
+
const clean = {
|
|
252
|
+
...name.family?.trim() && { family: name.family.trim() },
|
|
253
|
+
...name.given?.trim() && { given: name.given.trim() },
|
|
254
|
+
...name.literal?.trim() && { literal: name.literal.trim() }
|
|
255
|
+
};
|
|
256
|
+
return Object.keys(clean).length ? [clean] : [];
|
|
257
|
+
});
|
|
258
|
+
const item = { type: answer.type };
|
|
259
|
+
if (answer.title?.trim()) item.title = answer.title.trim();
|
|
260
|
+
if (author.length) item.author = author;
|
|
261
|
+
if (answer.year) item.issued = { "date-parts": [[answer.year]] };
|
|
262
|
+
for (const key of STRING_FIELDS) {
|
|
263
|
+
const value = answer[key]?.trim();
|
|
264
|
+
if (value) item[key] = value;
|
|
265
|
+
}
|
|
266
|
+
return item;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Bibliographic data of a work: from Crossref by `doi` (given, else the first DOI in `text`), else — no DOI, Crossref
|
|
270
|
+
* unreachable or without the record — extracted from `text` (the first pages) by the model. Falling back after a
|
|
271
|
+
* Crossref attempt adds the warning `crossref-unavailable`. 422 when nothing can be found.
|
|
272
|
+
*/
|
|
273
|
+
async function extractBibliography(options) {
|
|
274
|
+
const { signal } = options;
|
|
275
|
+
const doi = options.doi ?? doiIn(options.text);
|
|
276
|
+
const warnings = [];
|
|
277
|
+
if (doi) {
|
|
278
|
+
const item = await fetchCrossref(doi, signal);
|
|
279
|
+
if (item) return {
|
|
280
|
+
bib: item,
|
|
281
|
+
warnings
|
|
282
|
+
};
|
|
283
|
+
warnings.push("crossref-unavailable");
|
|
284
|
+
}
|
|
285
|
+
const text = options.text.trim().slice(0, BIBLIOGRAPHY_TEXT_LIMIT);
|
|
286
|
+
if (!text) throw new HttpError(422, doi ? "Literaturangaben nicht verfügbar: Crossref nicht erreichbar und kein Text zum Auswerten" : "Keine Literaturangaben gefunden");
|
|
287
|
+
const model = await options.resolveModel();
|
|
288
|
+
const bib = cslFromModel((await modelCall(signal, () => generateText({
|
|
289
|
+
model,
|
|
290
|
+
abortSignal: signal,
|
|
291
|
+
output: Output.object({ schema: bibliographySchema }),
|
|
292
|
+
messages: [{
|
|
293
|
+
role: "user",
|
|
294
|
+
content: `${BIB_PROMPT}\n\n${text}`
|
|
295
|
+
}]
|
|
296
|
+
}))).output);
|
|
297
|
+
if (doi && !bib.DOI) bib.DOI = doi;
|
|
298
|
+
return {
|
|
299
|
+
bib,
|
|
300
|
+
warnings
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/** The text the bibliography is read from: the first pages of the document. */
|
|
304
|
+
const bibliographyText = (pages) => pages.slice(0, 2).join("\n\n");
|
|
305
|
+
//#endregion
|
|
306
|
+
export { MAX_TRANSCRIBE_BYTES, RasterError, bibliographyText, describeImage, extractBibliography, isScanned, joinPages, ocrPages, pdfPagesText, transcribe };
|
|
307
|
+
|
|
308
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","names":[],"sources":["../../src/server/extract.ts"],"sourcesContent":["import { generateText, type LanguageModel, Output } from 'ai'\nimport { z } from 'zod'\nimport { CSL_TYPES, type CslItem, type CslName } from '../core/backend-types'\nimport type { TranscriptionConfig } from '../config'\nimport type { AssetRecord } from '../features/media/assets'\nimport { upstreamError } from './ai'\nimport { doiIn, fetchCrossref } from './crossref'\nimport { HttpError } from './errors'\nimport type { PdfFile } from './pdf'\n\n// The extraction methods behind `POST /api/ai/extract` (`extractPost`): transcription, PDF text and OCR, image\n// description and bibliographic data. Each takes the asset's bytes and returns Markdown (or a CSL item).\n\n/** Largest asset sent to speech-to-text (the limit of OpenAI's `/audio/transcriptions`; applied to every provider, since the asset is held in memory). */\nexport const MAX_TRANSCRIBE_BYTES = 25 * 1024 * 1024\n\n/** Pages whose text layer averages fewer characters than this count as scanned: `auto` then reads them with OCR. */\nexport const MIN_TEXT_CHARS_PER_PAGE = 50\n/** Resolution of the page images sent to the OCR model. */\nconst OCR_SCALE = 1.5\nconst OCR_CONCURRENCY = 2\n/** OCR makes one vision-model call per page: it reads at most this many pages (`ocr-truncated` warns about the rest), whatever `maxPages` allows. */\nexport const MAX_OCR_PAGES = 50\n\n/** The first pages of a PDF are where its DOI and bibliographic data are looked for; the model gets at most `BIBLIOGRAPHY_TEXT_LIMIT` characters of them. */\nexport const BIBLIOGRAPHY_PAGES = 2\nconst BIBLIOGRAPHY_TEXT_LIMIT = 12000\n\n// i18n-ignore-start: model-facing prompts (German prompt text, not UI)\nconst OCR_PROMPT = 'Lies den gesamten Text dieser Seite aus dem Bild und gib ihn als Markdown wieder: Überschriften als Markdown-Überschriften, Absätze durch Leerzeilen getrennt, Listen als Aufzählung, Tabellen als Markdown-Tabelle. Behalte die Lesereihenfolge bei, lass Kopf-, Fußzeilen und Seitenzahlen weg und füge nichts hinzu. Antworte nur mit dem Text der Seite, ohne Einleitung und ohne Codeblock. Enthält die Seite keinen Text, antworte mit einer leeren Antwort.'\nconst DESCRIBE_PROMPT = 'Beschreibe dieses Bild sachlich in 1 bis 3 Absätzen auf Deutsch: was zu sehen ist (Motiv, Aufbau, Diagramme mit ihren Werten), jeden lesbaren Text im Bild wörtlich und den erkennbaren Zusammenhang. Erfinde nichts, was nicht zu sehen ist. Antworte nur mit der Beschreibung.'\nconst BIB_PROMPT = 'Ermittle die bibliographischen Angaben des Werks aus dem folgenden Text seiner ersten Seiten (Titelseite, Kopfzeile, Impressum, Zitierhinweis). Gib nur Angaben an, die im Text stehen; lass alles Unbekannte weg. Namen als Familienname und Vorname getrennt, `year` ist das Erscheinungsjahr, `type` die passende CSL-Art (Zeitschriftenartikel: article-journal, Buch: book, Beitrag in Sammelband: chapter, Konferenzbeitrag: paper-conference, Abschlussarbeit: thesis, Bericht: report, sonst article).'\n// i18n-ignore-end\n\n/** A page of the PDF could not be rendered to an image (no `@napi-rs/canvas`, or rendering failed): OCR is unavailable. */\nexport class RasterError extends Error {}\n\n/** `text` with a wrapping Markdown code fence removed (models often fence their answer). */\nfunction unfenced(text: string): string {\n const match = /^```(?:markdown|md)?\\s*\\n([\\s\\S]*?)\\n?```\\s*$/i.exec(text.trim())\n return (match ? match[1]! : text).trim()\n}\n\n/** Runs a model call: its failures become the 502 of `upstreamError`; an abort of `signal` passes through unchanged. */\nasync function modelCall<T>(signal: AbortSignal | undefined, run: () => Promise<T>): Promise<T> {\n try {\n return await run()\n }\n catch (error) {\n if (error instanceof HttpError || signal?.aborted) throw error\n throw upstreamError(error)\n }\n}\n\n/** The pages as one Markdown text: `<!-- page N -->` marks the start of every page after the first. */\nexport function joinPages(pages: readonly string[]): string {\n return pages.map((text, index) => index === 0 ? text : `<!-- page ${index + 1} -->\\n\\n${text}`).join('\\n\\n')\n}\n\nexport interface TranscriptionResult { markdown: string, segments?: { start: number, end: number, text: string }[] }\n\ntype Segment = NonNullable<TranscriptionResult['segments']>[number]\n\n/** Timed segments out of a provider's list: entries without text or numeric `start`/`end` are skipped. */\nfunction timedSegments(list: unknown, textKey: 'text' | 'transcript'): Segment[] {\n if (!Array.isArray(list)) return []\n return list.flatMap((entry: unknown) => {\n if (!entry || typeof entry !== 'object') return []\n const { start, end } = entry as { start?: unknown, end?: unknown }\n const raw: unknown = (entry as Record<string, unknown>)[textKey]\n const text = typeof raw === 'string' ? raw.trim() : ''\n return text && typeof start === 'number' && typeof end === 'number' ? [{ start, end, text }] : []\n })\n}\n\nfunction transcriptionResult(segments: Segment[], text: unknown): TranscriptionResult {\n if (segments.length) return { markdown: segments.map(segment => segment.text).join('\\n\\n'), segments }\n return { markdown: typeof text === 'string' ? text.trim() : '' }\n}\n\n/**\n * The provider's request: OpenAI-compatible `<endpoint>/audio/transcriptions` as multipart with segment timestamps\n * (`verbose_json`), or Deepgram's `<endpoint>/listen` with the raw bytes and utterances.\n */\nfunction transcriptionRequest(config: TranscriptionConfig, asset: { record: AssetRecord, data: Uint8Array }): { url: string, init: RequestInit } {\n const endpoint = config.endpoint.replace(/\\/+$/, '')\n if (config.provider === 'deepgram') {\n const query = new URLSearchParams({ model: config.model, smart_format: 'true', utterances: 'true' })\n if (config.language) query.set('language', config.language)\n else query.set('detect_language', 'true')\n return {\n url: `${endpoint}/listen?${query}`,\n init: {\n method: 'POST',\n headers: { 'Content-Type': asset.record.mimeType, ...(config.apiKey && { Authorization: `Token ${config.apiKey}` }) },\n body: new Uint8Array(asset.data),\n },\n }\n }\n const form = new FormData()\n form.append('file', new Blob([new Uint8Array(asset.data)], { type: asset.record.mimeType }), asset.record.filename)\n form.append('model', config.model)\n form.append('response_format', 'verbose_json')\n form.append('timestamp_granularities[]', 'segment')\n if (config.language) form.append('language', config.language)\n return {\n url: `${endpoint}/audio/transcriptions`,\n init: { method: 'POST', headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : undefined, body: form },\n }\n}\n\n/** The provider's answer as text plus timed segments; an OpenAI-compatible endpoint may answer in plain text. */\nfunction transcriptionResponse(config: TranscriptionConfig, raw: string): TranscriptionResult {\n let body: Record<string, unknown> | null = null\n try {\n const parsed: unknown = JSON.parse(raw)\n if (parsed && typeof parsed === 'object') body = parsed as Record<string, unknown>\n }\n catch {\n // plain text answer\n }\n if (config.provider === 'deepgram') {\n // { results: { utterances: [{ start, end, transcript }], channels: [{ alternatives: [{ transcript }] }] } }\n const results = (body?.results ?? {}) as { utterances?: unknown, channels?: { alternatives?: { transcript?: unknown }[] }[] }\n return transcriptionResult(timedSegments(results.utterances, 'transcript'), results.channels?.[0]?.alternatives?.[0]?.transcript)\n }\n if (!body) return { markdown: raw }\n return transcriptionResult(timedSegments(body.segments, 'text'), body.text)\n}\n\n/**\n * Speech-to-text through the configured provider (`TranscriptionConfig.provider`) with segment timestamps. An\n * answer without segments yields the text only. Unreachable endpoint and non-2xx answers are 502.\n */\nexport async function transcribe(config: TranscriptionConfig, asset: { record: AssetRecord, data: Uint8Array }, signal: AbortSignal | undefined): Promise<TranscriptionResult> {\n const { url, init } = transcriptionRequest(config, asset)\n let response: Response\n try {\n response = await fetch(url, { ...init, signal })\n }\n catch (err) {\n if (signal?.aborted) throw err\n throw new HttpError(502, `Transkriptions-Endpunkt: ${err instanceof Error ? err.message : String(err)}`)\n }\n if (!response.ok) throw new HttpError(502, `Transkriptions-Endpunkt: ${response.status} ${response.statusText}`.trim())\n return transcriptionResponse(config, (await response.text()).trim())\n}\n\n/** The text layer of the first `count` pages, one string per page. An abort of `signal` stops between pages. */\nexport async function pdfPagesText(pdf: PdfFile, count: number, signal?: AbortSignal): Promise<string[]> {\n const pages: string[] = []\n for (let page = 1; page <= count; page++) {\n signal?.throwIfAborted()\n pages.push(await pdf.pageText(page))\n }\n return pages\n}\n\n/** True when the pages' text layer is too thin to be a real one: fewer than `MIN_TEXT_CHARS_PER_PAGE` characters per page on average. */\nexport function isScanned(pages: readonly string[]): boolean {\n return pages.reduce((sum, text) => sum + text.length, 0) / Math.max(1, pages.length) < MIN_TEXT_CHARS_PER_PAGE\n}\n\n/**\n * OCR of the first `count` pages through a vision-capable model: each page is rendered to a PNG and sent as an image\n * part; two pages run at a time. `resolveModel` is called after page 1 rendered, so a server that cannot render never\n * needs a model. A rendering failure throws `RasterError`, a model failure the 502 of `upstreamError`.\n */\nexport async function ocrPages(pdf: PdfFile, count: number, resolveModel: () => Promise<LanguageModel>, signal: AbortSignal | undefined): Promise<string[]> {\n const render = async (page: number): Promise<Uint8Array> => {\n try {\n return await pdf.pagePng(page, OCR_SCALE)\n }\n catch (error) {\n throw new RasterError(error instanceof Error ? error.message : String(error), { cause: error })\n }\n }\n const first = await render(1)\n const model = await resolveModel()\n\n const pages: string[] = Array.from({ length: count }, () => '')\n let next = 0\n let failed = false\n const worker = async () => {\n while (!failed && next < count) {\n signal?.throwIfAborted()\n const index = next++\n try {\n const png = index === 0 ? first : await render(index + 1)\n const result = await modelCall(signal, () => generateText({\n model,\n abortSignal: signal,\n // A `file` part with an image media type: the `image` part type is deprecated in AI SDK 7.\n messages: [{ role: 'user', content: [{ type: 'text', text: OCR_PROMPT }, { type: 'file', data: png, mediaType: 'image/png' }] }],\n }))\n pages[index] = unfenced(result.text)\n }\n catch (error) {\n failed = true\n throw error\n }\n }\n }\n await Promise.all(Array.from({ length: Math.min(OCR_CONCURRENCY, count) }, worker))\n return pages\n}\n\n/** A German description of the image `data` (content, text in the image, context) through a vision-capable model. */\nexport async function describeImage(model: LanguageModel, data: Uint8Array, mimeType: string, signal: AbortSignal | undefined): Promise<string> {\n const result = await modelCall(signal, () => generateText({\n model,\n abortSignal: signal,\n messages: [{ role: 'user', content: [{ type: 'text', text: DESCRIBE_PROMPT }, { type: 'file', data, mediaType: mimeType }] }],\n }))\n return unfenced(result.text)\n}\n\nconst nameSchema = z.object({ family: z.string().optional(), given: z.string().optional(), literal: z.string().optional() })\nconst bibliographySchema = z.object({\n title: z.string().optional(),\n type: z.enum(CSL_TYPES),\n author: z.array(nameSchema).optional(),\n year: z.number().int().optional(),\n 'container-title': z.string().optional(),\n volume: z.string().optional(),\n issue: z.string().optional(),\n page: z.string().optional(),\n publisher: z.string().optional(),\n 'publisher-place': z.string().optional(),\n DOI: z.string().optional(),\n ISBN: z.string().optional(),\n URL: z.string().optional(),\n abstract: z.string().optional(),\n language: z.string().optional(),\n})\n\nconst STRING_FIELDS = ['container-title', 'volume', 'issue', 'page', 'publisher', 'publisher-place', 'DOI', 'ISBN', 'URL', 'abstract', 'language'] as const\n\n/** The CSL item of the model's answer: empty strings and nameless authors dropped, `year` as `issued`. */\nfunction cslFromModel(answer: z.infer<typeof bibliographySchema>): CslItem & { title?: string } {\n const author = (answer.author ?? []).flatMap((name): CslName[] => {\n const clean: CslName = { ...(name.family?.trim() && { family: name.family.trim() }), ...(name.given?.trim() && { given: name.given.trim() }), ...(name.literal?.trim() && { literal: name.literal.trim() }) }\n return Object.keys(clean).length ? [clean] : []\n })\n const item: CslItem & { title?: string } = { type: answer.type as CslItem['type'] }\n if (answer.title?.trim()) item.title = answer.title.trim()\n if (author.length) item.author = author\n if (answer.year) item.issued = { 'date-parts': [[answer.year]] }\n for (const key of STRING_FIELDS) {\n const value = answer[key]?.trim()\n if (value) item[key] = value\n }\n return item\n}\n\n/**\n * Bibliographic data of a work: from Crossref by `doi` (given, else the first DOI in `text`), else — no DOI, Crossref\n * unreachable or without the record — extracted from `text` (the first pages) by the model. Falling back after a\n * Crossref attempt adds the warning `crossref-unavailable`. 422 when nothing can be found.\n */\nexport async function extractBibliography(options: { doi?: string, text: string, resolveModel: () => Promise<LanguageModel>, signal: AbortSignal | undefined }): Promise<{ bib: CslItem & { title?: string }, warnings: string[] }> {\n const { signal } = options\n const doi = options.doi ?? doiIn(options.text)\n const warnings: string[] = []\n if (doi) {\n const item = await fetchCrossref(doi, signal)\n if (item) return { bib: item, warnings }\n warnings.push('crossref-unavailable')\n }\n\n const text = options.text.trim().slice(0, BIBLIOGRAPHY_TEXT_LIMIT)\n if (!text) throw new HttpError(422, doi ? 'Literaturangaben nicht verfügbar: Crossref nicht erreichbar und kein Text zum Auswerten' : 'Keine Literaturangaben gefunden')\n const model = await options.resolveModel()\n const result = await modelCall(signal, () => generateText({\n model,\n abortSignal: signal,\n output: Output.object({ schema: bibliographySchema }),\n messages: [{ role: 'user', content: `${BIB_PROMPT}\\n\\n${text}` }],\n }))\n const bib = cslFromModel(result.output)\n if (doi && !bib.DOI) bib.DOI = doi\n return { bib, warnings }\n}\n\n/** The text the bibliography is read from: the first pages of the document. */\nexport const bibliographyText = (pages: readonly string[]): string => pages.slice(0, BIBLIOGRAPHY_PAGES).join('\\n\\n')\n"],"mappings":";;;;;;;;AAcA,MAAa,uBAAuB;;AAKpC,MAAM,YAAY;AAClB,MAAM,kBAAkB;AAMxB,MAAM,0BAA0B;AAGhC,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,aAAa;;AAInB,IAAa,cAAb,cAAiC,MAAM,CAAC;;AAGxC,SAAS,SAAS,MAAsB;CACtC,MAAM,QAAQ,iDAAiD,KAAK,KAAK,KAAK,CAAC;CAC/E,QAAQ,QAAQ,MAAM,KAAM,KAAA,CAAM,KAAK;AACzC;;AAGA,eAAe,UAAa,QAAiC,KAAmC;CAC9F,IAAI;EACF,OAAO,MAAM,IAAI;CACnB,SACO,OAAO;EACZ,IAAI,iBAAiB,aAAa,QAAQ,SAAS,MAAM;EACzD,MAAM,cAAc,KAAK;CAC3B;AACF;;AAGA,SAAgB,UAAU,OAAkC;CAC1D,OAAO,MAAM,KAAK,MAAM,UAAU,UAAU,IAAI,OAAO,aAAa,QAAQ,EAAE,UAAU,MAAM,CAAC,CAAC,KAAK,MAAM;AAC7G;;AAOA,SAAS,cAAc,MAAe,SAA2C;CAC/E,IAAI,CAAC,MAAM,QAAQ,IAAI,GAAG,OAAO,CAAC;CAClC,OAAO,KAAK,SAAS,UAAmB;EACtC,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO,CAAC;EACjD,MAAM,EAAE,OAAO,QAAQ;EACvB,MAAM,MAAgB,MAAkC;EACxD,MAAM,OAAO,OAAO,QAAQ,WAAW,IAAI,KAAK,IAAI;EACpD,OAAO,QAAQ,OAAO,UAAU,YAAY,OAAO,QAAQ,WAAW,CAAC;GAAE;GAAO;GAAK;EAAK,CAAC,IAAI,CAAC;CAClG,CAAC;AACH;AAEA,SAAS,oBAAoB,UAAqB,MAAoC;CACpF,IAAI,SAAS,QAAQ,OAAO;EAAE,UAAU,SAAS,KAAI,YAAW,QAAQ,IAAI,CAAC,CAAC,KAAK,MAAM;EAAG;CAAS;CACrG,OAAO,EAAE,UAAU,OAAO,SAAS,WAAW,KAAK,KAAK,IAAI,GAAG;AACjE;;;;;AAMA,SAAS,qBAAqB,QAA6B,OAAsF;CAC/I,MAAM,WAAW,OAAO,SAAS,QAAQ,QAAQ,EAAE;CACnD,IAAI,OAAO,aAAa,YAAY;EAClC,MAAM,QAAQ,IAAI,gBAAgB;GAAE,OAAO,OAAO;GAAO,cAAc;GAAQ,YAAY;EAAO,CAAC;EACnG,IAAI,OAAO,UAAU,MAAM,IAAI,YAAY,OAAO,QAAQ;OACrD,MAAM,IAAI,mBAAmB,MAAM;EACxC,OAAO;GACL,KAAK,GAAG,SAAS,UAAU;GAC3B,MAAM;IACJ,QAAQ;IACR,SAAS;KAAE,gBAAgB,MAAM,OAAO;KAAU,GAAI,OAAO,UAAU,EAAE,eAAe,SAAS,OAAO,SAAS;IAAG;IACpH,MAAM,IAAI,WAAW,MAAM,IAAI;GACjC;EACF;CACF;CACA,MAAM,OAAO,IAAI,SAAS;CAC1B,KAAK,OAAO,QAAQ,IAAI,KAAK,CAAC,IAAI,WAAW,MAAM,IAAI,CAAC,GAAG,EAAE,MAAM,MAAM,OAAO,SAAS,CAAC,GAAG,MAAM,OAAO,QAAQ;CAClH,KAAK,OAAO,SAAS,OAAO,KAAK;CACjC,KAAK,OAAO,mBAAmB,cAAc;CAC7C,KAAK,OAAO,6BAA6B,SAAS;CAClD,IAAI,OAAO,UAAU,KAAK,OAAO,YAAY,OAAO,QAAQ;CAC5D,OAAO;EACL,KAAK,GAAG,SAAS;EACjB,MAAM;GAAE,QAAQ;GAAQ,SAAS,OAAO,SAAS,EAAE,eAAe,UAAU,OAAO,SAAS,IAAI,KAAA;GAAW,MAAM;EAAK;CACxH;AACF;;AAGA,SAAS,sBAAsB,QAA6B,KAAkC;CAC5F,IAAI,OAAuC;CAC3C,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,GAAG;EACtC,IAAI,UAAU,OAAO,WAAW,UAAU,OAAO;CACnD,QACM,CAEN;CACA,IAAI,OAAO,aAAa,YAAY;EAElC,MAAM,UAAW,MAAM,WAAW,CAAC;EACnC,OAAO,oBAAoB,cAAc,QAAQ,YAAY,YAAY,GAAG,QAAQ,WAAW,EAAE,EAAE,eAAe,EAAE,EAAE,UAAU;CAClI;CACA,IAAI,CAAC,MAAM,OAAO,EAAE,UAAU,IAAI;CAClC,OAAO,oBAAoB,cAAc,KAAK,UAAU,MAAM,GAAG,KAAK,IAAI;AAC5E;;;;;AAMA,eAAsB,WAAW,QAA6B,OAAkD,QAA+D;CAC7K,MAAM,EAAE,KAAK,SAAS,qBAAqB,QAAQ,KAAK;CACxD,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,MAAM,KAAK;GAAE,GAAG;GAAM;EAAO,CAAC;CACjD,SACO,KAAK;EACV,IAAI,QAAQ,SAAS,MAAM;EAC3B,MAAM,IAAI,UAAU,KAAK,4BAA4B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG;CACzG;CACA,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,UAAU,KAAK,4BAA4B,SAAS,OAAO,GAAG,SAAS,aAAa,KAAK,CAAC;CACtH,OAAO,sBAAsB,SAAS,MAAM,SAAS,KAAK,EAAA,CAAG,KAAK,CAAC;AACrE;;AAGA,eAAsB,aAAa,KAAc,OAAe,QAAyC;CACvG,MAAM,QAAkB,CAAC;CACzB,KAAK,IAAI,OAAO,GAAG,QAAQ,OAAO,QAAQ;EACxC,QAAQ,eAAe;EACvB,MAAM,KAAK,MAAM,IAAI,SAAS,IAAI,CAAC;CACrC;CACA,OAAO;AACT;;AAGA,SAAgB,UAAU,OAAmC;CAC3D,OAAO,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,QAAQ,CAAC,IAAI,KAAK,IAAI,GAAG,MAAM,MAAM,IAAA;AACrF;;;;;;AAOA,eAAsB,SAAS,KAAc,OAAe,cAA4C,QAAoD;CAC1J,MAAM,SAAS,OAAO,SAAsC;EAC1D,IAAI;GACF,OAAO,MAAM,IAAI,QAAQ,MAAM,SAAS;EAC1C,SACO,OAAO;GACZ,MAAM,IAAI,YAAY,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG,EAAE,OAAO,MAAM,CAAC;EAChG;CACF;CACA,MAAM,QAAQ,MAAM,OAAO,CAAC;CAC5B,MAAM,QAAQ,MAAM,aAAa;CAEjC,MAAM,QAAkB,MAAM,KAAK,EAAE,QAAQ,MAAM,SAAS,EAAE;CAC9D,IAAI,OAAO;CACX,IAAI,SAAS;CACb,MAAM,SAAS,YAAY;EACzB,OAAO,CAAC,UAAU,OAAO,OAAO;GAC9B,QAAQ,eAAe;GACvB,MAAM,QAAQ;GACd,IAAI;IACF,MAAM,MAAM,UAAU,IAAI,QAAQ,MAAM,OAAO,QAAQ,CAAC;IACxD,MAAM,SAAS,MAAM,UAAU,cAAc,aAAa;KACxD;KACA,aAAa;KAEb,UAAU,CAAC;MAAE,MAAM;MAAQ,SAAS,CAAC;OAAE,MAAM;OAAQ,MAAM;MAAW,GAAG;OAAE,MAAM;OAAQ,MAAM;OAAK,WAAW;MAAY,CAAC;KAAE,CAAC;IACjI,CAAC,CAAC;IACF,MAAM,SAAS,SAAS,OAAO,IAAI;GACrC,SACO,OAAO;IACZ,SAAS;IACT,MAAM;GACR;EACF;CACF;CACA,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,iBAAiB,KAAK,EAAE,GAAG,MAAM,CAAC;CAClF,OAAO;AACT;;AAGA,eAAsB,cAAc,OAAsB,MAAkB,UAAkB,QAAkD;CAM9I,OAAO,UAAS,MALK,UAAU,cAAc,aAAa;EACxD;EACA,aAAa;EACb,UAAU,CAAC;GAAE,MAAM;GAAQ,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM;GAAgB,GAAG;IAAE,MAAM;IAAQ;IAAM,WAAW;GAAS,CAAC;EAAE,CAAC;CAC9H,CAAC,CAAC,EAAA,CACqB,IAAI;AAC7B;AAEA,MAAM,aAAa,EAAE,OAAO;CAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAAG,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAAG,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;AAAE,CAAC;AAC3H,MAAM,qBAAqB,EAAE,OAAO;CAClC,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,MAAM,EAAE,KAAK,SAAS;CACtB,QAAQ,EAAE,MAAM,UAAU,CAAC,CAAC,SAAS;CACrC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAChC,mBAAmB,EAAE,OAAO,CAAC,CAAC,SAAS;CACvC,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAC1B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,mBAAmB,EAAE,OAAO,CAAC,CAAC,SAAS;CACvC,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS;CACzB,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAC1B,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS;CACzB,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;CAC9B,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;AAChC,CAAC;AAED,MAAM,gBAAgB;CAAC;CAAmB;CAAU;CAAS;CAAQ;CAAa;CAAmB;CAAO;CAAQ;CAAO;CAAY;AAAU;;AAGjJ,SAAS,aAAa,QAA0E;CAC9F,MAAM,UAAU,OAAO,UAAU,CAAC,EAAA,CAAG,SAAS,SAAoB;EAChE,MAAM,QAAiB;GAAE,GAAI,KAAK,QAAQ,KAAK,KAAK,EAAE,QAAQ,KAAK,OAAO,KAAK,EAAE;GAAI,GAAI,KAAK,OAAO,KAAK,KAAK,EAAE,OAAO,KAAK,MAAM,KAAK,EAAE;GAAI,GAAI,KAAK,SAAS,KAAK,KAAK,EAAE,SAAS,KAAK,QAAQ,KAAK,EAAE;EAAG;EAC5M,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC;CAChD,CAAC;CACD,MAAM,OAAqC,EAAE,MAAM,OAAO,KAAwB;CAClF,IAAI,OAAO,OAAO,KAAK,GAAG,KAAK,QAAQ,OAAO,MAAM,KAAK;CACzD,IAAI,OAAO,QAAQ,KAAK,SAAS;CACjC,IAAI,OAAO,MAAM,KAAK,SAAS,EAAE,cAAc,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE;CAC/D,KAAK,MAAM,OAAO,eAAe;EAC/B,MAAM,QAAQ,OAAO,IAAI,EAAE,KAAK;EAChC,IAAI,OAAO,KAAK,OAAO;CACzB;CACA,OAAO;AACT;;;;;;AAOA,eAAsB,oBAAoB,SAA0L;CAClO,MAAM,EAAE,WAAW;CACnB,MAAM,MAAM,QAAQ,OAAO,MAAM,QAAQ,IAAI;CAC7C,MAAM,WAAqB,CAAC;CAC5B,IAAI,KAAK;EACP,MAAM,OAAO,MAAM,cAAc,KAAK,MAAM;EAC5C,IAAI,MAAM,OAAO;GAAE,KAAK;GAAM;EAAS;EACvC,SAAS,KAAK,sBAAsB;CACtC;CAEA,MAAM,OAAO,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,uBAAuB;CACjE,IAAI,CAAC,MAAM,MAAM,IAAI,UAAU,KAAK,MAAM,4FAA4F,iCAAiC;CACvK,MAAM,QAAQ,MAAM,QAAQ,aAAa;CAOzC,MAAM,MAAM,cAAa,MANJ,UAAU,cAAc,aAAa;EACxD;EACA,aAAa;EACb,QAAQ,OAAO,OAAO,EAAE,QAAQ,mBAAmB,CAAC;EACpD,UAAU,CAAC;GAAE,MAAM;GAAQ,SAAS,GAAG,WAAW,MAAM;EAAO,CAAC;CAClE,CAAC,CAAC,EAAA,CAC8B,MAAM;CACtC,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI,MAAM;CAC/B,OAAO;EAAE;EAAK;CAAS;AACzB;;AAGA,MAAa,oBAAoB,UAAqC,MAAM,MAAM,GAAA,CAAqB,CAAC,CAAC,KAAK,MAAM"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { HandlerRequest, HandlerResponse } from "./handlers/context.js";
|
|
2
|
+
import "./handlers/index.js";
|
|
3
|
+
import "h3";
|
|
4
|
+
//#region src/server/h3.d.ts
|
|
5
|
+
/** A route handler of `@ecsplico/editor/server/handlers`, over the context `C` its route needs. */
|
|
6
|
+
export type RouteHandler<C> = (ctx: C, request: HandlerRequest) => Promise<HandlerResponse>;
|
|
7
|
+
//#endregion
|
|
8
|
+
//# sourceMappingURL=h3.d.ts.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { HttpError } from "
|
|
2
|
-
import { useAiModels, useCollabServer, useEditorAdapter, useRateLimiter } from "../../../../server/host.js";
|
|
1
|
+
import { HttpError } from "./errors.js";
|
|
3
2
|
import { createError, defineEventHandler, getQuery, getRouterParams, readBody, readFormData, setResponseHeaders, setResponseStatus } from "h3";
|
|
4
|
-
//#region src/
|
|
3
|
+
//#region src/server/h3.ts
|
|
5
4
|
/** The `HandlerRequest` of a Nitro event. `signal` aborts when the client goes away. */
|
|
6
5
|
function requestOf(event) {
|
|
7
6
|
const abort = new AbortController();
|
|
@@ -15,26 +14,19 @@ function requestOf(event) {
|
|
|
15
14
|
signal: abort.signal
|
|
16
15
|
};
|
|
17
16
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
* with the adapter, AI models, rate limiter and collaboration server of this process, and sends the `HandlerResponse`.
|
|
21
|
-
*/
|
|
22
|
-
function defineEditorRoute(handler) {
|
|
17
|
+
/** An event handler that runs `handler` with `ctx` and sends its `HandlerResponse`; an `HttpError` becomes Nitro's error response. */
|
|
18
|
+
function routeOf(ctx, handler) {
|
|
23
19
|
return defineEventHandler(async (event) => {
|
|
24
20
|
let response;
|
|
25
21
|
try {
|
|
26
|
-
response = await handler(
|
|
27
|
-
adapter: useEditorAdapter(),
|
|
28
|
-
ai: useAiModels(),
|
|
29
|
-
limiter: useRateLimiter(),
|
|
30
|
-
collab: useCollabServer()
|
|
31
|
-
}, requestOf(event));
|
|
22
|
+
response = await handler(ctx, requestOf(event));
|
|
32
23
|
} catch (error) {
|
|
33
24
|
if (!(error instanceof HttpError)) throw error;
|
|
34
25
|
setResponseHeaders(event, error.headers);
|
|
35
26
|
throw createError({
|
|
36
27
|
statusCode: error.statusCode,
|
|
37
28
|
statusMessage: error.statusMessage,
|
|
29
|
+
message: error.statusMessage,
|
|
38
30
|
data: error.data
|
|
39
31
|
});
|
|
40
32
|
}
|
|
@@ -44,6 +36,6 @@ function defineEditorRoute(handler) {
|
|
|
44
36
|
});
|
|
45
37
|
}
|
|
46
38
|
//#endregion
|
|
47
|
-
export {
|
|
39
|
+
export { requestOf, routeOf };
|
|
48
40
|
|
|
49
|
-
//# sourceMappingURL=
|
|
41
|
+
//# sourceMappingURL=h3.js.map
|