@latentic/live-markdown 0.3.1 → 0.4.1
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 +44 -0
- package/README.md +2 -2
- package/dist/chunk-V6TY7KAL.js +8 -0
- package/dist/chunk-V6TY7KAL.js.map +1 -0
- package/dist/codemirror/CodeMirrorMarkdownEditor.d.ts +106 -0
- package/dist/codemirror/CodeMirrorMarkdownEditor.js +482 -0
- package/dist/codemirror/CodeMirrorMarkdownEditor.js.map +1 -0
- package/dist/codemirror/byteOffset.d.ts +17 -0
- package/dist/codemirror/byteOffset.js +19 -0
- package/dist/codemirror/byteOffset.js.map +1 -0
- package/dist/codemirror/caretLayer.d.ts +19 -0
- package/dist/codemirror/caretLayer.js +44 -0
- package/dist/codemirror/caretLayer.js.map +1 -0
- package/dist/codemirror/clipboard/copyRich.d.ts +37 -0
- package/dist/codemirror/clipboard/copyRich.js +72 -0
- package/dist/codemirror/clipboard/copyRich.js.map +1 -0
- package/dist/codemirror/clipboard/htmlToMarkdown.d.ts +20 -0
- package/dist/codemirror/clipboard/htmlToMarkdown.js +91 -0
- package/dist/codemirror/clipboard/htmlToMarkdown.js.map +1 -0
- package/dist/codemirror/clipboard/pasteMarkdown.d.ts +18 -0
- package/dist/codemirror/clipboard/pasteMarkdown.js +41 -0
- package/dist/codemirror/clipboard/pasteMarkdown.js.map +1 -0
- package/dist/codemirror/code/codeHighlight.d.ts +13 -0
- package/dist/codemirror/code/codeHighlight.js +15 -0
- package/dist/codemirror/code/codeHighlight.js.map +1 -0
- package/dist/codemirror/code/codeLangAffordance.d.ts +17 -0
- package/dist/codemirror/code/codeLangAffordance.js +80 -0
- package/dist/codemirror/code/codeLangAffordance.js.map +1 -0
- package/dist/codemirror/code/codeLanguageMenu.d.ts +40 -0
- package/dist/codemirror/code/codeLanguageMenu.js +184 -0
- package/dist/codemirror/code/codeLanguageMenu.js.map +1 -0
- package/dist/codemirror/code/codePalette.d.ts +39 -0
- package/dist/codemirror/code/codePalette.js +53 -0
- package/dist/codemirror/code/codePalette.js.map +1 -0
- package/dist/codemirror/code/fenceAutoClose.d.ts +30 -0
- package/dist/codemirror/code/fenceAutoClose.js +171 -0
- package/dist/codemirror/code/fenceAutoClose.js.map +1 -0
- package/dist/codemirror/code/fenceCaretGuard.d.ts +28 -0
- package/dist/codemirror/code/fenceCaretGuard.js +66 -0
- package/dist/codemirror/code/fenceCaretGuard.js.map +1 -0
- package/dist/codemirror/code/fenceDeleteGuards.d.ts +22 -0
- package/dist/codemirror/code/fenceDeleteGuards.js +105 -0
- package/dist/codemirror/code/fenceDeleteGuards.js.map +1 -0
- package/dist/codemirror/code/fenceTabIndent.d.ts +8 -0
- package/dist/codemirror/code/fenceTabIndent.js +50 -0
- package/dist/codemirror/code/fenceTabIndent.js.map +1 -0
- package/dist/codemirror/code/highlightFence.d.ts +25 -0
- package/dist/codemirror/code/highlightFence.js +45 -0
- package/dist/codemirror/code/highlightFence.js.map +1 -0
- package/dist/codemirror/code/index.d.ts +9 -0
- package/dist/codemirror/code/index.js +8 -0
- package/dist/codemirror/code/index.js.map +1 -0
- package/dist/codemirror/core/codeContext.d.ts +32 -0
- package/dist/codemirror/core/codeContext.js +22 -0
- package/dist/codemirror/core/codeContext.js.map +1 -0
- package/dist/codemirror/core/editorTestHarness.d.ts +31 -0
- package/dist/codemirror/core/editorTestHarness.js +69 -0
- package/dist/codemirror/core/editorTestHarness.js.map +1 -0
- package/dist/codemirror/core/editorTheme.d.ts +39 -0
- package/dist/codemirror/core/editorTheme.js +498 -0
- package/dist/codemirror/core/editorTheme.js.map +1 -0
- package/dist/codemirror/core/hostFacets.d.ts +58 -0
- package/dist/codemirror/core/hostFacets.js +27 -0
- package/dist/codemirror/core/hostFacets.js.map +1 -0
- package/dist/codemirror/core/hrWidget.d.ts +22 -0
- package/dist/codemirror/core/hrWidget.js +22 -0
- package/dist/codemirror/core/hrWidget.js.map +1 -0
- package/dist/codemirror/core/htmlEscape.d.ts +6 -0
- package/dist/codemirror/core/htmlEscape.js +12 -0
- package/dist/codemirror/core/htmlEscape.js.map +1 -0
- package/dist/codemirror/core/index.d.ts +13 -0
- package/dist/codemirror/core/index.js +9 -0
- package/dist/codemirror/core/index.js.map +1 -0
- package/dist/codemirror/core/inlineScan.d.ts +49 -0
- package/dist/codemirror/core/inlineScan.js +31 -0
- package/dist/codemirror/core/inlineScan.js.map +1 -0
- package/dist/codemirror/core/lineStructure.d.ts +47 -0
- package/dist/codemirror/core/lineStructure.js +64 -0
- package/dist/codemirror/core/lineStructure.js.map +1 -0
- package/dist/codemirror/core/linkResolution.d.ts +36 -0
- package/dist/codemirror/core/linkResolution.js +58 -0
- package/dist/codemirror/core/linkResolution.js.map +1 -0
- package/dist/codemirror/core/paint.d.ts +118 -0
- package/dist/codemirror/core/paint.js +42 -0
- package/dist/codemirror/core/paint.js.map +1 -0
- package/dist/codemirror/core/parseToEnd.d.ts +24 -0
- package/dist/codemirror/core/parseToEnd.js +38 -0
- package/dist/codemirror/core/parseToEnd.js.map +1 -0
- package/dist/codemirror/core/plugin.d.ts +32 -0
- package/dist/codemirror/core/plugin.js +161 -0
- package/dist/codemirror/core/plugin.js.map +1 -0
- package/dist/codemirror/core/registry.d.ts +38 -0
- package/dist/codemirror/core/registry.js +133 -0
- package/dist/codemirror/core/registry.js.map +1 -0
- package/dist/codemirror/core/testKeys.d.ts +17 -0
- package/dist/codemirror/core/testKeys.js +11 -0
- package/dist/codemirror/core/testKeys.js.map +1 -0
- package/dist/codemirror/core/treeAt.d.ts +31 -0
- package/dist/codemirror/core/treeAt.js +10 -0
- package/dist/codemirror/core/treeAt.js.map +1 -0
- package/dist/codemirror/extensions/footnoteExtension.d.ts +9 -0
- package/dist/codemirror/extensions/footnoteExtension.js +14 -0
- package/dist/codemirror/extensions/footnoteExtension.js.map +1 -0
- package/dist/codemirror/extensions/highlightExtension.d.ts +9 -0
- package/dist/codemirror/extensions/highlightExtension.js +14 -0
- package/dist/codemirror/extensions/highlightExtension.js.map +1 -0
- package/dist/codemirror/extensions/index.d.ts +12 -0
- package/dist/codemirror/extensions/index.js +19 -0
- package/dist/codemirror/extensions/index.js.map +1 -0
- package/dist/codemirror/extensions/loader.d.ts +24 -0
- package/dist/codemirror/extensions/loader.js +33 -0
- package/dist/codemirror/extensions/loader.js.map +1 -0
- package/dist/codemirror/extensions/mathExtension.d.ts +9 -0
- package/dist/codemirror/extensions/mathExtension.js +14 -0
- package/dist/codemirror/extensions/mathExtension.js.map +1 -0
- package/dist/codemirror/extensions/mermaidExtension.d.ts +9 -0
- package/dist/codemirror/extensions/mermaidExtension.js +12 -0
- package/dist/codemirror/extensions/mermaidExtension.js.map +1 -0
- package/dist/codemirror/extensions/tableExtension.d.ts +13 -0
- package/dist/codemirror/extensions/tableExtension.js +39 -0
- package/dist/codemirror/extensions/tableExtension.js.map +1 -0
- package/dist/codemirror/extensions/types.d.ts +39 -0
- package/dist/codemirror/extensions/types.js +1 -0
- package/dist/codemirror/extensions/types.js.map +1 -0
- package/dist/codemirror/extensions/wikilinkExtension.d.ts +9 -0
- package/dist/codemirror/extensions/wikilinkExtension.js +14 -0
- package/dist/codemirror/extensions/wikilinkExtension.js.map +1 -0
- package/dist/codemirror/features/blockCommandSteps.d.ts +20 -0
- package/dist/codemirror/features/blockCommandSteps.js +83 -0
- package/dist/codemirror/features/blockCommandSteps.js.map +1 -0
- package/dist/codemirror/footnote/footnotePlugin.d.ts +10 -0
- package/dist/codemirror/footnote/footnotePlugin.js +78 -0
- package/dist/codemirror/footnote/footnotePlugin.js.map +1 -0
- package/dist/codemirror/footnote/footnoteScanRule.d.ts +9 -0
- package/dist/codemirror/footnote/footnoteScanRule.js +12 -0
- package/dist/codemirror/footnote/footnoteScanRule.js.map +1 -0
- package/dist/codemirror/footnote/index.d.ts +2 -0
- package/dist/codemirror/footnote/index.js +2 -0
- package/dist/codemirror/footnote/index.js.map +1 -0
- package/dist/codemirror/format/blockCommands.d.ts +17 -0
- package/dist/codemirror/format/blockCommands.js +176 -0
- package/dist/codemirror/format/blockCommands.js.map +1 -0
- package/dist/codemirror/format/formatCommands.d.ts +11 -0
- package/dist/codemirror/format/formatCommands.js +132 -0
- package/dist/codemirror/format/formatCommands.js.map +1 -0
- package/dist/codemirror/format/index.d.ts +4 -0
- package/dist/codemirror/format/index.js +3 -0
- package/dist/codemirror/format/index.js.map +1 -0
- package/dist/codemirror/highlight/highlightPlugin.d.ts +10 -0
- package/dist/codemirror/highlight/highlightPlugin.js +66 -0
- package/dist/codemirror/highlight/highlightPlugin.js.map +1 -0
- package/dist/codemirror/highlight/highlightScanRule.d.ts +8 -0
- package/dist/codemirror/highlight/highlightScanRule.js +12 -0
- package/dist/codemirror/highlight/highlightScanRule.js.map +1 -0
- package/dist/codemirror/highlight/index.d.ts +2 -0
- package/dist/codemirror/highlight/index.js +2 -0
- package/dist/codemirror/highlight/index.js.map +1 -0
- package/dist/codemirror/html/htmlWidget.d.ts +23 -0
- package/dist/codemirror/html/htmlWidget.js +67 -0
- package/dist/codemirror/html/htmlWidget.js.map +1 -0
- package/dist/codemirror/html/index.d.ts +4 -0
- package/dist/codemirror/html/index.js +2 -0
- package/dist/codemirror/html/index.js.map +1 -0
- package/dist/codemirror/image/imageActionMenu.d.ts +14 -0
- package/dist/codemirror/image/imageActionMenu.js +112 -0
- package/dist/codemirror/image/imageActionMenu.js.map +1 -0
- package/dist/codemirror/image/imageEditEvent.d.ts +20 -0
- package/dist/codemirror/image/imageEditEvent.js +6 -0
- package/dist/codemirror/image/imageEditEvent.js.map +1 -0
- package/dist/codemirror/image/imageInsertHandlers.d.ts +13 -0
- package/dist/codemirror/image/imageInsertHandlers.js +64 -0
- package/dist/codemirror/image/imageInsertHandlers.js.map +1 -0
- package/dist/codemirror/image/imageWidget.d.ts +49 -0
- package/dist/codemirror/image/imageWidget.js +83 -0
- package/dist/codemirror/image/imageWidget.js.map +1 -0
- package/dist/codemirror/image/index.d.ts +8 -0
- package/dist/codemirror/image/index.js +5 -0
- package/dist/codemirror/image/index.js.map +1 -0
- package/dist/codemirror/interaction/clickModel.d.ts +43 -0
- package/dist/codemirror/interaction/clickModel.js +99 -0
- package/dist/codemirror/interaction/clickModel.js.map +1 -0
- package/dist/codemirror/interaction/cursorModel.d.ts +10 -0
- package/dist/codemirror/interaction/cursorModel.js +58 -0
- package/dist/codemirror/interaction/cursorModel.js.map +1 -0
- package/dist/codemirror/interaction/deleteNormalizer.d.ts +18 -0
- package/dist/codemirror/interaction/deleteNormalizer.js +157 -0
- package/dist/codemirror/interaction/deleteNormalizer.js.map +1 -0
- package/dist/codemirror/interaction/flankingGuard.d.ts +20 -0
- package/dist/codemirror/interaction/flankingGuard.js +48 -0
- package/dist/codemirror/interaction/flankingGuard.js.map +1 -0
- package/dist/codemirror/interaction/index.d.ts +7 -0
- package/dist/codemirror/interaction/index.js +6 -0
- package/dist/codemirror/interaction/index.js.map +1 -0
- package/dist/codemirror/interaction/visiblePosition.d.ts +43 -0
- package/dist/codemirror/interaction/visiblePosition.js +98 -0
- package/dist/codemirror/interaction/visiblePosition.js.map +1 -0
- package/dist/codemirror/list/bulletWidget.d.ts +61 -0
- package/dist/codemirror/list/bulletWidget.js +84 -0
- package/dist/codemirror/list/bulletWidget.js.map +1 -0
- package/dist/codemirror/list/index.d.ts +4 -0
- package/dist/codemirror/list/index.js +3 -0
- package/dist/codemirror/list/index.js.map +1 -0
- package/dist/codemirror/list/listContinuation.d.ts +28 -0
- package/dist/codemirror/list/listContinuation.js +61 -0
- package/dist/codemirror/list/listContinuation.js.map +1 -0
- package/dist/codemirror/list/listIndent.d.ts +8 -0
- package/dist/codemirror/list/listIndent.js +95 -0
- package/dist/codemirror/list/listIndent.js.map +1 -0
- package/dist/codemirror/list/taskCheckboxWidget.d.ts +42 -0
- package/dist/codemirror/list/taskCheckboxWidget.js +47 -0
- package/dist/codemirror/list/taskCheckboxWidget.js.map +1 -0
- package/dist/codemirror/math/index.d.ts +3 -0
- package/dist/codemirror/math/index.js +2 -0
- package/dist/codemirror/math/index.js.map +1 -0
- package/dist/codemirror/math/mathPlugin.d.ts +10 -0
- package/dist/codemirror/math/mathPlugin.js +73 -0
- package/dist/codemirror/math/mathPlugin.js.map +1 -0
- package/dist/codemirror/math/mathScanRule.d.ts +16 -0
- package/dist/codemirror/math/mathScanRule.js +18 -0
- package/dist/codemirror/math/mathScanRule.js.map +1 -0
- package/dist/codemirror/math/mathWidget.d.ts +17 -0
- package/dist/codemirror/math/mathWidget.js +58 -0
- package/dist/codemirror/math/mathWidget.js.map +1 -0
- package/dist/codemirror/mermaid/index.d.ts +4 -0
- package/dist/codemirror/mermaid/index.js +3 -0
- package/dist/codemirror/mermaid/index.js.map +1 -0
- package/dist/codemirror/mermaid/mermaidPlugin.d.ts +25 -0
- package/dist/codemirror/mermaid/mermaidPlugin.js +78 -0
- package/dist/codemirror/mermaid/mermaidPlugin.js.map +1 -0
- package/dist/codemirror/mermaid/mermaidRender.d.ts +42 -0
- package/dist/codemirror/mermaid/mermaidRender.js +136 -0
- package/dist/codemirror/mermaid/mermaidRender.js.map +1 -0
- package/dist/codemirror/mermaid/mermaidWidget.d.ts +42 -0
- package/dist/codemirror/mermaid/mermaidWidget.js +142 -0
- package/dist/codemirror/mermaid/mermaidWidget.js.map +1 -0
- package/dist/codemirror/selectionLayer.d.ts +43 -0
- package/dist/codemirror/selectionLayer.js +135 -0
- package/dist/codemirror/selectionLayer.js.map +1 -0
- package/dist/codemirror/table/cellDividerWidget.d.ts +9 -0
- package/dist/codemirror/table/cellDividerWidget.js +19 -0
- package/dist/codemirror/table/cellDividerWidget.js.map +1 -0
- package/dist/codemirror/table/index.d.ts +5 -0
- package/dist/codemirror/table/index.js +4 -0
- package/dist/codemirror/table/index.js.map +1 -0
- package/dist/codemirror/table/tableArmed.d.ts +27 -0
- package/dist/codemirror/table/tableArmed.js +43 -0
- package/dist/codemirror/table/tableArmed.js.map +1 -0
- package/dist/codemirror/table/tableCell.d.ts +19 -0
- package/dist/codemirror/table/tableCell.js +15 -0
- package/dist/codemirror/table/tableCell.js.map +1 -0
- package/dist/codemirror/table/tableCellNav.d.ts +51 -0
- package/dist/codemirror/table/tableCellNav.js +47 -0
- package/dist/codemirror/table/tableCellNav.js.map +1 -0
- package/dist/codemirror/table/tableContextMenu.d.ts +22 -0
- package/dist/codemirror/table/tableContextMenu.js +139 -0
- package/dist/codemirror/table/tableContextMenu.js.map +1 -0
- package/dist/codemirror/table/tableEditCommands.d.ts +29 -0
- package/dist/codemirror/table/tableEditCommands.js +86 -0
- package/dist/codemirror/table/tableEditCommands.js.map +1 -0
- package/dist/codemirror/table/tableExcerpt.d.ts +29 -0
- package/dist/codemirror/table/tableExcerpt.js +34 -0
- package/dist/codemirror/table/tableExcerpt.js.map +1 -0
- package/dist/codemirror/table/tableField.d.ts +6 -0
- package/dist/codemirror/table/tableField.js +37 -0
- package/dist/codemirror/table/tableField.js.map +1 -0
- package/dist/codemirror/table/tableGeometry.d.ts +33 -0
- package/dist/codemirror/table/tableGeometry.js +28 -0
- package/dist/codemirror/table/tableGeometry.js.map +1 -0
- package/dist/codemirror/table/tableInline.d.ts +37 -0
- package/dist/codemirror/table/tableInline.js +105 -0
- package/dist/codemirror/table/tableInline.js.map +1 -0
- package/dist/codemirror/table/tableModel.d.ts +45 -0
- package/dist/codemirror/table/tableModel.js +98 -0
- package/dist/codemirror/table/tableModel.js.map +1 -0
- package/dist/codemirror/tablev2/bridgeRules.d.ts +48 -0
- package/dist/codemirror/tablev2/bridgeRules.js +42 -0
- package/dist/codemirror/tablev2/bridgeRules.js.map +1 -0
- package/dist/codemirror/tablev2/cellEditingSurface.d.ts +79 -0
- package/dist/codemirror/tablev2/cellEditingSurface.js +70 -0
- package/dist/codemirror/tablev2/cellEditingSurface.js.map +1 -0
- package/dist/codemirror/tablev2/cellText.d.ts +11 -0
- package/dist/codemirror/tablev2/cellText.js +12 -0
- package/dist/codemirror/tablev2/cellText.js.map +1 -0
- package/dist/codemirror/tablev2/inlineCellSurface.d.ts +38 -0
- package/dist/codemirror/tablev2/inlineCellSurface.js +162 -0
- package/dist/codemirror/tablev2/inlineCellSurface.js.map +1 -0
- package/dist/codemirror/tablev2/overlayCellSurface.d.ts +36 -0
- package/dist/codemirror/tablev2/overlayCellSurface.js +150 -0
- package/dist/codemirror/tablev2/overlayCellSurface.js.map +1 -0
- package/dist/codemirror/tablev2/tableEditingSteps.d.ts +12 -0
- package/dist/codemirror/tablev2/tableEditingSteps.js +518 -0
- package/dist/codemirror/tablev2/tableEditingSteps.js.map +1 -0
- package/dist/codemirror/tablev2/tableV2HoverControls.d.ts +23 -0
- package/dist/codemirror/tablev2/tableV2HoverControls.js +89 -0
- package/dist/codemirror/tablev2/tableV2HoverControls.js.map +1 -0
- package/dist/codemirror/tablev2/tableV2Interaction.d.ts +29 -0
- package/dist/codemirror/tablev2/tableV2Interaction.js +224 -0
- package/dist/codemirror/tablev2/tableV2Interaction.js.map +1 -0
- package/dist/codemirror/tablev2/tableV2Selection.d.ts +45 -0
- package/dist/codemirror/tablev2/tableV2Selection.js +81 -0
- package/dist/codemirror/tablev2/tableV2Selection.js.map +1 -0
- package/dist/codemirror/tablev2/tableV2Sync.d.ts +13 -0
- package/dist/codemirror/tablev2/tableV2Sync.js +13 -0
- package/dist/codemirror/tablev2/tableV2Sync.js.map +1 -0
- package/dist/codemirror/tablev2/tableWidgetV2.d.ts +46 -0
- package/dist/codemirror/tablev2/tableWidgetV2.js +139 -0
- package/dist/codemirror/tablev2/tableWidgetV2.js.map +1 -0
- package/dist/codemirror/updateBus.d.ts +29 -0
- package/dist/codemirror/updateBus.js +26 -0
- package/dist/codemirror/updateBus.js.map +1 -0
- package/dist/codemirror/wikilink/index.d.ts +3 -0
- package/dist/codemirror/wikilink/index.js +2 -0
- package/dist/codemirror/wikilink/index.js.map +1 -0
- package/dist/codemirror/wikilink/wikilinkPlugin.d.ts +40 -0
- package/dist/codemirror/wikilink/wikilinkPlugin.js +89 -0
- package/dist/codemirror/wikilink/wikilinkPlugin.js.map +1 -0
- package/dist/codemirror/wikilink/wikilinkScanRule.d.ts +9 -0
- package/dist/codemirror/wikilink/wikilinkScanRule.js +13 -0
- package/dist/codemirror/wikilink/wikilinkScanRule.js.map +1 -0
- package/dist/frontmatter.d.ts +79 -0
- package/dist/frontmatter.js +58 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/imageInsert.d.ts +66 -0
- package/dist/imageInsert.js +111 -0
- package/dist/imageInsert.js.map +1 -0
- package/dist/imageSrcResolver.d.ts +55 -0
- package/dist/imageSrcResolver.js +49 -0
- package/dist/imageSrcResolver.js.map +1 -0
- package/dist/index.d.ts +36 -880
- package/dist/index.js +130 -5996
- package/dist/index.js.map +1 -1
- package/dist/links/wikilink.d.ts +26 -0
- package/dist/links/wikilink.js +54 -0
- package/dist/links/wikilink.js.map +1 -0
- package/dist/links/workspaceLink.d.ts +34 -0
- package/dist/links/workspaceLink.js +60 -0
- package/dist/links/workspaceLink.js.map +1 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +7 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/codemirror/CodeMirrorMarkdownEditor.tsx"],"sourcesContent":["/**\n * Phase-1 CodeMirror 6 markdown editor — the spike behind the\n * `compose.editorEngine.v1 = \"codemirror\"` flag.\n *\n * What is wired:\n * * @codemirror/lang-markdown for tokenization + the small built-in\n * Markdown keymap (continue-list-on-Enter, etc).\n * * `markdownDecorationsPlugin` for live-preview headings + bold +\n * italic. Lists, links, wikilinks, images, comments, tables are\n * Phase 2+.\n * * Same `value` / `onChange` contract as `TiptapMarkdownEditor`,\n * same frontmatter split, same hash-based loop guard. Switching\n * engines mid-session is safe — the file on disk is unchanged.\n * * `mode === \"source\"` disables decorations so the user sees raw\n * markdown. Same toggle, same shortcut, just no marker-hiding.\n *\n * Deliberately NOT wired in Phase 1 (so this file stays small and\n * the spike actually proves the scaling claim):\n * * Toolbar formatting buttons (B / I / H1…) — `EditorFileActions`\n * still renders so Save / History / Export / Comments / Chat\n * work from the toolbar. Formatting buttons port in Phase 2.\n * * Image paste / drop, link picker, wikilink rendering, comment\n * overlay, selection-to-chat bubble. The Tiptap editor handles\n * these today; the CodeMirror editor will once the spike clears.\n *\n * Public surface mirrors `TiptapMarkdownEditorProps` so AppShell's\n * dispatch is symmetric; Phase-2-or-later callbacks are accepted\n * (and ignored) here rather than guarded at every call site.\n */\n\nimport {\n memo,\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { defaultKeymap, history, historyKeymap } from \"@codemirror/commands\";\nimport { markdown, markdownKeymap, markdownLanguage } from \"@codemirror/lang-markdown\";\nimport { languages } from \"@codemirror/language-data\";\nimport { Annotation, EditorSelection, EditorState, type Extension } from \"@codemirror/state\";\nimport { EditorView, keymap } from \"@codemirror/view\";\n\nimport { parseFrontmatter, serializeMarkdown, type Frontmatter } from \"../frontmatter\";\nimport type { DocumentTextChange, SourceRange } from \"../types\";\nimport { byteRangeOf } from \"./byteOffset\";\nimport { drawnCaret } from \"./caretLayer\";\nimport { drawnSelection } from \"./selectionLayer\";\nimport { markdownPaste } from \"./clipboard/pasteMarkdown\";\nimport {\n renderClipboardHtmlFacet,\n richCopy,\n type RenderClipboardHtml,\n} from \"./clipboard/copyRich\";\nimport { onEditorUpdate, updateBus } from \"./updateBus\";\nimport {\n codeHighlight,\n codeLanguageUI,\n fenceAutoCloseKeymap,\n fenceCaretGuard,\n fenceTabKeymap,\n fenceTypeAutoClose,\n} from \"./code\";\nimport {\n commentOnExcerptFacet,\n editorBaseTheme,\n markdownDecorationsPlugin,\n openExternalUrlFacet,\n parseToEnd,\n resolveImageSrcFacet,\n saveImageBytesFacet,\n type CommentOnExcerpt,\n type OpenExternalUrl,\n type ResolveImageSrc,\n type SaveImageBytes,\n} from \"./core\";\nimport {\n clickModel,\n cursorModelKeymap,\n deleteNormalizerKeymap,\n flankingGuard,\n navigateToFacet,\n} from \"./interaction\";\nimport { tightListKeymap, listIndentKeymap } from \"./list\";\nimport { formatCommandsKeymap, blockCommandsKeymap } from \"./format\";\nimport { imageContextFacet, imageInsertHandlers } from \"./image\";\nimport { computeFileDir, type ImageResolveContext } from \"../imageSrcResolver\";\nimport { wikilinkFromPathFacet, wikilinkTargetsFacet } from \"./wikilink\";\nimport {\n mergeExtensions,\n type MarkdownExtension,\n type ToolbarContribution,\n footnoteExtension,\n highlightExtension,\n mathExtension,\n mermaidExtension,\n tableExtension,\n wikilinkExtension,\n} from \"./extensions\";\n\nexport type CodeMirrorEditorMode = \"wysiwyg\" | \"source\";\n\n/** A non-empty editor selection, in document byte offsets. */\nexport interface EditorSelectionSnapshot {\n range: SourceRange;\n text: string;\n}\n\nexport interface CodeMirrorMarkdownEditorProps {\n mode?: CodeMirrorEditorMode;\n onChange: (value: string, changes: DocumentTextChange[]) => void;\n value: string;\n workspaceRoot?: string;\n filePath?: string;\n linkTargets?: ReadonlySet<string>;\n onNavigateToLink?: (path: string) => void;\n /**\n * Extension modules contributed by the host, merged AFTER the built-ins so a\n * host can deliberately override a construct (the merger warns when a node\n * rule is redefined).\n *\n * Read when an editor state is built — on mount, and on the swaps that\n * rebuild one — not on every render, so passing a fresh array each time costs\n * nothing and cannot remount the editor. Changing it mid-session therefore\n * takes effect at the next rebuild.\n *\n * Applies in BOTH modes: a module's node rules simply go unread while the\n * decoration painter is off, but its keymaps and plain CodeMirror extensions\n * are not markdown-rendering concerns and should not be silently dropped in\n * source mode.\n */\n extensions?: readonly MarkdownExtension[];\n /** Host-rendered toolbar. `contributions` carries the toolbar items the\n * extension modules asked for; the host decides whether and how to render\n * them alongside its own. */\n toolbar?: (ctx: {\n view: EditorView;\n contributions: readonly ToolbarContribution[];\n }) => ReactNode;\n /**\n * Host-rendered actions for the current text selection (e.g. a comment / ask\n * bubble). Called with the live selection (or `null` when collapsed) and a\n * `dismiss` that collapses the selection back to a caret. Omit for none.\n */\n selectionActions?: (ctx: {\n selection: EditorSelectionSnapshot | null;\n dismiss: () => void;\n }) => ReactNode;\n // ── Host-environment seams (all optional; browser-friendly defaults) ───────\n /**\n * Map a markdown image `src` to a loadable URL. Default: render the reference\n * as-is. A desktop host maps workspace-relative paths onto its asset protocol.\n */\n resolveImageSrc?: ResolveImageSrc;\n /**\n * Persist a pasted/dropped image's bytes at a workspace-relative path.\n * Default: omitted ⇒ the image is inlined as a `data:` URL.\n */\n saveImageBytes?: SaveImageBytes;\n /**\n * Open a clicked external link. Default: a new browser tab. A desktop host\n * overrides this to leave the app's webview.\n */\n onOpenExternalUrl?: OpenExternalUrl;\n /**\n * Comment on a selected table row/column — wired to the table context menu's\n * \"Comment on this row / column\". The host opens its comment composer at the\n * given anchor, seeded with the excerpt. Default: omitted ⇒ no such menu items.\n */\n onCommentOnExcerpt?: CommentOnExcerpt;\n /**\n * Render a markdown selection to HTML for the clipboard, so a copy pastes\n * formatted into Google Docs / Slack / Word (the markdown source always\n * rides along as text/plain). Default: omitted ⇒ plain-only copies.\n */\n renderClipboardHtml?: RenderClipboardHtml;\n /**\n * Called once, right after a tab-switch content swap commits and paints.\n * Used by the host for latency instrumentation; no-op by default.\n */\n onAfterContentSwap?: () => void;\n /**\n * Receives a synchronous `flush()` that pulls the editor's live (debounce-\n * lagged) content into the last `onChange` immediately — the host calls it\n * before persisting. `null` is passed on unmount. Lets a host avoid writing\n * stale buffers on Cmd+S / tab close.\n */\n onFlushReady?: (flush: (() => void) | null) => void;\n}\n\n/** Tags the editor's own programmatic content swaps (external value patch,\n * save echo) so the autosave listener can tell them from user edits. A\n * cross-update boolean can't do this job: `setState` swaps fire NO update to\n * consume it, and the stale flag then swallowed the user's next real edit —\n * a lone RAW-mode fix looked saved but never was (#108). */\nconst programmaticSwap = Annotation.define<boolean>();\n\nconst AUTOSAVE_DEBOUNCE_MS = 500;\n\n// Same tiny FNV-1a hash the Tiptap editor uses — dedup of save\n// loops only, never used for identity. Kept duplicated rather than\n// importing across feature folders; a shared util can land later if\n// a third caller appears.\nfunction hashString(s: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i += 1) {\n h ^= s.charCodeAt(i);\n h = (h * 0x01000193) >>> 0;\n }\n return h.toString(16);\n}\n\nfunction CodeMirrorMarkdownEditorInner({\n mode = \"wysiwyg\",\n onChange,\n value,\n workspaceRoot,\n filePath,\n linkTargets,\n onNavigateToLink,\n extensions: hostExtensions,\n toolbar,\n selectionActions,\n resolveImageSrc,\n saveImageBytes,\n onOpenExternalUrl,\n onCommentOnExcerpt,\n renderClipboardHtml,\n onAfterContentSwap,\n onFlushReady,\n}: CodeMirrorMarkdownEditorProps) {\n // Frontmatter split — same shape as the Tiptap editor. The\n // editor surface only ever sees the body; YAML is held aside in\n // a ref and recombined on save.\n const parsedRef = useRef(parseFrontmatter(value));\n const frontmatterRef = useRef<Frontmatter | null>(parsedRef.current.frontmatter);\n const bodyRef = useRef<string>(parsedRef.current.body);\n\n // Loop guard: hash of the last markdown WE emitted. When `value`\n // comes back matching this hash the change is our own echo and\n // we skip the reload (otherwise file watcher → setContent →\n // update → save → file watcher loops).\n const lastEmittedHashRef = useRef<string>(hashString(value));\n\n // Debounce timer for autosave.\n const autosaveTimerRef = useRef<number | null>(null);\n\n // The mount node — given to CM6's `parent` option.\n const hostRef = useRef<HTMLDivElement | null>(null);\n // The live editor instance, held in a ref because React's\n // ref-callback pattern is enough for setup/teardown and we never\n // need a re-render on view changes.\n const viewRef = useRef<EditorView | null>(null);\n // Toolbar needs a re-renderable handle on the view so its pressed-\n // state buttons can subscribe to caret motion. Mirror the ref into\n // state once the view mounts.\n const [viewForToolbar, setViewForToolbar] = useState<EditorView | null>(null);\n\n // Selection snapshot driving the floating comment bubble. Same\n // shape as the Tiptap editor's `bubbleSelection`. Updated on\n // every selection change (rAF-polled); cleared when selection is\n // empty.\n const [bubbleSelection, setBubbleSelection] = useState<\n { range: SourceRange; text: string } | null\n >(null);\n\n const onChangeRef = useRef(onChange);\n useLayoutEffect(function syncLatestOnChangeRef() {\n onChangeRef.current = onChange;\n });\n\n // Host-seam callbacks are read through refs so the facet values (baked into\n // each per-tab EditorState at build time) always call the LATEST prop, never\n // a stale closure — even though the state is rebuilt only on mode/tab change.\n const resolveImageSrcRef = useRef(resolveImageSrc);\n const saveImageBytesRef = useRef(saveImageBytes);\n const openExternalUrlRef = useRef(onOpenExternalUrl);\n const onCommentOnExcerptRef = useRef(onCommentOnExcerpt);\n const renderClipboardHtmlRef = useRef(renderClipboardHtml);\n const onAfterContentSwapRef = useRef(onAfterContentSwap);\n const onFlushReadyRef = useRef(onFlushReady);\n useLayoutEffect(function syncHostSeamRefs() {\n resolveImageSrcRef.current = resolveImageSrc;\n saveImageBytesRef.current = saveImageBytes;\n openExternalUrlRef.current = onOpenExternalUrl;\n onCommentOnExcerptRef.current = onCommentOnExcerpt;\n renderClipboardHtmlRef.current = renderClipboardHtml;\n onAfterContentSwapRef.current = onAfterContentSwap;\n onFlushReadyRef.current = onFlushReady;\n });\n\n // Latest mode kept in a ref so the editor lifecycle effect\n // doesn't tear down and rebuild on a wysiwyg ↔ source toggle.\n // Decorations are added or removed via `Compartment` in the\n // Phase-2 cut; for the spike, we rebuild the view on mode change\n // (rare) but NOT on every keystroke.\n const decorationsEnabled = mode === \"wysiwyg\";\n\n // Image src resolution context — passed to the decoration plugin\n // via a CM6 facet so the inline image widget can turn workspace-\n // relative paths into asset:// URLs the webview can fetch.\n const imageCtx = useMemo<ImageResolveContext>(\n () => ({ fileDir: computeFileDir(workspaceRoot, filePath) }),\n [workspaceRoot, filePath],\n );\n\n // Per-tab EditorState cache. The dominant tab-switch cost on a 1 MB\n // note was CodeMirror re-parsing the whole document on every switch\n // (measured ~140ms p99, see docs/perf-spec.md §5). We keep the live\n // `EditorView` mounted across tab switches and swap its `EditorState`\n // instead — `view.setState(cached)` is a pointer swap, not a re-parse.\n // Switching back to an already-open tab restores its cursor, scroll,\n // and undo history for free.\n //\n // Keyed by file path. `syncedHashRef` records the content hash each\n // cached state reflects so we can tell \"the store's value still\n // matches this cached state\" (fast restore) from \"the file changed\n // under us\" (rebuild for correctness).\n const editorStatesRef = useRef<Map<string, EditorState>>(new Map());\n const syncedHashRef = useRef<Map<string, string>>(new Map());\n const currentFileRef = useRef<string | undefined>(filePath);\n // Tracks the decoration mode the live state was built for, so the\n // mode-change effect can skip its initial run (mount already built\n // the correct state).\n const modeInitializedRef = useRef<boolean>(decorationsEnabled);\n\n // Build the full extension list from the CURRENT props. Called when a\n // tab's state is first created (and on mode change). Each per-file\n // state bakes in that file's path-dependent facets (image dir,\n // wikilink source path), so a restored state stays correct for its\n // own file.\n // Host modules and the toolbar items they contribute, read at state-build\n // time rather than depended on, so a fresh array identity is free.\n const hostExtensionsRef = useRef(hostExtensions);\n hostExtensionsRef.current = hostExtensions;\n const toolbarContributionsRef = useRef<readonly ToolbarContribution[]>([]);\n\n function buildExtensions(): Extension[] {\n const base: Extension[] = [\n history(),\n // Update fan-out for reactive chrome (toolbar pressed-states, selection\n // bubble). Must ride the BASE extensions: anything appended to a single\n // state's config dies on the next setState (tab switch) and the chrome\n // freezes at the previous document's context.\n updateBus,\n // Whitespace typed at a bold/italic/strike content edge lands outside\n // the markers, or the closing delimiter stops parsing (#94).\n flankingGuard,\n // Caret and selection both draw from the editor's own selection state:\n // the native contentEditable caret double-paints at atomic widget\n // boundaries (#62), and native ::selection can only highlight rendered\n // DOM, so a whole-document selection loses its highlight outside the\n // virtualized viewport (#166). Table cells run their own subview\n // without either, keeping their native caret and selection.\n drawnCaret,\n drawnSelection,\n // Tight list continuation — Enter drops the next bullet directly below,\n // never with a blank-line gap (overrides the stock markdown Enter for\n // non-empty list items; see listContinuation.ts).\n tightListKeymap,\n // Enter on a just-typed ``` closes the fence with the caret inside —\n // an unclosed fence would swallow the rest of the document (#91).\n fenceAutoCloseKeymap,\n // The keystroke completing a bare ``` closes the fence immediately, so\n // an unclosed opener never swallows the document below (§9.5).\n fenceTypeAutoClose,\n // Tab indents inside a code block (Shift-Tab dedents) — otherwise the\n // browser's focus navigation steals the key mid-code (§12.8).\n fenceTabKeymap,\n // The caret never parks on a fence's marker rows: clicks land on the\n // nearest content edge, arrow motion crosses out of the block (§12.9).\n fenceCaretGuard,\n // Opener-row language pill (+ \"plain\" placeholder) opens the chooser;\n // right-click a block for set-language / copy-code (ADR 0002).\n codeLanguageUI,\n // Tab / Shift-Tab nest / promote a list item by the parent marker width\n // (list-aware; falls through to normal Tab outside a list — listIndent.ts).\n listIndentKeymap,\n keymap.of([...defaultKeymap, ...historyKeymap, ...markdownKeymap]),\n // markdownLanguage = CommonMark + GFM (tables, task lists,\n // strikethrough). Without `base`, `markdown()` uses bare\n // CommonMark — Lezer never produces Table / Task / Strikethrough\n // nodes and our registry can't decorate them.\n //\n // `remove: [\"SetextHeading\"]` disables setext headings — the rule that a\n // line of text immediately followed by a line of `---` (or `===`) turns\n // the text into a heading. That rule silently rewrites the line ABOVE\n // when you type `-` under a paragraph, which is surprising in a rich\n // editor (and fought our \"lone `-` is literal until a space\" bullet\n // rule). ATX `#` headings are unaffected.\n // `codeLanguages` gives fenced blocks a real nested parse (lazily\n // loaded per info string) — actual syntax highlighting via\n // codeHighlight, which styles only code-emitted tags (ADR 0002).\n markdown({\n base: markdownLanguage,\n codeLanguages: languages,\n extensions: [{ remove: [\"SetextHeading\"] }],\n }),\n codeHighlight,\n EditorView.lineWrapping,\n // Editor-internal styling (font, line-height, heading sizes,\n // marker widget styling) lives in `editorBaseTheme` so it\n // participates in CM6's line-metric measurement cycle. Don't\n // duplicate these rules in `global.scss` — that's where the\n // earlier click-drift bug came from.\n editorBaseTheme,\n // Image-src resolution context for the inline image widget.\n imageContextFacet.of(imageCtx),\n // Image paste / drop pipeline. Pulls bytes off the clipboard\n // or DataTransfer, saves through `insertImageBlob`, inserts\n // `` at the caret.\n imageInsertHandlers,\n // Clipboard interop (#134/#135): rich pastes convert to markdown\n // (image-file pastes stay with the handler above); copies carry\n // markdown + rendered HTML. After imageInsertHandlers so file pastes\n // resolve there first.\n markdownPaste,\n richCopy,\n renderClipboardHtmlFacet.of((markdown) =>\n (renderClipboardHtmlRef.current ?? (() => null))(markdown),\n ),\n // Host-environment seams. Registered as stable wrappers (reading the\n // latest prop through a ref) only when the host provides the capability;\n // otherwise the facet's browser default applies. See hostFacets.ts.\n ...(resolveImageSrc\n ? [\n resolveImageSrcFacet.of((rawSrc, ctx) =>\n (resolveImageSrcRef.current ?? ((s: string) => s))(rawSrc, ctx),\n ),\n ]\n : []),\n ...(saveImageBytes\n ? [\n saveImageBytesFacet.of((relPath, bytes) => {\n const fn = saveImageBytesRef.current;\n return fn ? fn(relPath, bytes) : Promise.reject(new Error(\"no saver\"));\n }),\n ]\n : []),\n ...(onOpenExternalUrl\n ? [\n openExternalUrlFacet.of((url) => {\n openExternalUrlRef.current?.(url);\n }),\n ]\n : []),\n ...(onCommentOnExcerpt\n ? [\n commentOnExcerptFacet.of((excerpt, anchor) => {\n onCommentOnExcerptRef.current?.(excerpt, anchor);\n }),\n ]\n : []),\n // Wikilink resolution facets stay in both modes so Cmd-click\n // can navigate from Raw view too — only the decorating plugin\n // itself is gated by `decorationsEnabled` below.\n wikilinkFromPathFacet.of(filePath),\n wikilinkTargetsFacet.of(linkTargets ?? new Set<string>()),\n ...(onNavigateToLink ? [navigateToFacet.of(onNavigateToLink)] : []),\n // Cursor model: arrow/shift+arrow → next visible source\n // position, skipping hidden markdown syntax in one step.\n cursorModelKeymap,\n // Backspace / Delete: removes visible content; collapses an\n // emptied styled span (no dangling `****`).\n deleteNormalizerKeymap,\n // Cmd+B / Cmd+I / Cmd+E: toggle bold / italic / inline code.\n formatCommandsKeymap,\n // Cmd+1..3 / Cmd+Shift+7..9: heading levels, lists, blockquote.\n blockCommandsKeymap,\n // Click model: Cmd/Ctrl-click on a link opens it; caret\n // placement never lands inside a hidden marker (snap-to-content).\n clickModel,\n // The editor invariant: the syntax tree covers the whole document, not\n // just what the viewport rendered — widgets, affordances and structural\n // commands anywhere in the note are wrong without it.\n parseToEnd,\n EditorView.updateListener.of((update) => {\n if (!update.docChanged) return;\n // Our own content swaps must not echo a save; anything involving a\n // real user transaction must.\n if (update.transactions.every((tr) => tr.annotation(programmaticSwap))) {\n return;\n }\n if (autosaveTimerRef.current !== null) {\n window.clearTimeout(autosaveTimerRef.current);\n }\n autosaveTimerRef.current = window.setTimeout(() => {\n autosaveTimerRef.current = null;\n const body = update.view.state.doc.toString();\n bodyRef.current = body;\n const full = serializeMarkdown({\n frontmatter: frontmatterRef.current,\n body,\n });\n const hash = hashString(full);\n if (hash === lastEmittedHashRef.current) return;\n lastEmittedHashRef.current = hash;\n if (currentFileRef.current !== undefined) {\n syncedHashRef.current.set(currentFileRef.current, hash);\n }\n onChangeRef.current(full, []);\n }, AUTOSAVE_DEBOUNCE_MS);\n }),\n ];\n if (decorationsEnabled) {\n base.push(markdownDecorationsPlugin);\n }\n // ONE merge pass over built-ins and host modules together, host last. Two\n // passes would each keep their own set of seen rule names, so a host\n // overriding a built-in construct would land silently instead of warning.\n // The built-ins are markdown rendering and belong to wysiwyg mode; a host's\n // module may be a keymap or a plain CodeMirror extension, which is not a\n // rendering concern, so it applies in both.\n const merged = mergeExtensions([\n ...(decorationsEnabled\n ? [\n wikilinkExtension,\n highlightExtension,\n footnoteExtension,\n mathExtension,\n mermaidExtension,\n tableExtension(),\n ]\n : []),\n ...(hostExtensionsRef.current ?? []),\n ]);\n base.push(...merged.extensions);\n toolbarContributionsRef.current = merged.toolbar;\n return base;\n }\n // `buildExtensions` is intentionally a plain closure read inside\n // effects (not a hook dep) — the effects below decide WHEN a state is\n // rebuilt, so we don't want a fresh `extensions` identity to force a\n // remount the way it did before this cache existed. The host's modules ride\n // through the same ref for the same reason: a host that builds its array\n // inline would otherwise remount the editor on every keystroke.\n const buildExtensionsRef = useRef(buildExtensions);\n useLayoutEffect(function syncBuildExtensionsRef() {\n buildExtensionsRef.current = buildExtensions;\n });\n\n // Mount the view ONCE. Subsequent tab switches swap state (below)\n // rather than tearing down — that's the whole point of the cache.\n //\n // Before the paint, not after it. A passive effect lets the browser show the\n // editor's empty host first and the document a frame later, which on a launch\n // is the app appearing without its text and then filling in. The work is the\n // same either way; doing it here means the first frame that shows the editor\n // shows what is in it.\n useLayoutEffect(function mountEditorView() {\n const host = hostRef.current;\n if (!host) return;\n const initial = EditorState.create({\n doc: bodyRef.current,\n extensions: buildExtensionsRef.current(),\n });\n if (currentFileRef.current !== undefined) {\n editorStatesRef.current.set(currentFileRef.current, initial);\n syncedHashRef.current.set(currentFileRef.current, lastEmittedHashRef.current);\n }\n const view = new EditorView({ state: initial, parent: host });\n viewRef.current = view;\n setViewForToolbar(view);\n return function teardownEditorView() {\n bodyRef.current = view.state.doc.toString();\n view.destroy();\n viewRef.current = null;\n setViewForToolbar(null);\n };\n // Mount-once: deps intentionally empty. Tab switches go through the\n // sync effect; mode changes through the rebuild effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(function syncEditorToActiveFile() {\n const view = viewRef.current;\n if (!view) return;\n const incomingHash = hashString(value);\n const switchingFile = currentFileRef.current !== filePath;\n\n if (!switchingFile) {\n // Same file, external value change (LLM write / file watcher) or\n // our own save echo. Patch the live doc in place.\n if (incomingHash === lastEmittedHashRef.current) return;\n const parsed = parseFrontmatter(value);\n frontmatterRef.current = parsed.frontmatter;\n bodyRef.current = parsed.body;\n lastEmittedHashRef.current = incomingHash;\n view.dispatch({\n changes: { from: 0, to: view.state.doc.length, insert: parsed.body },\n annotations: programmaticSwap.of(true),\n });\n if (filePath !== undefined) syncedHashRef.current.set(filePath, incomingHash);\n return;\n }\n\n // --- Tab switch ---\n const prev = currentFileRef.current;\n\n // Flush any pending autosave for the OUTGOING file FIRST. The view\n // now persists across switches, so a stale timer would otherwise\n // fire against the incoming file's state and write the wrong\n // content. We run the save synchronously with the outgoing doc +\n // frontmatter (still current at this point).\n if (autosaveTimerRef.current !== null) {\n window.clearTimeout(autosaveTimerRef.current);\n autosaveTimerRef.current = null;\n const outgoingBody = view.state.doc.toString();\n const outgoingFull = serializeMarkdown({\n frontmatter: frontmatterRef.current,\n body: outgoingBody,\n });\n const outgoingHash = hashString(outgoingFull);\n if (outgoingHash !== lastEmittedHashRef.current) {\n if (prev !== undefined) syncedHashRef.current.set(prev, outgoingHash);\n onChangeRef.current(outgoingFull, []);\n }\n }\n\n // Stash the outgoing tab's live state (cursor + scroll + undo + any\n // edits) for instant restore later.\n if (prev !== undefined) editorStatesRef.current.set(prev, view.state);\n\n const parsed = parseFrontmatter(value);\n frontmatterRef.current = parsed.frontmatter;\n bodyRef.current = parsed.body;\n lastEmittedHashRef.current = incomingHash;\n currentFileRef.current = filePath;\n\n const key = filePath ?? \"\";\n const cached = editorStatesRef.current.get(key);\n const cachedHash = syncedHashRef.current.get(key);\n if (cached && cachedHash === incomingHash) {\n // FAST PATH — pointer swap, no parse. ~5ms regardless of doc size.\n view.setState(cached);\n } else {\n // Cache miss (first open) or the file changed under us — build a\n // fresh state and cache it.\n const state = EditorState.create({\n doc: parsed.body,\n extensions: buildExtensionsRef.current(),\n });\n editorStatesRef.current.set(key, state);\n syncedHashRef.current.set(key, incomingHash);\n view.setState(state);\n }\n // Content-swap end signal: rAF puts us after CM6's measure +\n // browser paint, so a host's latency probe covers the full \"click\n // → new content visible\" interval (see docs/perf-spec.md §5).\n requestAnimationFrame(() => onAfterContentSwapRef.current?.());\n }, [filePath, value]);\n\n // Mode toggle (Rich ↔ Raw) changes the decoration extensions, so\n // every cached state is stale. Rebuild the active file's state from\n // its live doc and drop the cache; other tabs rebuild lazily on next\n // visit. Rare event — not on the hot path.\n useEffect(function rebuildOnModeChange() {\n const view = viewRef.current;\n if (!view) return;\n // Skip the initial run (mount already built the right state).\n if (modeInitializedRef.current === decorationsEnabled) return;\n modeInitializedRef.current = decorationsEnabled;\n const liveBody = view.state.doc.toString();\n editorStatesRef.current.clear();\n syncedHashRef.current.clear();\n const state = EditorState.create({\n doc: liveBody,\n extensions: buildExtensionsRef.current(),\n });\n if (currentFileRef.current !== undefined) {\n editorStatesRef.current.set(currentFileRef.current, state);\n syncedHashRef.current.set(currentFileRef.current, lastEmittedHashRef.current);\n }\n view.setState(state);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [decorationsEnabled]);\n\n // Pull the live editor content into the store buffer synchronously,\n // cancelling the pending 500ms autosave debounce. The store's save path\n // (`saveActiveFile` — Cmd+S / autosave / close) calls this first so it\n // never persists the stale, debounce-lagged buffer. Reads only refs, so\n // a single stable identity is fine.\n const flushPendingToBuffer = useCallback(function flushPendingToBuffer() {\n const view = viewRef.current;\n if (!view) return;\n if (autosaveTimerRef.current !== null) {\n window.clearTimeout(autosaveTimerRef.current);\n autosaveTimerRef.current = null;\n }\n const body = view.state.doc.toString();\n bodyRef.current = body;\n const full = serializeMarkdown({ frontmatter: frontmatterRef.current, body });\n const hash = hashString(full);\n if (hash === lastEmittedHashRef.current) return;\n lastEmittedHashRef.current = hash;\n onChangeRef.current(full, []);\n }, []);\n\n useEffect(function registerFlushBridge() {\n onFlushReadyRef.current?.(flushPendingToBuffer);\n return function unregisterFlushBridge() {\n onFlushReadyRef.current?.(null);\n };\n }, [flushPendingToBuffer]);\n\n useEffect(function cancelPendingAutosaveOnUnmount() {\n return function clearAutosaveTimer() {\n if (autosaveTimerRef.current !== null) {\n window.clearTimeout(autosaveTimerRef.current);\n }\n };\n }, []);\n\n useEffect(\n function trackSelectionForCommentBubble() {\n const view = viewForToolbar;\n if (!view) return;\n const refresh = () => {\n const main = view.state.selection.main;\n if (main.empty) {\n setBubbleSelection(null);\n } else {\n setBubbleSelection({\n range: byteRangeOf(view.state, main.from, main.to),\n text: view.state.sliceDoc(main.from, main.to),\n });\n }\n };\n refresh();\n // Subscribe via the update bus (not an appended updateListener — that\n // dies on the next setState and the bubble freezes; see updateBus.ts).\n return onEditorUpdate(view, (update) => {\n if (update.docChanged || update.selectionSet) refresh();\n });\n },\n [viewForToolbar],\n );\n\n // Collapse the selection back to a caret — handed to the selection-actions\n // slot so a host can dismiss its bubble after an action lands. Stable (reads\n // the view ref), so it doesn't churn the host's memoised slot.\n const dismissSelection = useCallback(() => {\n const view = viewRef.current;\n if (!view) return;\n const head = view.state.selection.main.head;\n view.dispatch({ selection: EditorSelection.cursor(head) });\n }, []);\n\n // The toolbar slot is invoked through a memo keyed on the (mount-stable) view\n // and the host's slot identity — NOT on every render. So when a keystroke\n // re-renders this leaf, the toolbar element is referentially unchanged and a\n // memoised host toolbar holds (the perf win we'd otherwise lose by calling\n // the slot inline every render).\n const toolbarNode = useMemo(\n () =>\n viewForToolbar && toolbar\n ? toolbar({ view: viewForToolbar, contributions: toolbarContributionsRef.current })\n : null,\n [viewForToolbar, toolbar],\n );\n // The selection slot, by contrast, SHOULD refresh when the selection moves —\n // that's its input. Memoised on the selection so it's stable between\n // selection changes but updates when one happens.\n const selectionNode = useMemo(\n () =>\n selectionActions\n ? selectionActions({ selection: bubbleSelection, dismiss: dismissSelection })\n : null,\n [selectionActions, bubbleSelection, dismissSelection],\n );\n\n // `cm-editor-host` owns the editor surface layout (toolbar row + scroll area\n // + selection overlay). The toolbar and selection UI are host-rendered slots\n // — the editor stays agnostic about save / export / comments / chat.\n return (\n <div className=\"tiptap-editor cm-editor-host\">\n {toolbarNode}\n <div className=\"tiptap-editor__scroll cm-editor-host__scroll\" ref={hostRef} />\n {selectionNode}\n </div>\n );\n}\n\n/**\n * Memoised export — same reason as the Tiptap editor. AppShell\n * re-renders on every chat-thread token; without memoisation each\n * token would blow through the editor's render path.\n */\nexport const CodeMirrorMarkdownEditor = memo(CodeMirrorMarkdownEditorInner);\n"],"mappings":"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","names":["markdown","parsed","flushPendingToBuffer"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EditorState } from '@codemirror/state';
|
|
2
|
+
import { SourceRange } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The editor's public {@link SourceRange} is contracted in UTF-8 byte offsets
|
|
6
|
+
* (see types.ts) — that's what the host persists and maps with (chat excerpt
|
|
7
|
+
* line:col, comment anchoring). CodeMirror, though, indexes the document in
|
|
8
|
+
* UTF-16 code units. Convert at the one boundary where a range leaves the
|
|
9
|
+
* editor (the selection snapshot, the table excerpt) so a multi-byte character
|
|
10
|
+
* before the position doesn't skew the reported offset. For all-ASCII text the
|
|
11
|
+
* two coincide and this is the identity.
|
|
12
|
+
*/
|
|
13
|
+
declare function byteOffsetAt(state: EditorState, pos: number): number;
|
|
14
|
+
/** A {@link SourceRange} (byte offsets) for the CM code-unit span `[from, to)`. */
|
|
15
|
+
declare function byteRangeOf(state: EditorState, from: number, to: number): SourceRange;
|
|
16
|
+
|
|
17
|
+
export { byteOffsetAt, byteRangeOf };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "../chunk-V6TY7KAL.js";
|
|
2
|
+
function byteOffsetAt(state, pos) {
|
|
3
|
+
const prefix = state.sliceDoc(0, pos);
|
|
4
|
+
let bytes = 0;
|
|
5
|
+
for (let i = 0; i < prefix.length; ) {
|
|
6
|
+
const codePoint = prefix.codePointAt(i);
|
|
7
|
+
bytes += codePoint < 128 ? 1 : codePoint < 2048 ? 2 : codePoint < 65536 ? 3 : 4;
|
|
8
|
+
i += codePoint >= 65536 ? 2 : 1;
|
|
9
|
+
}
|
|
10
|
+
return bytes;
|
|
11
|
+
}
|
|
12
|
+
function byteRangeOf(state, from, to) {
|
|
13
|
+
return { start: byteOffsetAt(state, from), end: byteOffsetAt(state, to) };
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
byteOffsetAt,
|
|
17
|
+
byteRangeOf
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=byteOffset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/codemirror/byteOffset.ts"],"sourcesContent":["import { type EditorState } from \"@codemirror/state\";\n\nimport { type SourceRange } from \"../types\";\n\n/**\n * The editor's public {@link SourceRange} is contracted in UTF-8 byte offsets\n * (see types.ts) — that's what the host persists and maps with (chat excerpt\n * line:col, comment anchoring). CodeMirror, though, indexes the document in\n * UTF-16 code units. Convert at the one boundary where a range leaves the\n * editor (the selection snapshot, the table excerpt) so a multi-byte character\n * before the position doesn't skew the reported offset. For all-ASCII text the\n * two coincide and this is the identity.\n */\nexport function byteOffsetAt(state: EditorState, pos: number): number {\n const prefix = state.sliceDoc(0, pos);\n let bytes = 0;\n for (let i = 0; i < prefix.length; ) {\n const codePoint = prefix.codePointAt(i)!;\n bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4;\n i += codePoint >= 0x10000 ? 2 : 1;\n }\n return bytes;\n}\n\n/** A {@link SourceRange} (byte offsets) for the CM code-unit span `[from, to)`. */\nexport function byteRangeOf(state: EditorState, from: number, to: number): SourceRange {\n return { start: byteOffsetAt(state, from), end: byteOffsetAt(state, to) };\n}\n"],"mappings":";AAaO,SAAS,aAAa,OAAoB,KAAqB;AACpE,QAAM,SAAS,MAAM,SAAS,GAAG,GAAG;AACpC,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,OAAO,UAAU;AACnC,UAAM,YAAY,OAAO,YAAY,CAAC;AACtC,aAAS,YAAY,MAAO,IAAI,YAAY,OAAQ,IAAI,YAAY,QAAU,IAAI;AAClF,SAAK,aAAa,QAAU,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAGO,SAAS,YAAY,OAAoB,MAAc,IAAyB;AACrF,SAAO,EAAE,OAAO,aAAa,OAAO,IAAI,GAAG,KAAK,aAAa,OAAO,EAAE,EAAE;AAC1E;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Extension } from '@codemirror/state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drawn caret (#62 — interaction-spec §7.4).
|
|
5
|
+
*
|
|
6
|
+
* The caret must be drawn from `EditorState.selection`: WKWebView paints the
|
|
7
|
+
* native contentEditable caret on BOTH sides of an atomic widget boundary
|
|
8
|
+
* (#62). Selection RANGES are painted by the sibling layer in
|
|
9
|
+
* `selectionLayer.ts` — also from logical state, since native ::selection
|
|
10
|
+
* can't survive the virtualized viewport — using forward-geometry rects that
|
|
11
|
+
* avoid `drawSelection()`'s far-edge `posAtCoords` probe (the #90 flashing).
|
|
12
|
+
*
|
|
13
|
+
* The layer keeps the `cm-cursorLayer` class so existing styling hooks
|
|
14
|
+
* (table arming's caret-hide, the base theme's blink animation) apply.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
declare const drawnCaret: Extension;
|
|
18
|
+
|
|
19
|
+
export { drawnCaret };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "../chunk-V6TY7KAL.js";
|
|
2
|
+
import { Prec } from "@codemirror/state";
|
|
3
|
+
import { EditorView, layer, RectangleMarker } from "@codemirror/view";
|
|
4
|
+
function relevant(update) {
|
|
5
|
+
return update.docChanged || update.selectionSet || update.geometryChanged || update.viewportChanged || update.focusChanged;
|
|
6
|
+
}
|
|
7
|
+
const caretLayer = layer({
|
|
8
|
+
above: true,
|
|
9
|
+
class: "cm-cursorLayer",
|
|
10
|
+
update(update, dom) {
|
|
11
|
+
if (update.selectionSet) {
|
|
12
|
+
dom.style.animationName = dom.style.animationName === "cm-blink" ? "cm-blink2" : "cm-blink";
|
|
13
|
+
}
|
|
14
|
+
return relevant(update);
|
|
15
|
+
},
|
|
16
|
+
markers(view) {
|
|
17
|
+
const markers = [];
|
|
18
|
+
for (const range of view.state.selection.ranges) {
|
|
19
|
+
if (!range.empty) continue;
|
|
20
|
+
const cls = range === view.state.selection.main ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
|
|
21
|
+
markers.push(...RectangleMarker.forRange(view, cls, range));
|
|
22
|
+
}
|
|
23
|
+
return markers;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const hideNativeCaret = Prec.highest(
|
|
27
|
+
EditorView.theme({
|
|
28
|
+
".cm-content": { caretColor: "transparent" },
|
|
29
|
+
// Native-caret islands: tablev2 cells edit with the BROWSER caret (no
|
|
30
|
+
// drawn caret of their own), so they win the color back — a direct rule
|
|
31
|
+
// on the element beats the inherited transparent.
|
|
32
|
+
'.cm-content [contenteditable="plaintext-only"]': {
|
|
33
|
+
caretColor: "var(--cds-text-primary, #161616)"
|
|
34
|
+
},
|
|
35
|
+
// While a cell edit is active the drawn caret would ghost at the parked
|
|
36
|
+
// main-selection position; the surface stamps this class on the editor.
|
|
37
|
+
"&.cm-tablev2-editing .cm-cursorLayer": { display: "none" }
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
const drawnCaret = [caretLayer, hideNativeCaret];
|
|
41
|
+
export {
|
|
42
|
+
drawnCaret
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=caretLayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/codemirror/caretLayer.ts"],"sourcesContent":["/**\n * Drawn caret (#62 — interaction-spec §7.4).\n *\n * The caret must be drawn from `EditorState.selection`: WKWebView paints the\n * native contentEditable caret on BOTH sides of an atomic widget boundary\n * (#62). Selection RANGES are painted by the sibling layer in\n * `selectionLayer.ts` — also from logical state, since native ::selection\n * can't survive the virtualized viewport — using forward-geometry rects that\n * avoid `drawSelection()`'s far-edge `posAtCoords` probe (the #90 flashing).\n *\n * The layer keeps the `cm-cursorLayer` class so existing styling hooks\n * (table arming's caret-hide, the base theme's blink animation) apply.\n */\n\nimport { Prec, type Extension } from \"@codemirror/state\";\nimport { EditorView, layer, RectangleMarker, type ViewUpdate } from \"@codemirror/view\";\n\nfunction relevant(update: ViewUpdate): boolean {\n return (\n update.docChanged ||\n update.selectionSet ||\n update.geometryChanged ||\n update.viewportChanged ||\n update.focusChanged\n );\n}\n\nconst caretLayer = layer({\n above: true,\n class: \"cm-cursorLayer\",\n update(update, dom) {\n // Restart the blink so a just-moved caret is immediately visible.\n if (update.selectionSet) {\n dom.style.animationName = dom.style.animationName === \"cm-blink\" ? \"cm-blink2\" : \"cm-blink\";\n }\n return relevant(update);\n },\n markers(view) {\n const markers: RectangleMarker[] = [];\n for (const range of view.state.selection.ranges) {\n if (!range.empty) continue;\n const cls =\n range === view.state.selection.main\n ? \"cm-cursor cm-cursor-primary\"\n : \"cm-cursor cm-cursor-secondary\";\n markers.push(...RectangleMarker.forRange(view, cls, range));\n }\n return markers;\n },\n});\n\n// The drawn caret replaces the native one; the native SELECTION stays.\nconst hideNativeCaret = Prec.highest(\n EditorView.theme({\n \".cm-content\": { caretColor: \"transparent\" },\n // Native-caret islands: tablev2 cells edit with the BROWSER caret (no\n // drawn caret of their own), so they win the color back — a direct rule\n // on the element beats the inherited transparent.\n '.cm-content [contenteditable=\"plaintext-only\"]': {\n caretColor: \"var(--cds-text-primary, #161616)\",\n },\n // While a cell edit is active the drawn caret would ghost at the parked\n // main-selection position; the surface stamps this class on the editor.\n \"&.cm-tablev2-editing .cm-cursorLayer\": { display: \"none\" },\n }),\n);\n\nexport const drawnCaret: Extension = [caretLayer, hideNativeCaret];\n"],"mappings":";AAcA,SAAS,YAA4B;AACrC,SAAS,YAAY,OAAO,uBAAwC;AAEpE,SAAS,SAAS,QAA6B;AAC7C,SACE,OAAO,cACP,OAAO,gBACP,OAAO,mBACP,OAAO,mBACP,OAAO;AAEX;AAEA,MAAM,aAAa,MAAM;AAAA,EACvB,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO,QAAQ,KAAK;AAElB,QAAI,OAAO,cAAc;AACvB,UAAI,MAAM,gBAAgB,IAAI,MAAM,kBAAkB,aAAa,cAAc;AAAA,IACnF;AACA,WAAO,SAAS,MAAM;AAAA,EACxB;AAAA,EACA,QAAQ,MAAM;AACZ,UAAM,UAA6B,CAAC;AACpC,eAAW,SAAS,KAAK,MAAM,UAAU,QAAQ;AAC/C,UAAI,CAAC,MAAM,MAAO;AAClB,YAAM,MACJ,UAAU,KAAK,MAAM,UAAU,OAC3B,gCACA;AACN,cAAQ,KAAK,GAAG,gBAAgB,SAAS,MAAM,KAAK,KAAK,CAAC;AAAA,IAC5D;AACA,WAAO;AAAA,EACT;AACF,CAAC;AAGD,MAAM,kBAAkB,KAAK;AAAA,EAC3B,WAAW,MAAM;AAAA,IACf,eAAe,EAAE,YAAY,cAAc;AAAA;AAAA;AAAA;AAAA,IAI3C,kDAAkD;AAAA,MAChD,YAAY;AAAA,IACd;AAAA;AAAA;AAAA,IAGA,wCAAwC,EAAE,SAAS,OAAO;AAAA,EAC5D,CAAC;AACH;AAEO,MAAM,aAAwB,CAAC,YAAY,eAAe;","names":[]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EditorState, Facet, Extension } from '@codemirror/state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rich copy/cut (#135): every copy writes BOTH clipboard flavors —
|
|
5
|
+
*
|
|
6
|
+
* text/plain — the markdown SOURCE of the selection (lossless: editors,
|
|
7
|
+
* terminals, and Compose→Compose round trips);
|
|
8
|
+
* text/html — the selection rendered by the host (Google Docs, Slack,
|
|
9
|
+
* Word, and Gmail paste it formatted).
|
|
10
|
+
*
|
|
11
|
+
* The HTML carries {@link COMPOSE_CLIPBOARD_ATTR} so our own paste handler
|
|
12
|
+
* prefers the markdown flavor instead of re-converting our rendering.
|
|
13
|
+
*
|
|
14
|
+
* Stays out of the way of: the table surface's TSV cell-selection copy (a
|
|
15
|
+
* capture-phase document listener that preventDefaults first), and CM's
|
|
16
|
+
* native empty-selection behavior. Without a host renderer the handler still
|
|
17
|
+
* runs — plain-only, but through one code path.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Host-supplied markdown → HTML renderer for clipboard writes. Null (the
|
|
21
|
+
* default) copies plain markdown only. Wired like the other host seams —
|
|
22
|
+
* see hostFacets.ts. */
|
|
23
|
+
type RenderClipboardHtml = (markdown: string) => string | null;
|
|
24
|
+
declare const renderClipboardHtmlFacet: Facet<RenderClipboardHtml | null, RenderClipboardHtml | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Slice the selection as balanced markdown: when an edge lands inside a
|
|
27
|
+
* marker-paired node, the missing delimiter is synthesized onto that edge —
|
|
28
|
+
* copy what the user SAW (`**Priorties**`), not where the hidden markers
|
|
29
|
+
* happened to fall. A mid-content selection (neither marker covered) stays a
|
|
30
|
+
* plain slice; delimiter style (`*` vs `_`, `~~`, backticks) is preserved by
|
|
31
|
+
* copying the node's actual mark text.
|
|
32
|
+
*/
|
|
33
|
+
declare function balancedSlice(state: EditorState, from: number, to: number): string;
|
|
34
|
+
/** The copy half of clipboard interop. */
|
|
35
|
+
declare const richCopy: Extension;
|
|
36
|
+
|
|
37
|
+
export { type RenderClipboardHtml, balancedSlice, renderClipboardHtmlFacet, richCopy };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "../../chunk-V6TY7KAL.js";
|
|
2
|
+
import { EditorView } from "@codemirror/view";
|
|
3
|
+
import { Facet } from "@codemirror/state";
|
|
4
|
+
import { COMPOSE_CLIPBOARD_ATTR } from "./htmlToMarkdown.js";
|
|
5
|
+
import { treeAt } from "../core/treeAt.js";
|
|
6
|
+
const renderClipboardHtmlFacet = Facet.define({
|
|
7
|
+
combine: (values) => values[0] ?? null
|
|
8
|
+
});
|
|
9
|
+
const MARKER_PAIRED = /* @__PURE__ */ new Set(["Emphasis", "StrongEmphasis", "Strikethrough", "InlineCode"]);
|
|
10
|
+
function balancedSlice(state, from, to) {
|
|
11
|
+
const tree = treeAt(state, to);
|
|
12
|
+
let prefix = "";
|
|
13
|
+
let suffix = "";
|
|
14
|
+
tree.iterate({
|
|
15
|
+
from,
|
|
16
|
+
to,
|
|
17
|
+
enter: (node) => {
|
|
18
|
+
if (!MARKER_PAIRED.has(node.name)) return;
|
|
19
|
+
if (node.from >= from && node.to <= to) return false;
|
|
20
|
+
const first = node.node.firstChild;
|
|
21
|
+
const last = node.node.lastChild;
|
|
22
|
+
if (!first || !last || !first.name.endsWith("Mark") || !last.name.endsWith("Mark")) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const contentStart = first.to;
|
|
26
|
+
const contentEnd = last.from;
|
|
27
|
+
if (from >= contentStart && from < contentEnd) {
|
|
28
|
+
prefix = prefix + state.sliceDoc(first.from, first.to);
|
|
29
|
+
}
|
|
30
|
+
if (to > contentStart && to <= contentEnd) {
|
|
31
|
+
suffix = state.sliceDoc(last.from, last.to) + suffix;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return prefix + state.sliceDoc(from, to) + suffix;
|
|
36
|
+
}
|
|
37
|
+
function handleClipboard(event, view, cut) {
|
|
38
|
+
if (event.defaultPrevented) return false;
|
|
39
|
+
const data = event.clipboardData;
|
|
40
|
+
if (!data) return false;
|
|
41
|
+
const ranges = view.state.selection.ranges.filter((range) => !range.empty);
|
|
42
|
+
if (ranges.length === 0) return false;
|
|
43
|
+
const markdown = ranges.map((range) => balancedSlice(view.state, range.from, range.to)).join(view.state.lineBreak);
|
|
44
|
+
data.setData("text/plain", markdown);
|
|
45
|
+
const render = view.state.facet(renderClipboardHtmlFacet);
|
|
46
|
+
const html = render?.(markdown);
|
|
47
|
+
if (html) {
|
|
48
|
+
data.setData("text/html", `<div ${COMPOSE_CLIPBOARD_ATTR}="true">${html}</div>`);
|
|
49
|
+
}
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
if (cut && !view.state.readOnly) {
|
|
52
|
+
view.dispatch(view.state.replaceSelection(""), {
|
|
53
|
+
userEvent: "delete.cut",
|
|
54
|
+
scrollIntoView: true
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const richCopy = EditorView.domEventHandlers({
|
|
60
|
+
copy(event, view) {
|
|
61
|
+
return handleClipboard(event, view, false);
|
|
62
|
+
},
|
|
63
|
+
cut(event, view) {
|
|
64
|
+
return handleClipboard(event, view, true);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
export {
|
|
68
|
+
balancedSlice,
|
|
69
|
+
renderClipboardHtmlFacet,
|
|
70
|
+
richCopy
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=copyRich.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/clipboard/copyRich.ts"],"sourcesContent":["/**\n * Rich copy/cut (#135): every copy writes BOTH clipboard flavors —\n *\n * text/plain — the markdown SOURCE of the selection (lossless: editors,\n * terminals, and Compose→Compose round trips);\n * text/html — the selection rendered by the host (Google Docs, Slack,\n * Word, and Gmail paste it formatted).\n *\n * The HTML carries {@link COMPOSE_CLIPBOARD_ATTR} so our own paste handler\n * prefers the markdown flavor instead of re-converting our rendering.\n *\n * Stays out of the way of: the table surface's TSV cell-selection copy (a\n * capture-phase document listener that preventDefaults first), and CM's\n * native empty-selection behavior. Without a host renderer the handler still\n * runs — plain-only, but through one code path.\n */\n\nimport { EditorView } from \"@codemirror/view\";\nimport { Facet, type EditorState, type Extension } from \"@codemirror/state\";\n\nimport { COMPOSE_CLIPBOARD_ATTR } from \"./htmlToMarkdown\";\nimport { treeAt } from \"../core/treeAt\";\n\n/** Host-supplied markdown → HTML renderer for clipboard writes. Null (the\n * default) copies plain markdown only. Wired like the other host seams —\n * see hostFacets.ts. */\nexport type RenderClipboardHtml = (markdown: string) => string | null;\n\nexport const renderClipboardHtmlFacet = Facet.define<\n RenderClipboardHtml | null,\n RenderClipboardHtml | null\n>({\n combine: (values) => values[0] ?? null,\n});\n\n/** Inline constructs whose delimiters hide in rich mode. A selection made by\n * eye starts at the first VISIBLE character — in source, that's just past\n * the opening marker — so a naive slice ships an orphaned `**` (#140). */\nconst MARKER_PAIRED = new Set([\"Emphasis\", \"StrongEmphasis\", \"Strikethrough\", \"InlineCode\"]);\n\n/**\n * Slice the selection as balanced markdown: when an edge lands inside a\n * marker-paired node, the missing delimiter is synthesized onto that edge —\n * copy what the user SAW (`**Priorties**`), not where the hidden markers\n * happened to fall. A mid-content selection (neither marker covered) stays a\n * plain slice; delimiter style (`*` vs `_`, `~~`, backticks) is preserved by\n * copying the node's actual mark text.\n */\nexport function balancedSlice(state: EditorState, from: number, to: number): string {\n const tree = treeAt(state, to);\n let prefix = \"\";\n let suffix = \"\";\n tree.iterate({\n from,\n to,\n enter: (node) => {\n if (!MARKER_PAIRED.has(node.name)) return;\n if (node.from >= from && node.to <= to) return false; // marks travel with the slice\n const first = node.node.firstChild;\n const last = node.node.lastChild;\n if (!first || !last || !first.name.endsWith(\"Mark\") || !last.name.endsWith(\"Mark\")) {\n return;\n }\n // An edge landing in the CONTENT region (between the marks) gets that\n // side's marker synthesized. Each edge repairs independently: the\n // screen shows the selected span formatted, so the copy carries the\n // formatting even when both marks fall outside the selection. An edge\n // inside a marker itself (raw-mode oddity) is left exactly as sliced.\n const contentStart = first.to;\n const contentEnd = last.from;\n // Outer nodes visit first: opening repairs append (outer→inner order),\n // closing repairs prepend (inner→outer), so nesting stays well-formed.\n if (from >= contentStart && from < contentEnd) {\n prefix = prefix + state.sliceDoc(first.from, first.to);\n }\n if (to > contentStart && to <= contentEnd) {\n suffix = state.sliceDoc(last.from, last.to) + suffix;\n }\n },\n });\n return prefix + state.sliceDoc(from, to) + suffix;\n}\n\nfunction handleClipboard(event: ClipboardEvent, view: EditorView, cut: boolean): boolean {\n if (event.defaultPrevented) return false;\n const data = event.clipboardData;\n if (!data) return false;\n const ranges = view.state.selection.ranges.filter((range) => !range.empty);\n if (ranges.length === 0) return false;\n\n const markdown = ranges\n .map((range) => balancedSlice(view.state, range.from, range.to))\n .join(view.state.lineBreak);\n data.setData(\"text/plain\", markdown);\n const render = view.state.facet(renderClipboardHtmlFacet);\n const html = render?.(markdown);\n if (html) {\n data.setData(\"text/html\", `<div ${COMPOSE_CLIPBOARD_ATTR}=\"true\">${html}</div>`);\n }\n event.preventDefault();\n\n if (cut && !view.state.readOnly) {\n view.dispatch(view.state.replaceSelection(\"\"), {\n userEvent: \"delete.cut\",\n scrollIntoView: true,\n });\n }\n return true;\n}\n\n/** The copy half of clipboard interop. */\nexport const richCopy: Extension = EditorView.domEventHandlers({\n copy(event, view) {\n return handleClipboard(event, view, false);\n },\n cut(event, view) {\n return handleClipboard(event, view, true);\n },\n});\n"],"mappings":";AAiBA,SAAS,kBAAkB;AAC3B,SAAS,aAA+C;AAExD,SAAS,8BAA8B;AACvC,SAAS,cAAc;AAOhB,MAAM,2BAA2B,MAAM,OAG5C;AAAA,EACA,SAAS,CAAC,WAAW,OAAO,CAAC,KAAK;AACpC,CAAC;AAKD,MAAM,gBAAgB,oBAAI,IAAI,CAAC,YAAY,kBAAkB,iBAAiB,YAAY,CAAC;AAUpF,SAAS,cAAc,OAAoB,MAAc,IAAoB;AAClF,QAAM,OAAO,OAAO,OAAO,EAAE;AAC7B,MAAI,SAAS;AACb,MAAI,SAAS;AACb,OAAK,QAAQ;AAAA,IACX;AAAA,IACA;AAAA,IACA,OAAO,CAAC,SAAS;AACf,UAAI,CAAC,cAAc,IAAI,KAAK,IAAI,EAAG;AACnC,UAAI,KAAK,QAAQ,QAAQ,KAAK,MAAM,GAAI,QAAO;AAC/C,YAAM,QAAQ,KAAK,KAAK;AACxB,YAAM,OAAO,KAAK,KAAK;AACvB,UAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS,MAAM,KAAK,CAAC,KAAK,KAAK,SAAS,MAAM,GAAG;AAClF;AAAA,MACF;AAMA,YAAM,eAAe,MAAM;AAC3B,YAAM,aAAa,KAAK;AAGxB,UAAI,QAAQ,gBAAgB,OAAO,YAAY;AAC7C,iBAAS,SAAS,MAAM,SAAS,MAAM,MAAM,MAAM,EAAE;AAAA,MACvD;AACA,UAAI,KAAK,gBAAgB,MAAM,YAAY;AACzC,iBAAS,MAAM,SAAS,KAAK,MAAM,KAAK,EAAE,IAAI;AAAA,MAChD;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO,SAAS,MAAM,SAAS,MAAM,EAAE,IAAI;AAC7C;AAEA,SAAS,gBAAgB,OAAuB,MAAkB,KAAuB;AACvF,MAAI,MAAM,iBAAkB,QAAO;AACnC,QAAM,OAAO,MAAM;AACnB,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,SAAS,KAAK,MAAM,UAAU,OAAO,OAAO,CAAC,UAAU,CAAC,MAAM,KAAK;AACzE,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,WAAW,OACd,IAAI,CAAC,UAAU,cAAc,KAAK,OAAO,MAAM,MAAM,MAAM,EAAE,CAAC,EAC9D,KAAK,KAAK,MAAM,SAAS;AAC5B,OAAK,QAAQ,cAAc,QAAQ;AACnC,QAAM,SAAS,KAAK,MAAM,MAAM,wBAAwB;AACxD,QAAM,OAAO,SAAS,QAAQ;AAC9B,MAAI,MAAM;AACR,SAAK,QAAQ,aAAa,QAAQ,sBAAsB,WAAW,IAAI,QAAQ;AAAA,EACjF;AACA,QAAM,eAAe;AAErB,MAAI,OAAO,CAAC,KAAK,MAAM,UAAU;AAC/B,SAAK,SAAS,KAAK,MAAM,iBAAiB,EAAE,GAAG;AAAA,MAC7C,WAAW;AAAA,MACX,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAGO,MAAM,WAAsB,WAAW,iBAAiB;AAAA,EAC7D,KAAK,OAAO,MAAM;AAChB,WAAO,gBAAgB,OAAO,MAAM,KAAK;AAAA,EAC3C;AAAA,EACA,IAAI,OAAO,MAAM;AACf,WAAO,gBAAgB,OAAO,MAAM,IAAI;AAAA,EAC1C;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipboard HTML → Markdown (#134), the paste half of clipboard interop.
|
|
3
|
+
*
|
|
4
|
+
* Real-world clipboard HTML is hostile: Google Docs wraps everything in a
|
|
5
|
+
* `<b id="docs-internal-guid…" style="font-weight:normal">` and expresses ALL
|
|
6
|
+
* formatting as `<span style>`; Word exports `mso-` styled soup. The DOM is
|
|
7
|
+
* normalized here first — styled spans become real `<strong>/<em>/<s>`
|
|
8
|
+
* elements, wrapper lies are unwrapped — so turndown converts semantics, not
|
|
9
|
+
* vendor quirks.
|
|
10
|
+
*/
|
|
11
|
+
/** Marks HTML that Compose itself put on the clipboard (see copyRich.ts).
|
|
12
|
+
* The paste handler sees it and uses the lossless text/plain markdown
|
|
13
|
+
* instead of re-converting our own rendering. */
|
|
14
|
+
declare const COMPOSE_CLIPBOARD_ATTR = "data-compose-markdown";
|
|
15
|
+
declare function isComposeClipboardHtml(html: string): boolean;
|
|
16
|
+
/** Convert clipboard HTML to house-style Markdown. Returns "" when nothing
|
|
17
|
+
* convertible remains (caller falls back to the native plain paste). */
|
|
18
|
+
declare function htmlToMarkdown(html: string): string;
|
|
19
|
+
|
|
20
|
+
export { COMPOSE_CLIPBOARD_ATTR, htmlToMarkdown, isComposeClipboardHtml };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import "../../chunk-V6TY7KAL.js";
|
|
2
|
+
import TurndownService from "turndown";
|
|
3
|
+
import { gfm } from "@joplin/turndown-plugin-gfm";
|
|
4
|
+
const COMPOSE_CLIPBOARD_ATTR = "data-compose-markdown";
|
|
5
|
+
function isComposeClipboardHtml(html) {
|
|
6
|
+
return html.includes(COMPOSE_CLIPBOARD_ATTR);
|
|
7
|
+
}
|
|
8
|
+
function isBoldStyle(style) {
|
|
9
|
+
const weight = style.fontWeight;
|
|
10
|
+
const numeric = Number(weight);
|
|
11
|
+
return weight === "bold" || weight === "bolder" || !Number.isNaN(numeric) && numeric >= 600;
|
|
12
|
+
}
|
|
13
|
+
function normalizeVendorDom(root) {
|
|
14
|
+
root.querySelectorAll("b[id^='docs-internal-guid']").forEach((wrapper) => {
|
|
15
|
+
wrapper.replaceWith(...Array.from(wrapper.childNodes));
|
|
16
|
+
});
|
|
17
|
+
root.querySelectorAll("li > p:only-child").forEach((p) => {
|
|
18
|
+
p.replaceWith(...Array.from(p.childNodes));
|
|
19
|
+
});
|
|
20
|
+
root.querySelectorAll("span").forEach((span) => {
|
|
21
|
+
const style = span.style;
|
|
22
|
+
if (!style) return;
|
|
23
|
+
const tags = [];
|
|
24
|
+
if (isBoldStyle(style)) tags.push("strong");
|
|
25
|
+
if (style.fontStyle === "italic") tags.push("em");
|
|
26
|
+
if (style.textDecoration.includes("line-through")) tags.push("s");
|
|
27
|
+
if (tags.length === 0) return;
|
|
28
|
+
const doc = span.ownerDocument;
|
|
29
|
+
const outermost = doc.createElement(tags[0]);
|
|
30
|
+
let innermost = outermost;
|
|
31
|
+
for (const tag of tags.slice(1)) {
|
|
32
|
+
const next = doc.createElement(tag);
|
|
33
|
+
innermost.appendChild(next);
|
|
34
|
+
innermost = next;
|
|
35
|
+
}
|
|
36
|
+
innermost.append(...Array.from(span.childNodes));
|
|
37
|
+
span.replaceWith(outermost);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function buildTurndown() {
|
|
41
|
+
const service2 = new TurndownService({
|
|
42
|
+
headingStyle: "atx",
|
|
43
|
+
hr: "---",
|
|
44
|
+
bulletListMarker: "-",
|
|
45
|
+
codeBlockStyle: "fenced",
|
|
46
|
+
emDelimiter: "*",
|
|
47
|
+
strongDelimiter: "**"
|
|
48
|
+
});
|
|
49
|
+
service2.use(gfm);
|
|
50
|
+
service2.addRule("tightListItem", {
|
|
51
|
+
filter: "li",
|
|
52
|
+
replacement: (content, node, options) => {
|
|
53
|
+
const parent = node.parentNode;
|
|
54
|
+
let prefix = `${options.bulletListMarker} `;
|
|
55
|
+
if (parent.nodeName === "OL") {
|
|
56
|
+
const items = Array.from(parent.children).filter((child) => child.nodeName === "LI");
|
|
57
|
+
const start = Number(parent.getAttribute("start") ?? "1");
|
|
58
|
+
prefix = `${start + items.indexOf(node)}. `;
|
|
59
|
+
}
|
|
60
|
+
const indent = " ".repeat(prefix.length);
|
|
61
|
+
const inner = content.replace(/^\n+/, "").replace(/\n+$/, "\n").replace(/\n/gm, `
|
|
62
|
+
${indent}`);
|
|
63
|
+
return prefix + inner + (node.nextSibling && !/\n$/.test(inner) ? "\n" : "");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
service2.addRule("imagesAsLinks", {
|
|
67
|
+
filter: "img",
|
|
68
|
+
replacement: (_content, node) => {
|
|
69
|
+
const img = node;
|
|
70
|
+
const alt = img.getAttribute("alt")?.trim() || "image";
|
|
71
|
+
const src = img.getAttribute("src") ?? "";
|
|
72
|
+
if (!src || src.startsWith("data:")) return alt === "image" ? "" : alt;
|
|
73
|
+
return `[${alt}](${src})`;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return service2;
|
|
77
|
+
}
|
|
78
|
+
let service = null;
|
|
79
|
+
function htmlToMarkdown(html) {
|
|
80
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
81
|
+
doc.body.querySelectorAll("style,script,meta,head,title").forEach((el) => el.remove());
|
|
82
|
+
normalizeVendorDom(doc.body);
|
|
83
|
+
service ?? (service = buildTurndown());
|
|
84
|
+
return service.turndown(doc.body.innerHTML).trim();
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
COMPOSE_CLIPBOARD_ATTR,
|
|
88
|
+
htmlToMarkdown,
|
|
89
|
+
isComposeClipboardHtml
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=htmlToMarkdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/clipboard/htmlToMarkdown.ts"],"sourcesContent":["/**\n * Clipboard HTML → Markdown (#134), the paste half of clipboard interop.\n *\n * Real-world clipboard HTML is hostile: Google Docs wraps everything in a\n * `<b id=\"docs-internal-guid…\" style=\"font-weight:normal\">` and expresses ALL\n * formatting as `<span style>`; Word exports `mso-` styled soup. The DOM is\n * normalized here first — styled spans become real `<strong>/<em>/<s>`\n * elements, wrapper lies are unwrapped — so turndown converts semantics, not\n * vendor quirks.\n */\n\nimport TurndownService from \"turndown\";\n// @ts-expect-error — no published types; the joplin fork is the maintained\n// GFM ruleset (tables, strikethrough, task lists).\nimport { gfm } from \"@joplin/turndown-plugin-gfm\";\n\n/** Marks HTML that Compose itself put on the clipboard (see copyRich.ts).\n * The paste handler sees it and uses the lossless text/plain markdown\n * instead of re-converting our own rendering. */\nexport const COMPOSE_CLIPBOARD_ATTR = \"data-compose-markdown\";\n\nexport function isComposeClipboardHtml(html: string): boolean {\n return html.includes(COMPOSE_CLIPBOARD_ATTR);\n}\n\nfunction isBoldStyle(style: CSSStyleDeclaration): boolean {\n const weight = style.fontWeight;\n const numeric = Number(weight);\n return weight === \"bold\" || weight === \"bolder\" || (!Number.isNaN(numeric) && numeric >= 600);\n}\n\n/** Rewrite vendor styling into semantic elements, in place. */\nfunction normalizeVendorDom(root: HTMLElement): void {\n // Google Docs signs its fragments with a guid-carrying <b> whose\n // font-weight is NORMAL — unwrap it or the whole paste turns bold.\n root.querySelectorAll(\"b[id^='docs-internal-guid']\").forEach((wrapper) => {\n wrapper.replaceWith(...Array.from(wrapper.childNodes));\n });\n\n // Docs also wraps every list item's content in a <p>, which converts as a\n // LOOSE list (`- item` + blank lines). Unwrap sole-child paragraphs so\n // lists come out tight, the house style.\n root.querySelectorAll(\"li > p:only-child\").forEach((p) => {\n p.replaceWith(...Array.from(p.childNodes));\n });\n\n // Styled spans → semantic elements (Docs never emits <strong>/<em>).\n // Nested wrappers so bold+italic+strike combinations all survive.\n root.querySelectorAll(\"span\").forEach((span) => {\n const style = span.style;\n if (!style) return;\n const tags: string[] = [];\n if (isBoldStyle(style)) tags.push(\"strong\");\n if (style.fontStyle === \"italic\") tags.push(\"em\");\n if (style.textDecoration.includes(\"line-through\")) tags.push(\"s\");\n if (tags.length === 0) return;\n const doc = span.ownerDocument;\n const outermost = doc.createElement(tags[0]);\n let innermost = outermost;\n for (const tag of tags.slice(1)) {\n const next = doc.createElement(tag);\n innermost.appendChild(next);\n innermost = next;\n }\n innermost.append(...Array.from(span.childNodes));\n span.replaceWith(outermost);\n });\n}\n\nfunction buildTurndown(): TurndownService {\n const service = new TurndownService({\n headingStyle: \"atx\",\n hr: \"---\",\n bulletListMarker: \"-\",\n codeBlockStyle: \"fenced\",\n emDelimiter: \"*\",\n strongDelimiter: \"**\",\n });\n service.use(gfm);\n // Stock turndown pads every bullet to `- ` (a 4-char unit). House style\n // is the tight `- item` / `1. item`, with continuation lines indented to\n // the marker's own width — still CommonMark-correct for nesting.\n service.addRule(\"tightListItem\", {\n filter: \"li\",\n replacement: (content, node, options) => {\n const parent = node.parentNode as HTMLElement;\n let prefix = `${options.bulletListMarker} `;\n if (parent.nodeName === \"OL\") {\n const items = Array.from(parent.children).filter((child) => child.nodeName === \"LI\");\n const start = Number(parent.getAttribute(\"start\") ?? \"1\");\n prefix = `${start + items.indexOf(node as Element)}. `;\n }\n const indent = \" \".repeat(prefix.length);\n const inner = content\n .replace(/^\\n+/, \"\")\n .replace(/\\n+$/, \"\\n\")\n .replace(/\\n/gm, `\\n${indent}`);\n return prefix + inner + (node.nextSibling && !/\\n$/.test(inner) ? \"\\n\" : \"\");\n },\n });\n // Images degrade honestly (#134): a remote image becomes a LINK — never a\n // hot-loading `![...]` embed — and an inline data: blob keeps only its alt\n // text (a base64 wall would bury the document).\n service.addRule(\"imagesAsLinks\", {\n filter: \"img\",\n replacement: (_content, node) => {\n const img = node as HTMLImageElement;\n const alt = img.getAttribute(\"alt\")?.trim() || \"image\";\n const src = img.getAttribute(\"src\") ?? \"\";\n if (!src || src.startsWith(\"data:\")) return alt === \"image\" ? \"\" : alt;\n return `[${alt}](${src})`;\n },\n });\n return service;\n}\n\nlet service: TurndownService | null = null;\n\n/** Convert clipboard HTML to house-style Markdown. Returns \"\" when nothing\n * convertible remains (caller falls back to the native plain paste). */\nexport function htmlToMarkdown(html: string): string {\n const doc = new DOMParser().parseFromString(html, \"text/html\");\n doc.body.querySelectorAll(\"style,script,meta,head,title\").forEach((el) => el.remove());\n normalizeVendorDom(doc.body);\n service ??= buildTurndown();\n return service.turndown(doc.body.innerHTML).trim();\n}\n"],"mappings":";AAWA,OAAO,qBAAqB;AAG5B,SAAS,WAAW;AAKb,MAAM,yBAAyB;AAE/B,SAAS,uBAAuB,MAAuB;AAC5D,SAAO,KAAK,SAAS,sBAAsB;AAC7C;AAEA,SAAS,YAAY,OAAqC;AACxD,QAAM,SAAS,MAAM;AACrB,QAAM,UAAU,OAAO,MAAM;AAC7B,SAAO,WAAW,UAAU,WAAW,YAAa,CAAC,OAAO,MAAM,OAAO,KAAK,WAAW;AAC3F;AAGA,SAAS,mBAAmB,MAAyB;AAGnD,OAAK,iBAAiB,6BAA6B,EAAE,QAAQ,CAAC,YAAY;AACxE,YAAQ,YAAY,GAAG,MAAM,KAAK,QAAQ,UAAU,CAAC;AAAA,EACvD,CAAC;AAKD,OAAK,iBAAiB,mBAAmB,EAAE,QAAQ,CAAC,MAAM;AACxD,MAAE,YAAY,GAAG,MAAM,KAAK,EAAE,UAAU,CAAC;AAAA,EAC3C,CAAC;AAID,OAAK,iBAAiB,MAAM,EAAE,QAAQ,CAAC,SAAS;AAC9C,UAAM,QAAQ,KAAK;AACnB,QAAI,CAAC,MAAO;AACZ,UAAM,OAAiB,CAAC;AACxB,QAAI,YAAY,KAAK,EAAG,MAAK,KAAK,QAAQ;AAC1C,QAAI,MAAM,cAAc,SAAU,MAAK,KAAK,IAAI;AAChD,QAAI,MAAM,eAAe,SAAS,cAAc,EAAG,MAAK,KAAK,GAAG;AAChE,QAAI,KAAK,WAAW,EAAG;AACvB,UAAM,MAAM,KAAK;AACjB,UAAM,YAAY,IAAI,cAAc,KAAK,CAAC,CAAC;AAC3C,QAAI,YAAY;AAChB,eAAW,OAAO,KAAK,MAAM,CAAC,GAAG;AAC/B,YAAM,OAAO,IAAI,cAAc,GAAG;AAClC,gBAAU,YAAY,IAAI;AAC1B,kBAAY;AAAA,IACd;AACA,cAAU,OAAO,GAAG,MAAM,KAAK,KAAK,UAAU,CAAC;AAC/C,SAAK,YAAY,SAAS;AAAA,EAC5B,CAAC;AACH;AAEA,SAAS,gBAAiC;AACxC,QAAMA,WAAU,IAAI,gBAAgB;AAAA,IAClC,cAAc;AAAA,IACd,IAAI;AAAA,IACJ,kBAAkB;AAAA,IAClB,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB,CAAC;AACD,EAAAA,SAAQ,IAAI,GAAG;AAIf,EAAAA,SAAQ,QAAQ,iBAAiB;AAAA,IAC/B,QAAQ;AAAA,IACR,aAAa,CAAC,SAAS,MAAM,YAAY;AACvC,YAAM,SAAS,KAAK;AACpB,UAAI,SAAS,GAAG,QAAQ,gBAAgB;AACxC,UAAI,OAAO,aAAa,MAAM;AAC5B,cAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,EAAE,OAAO,CAAC,UAAU,MAAM,aAAa,IAAI;AACnF,cAAM,QAAQ,OAAO,OAAO,aAAa,OAAO,KAAK,GAAG;AACxD,iBAAS,GAAG,QAAQ,MAAM,QAAQ,IAAe,CAAC;AAAA,MACpD;AACA,YAAM,SAAS,IAAI,OAAO,OAAO,MAAM;AACvC,YAAM,QAAQ,QACX,QAAQ,QAAQ,EAAE,EAClB,QAAQ,QAAQ,IAAI,EACpB,QAAQ,QAAQ;AAAA,EAAK,MAAM,EAAE;AAChC,aAAO,SAAS,SAAS,KAAK,eAAe,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO;AAAA,IAC3E;AAAA,EACF,CAAC;AAID,EAAAA,SAAQ,QAAQ,iBAAiB;AAAA,IAC/B,QAAQ;AAAA,IACR,aAAa,CAAC,UAAU,SAAS;AAC/B,YAAM,MAAM;AACZ,YAAM,MAAM,IAAI,aAAa,KAAK,GAAG,KAAK,KAAK;AAC/C,YAAM,MAAM,IAAI,aAAa,KAAK,KAAK;AACvC,UAAI,CAAC,OAAO,IAAI,WAAW,OAAO,EAAG,QAAO,QAAQ,UAAU,KAAK;AACnE,aAAO,IAAI,GAAG,KAAK,GAAG;AAAA,IACxB;AAAA,EACF,CAAC;AACD,SAAOA;AACT;AAEA,IAAI,UAAkC;AAI/B,SAAS,eAAe,MAAsB;AACnD,QAAM,MAAM,IAAI,UAAU,EAAE,gBAAgB,MAAM,WAAW;AAC7D,MAAI,KAAK,iBAAiB,8BAA8B,EAAE,QAAQ,CAAC,OAAO,GAAG,OAAO,CAAC;AACrF,qBAAmB,IAAI,IAAI;AAC3B,wBAAY,cAAc;AAC1B,SAAO,QAAQ,SAAS,IAAI,KAAK,SAAS,EAAE,KAAK;AACnD;","names":["service"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Extension } from '@codemirror/state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rich paste (#134): clipboard HTML converts to Markdown at the caret, so a
|
|
5
|
+
* paste from Google Docs / Word / the web keeps headings, emphasis, lists,
|
|
6
|
+
* links, and tables instead of flattening to plain text.
|
|
7
|
+
*
|
|
8
|
+
* Precedence contract:
|
|
9
|
+
* - image files on the clipboard belong to imageInsertHandlers — skipped;
|
|
10
|
+
* - Compose's own copies carry {@link COMPOSE_CLIPBOARD_ATTR} — skipped, so
|
|
11
|
+
* the native path pastes the lossless text/plain markdown;
|
|
12
|
+
* - Mod-Shift-v pastes verbatim plain text (the escape hatch).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The paste half of clipboard interop; compose after imageInsertHandlers. */
|
|
16
|
+
declare const markdownPaste: Extension;
|
|
17
|
+
|
|
18
|
+
export { markdownPaste };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import "../../chunk-V6TY7KAL.js";
|
|
2
|
+
import { EditorView, keymap } from "@codemirror/view";
|
|
3
|
+
import { htmlToMarkdown, isComposeClipboardHtml } from "./htmlToMarkdown.js";
|
|
4
|
+
function insertText(view, text) {
|
|
5
|
+
view.dispatch(view.state.replaceSelection(text), {
|
|
6
|
+
userEvent: "input.paste",
|
|
7
|
+
scrollIntoView: true
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
const pasteHandler = EditorView.domEventHandlers({
|
|
11
|
+
paste(event, view) {
|
|
12
|
+
if (event.defaultPrevented) return false;
|
|
13
|
+
const data = event.clipboardData;
|
|
14
|
+
if (!data) return false;
|
|
15
|
+
if (data.files.length > 0) return false;
|
|
16
|
+
const html = data.getData("text/html");
|
|
17
|
+
if (!html || isComposeClipboardHtml(html)) return false;
|
|
18
|
+
const markdown = htmlToMarkdown(html);
|
|
19
|
+
if (!markdown) return false;
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
insertText(view, markdown);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const verbatimPasteKeymap = keymap.of([
|
|
26
|
+
{
|
|
27
|
+
key: "Mod-Shift-v",
|
|
28
|
+
run: (view) => {
|
|
29
|
+
void navigator.clipboard.readText().then((text) => {
|
|
30
|
+
if (text) insertText(view, text);
|
|
31
|
+
}).catch(() => {
|
|
32
|
+
});
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
37
|
+
const markdownPaste = [pasteHandler, verbatimPasteKeymap];
|
|
38
|
+
export {
|
|
39
|
+
markdownPaste
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=pasteMarkdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/clipboard/pasteMarkdown.ts"],"sourcesContent":["/**\n * Rich paste (#134): clipboard HTML converts to Markdown at the caret, so a\n * paste from Google Docs / Word / the web keeps headings, emphasis, lists,\n * links, and tables instead of flattening to plain text.\n *\n * Precedence contract:\n * - image files on the clipboard belong to imageInsertHandlers — skipped;\n * - Compose's own copies carry {@link COMPOSE_CLIPBOARD_ATTR} — skipped, so\n * the native path pastes the lossless text/plain markdown;\n * - Mod-Shift-v pastes verbatim plain text (the escape hatch).\n */\n\nimport { EditorView, keymap } from \"@codemirror/view\";\nimport { type Extension } from \"@codemirror/state\";\n\nimport { htmlToMarkdown, isComposeClipboardHtml } from \"./htmlToMarkdown\";\n\nfunction insertText(view: EditorView, text: string): void {\n view.dispatch(view.state.replaceSelection(text), {\n userEvent: \"input.paste\",\n scrollIntoView: true,\n });\n}\n\nconst pasteHandler = EditorView.domEventHandlers({\n paste(event, view) {\n if (event.defaultPrevented) return false;\n const data = event.clipboardData;\n if (!data) return false;\n // Image bytes → the image pipeline, not text conversion.\n if (data.files.length > 0) return false;\n const html = data.getData(\"text/html\");\n if (!html || isComposeClipboardHtml(html)) return false;\n const markdown = htmlToMarkdown(html);\n if (!markdown) return false;\n event.preventDefault();\n insertText(view, markdown);\n return true;\n },\n});\n\nconst verbatimPasteKeymap = keymap.of([\n {\n key: \"Mod-Shift-v\",\n run: (view) => {\n // Async clipboard read — allowed here because it rides a user gesture.\n // A denial (or an empty clipboard) quietly does nothing rather than\n // erroring into the document.\n void navigator.clipboard\n .readText()\n .then((text) => {\n if (text) insertText(view, text);\n })\n .catch(() => {});\n return true;\n },\n },\n]);\n\n/** The paste half of clipboard interop; compose after imageInsertHandlers. */\nexport const markdownPaste: Extension = [pasteHandler, verbatimPasteKeymap];\n"],"mappings":";AAYA,SAAS,YAAY,cAAc;AAGnC,SAAS,gBAAgB,8BAA8B;AAEvD,SAAS,WAAW,MAAkB,MAAoB;AACxD,OAAK,SAAS,KAAK,MAAM,iBAAiB,IAAI,GAAG;AAAA,IAC/C,WAAW;AAAA,IACX,gBAAgB;AAAA,EAClB,CAAC;AACH;AAEA,MAAM,eAAe,WAAW,iBAAiB;AAAA,EAC/C,MAAM,OAAO,MAAM;AACjB,QAAI,MAAM,iBAAkB,QAAO;AACnC,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,KAAM,QAAO;AAElB,QAAI,KAAK,MAAM,SAAS,EAAG,QAAO;AAClC,UAAM,OAAO,KAAK,QAAQ,WAAW;AACrC,QAAI,CAAC,QAAQ,uBAAuB,IAAI,EAAG,QAAO;AAClD,UAAM,WAAW,eAAe,IAAI;AACpC,QAAI,CAAC,SAAU,QAAO;AACtB,UAAM,eAAe;AACrB,eAAW,MAAM,QAAQ;AACzB,WAAO;AAAA,EACT;AACF,CAAC;AAED,MAAM,sBAAsB,OAAO,GAAG;AAAA,EACpC;AAAA,IACE,KAAK;AAAA,IACL,KAAK,CAAC,SAAS;AAIb,WAAK,UAAU,UACZ,SAAS,EACT,KAAK,CAAC,SAAS;AACd,YAAI,KAAM,YAAW,MAAM,IAAI;AAAA,MACjC,CAAC,EACA,MAAM,MAAM;AAAA,MAAC,CAAC;AACjB,aAAO;AAAA,IACT;AAAA,EACF;AACF,CAAC;AAGM,MAAM,gBAA2B,CAAC,cAAc,mBAAmB;","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as _codemirror_state from '@codemirror/state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Syntax colors for fenced-code content (ADR 0002). The markdown grammar's
|
|
5
|
+
* own constructs (headings, emphasis, links…) are styled by the decoration
|
|
6
|
+
* registry + editorTheme, NOT here — so this style deliberately covers only
|
|
7
|
+
* tags that code languages emit and markdown does not. The palette itself
|
|
8
|
+
* lives in [codePalette](./codePalette.ts), shared with the clipboard's
|
|
9
|
+
* inline-styled renderer.
|
|
10
|
+
*/
|
|
11
|
+
declare const codeHighlight: _codemirror_state.Extension;
|
|
12
|
+
|
|
13
|
+
export { codeHighlight };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "../../chunk-V6TY7KAL.js";
|
|
2
|
+
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
|
3
|
+
import { CODE_PALETTE, themedColor } from "./codePalette.js";
|
|
4
|
+
const style = HighlightStyle.define(
|
|
5
|
+
CODE_PALETTE.map((spec) => ({
|
|
6
|
+
tag: spec.tag,
|
|
7
|
+
color: themedColor(spec),
|
|
8
|
+
fontStyle: spec.fontStyle
|
|
9
|
+
}))
|
|
10
|
+
);
|
|
11
|
+
const codeHighlight = syntaxHighlighting(style);
|
|
12
|
+
export {
|
|
13
|
+
codeHighlight
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=codeHighlight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/code/codeHighlight.ts"],"sourcesContent":["/**\n * Syntax colors for fenced-code content (ADR 0002). The markdown grammar's\n * own constructs (headings, emphasis, links…) are styled by the decoration\n * registry + editorTheme, NOT here — so this style deliberately covers only\n * tags that code languages emit and markdown does not. The palette itself\n * lives in [codePalette](./codePalette.ts), shared with the clipboard's\n * inline-styled renderer.\n */\n\nimport { HighlightStyle, syntaxHighlighting } from \"@codemirror/language\";\n\nimport { CODE_PALETTE, themedColor } from \"./codePalette\";\n\nconst style = HighlightStyle.define(\n CODE_PALETTE.map((spec) => ({\n tag: spec.tag,\n color: themedColor(spec),\n fontStyle: spec.fontStyle,\n })),\n);\n\nexport const codeHighlight = syntaxHighlighting(style);\n"],"mappings":";AASA,SAAS,gBAAgB,0BAA0B;AAEnD,SAAS,cAAc,mBAAmB;AAE1C,MAAM,QAAQ,eAAe;AAAA,EAC3B,aAAa,IAAI,CAAC,UAAU;AAAA,IAC1B,KAAK,KAAK;AAAA,IACV,OAAO,YAAY,IAAI;AAAA,IACvB,WAAW,KAAK;AAAA,EAClB,EAAE;AACJ;AAEO,MAAM,gBAAgB,mBAAmB,KAAK;","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Extension } from '@codemirror/state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The opener-row language affordance (ADR 0002).
|
|
5
|
+
*
|
|
6
|
+
* A CLOSED fence with a language shows its styled CodeInfo text (the pill);
|
|
7
|
+
* one without gets a "plain" placeholder pill widget so every block has a
|
|
8
|
+
* click target. Clicking either opens the searchable language chooser;
|
|
9
|
+
* right-clicking anywhere in a block opens the block menu (set language,
|
|
10
|
+
* copy code). Pointer handling is delegated — one listener per editor — and
|
|
11
|
+
* clicks are swallowed on mousedown so the fence caret guard (§12.9) never
|
|
12
|
+
* sees them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
declare const codeLanguageUI: Extension;
|
|
16
|
+
|
|
17
|
+
export { codeLanguageUI };
|