From eaf072b0effd5ba3ba6f464a8eee0866bc574a94 Mon Sep 17 00:00:00 2001 From: Vadim Melnicuk Date: Sat, 5 Sep 2026 22:45:34 +0100 Subject: [PATCH] feat: enhance markdown export with visible link-reference definitions and improve copy functionality in editor --- CHANGELOG.md | 4 +- package.json | 2 +- src/export/renderMarkdown.ts | 110 ++++++++++++++++++++++++++++++++++- webview/src/editor.ts | 20 +++++++ webview/src/liveMode.ts | 3 +- 5 files changed, 134 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59739f1..4c843ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Markdown Editor Optimized (MEO) --- ## Unreleased +- Added link-reference definitions as visible linked lists in HTML and PDF exports, and restored copying selections across rendered blocks +- Added configurable CodeMirror keymap via `markdownEditorOptimized.keymap` - Hardened external document sync (git/LLM/outside editors): prefer host content on conflict, fix stuck applyingExternal, Reload recovery -- Avoid silent local-draft overwrite of external writes; surface appliedFailed + requestReload paths -- Added configurable CodeMirror keymap via `markdownEditorOptimized.keymap` (whitelist commands + `passthrough`) ## 0.1.26 - Improved dark Mermaid diagram line contrast diff --git a/package.json b/package.json index fadd741..4b9b9e0 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "customEditors": [ { "viewType": "markdownEditorOptimized.editor", - "displayName": "Markdown Editor Optimized", + "displayName": "MEO", "selector": [ { "filenamePattern": "*.md" diff --git a/src/export/renderMarkdown.ts b/src/export/renderMarkdown.ts index 7363bce..c287afb 100644 --- a/src/export/renderMarkdown.ts +++ b/src/export/renderMarkdown.ts @@ -220,7 +220,115 @@ export function renderMarkdownToHtml(options: RenderMarkdownOptions): RenderMark } function normalizeMarkdownForExport(markdownText: string): string { - return ensureBlankLinesAroundTableBlocks(normalizeMermaidColonFences(markdownText)); + return ensureVisibleReferenceSections( + ensureBlankLinesAroundTableBlocks(normalizeMermaidColonFences(markdownText)) + ); +} + +type LinkReferenceDefinition = { + label: string; + title: string; +}; + +const referenceSectionHeadingPattern = /^([ \t]{0,3})(#{1,6})[ \t]+(?:references|sources|citations)[ \t]*#*[ \t]*$/i; +const linkReferenceDefinitionPattern = /^[ \t]{0,3}\[([^\]^][^\]]*)\]:[ \t]*(?:<[^>\r\n]+>|\S+)(?:[ \t]+(?:"([^"]*)"|'([^']*)'|\(([^)]*)\)))?[ \t]*$/; + +function ensureVisibleReferenceSections(markdownText: string): string { + const lines = String(markdownText ?? '').split(/\r?\n/); + const out: string[] = []; + const fenceState = { inFence: false, char: '', length: 0 }; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] ?? ''; + updateExportFenceState(fenceState, line); + out.push(line); + + if (fenceState.inFence) { + continue; + } + + const headingMatch = referenceSectionHeadingPattern.exec(line); + if (!headingMatch) { + continue; + } + + const definitions = collectReferenceDefinitions(lines, index + 1, headingMatch[2].length); + if (!definitions.length) { + continue; + } + + out.push('', ...definitions.map(renderVisibleReferenceDefinition), ''); + } + + return out.join('\n'); +} + +function collectReferenceDefinitions( + lines: string[], + startIndex: number, + sectionHeadingLevel: number +): LinkReferenceDefinition[] { + const definitions: LinkReferenceDefinition[] = []; + + for (let index = startIndex; index < lines.length; index += 1) { + const line = lines[index] ?? ''; + if (!line.trim()) { + continue; + } + + const heading = /^[ \t]{0,3}(#{1,6})[ \t]+/.exec(line); + if (heading && heading[1].length <= sectionHeadingLevel) { + break; + } + + const definitionMatch = linkReferenceDefinitionPattern.exec(line); + if (!definitionMatch) { + return []; + } + + definitions.push({ + label: definitionMatch[1].trim(), + title: (definitionMatch[2] ?? definitionMatch[3] ?? definitionMatch[4] ?? '').trim() + }); + } + + return definitions; +} + +function renderVisibleReferenceDefinition(definition: LinkReferenceDefinition): string { + const text = escapeMarkdownLinkText(definition.title || definition.label); + const label = definition.label.replace(/\\/g, '\\\\').replace(/\]/g, '\\]'); + return /^\d+$/.test(definition.label) + ? `${definition.label}. [${text}][${label}]` + : `- [${text}][${label}]`; +} + +function escapeMarkdownLinkText(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/([\[\]])/g, '\\$1'); +} + +function updateExportFenceState( + state: { inFence: boolean; char: string; length: number }, + line: string +): void { + const fence = /^[ \t]{0,3}([`~]{3,})/.exec(line); + if (!fence) { + return; + } + + const marker = fence[1]; + if (!state.inFence) { + state.inFence = true; + state.char = marker[0]; + state.length = marker.length; + return; + } + + if (marker[0] === state.char && marker.length >= state.length) { + state.inFence = false; + state.char = ''; + state.length = 0; + } } function normalizeMermaidColonFences(markdownText: string): string { diff --git a/webview/src/editor.ts b/webview/src/editor.ts index 20ea0c8..2b14dcd 100644 --- a/webview/src/editor.ts +++ b/webview/src/editor.ts @@ -1518,6 +1518,26 @@ export function createEditor({ EditorView.lineWrapping, scrollPastEnd(), EditorView.domEventHandlers({ + copy(event, view) { + const selectedRanges = view.state.selection.ranges.filter((range) => !range.empty); + if (!selectedRanges.length) { + return false; + } + + if (!event.clipboardData) { + return false; + } + + const selectedMarkdown = selectedRanges + .map((range) => view.state.doc.sliceString( + Math.min(range.from, range.to), + Math.max(range.from, range.to) + )) + .join(view.state.lineBreak); + event.clipboardData.setData('text/plain', selectedMarkdown); + event.preventDefault(); + return true; + }, pointerdown(event, view) { if (event.button !== 0) { frontmatterBoundaryClick = null; diff --git a/webview/src/liveMode.ts b/webview/src/liveMode.ts index 7412b99..89d3b79 100644 --- a/webview/src/liveMode.ts +++ b/webview/src/liveMode.ts @@ -944,6 +944,7 @@ function addDetailsBlockDecorations(builder, state, detailsBlocks, activeLines) const openingActive = rangeTouchesActiveLine(state, detailsBlock.anchorFrom, detailsBlock.anchorTo, activeLines); const closingActive = rangeTouchesActiveLine(state, detailsBlock.closingFrom, detailsBlock.closingTo, activeLines); const editingBoundary = openingActive || closingActive; + const selectingBlock = overlapsSelection(state, detailsBlock.sectionFrom, detailsBlock.sectionTo); if (!editingBoundary) { addLineClass(builder, state, detailsBlock.lineFrom, detailsBlock.lineTo, lineStyleDecos.detailsSummary); @@ -976,7 +977,7 @@ function addDetailsBlockDecorations(builder, state, detailsBlocks, activeLines) builder.push(collapsedHeadingBodyDeco.range(detailsBlock.closingFrom, detailsBlock.closingTo)); } - if (detailsBlock.collapsed && detailsBlock.bodyTo > detailsBlock.bodyFrom) { + if (detailsBlock.collapsed && !selectingBlock && detailsBlock.bodyTo > detailsBlock.bodyFrom) { builder.push(collapsedHeadingBodyDeco.range(detailsBlock.bodyFrom, detailsBlock.bodyTo)); } }