@latentic/live-markdown 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -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 +15 -0
- package/dist/codemirror/math/mathWidget.js +36 -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 +35 -880
- package/dist/index.js +128 -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,73 @@
|
|
|
1
|
+
import "../../chunk-V6TY7KAL.js";
|
|
2
|
+
import { StateField } from "@codemirror/state";
|
|
3
|
+
import { Decoration, EditorView } from "@codemirror/view";
|
|
4
|
+
import { docTree, inCode } from "../core/codeContext.js";
|
|
5
|
+
import { MathWidget } from "./mathWidget.js";
|
|
6
|
+
const INLINE_MATH_RE = /(?<![\\$])\$(?!\s)([^\n$]+?)(?<!\s)\$(?!\d)/g;
|
|
7
|
+
function computeDecorations(state) {
|
|
8
|
+
const marks = [];
|
|
9
|
+
const atomic = [];
|
|
10
|
+
const doc = state.doc;
|
|
11
|
+
const totalLines = doc.lines;
|
|
12
|
+
const tree = docTree(state);
|
|
13
|
+
const blockMarkAt = (num) => {
|
|
14
|
+
const line = doc.line(num);
|
|
15
|
+
return line.text.trim() === "$$" && !inCode(tree, line.from + line.text.indexOf("$"));
|
|
16
|
+
};
|
|
17
|
+
let lineNum = 1;
|
|
18
|
+
while (lineNum <= totalLines) {
|
|
19
|
+
const line = doc.line(lineNum);
|
|
20
|
+
const text = line.text;
|
|
21
|
+
const single = text.match(/^\$\$(.+)\$\$\s*$/);
|
|
22
|
+
if (single && !inCode(tree, line.from)) {
|
|
23
|
+
const replace = Decoration.replace({ widget: new MathWidget(single[1], true) });
|
|
24
|
+
marks.push(replace.range(line.from, line.to));
|
|
25
|
+
atomic.push(replace.range(line.from, line.to));
|
|
26
|
+
lineNum += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (blockMarkAt(lineNum)) {
|
|
30
|
+
let close = lineNum + 1;
|
|
31
|
+
while (close <= totalLines && !blockMarkAt(close)) {
|
|
32
|
+
close += 1;
|
|
33
|
+
}
|
|
34
|
+
if (close <= totalLines) {
|
|
35
|
+
const closeLine = doc.line(close);
|
|
36
|
+
const tex = doc.sliceString(line.to, closeLine.from).trim();
|
|
37
|
+
const replace = Decoration.replace({ widget: new MathWidget(tex, true), block: true });
|
|
38
|
+
marks.push(replace.range(line.from, closeLine.to));
|
|
39
|
+
atomic.push(replace.range(line.from, closeLine.to));
|
|
40
|
+
lineNum = close + 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
INLINE_MATH_RE.lastIndex = 0;
|
|
45
|
+
let m;
|
|
46
|
+
while ((m = INLINE_MATH_RE.exec(text)) !== null) {
|
|
47
|
+
const matchStart = line.from + m.index;
|
|
48
|
+
const matchEnd = matchStart + m[0].length;
|
|
49
|
+
if (inCode(tree, matchStart) || inCode(tree, matchEnd - 1)) continue;
|
|
50
|
+
const replace = Decoration.replace({ widget: new MathWidget(m[1], false) });
|
|
51
|
+
marks.push(replace.range(matchStart, matchEnd));
|
|
52
|
+
atomic.push(replace.range(matchStart, matchEnd));
|
|
53
|
+
}
|
|
54
|
+
lineNum += 1;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
decorations: Decoration.set(marks, true),
|
|
58
|
+
atomic: Decoration.set(atomic, true)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const mathPlugin = StateField.define({
|
|
62
|
+
create: (state) => computeDecorations(state),
|
|
63
|
+
update: (value, tr) => tr.docChanged ? computeDecorations(tr.state) : value,
|
|
64
|
+
provide: (field) => [
|
|
65
|
+
EditorView.decorations.from(field, (value) => value.decorations),
|
|
66
|
+
EditorView.atomicRanges.of((view) => view.state.field(field, false)?.atomic ?? Decoration.none)
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
export {
|
|
70
|
+
INLINE_MATH_RE,
|
|
71
|
+
mathPlugin
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=mathPlugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/math/mathPlugin.ts"],"sourcesContent":["import { type EditorState, type Range, StateField } from \"@codemirror/state\";\nimport { Decoration, type DecorationSet, EditorView } from \"@codemirror/view\";\n\nimport { docTree, inCode } from \"../core/codeContext\";\nimport { MathWidget } from \"./mathWidget\";\n\nexport const INLINE_MATH_RE = /(?<![\\\\$])\\$(?!\\s)([^\\n$]+?)(?<!\\s)\\$(?!\\d)/g;\n\nfunction computeDecorations(state: EditorState): { decorations: DecorationSet; atomic: DecorationSet } {\n const marks: Range<Decoration>[] = [];\n const atomic: Range<Decoration>[] = [];\n const doc = state.doc;\n const totalLines = doc.lines;\n const tree = docTree(state);\n // A `$$` line delimits a block only in prose — inside a code block it is\n // literal text, neither an opener nor a closer.\n const blockMarkAt = (num: number): boolean => {\n const line = doc.line(num);\n return line.text.trim() === \"$$\" && !inCode(tree, line.from + line.text.indexOf(\"$\"));\n };\n\n let lineNum = 1;\n while (lineNum <= totalLines) {\n const line = doc.line(lineNum);\n const text = line.text;\n\n // Single-line block: `$$ … $$` on one line.\n const single = text.match(/^\\$\\$(.+)\\$\\$\\s*$/);\n if (single && !inCode(tree, line.from)) {\n const replace = Decoration.replace({ widget: new MathWidget(single[1], true) });\n marks.push(replace.range(line.from, line.to));\n atomic.push(replace.range(line.from, line.to));\n lineNum += 1;\n continue;\n }\n\n // Multi-line block: a lone `$$` opens it; scan to the next lone `$$`. This\n // span crosses line breaks, so it must be a `block: true` replacement and\n // is delivered via a StateField — CM6 forbids both a plugin-supplied and a\n // non-block replace from covering a line break, and a real layout engine\n // renders such a span as raw source rather than the widget.\n if (blockMarkAt(lineNum)) {\n let close = lineNum + 1;\n while (close <= totalLines && !blockMarkAt(close)) {\n close += 1;\n }\n if (close <= totalLines) {\n const closeLine = doc.line(close);\n const tex = doc.sliceString(line.to, closeLine.from).trim();\n const replace = Decoration.replace({ widget: new MathWidget(tex, true), block: true });\n marks.push(replace.range(line.from, closeLine.to));\n atomic.push(replace.range(line.from, closeLine.to));\n lineNum = close + 1;\n continue;\n }\n }\n\n // Inline `$ … $`.\n INLINE_MATH_RE.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = INLINE_MATH_RE.exec(text)) !== null) {\n const matchStart = line.from + m.index;\n const matchEnd = matchStart + m[0].length;\n if (inCode(tree, matchStart) || inCode(tree, matchEnd - 1)) continue;\n const replace = Decoration.replace({ widget: new MathWidget(m[1], false) });\n marks.push(replace.range(matchStart, matchEnd));\n atomic.push(replace.range(matchStart, matchEnd));\n }\n lineNum += 1;\n }\n\n return {\n decorations: Decoration.set(marks, true),\n atomic: Decoration.set(atomic, true),\n };\n}\n\nexport const mathPlugin = StateField.define<{ decorations: DecorationSet; atomic: DecorationSet }>({\n create: (state) => computeDecorations(state),\n update: (value, tr) => (tr.docChanged ? computeDecorations(tr.state) : value),\n provide: (field) => [\n EditorView.decorations.from(field, (value) => value.decorations),\n EditorView.atomicRanges.of((view) => view.state.field(field, false)?.atomic ?? Decoration.none),\n ],\n});\n"],"mappings":";AAAA,SAAuC,kBAAkB;AACzD,SAAS,YAAgC,kBAAkB;AAE3D,SAAS,SAAS,cAAc;AAChC,SAAS,kBAAkB;AAEpB,MAAM,iBAAiB;AAE9B,SAAS,mBAAmB,OAA2E;AACrG,QAAM,QAA6B,CAAC;AACpC,QAAM,SAA8B,CAAC;AACrC,QAAM,MAAM,MAAM;AAClB,QAAM,aAAa,IAAI;AACvB,QAAM,OAAO,QAAQ,KAAK;AAG1B,QAAM,cAAc,CAAC,QAAyB;AAC5C,UAAM,OAAO,IAAI,KAAK,GAAG;AACzB,WAAO,KAAK,KAAK,KAAK,MAAM,QAAQ,CAAC,OAAO,MAAM,KAAK,OAAO,KAAK,KAAK,QAAQ,GAAG,CAAC;AAAA,EACtF;AAEA,MAAI,UAAU;AACd,SAAO,WAAW,YAAY;AAC5B,UAAM,OAAO,IAAI,KAAK,OAAO;AAC7B,UAAM,OAAO,KAAK;AAGlB,UAAM,SAAS,KAAK,MAAM,mBAAmB;AAC7C,QAAI,UAAU,CAAC,OAAO,MAAM,KAAK,IAAI,GAAG;AACtC,YAAM,UAAU,WAAW,QAAQ,EAAE,QAAQ,IAAI,WAAW,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;AAC9E,YAAM,KAAK,QAAQ,MAAM,KAAK,MAAM,KAAK,EAAE,CAAC;AAC5C,aAAO,KAAK,QAAQ,MAAM,KAAK,MAAM,KAAK,EAAE,CAAC;AAC7C,iBAAW;AACX;AAAA,IACF;AAOA,QAAI,YAAY,OAAO,GAAG;AACxB,UAAI,QAAQ,UAAU;AACtB,aAAO,SAAS,cAAc,CAAC,YAAY,KAAK,GAAG;AACjD,iBAAS;AAAA,MACX;AACA,UAAI,SAAS,YAAY;AACvB,cAAM,YAAY,IAAI,KAAK,KAAK;AAChC,cAAM,MAAM,IAAI,YAAY,KAAK,IAAI,UAAU,IAAI,EAAE,KAAK;AAC1D,cAAM,UAAU,WAAW,QAAQ,EAAE,QAAQ,IAAI,WAAW,KAAK,IAAI,GAAG,OAAO,KAAK,CAAC;AACrF,cAAM,KAAK,QAAQ,MAAM,KAAK,MAAM,UAAU,EAAE,CAAC;AACjD,eAAO,KAAK,QAAQ,MAAM,KAAK,MAAM,UAAU,EAAE,CAAC;AAClD,kBAAU,QAAQ;AAClB;AAAA,MACF;AAAA,IACF;AAGA,mBAAe,YAAY;AAC3B,QAAI;AACJ,YAAQ,IAAI,eAAe,KAAK,IAAI,OAAO,MAAM;AAC/C,YAAM,aAAa,KAAK,OAAO,EAAE;AACjC,YAAM,WAAW,aAAa,EAAE,CAAC,EAAE;AACnC,UAAI,OAAO,MAAM,UAAU,KAAK,OAAO,MAAM,WAAW,CAAC,EAAG;AAC5D,YAAM,UAAU,WAAW,QAAQ,EAAE,QAAQ,IAAI,WAAW,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC;AAC1E,YAAM,KAAK,QAAQ,MAAM,YAAY,QAAQ,CAAC;AAC9C,aAAO,KAAK,QAAQ,MAAM,YAAY,QAAQ,CAAC;AAAA,IACjD;AACA,eAAW;AAAA,EACb;AAEA,SAAO;AAAA,IACL,aAAa,WAAW,IAAI,OAAO,IAAI;AAAA,IACvC,QAAQ,WAAW,IAAI,QAAQ,IAAI;AAAA,EACrC;AACF;AAEO,MAAM,aAAa,WAAW,OAA8D;AAAA,EACjG,QAAQ,CAAC,UAAU,mBAAmB,KAAK;AAAA,EAC3C,QAAQ,CAAC,OAAO,OAAQ,GAAG,aAAa,mBAAmB,GAAG,KAAK,IAAI;AAAA,EACvE,SAAS,CAAC,UAAU;AAAA,IAClB,WAAW,YAAY,KAAK,OAAO,CAAC,UAAU,MAAM,WAAW;AAAA,IAC/D,WAAW,aAAa,GAAG,CAAC,SAAS,KAAK,MAAM,MAAM,OAAO,KAAK,GAAG,UAAU,WAAW,IAAI;AAAA,EAChG;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { InlineScanRule } from '../core/inlineScan.js';
|
|
2
|
+
import '@codemirror/state';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `$…$` for the string renderers.
|
|
6
|
+
*
|
|
7
|
+
* The only rule that hydrates: KaTeX builds DOM, and its markup carries inline
|
|
8
|
+
* styles a cell's sanitiser allow-list deliberately drops. So the render pass
|
|
9
|
+
* emits the TeX as inert text, the sanitiser sees only that, and KaTeX — trusted
|
|
10
|
+
* code, reading the element's own `textContent` — replaces it afterwards. The
|
|
11
|
+
* allow-list stays as tight as it is for prose.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
declare const mathScanRule: InlineScanRule;
|
|
15
|
+
|
|
16
|
+
export { mathScanRule };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "../../chunk-V6TY7KAL.js";
|
|
2
|
+
import { escapeText } from "../core/htmlEscape.js";
|
|
3
|
+
import { INLINE_MATH_RE } from "./mathPlugin.js";
|
|
4
|
+
import { renderMathInto } from "./mathWidget.js";
|
|
5
|
+
const mathScanRule = {
|
|
6
|
+
name: "math",
|
|
7
|
+
pattern: INLINE_MATH_RE,
|
|
8
|
+
render: (match) => `<span class="cm-math-inline">${escapeText(match[1] ?? "")}</span>`,
|
|
9
|
+
hydrate: (root) => {
|
|
10
|
+
for (const el of root.querySelectorAll(".cm-math-inline")) {
|
|
11
|
+
renderMathInto(el, el.textContent ?? "", false);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
mathScanRule
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=mathScanRule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/math/mathScanRule.ts"],"sourcesContent":["/**\n * `$…$` for the string renderers.\n *\n * The only rule that hydrates: KaTeX builds DOM, and its markup carries inline\n * styles a cell's sanitiser allow-list deliberately drops. So the render pass\n * emits the TeX as inert text, the sanitiser sees only that, and KaTeX — trusted\n * code, reading the element's own `textContent` — replaces it afterwards. The\n * allow-list stays as tight as it is for prose.\n */\n\nimport { escapeText } from \"../core/htmlEscape\";\nimport { type InlineScanRule } from \"../core/inlineScan\";\nimport { INLINE_MATH_RE } from \"./mathPlugin\";\nimport { renderMathInto } from \"./mathWidget\";\n\nexport const mathScanRule: InlineScanRule = {\n name: \"math\",\n pattern: INLINE_MATH_RE,\n render: (match) => `<span class=\"cm-math-inline\">${escapeText(match[1] ?? \"\")}</span>`,\n hydrate: (root) => {\n for (const el of root.querySelectorAll(\".cm-math-inline\")) {\n renderMathInto(el as HTMLElement, el.textContent ?? \"\", false);\n }\n },\n};\n"],"mappings":";AAUA,SAAS,kBAAkB;AAE3B,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAExB,MAAM,eAA+B;AAAA,EAC1C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,QAAQ,CAAC,UAAU,gCAAgC,WAAW,MAAM,CAAC,KAAK,EAAE,CAAC;AAAA,EAC7E,SAAS,CAAC,SAAS;AACjB,eAAW,MAAM,KAAK,iBAAiB,iBAAiB,GAAG;AACzD,qBAAe,IAAmB,GAAG,eAAe,IAAI,KAAK;AAAA,IAC/D;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { WidgetType, EditorView } from '@codemirror/view';
|
|
2
|
+
|
|
3
|
+
/** Typeset `tex` into `el`, falling back to the source on a KaTeX failure so a
|
|
4
|
+
* malformed expression shows what the author wrote rather than nothing. */
|
|
5
|
+
declare function renderMathInto(el: HTMLElement, tex: string, displayMode: boolean): void;
|
|
6
|
+
declare class MathWidget extends WidgetType {
|
|
7
|
+
readonly tex: string;
|
|
8
|
+
readonly displayMode: boolean;
|
|
9
|
+
constructor(tex: string, displayMode: boolean);
|
|
10
|
+
eq(other: MathWidget): boolean;
|
|
11
|
+
toDOM(_view: EditorView): HTMLElement;
|
|
12
|
+
ignoreEvent(): boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { MathWidget, renderMathInto };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__publicField
|
|
3
|
+
} from "../../chunk-V6TY7KAL.js";
|
|
4
|
+
import katex from "katex";
|
|
5
|
+
import { WidgetType } from "@codemirror/view";
|
|
6
|
+
function renderMathInto(el, tex, displayMode) {
|
|
7
|
+
try {
|
|
8
|
+
katex.render(tex, el, { displayMode, throwOnError: false, output: "html" });
|
|
9
|
+
} catch {
|
|
10
|
+
el.textContent = tex;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
class MathWidget extends WidgetType {
|
|
14
|
+
constructor(tex, displayMode) {
|
|
15
|
+
super();
|
|
16
|
+
__publicField(this, "tex", tex);
|
|
17
|
+
__publicField(this, "displayMode", displayMode);
|
|
18
|
+
}
|
|
19
|
+
eq(other) {
|
|
20
|
+
return other.tex === this.tex && other.displayMode === this.displayMode;
|
|
21
|
+
}
|
|
22
|
+
toDOM(_view) {
|
|
23
|
+
const span = document.createElement(this.displayMode ? "div" : "span");
|
|
24
|
+
span.className = this.displayMode ? "cm-math-block" : "cm-math-inline";
|
|
25
|
+
renderMathInto(span, this.tex, this.displayMode);
|
|
26
|
+
return span;
|
|
27
|
+
}
|
|
28
|
+
ignoreEvent() {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
MathWidget,
|
|
34
|
+
renderMathInto
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=mathWidget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/math/mathWidget.ts"],"sourcesContent":["import katex from \"katex\";\nimport { EditorView, WidgetType } from \"@codemirror/view\";\n\n/** Typeset `tex` into `el`, falling back to the source on a KaTeX failure so a\n * malformed expression shows what the author wrote rather than nothing. */\nexport function renderMathInto(el: HTMLElement, tex: string, displayMode: boolean): void {\n try {\n katex.render(tex, el, { displayMode, throwOnError: false, output: \"html\" });\n } catch {\n el.textContent = tex;\n }\n}\n\nexport class MathWidget extends WidgetType {\n constructor(\n readonly tex: string,\n readonly displayMode: boolean,\n ) {\n super();\n }\n\n override eq(other: MathWidget): boolean {\n return other.tex === this.tex && other.displayMode === this.displayMode;\n }\n\n override toDOM(_view: EditorView): HTMLElement {\n const span = document.createElement(this.displayMode ? \"div\" : \"span\");\n span.className = this.displayMode ? \"cm-math-block\" : \"cm-math-inline\";\n renderMathInto(span, this.tex, this.displayMode);\n return span;\n }\n\n override ignoreEvent(): boolean {\n return false;\n }\n}\n"],"mappings":";;;AAAA,OAAO,WAAW;AAClB,SAAqB,kBAAkB;AAIhC,SAAS,eAAe,IAAiB,KAAa,aAA4B;AACvF,MAAI;AACF,UAAM,OAAO,KAAK,IAAI,EAAE,aAAa,cAAc,OAAO,QAAQ,OAAO,CAAC;AAAA,EAC5E,QAAQ;AACN,OAAG,cAAc;AAAA,EACnB;AACF;AAEO,MAAM,mBAAmB,WAAW;AAAA,EACzC,YACW,KACA,aACT;AACA,UAAM;AAHG;AACA;AAAA,EAGX;AAAA,EAES,GAAG,OAA4B;AACtC,WAAO,MAAM,QAAQ,KAAK,OAAO,MAAM,gBAAgB,KAAK;AAAA,EAC9D;AAAA,EAES,MAAM,OAAgC;AAC7C,UAAM,OAAO,SAAS,cAAc,KAAK,cAAc,QAAQ,MAAM;AACrE,SAAK,YAAY,KAAK,cAAc,kBAAkB;AACtD,mBAAe,MAAM,KAAK,KAAK,KAAK,WAAW;AAC/C,WAAO;AAAA,EACT;AAAA,EAES,cAAuB;AAC9B,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { MermaidFence, mermaidField } from './mermaidPlugin.js';
|
|
2
|
+
export { MermaidRenderResult, getCachedMermaidPng, getCachedMermaidSvg, isMermaidFenceInfo, rasterizeSvgToPngDataUri, renderMermaidToSvg, warmMermaidPng } from './mermaidRender.js';
|
|
3
|
+
import '@codemirror/state';
|
|
4
|
+
import '@codemirror/view';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/mermaid/index.ts"],"sourcesContent":["/**\n * Mermaid diagrams: the fence-to-diagram StateField and the CodeMirror-free\n * renderer (SVG + rasterized PNG caches) shared with export and clipboard.\n */\nexport * from \"./mermaidPlugin\";\nexport * from \"./mermaidRender\";\n"],"mappings":"AAIA,cAAc;AACd,cAAc;","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StateField } from '@codemirror/state';
|
|
2
|
+
import { DecorationSet } from '@codemirror/view';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mermaid fences (#125) — a CLOSED ```mermaid fence renders as a diagram
|
|
6
|
+
* block while the caret is outside it; a caret or selection touching the
|
|
7
|
+
* fence reveals the source (the code-block contract), and leaving re-renders.
|
|
8
|
+
*
|
|
9
|
+
* A StateField, not a ViewPlugin: the replacement spans line breaks, and CM6
|
|
10
|
+
* only honours block-level multi-line replaces delivered via a field (same
|
|
11
|
+
* constraint as tableField/mathPlugin).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface MermaidFence {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
source: string;
|
|
18
|
+
}
|
|
19
|
+
interface MermaidFieldValue {
|
|
20
|
+
fences: MermaidFence[];
|
|
21
|
+
decorations: DecorationSet;
|
|
22
|
+
}
|
|
23
|
+
declare const mermaidField: StateField<MermaidFieldValue>;
|
|
24
|
+
|
|
25
|
+
export { type MermaidFence, mermaidField };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import "../../chunk-V6TY7KAL.js";
|
|
2
|
+
import { isMermaidFenceInfo } from "./mermaidRender.js";
|
|
3
|
+
import { syntaxTree } from "@codemirror/language";
|
|
4
|
+
import { StateField } from "@codemirror/state";
|
|
5
|
+
import { Decoration, EditorView } from "@codemirror/view";
|
|
6
|
+
import { MermaidWidget } from "./mermaidWidget.js";
|
|
7
|
+
function mermaidFenceAt(state, node) {
|
|
8
|
+
const info = node.getChild("CodeInfo");
|
|
9
|
+
if (!info || !isMermaidFenceInfo(state.sliceDoc(info.from, info.to))) return null;
|
|
10
|
+
if (node.getChildren("CodeMark").length < 2) return null;
|
|
11
|
+
const opener = state.doc.lineAt(node.from);
|
|
12
|
+
const closer = state.doc.lineAt(node.to);
|
|
13
|
+
if (node.from !== opener.from || node.to !== closer.to) return null;
|
|
14
|
+
const text = node.getChild("CodeText");
|
|
15
|
+
return {
|
|
16
|
+
from: node.from,
|
|
17
|
+
to: node.to,
|
|
18
|
+
source: text ? state.sliceDoc(text.from, text.to) : ""
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function findFences(state) {
|
|
22
|
+
const fences = [];
|
|
23
|
+
syntaxTree(state).iterate({
|
|
24
|
+
enter(node) {
|
|
25
|
+
if (node.name !== "FencedCode") return;
|
|
26
|
+
const fence = mermaidFenceAt(state, node.node);
|
|
27
|
+
if (fence) fences.push(fence);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return fences;
|
|
31
|
+
}
|
|
32
|
+
function selectionReveals(state, fence) {
|
|
33
|
+
const inside = (pos) => pos > fence.from && pos < fence.to;
|
|
34
|
+
return state.selection.ranges.some((range) => inside(range.anchor) || inside(range.head));
|
|
35
|
+
}
|
|
36
|
+
function selectionCovers(state, fence) {
|
|
37
|
+
return state.selection.ranges.some(
|
|
38
|
+
(range) => !range.empty && range.from <= fence.from && range.to >= fence.to
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function build(state, fences) {
|
|
42
|
+
const ranges = [];
|
|
43
|
+
for (const fence of fences) {
|
|
44
|
+
if (selectionReveals(state, fence)) continue;
|
|
45
|
+
ranges.push(
|
|
46
|
+
Decoration.replace({
|
|
47
|
+
widget: new MermaidWidget(fence.source, selectionCovers(state, fence)),
|
|
48
|
+
block: true
|
|
49
|
+
}).range(fence.from, fence.to)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return { fences, decorations: Decoration.set(ranges, true) };
|
|
53
|
+
}
|
|
54
|
+
const mermaidField = StateField.define({
|
|
55
|
+
create: (state) => build(state, findFences(state)),
|
|
56
|
+
update(value, tr) {
|
|
57
|
+
if (tr.docChanged || syntaxTree(tr.state) !== syntaxTree(tr.startState)) {
|
|
58
|
+
return build(tr.state, findFences(tr.state));
|
|
59
|
+
}
|
|
60
|
+
if (tr.selection) {
|
|
61
|
+
return build(tr.state, value.fences);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
},
|
|
65
|
+
provide: (field) => [
|
|
66
|
+
EditorView.decorations.from(field, (value) => value.decorations),
|
|
67
|
+
// The replaced span is atomic: the caret skips over the widget instead
|
|
68
|
+
// of stepping through hidden fence source. Revealed fences carry no
|
|
69
|
+
// replace decoration, so caret motion inside them stays free.
|
|
70
|
+
EditorView.atomicRanges.of(
|
|
71
|
+
(view) => view.state.field(field, false)?.decorations ?? Decoration.none
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
export {
|
|
76
|
+
mermaidField
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=mermaidPlugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/mermaid/mermaidPlugin.ts"],"sourcesContent":["/**\n * Mermaid fences (#125) — a CLOSED ```mermaid fence renders as a diagram\n * block while the caret is outside it; a caret or selection touching the\n * fence reveals the source (the code-block contract), and leaving re-renders.\n *\n * A StateField, not a ViewPlugin: the replacement spans line breaks, and CM6\n * only honours block-level multi-line replaces delivered via a field (same\n * constraint as tableField/mathPlugin).\n */\n\nimport { isMermaidFenceInfo } from \"./mermaidRender\";\nimport { syntaxTree } from \"@codemirror/language\";\nimport { type EditorState, type Range, StateField } from \"@codemirror/state\";\nimport { Decoration, type DecorationSet, EditorView } from \"@codemirror/view\";\n\nimport { MermaidWidget } from \"./mermaidWidget\";\n\n// Structural stand-in for a Lezer node — `@lezer/common` is a transitive\n// dep this package deliberately doesn't import from (same convention as\n// tableModel's SyntaxNodeLike).\ntype FenceNodeLike = {\n readonly from: number;\n readonly to: number;\n getChild(type: string): { readonly from: number; readonly to: number } | null;\n getChildren(type: string): readonly unknown[];\n};\n\nexport interface MermaidFence {\n from: number;\n to: number;\n source: string;\n}\n\n\nfunction mermaidFenceAt(state: EditorState, node: FenceNodeLike): MermaidFence | null {\n const info = node.getChild(\"CodeInfo\");\n if (!info || !isMermaidFenceInfo(state.sliceDoc(info.from, info.to))) return null;\n // Closed fences only (opener AND closer CodeMark). An unclosed fence runs\n // to the end of the doc — rendering it while the user is still typing\n // lines would yank the block out from under the caret.\n if (node.getChildren(\"CodeMark\").length < 2) return null;\n // A block replace must cover whole lines. A fence indented inside a list\n // item starts mid-line, so it stays as source instead.\n const opener = state.doc.lineAt(node.from);\n const closer = state.doc.lineAt(node.to);\n if (node.from !== opener.from || node.to !== closer.to) return null;\n const text = node.getChild(\"CodeText\");\n return {\n from: node.from,\n to: node.to,\n source: text ? state.sliceDoc(text.from, text.to) : \"\",\n };\n}\n\nfunction findFences(state: EditorState): MermaidFence[] {\n const fences: MermaidFence[] = [];\n syntaxTree(state).iterate({\n enter(node) {\n if (node.name !== \"FencedCode\") return;\n const fence = mermaidFenceAt(state, node.node);\n if (fence) fences.push(fence);\n },\n });\n return fences;\n}\n\n/** A fence reveals its source only when a selection ENDPOINT sits strictly\n * inside it — click-to-edit places the caret there, and selecting within the\n * revealed source keeps it revealed. A selection merely SPANNING the fence\n * (drag-across, select-all, copying a region) keeps the diagram rendered:\n * flipping to source mid-drag read as \"copying turns it back into code\",\n * and the height jump made the drag jumpy. */\nfunction selectionReveals(state: EditorState, fence: MermaidFence): boolean {\n const inside = (pos: number) => pos > fence.from && pos < fence.to;\n return state.selection.ranges.some((range) => inside(range.anchor) || inside(range.head));\n}\n\ninterface MermaidFieldValue {\n fences: MermaidFence[];\n decorations: DecorationSet;\n}\n\n/** A selection fully covering the fence — the click-to-select state. The\n * widget paints it (native selection can't draw over a block widget). */\nfunction selectionCovers(state: EditorState, fence: MermaidFence): boolean {\n return state.selection.ranges.some(\n (range) => !range.empty && range.from <= fence.from && range.to >= fence.to,\n );\n}\n\nfunction build(state: EditorState, fences: MermaidFence[]): MermaidFieldValue {\n const ranges: Range<Decoration>[] = [];\n for (const fence of fences) {\n if (selectionReveals(state, fence)) continue;\n ranges.push(\n Decoration.replace({\n widget: new MermaidWidget(fence.source, selectionCovers(state, fence)),\n block: true,\n }).range(fence.from, fence.to),\n );\n }\n return { fences, decorations: Decoration.set(ranges, true) };\n}\n\nexport const mermaidField = StateField.define<MermaidFieldValue>({\n create: (state) => build(state, findFences(state)),\n update(value, tr) {\n // Fences re-extract on edits AND on parse progress — CM parses lazily,\n // so fences far down a long doc only enter the tree as the user scrolls\n // (the tableField lesson). Selection-only changes rebuild from the\n // cached fence list: that's the cheap path that flips a fence between\n // widget and source on every caret move.\n if (tr.docChanged || syntaxTree(tr.state) !== syntaxTree(tr.startState)) {\n return build(tr.state, findFences(tr.state));\n }\n if (tr.selection) {\n return build(tr.state, value.fences);\n }\n return value;\n },\n provide: (field) => [\n EditorView.decorations.from(field, (value) => value.decorations),\n // The replaced span is atomic: the caret skips over the widget instead\n // of stepping through hidden fence source. Revealed fences carry no\n // replace decoration, so caret motion inside them stays free.\n EditorView.atomicRanges.of(\n (view) => view.state.field(field, false)?.decorations ?? Decoration.none,\n ),\n ],\n});\n"],"mappings":";AAUA,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;AAC3B,SAAuC,kBAAkB;AACzD,SAAS,YAAgC,kBAAkB;AAE3D,SAAS,qBAAqB;AAmB9B,SAAS,eAAe,OAAoB,MAA0C;AACpF,QAAM,OAAO,KAAK,SAAS,UAAU;AACrC,MAAI,CAAC,QAAQ,CAAC,mBAAmB,MAAM,SAAS,KAAK,MAAM,KAAK,EAAE,CAAC,EAAG,QAAO;AAI7E,MAAI,KAAK,YAAY,UAAU,EAAE,SAAS,EAAG,QAAO;AAGpD,QAAM,SAAS,MAAM,IAAI,OAAO,KAAK,IAAI;AACzC,QAAM,SAAS,MAAM,IAAI,OAAO,KAAK,EAAE;AACvC,MAAI,KAAK,SAAS,OAAO,QAAQ,KAAK,OAAO,OAAO,GAAI,QAAO;AAC/D,QAAM,OAAO,KAAK,SAAS,UAAU;AACrC,SAAO;AAAA,IACL,MAAM,KAAK;AAAA,IACX,IAAI,KAAK;AAAA,IACT,QAAQ,OAAO,MAAM,SAAS,KAAK,MAAM,KAAK,EAAE,IAAI;AAAA,EACtD;AACF;AAEA,SAAS,WAAW,OAAoC;AACtD,QAAM,SAAyB,CAAC;AAChC,aAAW,KAAK,EAAE,QAAQ;AAAA,IACxB,MAAM,MAAM;AACV,UAAI,KAAK,SAAS,aAAc;AAChC,YAAM,QAAQ,eAAe,OAAO,KAAK,IAAI;AAC7C,UAAI,MAAO,QAAO,KAAK,KAAK;AAAA,IAC9B;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAQA,SAAS,iBAAiB,OAAoB,OAA8B;AAC1E,QAAM,SAAS,CAAC,QAAgB,MAAM,MAAM,QAAQ,MAAM,MAAM;AAChE,SAAO,MAAM,UAAU,OAAO,KAAK,CAAC,UAAU,OAAO,MAAM,MAAM,KAAK,OAAO,MAAM,IAAI,CAAC;AAC1F;AASA,SAAS,gBAAgB,OAAoB,OAA8B;AACzE,SAAO,MAAM,UAAU,OAAO;AAAA,IAC5B,CAAC,UAAU,CAAC,MAAM,SAAS,MAAM,QAAQ,MAAM,QAAQ,MAAM,MAAM,MAAM;AAAA,EAC3E;AACF;AAEA,SAAS,MAAM,OAAoB,QAA2C;AAC5E,QAAM,SAA8B,CAAC;AACrC,aAAW,SAAS,QAAQ;AAC1B,QAAI,iBAAiB,OAAO,KAAK,EAAG;AACpC,WAAO;AAAA,MACL,WAAW,QAAQ;AAAA,QACjB,QAAQ,IAAI,cAAc,MAAM,QAAQ,gBAAgB,OAAO,KAAK,CAAC;AAAA,QACrE,OAAO;AAAA,MACT,CAAC,EAAE,MAAM,MAAM,MAAM,MAAM,EAAE;AAAA,IAC/B;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,aAAa,WAAW,IAAI,QAAQ,IAAI,EAAE;AAC7D;AAEO,MAAM,eAAe,WAAW,OAA0B;AAAA,EAC/D,QAAQ,CAAC,UAAU,MAAM,OAAO,WAAW,KAAK,CAAC;AAAA,EACjD,OAAO,OAAO,IAAI;AAMhB,QAAI,GAAG,cAAc,WAAW,GAAG,KAAK,MAAM,WAAW,GAAG,UAAU,GAAG;AACvE,aAAO,MAAM,GAAG,OAAO,WAAW,GAAG,KAAK,CAAC;AAAA,IAC7C;AACA,QAAI,GAAG,WAAW;AAChB,aAAO,MAAM,GAAG,OAAO,MAAM,MAAM;AAAA,IACrC;AACA,WAAO;AAAA,EACT;AAAA,EACA,SAAS,CAAC,UAAU;AAAA,IAClB,WAAW,YAAY,KAAK,OAAO,CAAC,UAAU,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA,IAI/D,WAAW,aAAa;AAAA,MACtB,CAAC,SAAS,KAAK,MAAM,MAAM,OAAO,KAAK,GAAG,eAAe,WAAW;AAAA,IACtE;AAAA,EACF;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid rendering, decoupled from CodeMirror — shared by the editor widget,
|
|
3
|
+
* the document export (which ships the SVG to the backend), and the clipboard
|
|
4
|
+
* (which needs a PNG, since Google Docs/Word drop SVG on paste).
|
|
5
|
+
*
|
|
6
|
+
* The mermaid library is heavy (~2 MB minified), so it loads lazily on the
|
|
7
|
+
* first diagram — bundled with the app, never fetched at runtime, so diagrams
|
|
8
|
+
* work fully offline. Render results are cached by source text; PNGs are
|
|
9
|
+
* rasterised once per diagram in the background (see {@link warmMermaidPng})
|
|
10
|
+
* so the SYNCHRONOUS copy event can embed them from cache.
|
|
11
|
+
*/
|
|
12
|
+
type MermaidRenderResult = {
|
|
13
|
+
ok: true;
|
|
14
|
+
svg: string;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
/** Whether a fence info string denotes a rendered mermaid diagram — the tag
|
|
20
|
+
* alone, any casing, no trailing meta ("mermaid title=x" is source, not a
|
|
21
|
+
* diagram). The one definition shared by the editor plugin, the export's SVG
|
|
22
|
+
* collector, and the clipboard, so every surface classifies identically. */
|
|
23
|
+
declare function isMermaidFenceInfo(info: string): boolean;
|
|
24
|
+
/** The cached render for a source, or null — the SYNCHRONOUS path. The widget
|
|
25
|
+
* uses it to fill a re-created diagram in the same task (no pending flash, no
|
|
26
|
+
* measure churn on caret-in/out toggles). */
|
|
27
|
+
declare function getCachedMermaidSvg(source: string): MermaidRenderResult | null;
|
|
28
|
+
/** Render a mermaid diagram source to SVG, memoised by source. Never rejects —
|
|
29
|
+
* a parse error resolves to `{ ok: false, message }`. */
|
|
30
|
+
declare function renderMermaidToSvg(source: string): Promise<MermaidRenderResult>;
|
|
31
|
+
/** The cached PNG data URI for a diagram source, if it has been rasterised. */
|
|
32
|
+
declare function getCachedMermaidPng(source: string): string | null;
|
|
33
|
+
/** Render + rasterise a diagram in the background so a LATER (synchronous)
|
|
34
|
+
* clipboard copy can embed it from cache. Fire-and-forget: failures leave the
|
|
35
|
+
* cache empty and the copy degrades to the source block. */
|
|
36
|
+
declare function warmMermaidPng(source: string): Promise<void>;
|
|
37
|
+
/** Rasterise an SVG string to a PNG data URI at `scale`× its intrinsic size
|
|
38
|
+
* (2× by default, so the pasted image stays sharp on retina targets).
|
|
39
|
+
* Resolves null when the SVG has no readable dimensions or drawing fails. */
|
|
40
|
+
declare function rasterizeSvgToPngDataUri(svg: string, scale?: number): Promise<string | null>;
|
|
41
|
+
|
|
42
|
+
export { type MermaidRenderResult, getCachedMermaidPng, getCachedMermaidSvg, isMermaidFenceInfo, rasterizeSvgToPngDataUri, renderMermaidToSvg, warmMermaidPng };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import "../../chunk-V6TY7KAL.js";
|
|
2
|
+
function isMermaidFenceInfo(info) {
|
|
3
|
+
return /^mermaid\s*$/i.test(info.trim());
|
|
4
|
+
}
|
|
5
|
+
let mermaidLoader = null;
|
|
6
|
+
function loadMermaid() {
|
|
7
|
+
mermaidLoader ?? (mermaidLoader = import("mermaid").then((mod) => {
|
|
8
|
+
const mermaid = mod.default;
|
|
9
|
+
mermaid.initialize({
|
|
10
|
+
startOnLoad: false,
|
|
11
|
+
// "strict" escapes HTML in labels and blocks script/click directives —
|
|
12
|
+
// fence content is the user's own doc, but pasted diagrams shouldn't
|
|
13
|
+
// be able to inject markup either.
|
|
14
|
+
securityLevel: "strict",
|
|
15
|
+
theme: "neutral",
|
|
16
|
+
fontFamily: "inherit",
|
|
17
|
+
// SVG-native <text> labels, not <foreignObject> HTML: canvas refuses to
|
|
18
|
+
// rasterise foreignObject content (the clipboard PNG taints), and
|
|
19
|
+
// text-only SVGs travel better through the export paths. The root key
|
|
20
|
+
// is what mermaid 11.16 honours (`flowchart.htmlLabels` is deprecated
|
|
21
|
+
// and loses to the root's default).
|
|
22
|
+
htmlLabels: false,
|
|
23
|
+
flowchart: { htmlLabels: false },
|
|
24
|
+
class: { htmlLabels: false },
|
|
25
|
+
// On a parse error mermaid otherwise injects its own bomb-icon SVG
|
|
26
|
+
// into <body>; callers render their own error states.
|
|
27
|
+
suppressErrorRendering: true
|
|
28
|
+
});
|
|
29
|
+
return mermaid;
|
|
30
|
+
}));
|
|
31
|
+
return mermaidLoader;
|
|
32
|
+
}
|
|
33
|
+
let renderSeq = 0;
|
|
34
|
+
const svgCache = /* @__PURE__ */ new Map();
|
|
35
|
+
const SVG_CACHE_CAP = 100;
|
|
36
|
+
function remember(source, result) {
|
|
37
|
+
if (svgCache.size >= SVG_CACHE_CAP) {
|
|
38
|
+
const oldest = svgCache.keys().next().value;
|
|
39
|
+
if (oldest !== void 0) svgCache.delete(oldest);
|
|
40
|
+
}
|
|
41
|
+
svgCache.set(source, result);
|
|
42
|
+
}
|
|
43
|
+
function getCachedMermaidSvg(source) {
|
|
44
|
+
return svgCache.get(source) ?? null;
|
|
45
|
+
}
|
|
46
|
+
async function renderMermaidToSvg(source) {
|
|
47
|
+
const cached = svgCache.get(source);
|
|
48
|
+
if (cached) return cached;
|
|
49
|
+
let result;
|
|
50
|
+
try {
|
|
51
|
+
const mermaid = await loadMermaid();
|
|
52
|
+
const { svg } = await mermaid.render(`cm-mermaid-${renderSeq++}`, source);
|
|
53
|
+
result = { ok: true, svg };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
result = { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
56
|
+
}
|
|
57
|
+
remember(source, result);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
const pngCache = /* @__PURE__ */ new Map();
|
|
61
|
+
const pngPending = /* @__PURE__ */ new Set();
|
|
62
|
+
const PNG_CACHE_CAP = 24;
|
|
63
|
+
function getCachedMermaidPng(source) {
|
|
64
|
+
return pngCache.get(source.trim()) ?? null;
|
|
65
|
+
}
|
|
66
|
+
async function warmMermaidPng(source) {
|
|
67
|
+
const key = source.trim();
|
|
68
|
+
if (key === "" || pngCache.has(key) || pngPending.has(key)) return;
|
|
69
|
+
pngPending.add(key);
|
|
70
|
+
try {
|
|
71
|
+
const rendered = await renderMermaidToSvg(key);
|
|
72
|
+
if (!rendered.ok) return;
|
|
73
|
+
const png = await rasterizeSvgToPngDataUri(rendered.svg);
|
|
74
|
+
if (!png) return;
|
|
75
|
+
if (pngCache.size >= PNG_CACHE_CAP) {
|
|
76
|
+
const oldest = pngCache.keys().next().value;
|
|
77
|
+
if (oldest !== void 0) pngCache.delete(oldest);
|
|
78
|
+
}
|
|
79
|
+
pngCache.set(key, png);
|
|
80
|
+
} finally {
|
|
81
|
+
pngPending.delete(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function rasterizeSvgToPngDataUri(svg, scale = 2) {
|
|
85
|
+
const dims = svgDimensions(svg);
|
|
86
|
+
if (!dims) return null;
|
|
87
|
+
const sized = withExplicitSize(svg, dims);
|
|
88
|
+
const url = URL.createObjectURL(new Blob([sized], { type: "image/svg+xml" }));
|
|
89
|
+
try {
|
|
90
|
+
const image = new Image();
|
|
91
|
+
await new Promise((resolve, reject) => {
|
|
92
|
+
image.onload = () => resolve();
|
|
93
|
+
image.onerror = () => reject(new Error("svg image failed to load"));
|
|
94
|
+
image.src = url;
|
|
95
|
+
});
|
|
96
|
+
const canvas = document.createElement("canvas");
|
|
97
|
+
canvas.width = Math.max(1, Math.round(dims.width * scale));
|
|
98
|
+
canvas.height = Math.max(1, Math.round(dims.height * scale));
|
|
99
|
+
const ctx = canvas.getContext("2d");
|
|
100
|
+
if (!ctx) return null;
|
|
101
|
+
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
102
|
+
return canvas.toDataURL("image/png");
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
} finally {
|
|
106
|
+
URL.revokeObjectURL(url);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function svgDimensions(svg) {
|
|
110
|
+
const root = svg.match(/<svg[^>]*>/)?.[0];
|
|
111
|
+
if (!root) return null;
|
|
112
|
+
const viewBox = root.match(/viewBox\s*=\s*"([\d.\s+-]+)"/)?.[1]?.trim().split(/\s+/);
|
|
113
|
+
if (viewBox?.length === 4) {
|
|
114
|
+
const width2 = Number(viewBox[2]);
|
|
115
|
+
const height2 = Number(viewBox[3]);
|
|
116
|
+
if (width2 > 0 && height2 > 0) return { width: width2, height: height2 };
|
|
117
|
+
}
|
|
118
|
+
const width = Number(root.match(/\bwidth\s*=\s*"([\d.]+)"/)?.[1]);
|
|
119
|
+
const height = Number(root.match(/\bheight\s*=\s*"([\d.]+)"/)?.[1]);
|
|
120
|
+
return width > 0 && height > 0 ? { width, height } : null;
|
|
121
|
+
}
|
|
122
|
+
function withExplicitSize(svg, dims) {
|
|
123
|
+
return svg.replace(/<svg([^>]*)>/, (_match, attrs) => {
|
|
124
|
+
const cleaned = attrs.replace(/\swidth\s*=\s*"[^"]*"/, "").replace(/\sheight\s*=\s*"[^"]*"/, "");
|
|
125
|
+
return `<svg${cleaned} width="${dims.width}" height="${dims.height}">`;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
getCachedMermaidPng,
|
|
130
|
+
getCachedMermaidSvg,
|
|
131
|
+
isMermaidFenceInfo,
|
|
132
|
+
rasterizeSvgToPngDataUri,
|
|
133
|
+
renderMermaidToSvg,
|
|
134
|
+
warmMermaidPng
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=mermaidRender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/codemirror/mermaid/mermaidRender.ts"],"sourcesContent":["/**\n * Mermaid rendering, decoupled from CodeMirror — shared by the editor widget,\n * the document export (which ships the SVG to the backend), and the clipboard\n * (which needs a PNG, since Google Docs/Word drop SVG on paste).\n *\n * The mermaid library is heavy (~2 MB minified), so it loads lazily on the\n * first diagram — bundled with the app, never fetched at runtime, so diagrams\n * work fully offline. Render results are cached by source text; PNGs are\n * rasterised once per diagram in the background (see {@link warmMermaidPng})\n * so the SYNCHRONOUS copy event can embed them from cache.\n */\n\nexport type MermaidRenderResult = { ok: true; svg: string } | { ok: false; message: string };\n\n/** Whether a fence info string denotes a rendered mermaid diagram — the tag\n * alone, any casing, no trailing meta (\"mermaid title=x\" is source, not a\n * diagram). The one definition shared by the editor plugin, the export's SVG\n * collector, and the clipboard, so every surface classifies identically. */\nexport function isMermaidFenceInfo(info: string): boolean {\n return /^mermaid\\s*$/i.test(info.trim());\n}\n\nlet mermaidLoader: Promise<typeof import(\"mermaid\").default> | null = null;\n\nfunction loadMermaid(): Promise<typeof import(\"mermaid\").default> {\n mermaidLoader ??= import(\"mermaid\").then((mod) => {\n const mermaid = mod.default;\n mermaid.initialize({\n startOnLoad: false,\n // \"strict\" escapes HTML in labels and blocks script/click directives —\n // fence content is the user's own doc, but pasted diagrams shouldn't\n // be able to inject markup either.\n securityLevel: \"strict\",\n theme: \"neutral\",\n fontFamily: \"inherit\",\n // SVG-native <text> labels, not <foreignObject> HTML: canvas refuses to\n // rasterise foreignObject content (the clipboard PNG taints), and\n // text-only SVGs travel better through the export paths. The root key\n // is what mermaid 11.16 honours (`flowchart.htmlLabels` is deprecated\n // and loses to the root's default).\n htmlLabels: false,\n flowchart: { htmlLabels: false },\n class: { htmlLabels: false },\n // On a parse error mermaid otherwise injects its own bomb-icon SVG\n // into <body>; callers render their own error states.\n suppressErrorRendering: true,\n });\n return mermaid;\n });\n return mermaidLoader;\n}\n\nlet renderSeq = 0;\n\n/** Render results by fence source. Bounded: a doc being actively rewritten\n * produces a new key per keystroke-burst, and SVG strings are not small. */\nconst svgCache = new Map<string, MermaidRenderResult>();\nconst SVG_CACHE_CAP = 100;\n\nfunction remember(source: string, result: MermaidRenderResult): void {\n if (svgCache.size >= SVG_CACHE_CAP) {\n const oldest = svgCache.keys().next().value;\n if (oldest !== undefined) svgCache.delete(oldest);\n }\n svgCache.set(source, result);\n}\n\n/** The cached render for a source, or null — the SYNCHRONOUS path. The widget\n * uses it to fill a re-created diagram in the same task (no pending flash, no\n * measure churn on caret-in/out toggles). */\nexport function getCachedMermaidSvg(source: string): MermaidRenderResult | null {\n return svgCache.get(source) ?? null;\n}\n\n/** Render a mermaid diagram source to SVG, memoised by source. Never rejects —\n * a parse error resolves to `{ ok: false, message }`. */\nexport async function renderMermaidToSvg(source: string): Promise<MermaidRenderResult> {\n const cached = svgCache.get(source);\n if (cached) return cached;\n let result: MermaidRenderResult;\n try {\n const mermaid = await loadMermaid();\n const { svg } = await mermaid.render(`cm-mermaid-${renderSeq++}`, source);\n result = { ok: true, svg };\n } catch (error) {\n result = { ok: false, message: error instanceof Error ? error.message : String(error) };\n }\n remember(source, result);\n return result;\n}\n\n// ── PNG side (clipboard) ─────────────────────────────────────────────────────\n//\n// Keys are the TRIMMED diagram source: the markdown pipeline hands the\n// clipboard a fence body with a trailing newline, the widget hands the warm\n// path the fence's inner text — trimming is the meeting point (the export's\n// SVG map uses the same convention).\n\nconst pngCache = new Map<string, string>();\nconst pngPending = new Set<string>();\nconst PNG_CACHE_CAP = 24;\n\n/** The cached PNG data URI for a diagram source, if it has been rasterised. */\nexport function getCachedMermaidPng(source: string): string | null {\n return pngCache.get(source.trim()) ?? null;\n}\n\n/** Render + rasterise a diagram in the background so a LATER (synchronous)\n * clipboard copy can embed it from cache. Fire-and-forget: failures leave the\n * cache empty and the copy degrades to the source block. */\nexport async function warmMermaidPng(source: string): Promise<void> {\n const key = source.trim();\n if (key === \"\" || pngCache.has(key) || pngPending.has(key)) return;\n pngPending.add(key);\n try {\n const rendered = await renderMermaidToSvg(key);\n if (!rendered.ok) return;\n const png = await rasterizeSvgToPngDataUri(rendered.svg);\n if (!png) return;\n if (pngCache.size >= PNG_CACHE_CAP) {\n const oldest = pngCache.keys().next().value;\n if (oldest !== undefined) pngCache.delete(oldest);\n }\n pngCache.set(key, png);\n } finally {\n pngPending.delete(key);\n }\n}\n\n/** Rasterise an SVG string to a PNG data URI at `scale`× its intrinsic size\n * (2× by default, so the pasted image stays sharp on retina targets).\n * Resolves null when the SVG has no readable dimensions or drawing fails. */\nexport async function rasterizeSvgToPngDataUri(svg: string, scale = 2): Promise<string | null> {\n const dims = svgDimensions(svg);\n if (!dims) return null;\n // WebKit refuses to draw an SVG image without explicit width/height (mermaid\n // emits width=\"100%\" + a viewBox), so pin the intrinsic size first.\n const sized = withExplicitSize(svg, dims);\n const url = URL.createObjectURL(new Blob([sized], { type: \"image/svg+xml\" }));\n try {\n const image = new Image();\n await new Promise<void>((resolve, reject) => {\n image.onload = () => resolve();\n image.onerror = () => reject(new Error(\"svg image failed to load\"));\n image.src = url;\n });\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(dims.width * scale));\n canvas.height = Math.max(1, Math.round(dims.height * scale));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n ctx.drawImage(image, 0, 0, canvas.width, canvas.height);\n return canvas.toDataURL(\"image/png\");\n } catch {\n return null;\n } finally {\n URL.revokeObjectURL(url);\n }\n}\n\n/** Intrinsic diagram size from the root svg's viewBox (mermaid always emits\n * one), falling back to numeric width/height attributes. */\nfunction svgDimensions(svg: string): { width: number; height: number } | null {\n const root = svg.match(/<svg[^>]*>/)?.[0];\n if (!root) return null;\n const viewBox = root.match(/viewBox\\s*=\\s*\"([\\d.\\s+-]+)\"/)?.[1]?.trim().split(/\\s+/);\n if (viewBox?.length === 4) {\n const width = Number(viewBox[2]);\n const height = Number(viewBox[3]);\n if (width > 0 && height > 0) return { width, height };\n }\n const width = Number(root.match(/\\bwidth\\s*=\\s*\"([\\d.]+)\"/)?.[1]);\n const height = Number(root.match(/\\bheight\\s*=\\s*\"([\\d.]+)\"/)?.[1]);\n return width > 0 && height > 0 ? { width, height } : null;\n}\n\nfunction withExplicitSize(svg: string, dims: { width: number; height: number }): string {\n return svg.replace(/<svg([^>]*)>/, (_match, attrs: string) => {\n const cleaned = attrs\n .replace(/\\swidth\\s*=\\s*\"[^\"]*\"/, \"\")\n .replace(/\\sheight\\s*=\\s*\"[^\"]*\"/, \"\");\n return `<svg${cleaned} width=\"${dims.width}\" height=\"${dims.height}\">`;\n });\n}\n"],"mappings":"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","names":["width","height"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { WidgetType, EditorView } from '@codemirror/view';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mermaid diagram widget — a closed ```mermaid fence rendered as a diagram
|
|
5
|
+
* block. Clicking the diagram moves the caret into the fence, which flips the
|
|
6
|
+
* block back to source (mermaidPlugin owns that swap).
|
|
7
|
+
*
|
|
8
|
+
* Rendering itself (lazy mermaid load, SVG cache, clipboard PNG warm-up) lives
|
|
9
|
+
* in [mermaidRender](./mermaidRender.ts) — this file owns only the widget:
|
|
10
|
+
* measured heights are cached by source so `estimatedHeight` answers with the
|
|
11
|
+
* real height and re-created widgets never shift the scroll position (the
|
|
12
|
+
* #120 contract).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Pre-measure estimate: diagram height doesn't map cleanly onto source
|
|
16
|
+
* lines, but ~a node-row per line is the right order of magnitude, and the
|
|
17
|
+
* measured cache replaces the guess after the first real render. */
|
|
18
|
+
declare function estimateMermaidHeight(source: string): number;
|
|
19
|
+
declare class MermaidWidget extends WidgetType {
|
|
20
|
+
readonly source: string;
|
|
21
|
+
/** The fence is covered by a (spanning) selection — native selection
|
|
22
|
+
* paints nothing over a block widget, so the widget shows its own
|
|
23
|
+
* selected state. */
|
|
24
|
+
readonly selected: boolean;
|
|
25
|
+
constructor(source: string,
|
|
26
|
+
/** The fence is covered by a (spanning) selection — native selection
|
|
27
|
+
* paints nothing over a block widget, so the widget shows its own
|
|
28
|
+
* selected state. */
|
|
29
|
+
selected?: boolean);
|
|
30
|
+
/** Identity = source + selected. Edits elsewhere shift the fence's position
|
|
31
|
+
* but must not tear down the diagram DOM; a selected flip alone updates
|
|
32
|
+
* the class in place via {@link updateDOM}. */
|
|
33
|
+
eq(other: MermaidWidget): boolean;
|
|
34
|
+
updateDOM(dom: HTMLElement): boolean;
|
|
35
|
+
toDOM(view: EditorView): HTMLElement;
|
|
36
|
+
private fill;
|
|
37
|
+
/** The widget owns its events (click-to-edit); CM must not also react. */
|
|
38
|
+
ignoreEvent(): boolean;
|
|
39
|
+
get estimatedHeight(): number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { MermaidWidget, estimateMermaidHeight };
|