feat: enhance markdown export with visible link-reference definitions and improve copy functionality in editor

This commit is contained in:
Vadim Melnicuk
2026-09-05 22:45:34 +01:00
parent de3cddc7b9
commit eaf072b0ef
5 changed files with 134 additions and 5 deletions
+2 -2
View File
@@ -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
+1 -1
View File
@@ -98,7 +98,7 @@
"customEditors": [
{
"viewType": "markdownEditorOptimized.editor",
"displayName": "Markdown Editor Optimized",
"displayName": "MEO",
"selector": [
{
"filenamePattern": "*.md"
+109 -1
View File
@@ -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 {
+20
View File
@@ -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;
+2 -1
View File
@@ -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));
}
}