diff --git a/README.md b/README.md index 8aeab47..30219e8 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,10 @@ All per-file display preferences are written to frontmatter and restored on next - Command palette: **Toggle mindmap / source view** - Command palette: **Cycle mindmap layout (balanced / right / left)** +### External Links + +Markdown links in heading text (`[text](url)`) are rendered as clickable links directly on the mindmap canvas. Clicking a link opens it in the default browser. During node editing, links are shown as plain text for easy modification. + ### Export PNG Click the **Export PNG** button in the toolbar to save the current mindmap as a high-resolution PNG image (2x scale). A system file dialog will let you choose the save location. diff --git a/README.zh-CN.md b/README.zh-CN.md index f262a3b..56e0190 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -144,6 +144,10 @@ type: mindmap - 命令面板:**Toggle mindmap / source view** - 命令面板:**Cycle mindmap layout (balanced / right / left)** +### 外部链接 + +标题中的 Markdown 链接(`[文本](url)`)会直接在思维导图画布上渲染为可点击的链接。点击链接会在默认浏览器中打开。编辑节点时,链接会显示为纯文本以便修改。 + ### 导出 PNG 点击工具栏中的 **Export PNG** 按钮,将当前思维导图保存为高清 PNG 图片(2 倍分辨率)。系统文件对话框允许你选择保存位置。 diff --git a/main.js b/main.js index 32215d8..7a1bbf9 100644 --- a/main.js +++ b/main.js @@ -235,6 +235,37 @@ class LightMindMapPlugin extends obsidian.Plugin { .trim(); } + _renderNodeContent(el, node, overlay) { + const raw = node.rawText || node.text || ''; + if (!raw.includes('](')) { + el.textContent = node.text || PLACEHOLDER; + return; + } + el.textContent = ''; + const re = /\[([^\]]+)\]\(([^)]+)\)/g; + let last = 0; + let m; + while ((m = re.exec(raw)) !== null) { + if (m.index > last) { + el.appendChild(document.createTextNode(raw.slice(last, m.index))); + } + const a = document.createElement('a'); + a.className = 'lmm-link'; + a.textContent = m[1]; + a.href = m[2]; + a.target = '_blank'; + a.rel = 'noopener'; + el.appendChild(a); + last = m.index + m[0].length; + } + if (last < raw.length) { + el.appendChild(document.createTextNode(raw.slice(last))); + } + if (el.childNodes.length === 0) { + el.textContent = node.text || PLACEHOLDER; + } + } + _parseStructured(content) { const { frontmatterRaw, body } = this._splitFrontmatter(content); const lines = body.split('\n'); @@ -356,9 +387,7 @@ class LightMindMapPlugin extends obsidian.Plugin { _serializeNode(node, level) { const cap = Math.min(6, Math.max(1, level)); - let text = (node.dirty || node.isNew) - ? (node.text || PLACEHOLDER) - : (node.rawText || node.text || PLACEHOLDER); + let text = node.rawText || node.text || PLACEHOLDER; if (node.collapsed) { const inner = text.replace(/^\*(?!\*)(.+)\*(?!\*)$/, '$1'); text = '*' + inner + '*'; @@ -713,13 +742,13 @@ class LightMindMapPlugin extends obsidian.Plugin { if (node.collapsed && node.children.length) { const textSpan = document.createElement('span'); textSpan.className = 'lmm-node-text'; - textSpan.textContent = node.text || PLACEHOLDER; + this._renderNodeContent(textSpan, node, overlay); el.appendChild(textSpan); const badge = document.createElement('span'); badge.className = 'lmm-collapse-badge'; el.appendChild(badge); } else { - el.textContent = node.text || PLACEHOLDER; + this._renderNodeContent(el, node, overlay); } node._el = el; this._attachNodeHandlers(el, node, overlay); @@ -740,6 +769,7 @@ class LightMindMapPlugin extends obsidian.Plugin { el.addEventListener('click', (e) => { e.stopPropagation(); if (el.isContentEditable) return; + if (e.target.closest('.lmm-link')) return; this._selectNode(overlay, node, true); }); el.addEventListener('dblclick', (e) => { @@ -820,6 +850,7 @@ class LightMindMapPlugin extends obsidian.Plugin { } this._selectNode(overlay, node, false); overlay._lmmEditingNode = node; + el.textContent = node.rawText || node.text || PLACEHOLDER; el.contentEditable = 'true'; el.classList.add('lmm-editing'); el.spellcheck = false; @@ -835,10 +866,10 @@ class LightMindMapPlugin extends obsidian.Plugin { if (overlay._lmmEditingBlur === onBlur) overlay._lmmEditingBlur = null; if (!el.isContentEditable) return; const text = el.textContent; - const had = node.text; + const had = node.rawText || node.text; this._exitEditMode(overlay, node); this._updateNodeText(node, text); - if (node.text !== had) this._persistAndRelayout(overlay); + if ((node.rawText || node.text) !== had) this._persistAndRelayout(overlay); }; el.addEventListener('blur', onBlur); overlay._lmmEditingBlur = onBlur; @@ -854,21 +885,22 @@ class LightMindMapPlugin extends obsidian.Plugin { el.contentEditable = 'false'; el.classList.remove('lmm-editing'); if (overlay._lmmEditingNode === node) overlay._lmmEditingNode = null; + this._renderNodeContent(el, node, overlay); } _updateNodeText(node, newText) { const text = (newText || '').replace(/\s+/g, ' ').trim(); if (!text) return; - if (text !== node.text) { - node.text = text; + const hadRaw = node.rawText || node.text; + if (text !== hadRaw) { + node.rawText = text; + node.text = this._stripInline(text); node.dirty = true; - if (node._el) node._el.textContent = text; } } _cancelEdit(overlay, node) { this._exitEditMode(overlay, node); - if (node._el) node._el.textContent = node.text || PLACEHOLDER; } _toggleCollapse(overlay, node) { diff --git a/manifest.json b/manifest.json index 0d84750..ffafcd0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "light-mindmap", "name": "Light Mindmap", - "version": "1.2.1", + "version": "1.2.2", "minAppVersion": "1.4.0", "description": "Auto-renders markdown headings as a colorful, interactive mindmap — no extra syntax required.", "author": "Light Ning", diff --git a/styles.css b/styles.css index e9b58ed..f807100 100644 --- a/styles.css +++ b/styles.css @@ -560,3 +560,24 @@ .lmm-fab:active { transform: translateY(0); } + +/* ── Node links ── */ + +.lmm-link { + color: inherit; + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 2px; + cursor: pointer; + pointer-events: auto; +} + +.lmm-link:hover { + text-decoration-style: solid; + opacity: 0.85; +} + +.lmm-node.lmm-editing .lmm-link { + text-decoration: none; + pointer-events: none; +}