---
name: 从剪贴板导入脑图
description: 将剪贴板中的 Markdown 列表文本解析为层级树结构,并在当前画布生成带样式的脑图。
author: ymjr
version: 1.1.1
license: MIT
usage: 复制任意 Markdown 列表文本,运行此脚本。在弹出的面板中选择需要的排版方向、连线样式和主题配色,确认后脚本将在画布中心自动生成完整的脑图。
features:
- 自动解析剪贴板内的 Markdown 缩进结构为 JSON Tree
- 支持调用 Feature-Mindmap_Engine 引擎进行完美的脑图渲染和布局
- 提供可视化的生成前配置面板
dependencies:
- 必须依赖 Feature-Mindmap_Engine 提供后台渲染与自动排版支持
autorun: false
---
/*
```javascript
*/
var locales = {
zh: {
theme_rainbow: "🌈 彩虹分枝",
theme_morandi: "🎨 莫兰迪色",
theme_business: "💼 商务蓝调",
theme_custom: "⚙️ 自定义色",
notice_empty: "⚠️ 剪贴板为空或没有文本内容!",
notice_parse_fail: "⚠️ 无法将剪贴板文本解析为有效的层级结构!",
prompt_paste_text: "请在此粘贴 Markdown 脑图大纲文本:",
ui_title: "导入剪贴板脑图设置",
ui_theme: "主题配色",
ui_direction: "排版方向",
ui_dir_lr: "向右展开 (LR)",
ui_dir_rl: "向左展开 (RL)",
ui_dir_td: "向下展开 (TD)",
ui_dir_bu: "向上展开 (BU)",
ui_arrow_style: "连线样式",
ui_arrow_normal: "平滑曲线",
ui_arrow_elbow: "折线",
ui_arrow_curve: "S型曲线",
ui_fixed_center: "固定在中点",
ui_auto_slide: "自动滑动",
ui_fixed_point: "箭头固定中点",
ui_cancel: "取消",
ui_confirm: "生成",
notice_success: "✨ 解析生成完成,正在自动排版...",
notice_no_engine: "⚠️ 脑图核心引擎未运行,无法自动排版,请手动开启 Engine 脚本!"
},
en: {
theme_rainbow: "🌈 Rainbow",
theme_morandi: "🎨 Morandi",
theme_business: "💼 Business Blue",
theme_custom: "⚙️ Custom",
notice_empty: "⚠️ Clipboard is empty or contains no text!",
notice_parse_fail: "⚠️ Failed to parse clipboard text into a valid hierarchy!",
prompt_paste_text: "Please paste Markdown mindmap outline text here:",
ui_title: "Import Mindmap from Clipboard",
ui_theme: "Theme",
ui_direction: "Direction",
ui_dir_lr: "Expand Right (LR)",
ui_dir_rl: "Expand Left (RL)",
ui_dir_td: "Expand Down (TD)",
ui_dir_bu: "Expand Up (BU)",
ui_arrow_style: "Connector Style",
ui_arrow_normal: "Normal Curve",
ui_arrow_elbow: "Elbow Connector",
ui_arrow_curve: "S-Curve",
ui_fixed_center: "Fixed at Center",
ui_auto_slide: "Auto Sliding",
ui_fixed_point: "Fixed Point Arrow",
ui_cancel: "Cancel",
ui_confirm: "Generate",
notice_success: "✨ Parse complete, re-layout in progress...",
notice_no_engine: "⚠️ Mindmap engine not running, layout skipped. Enable Engine script!"
}
};
locales["zh-cn"] = locales.zh;
locales["zh-tw"] = locales.zh;
const { Notice } = ea.obsidian;
const DEFAULT_LEVELS = [
{ level: 1, strokeColor: "black", textColor: "#ffffff", fontSize: 24, strokeWidth: 2 },
{ level: 2, strokeColor: "black", textColor: "black", fontSize: 20, strokeWidth: 1 },
{ level: 3, strokeColor: "black", textColor: "black", fontSize: 16, strokeWidth: 1 },
{ level: 4, strokeColor: "black", textColor: "black", fontSize: 14, strokeWidth: 1 }
];
const FALLBACK_THEMES = {
"rainbow": { name: "🌈 彩虹分枝", rootBg: "#343a40", colors: ["#e03131", "#1971c2", "#099268", "#e8590c", "#9c36b5", "#fcc419", "#3bc9db"], levels: DEFAULT_LEVELS },
"morandi": { name: "🎨 莫兰迪色", rootBg: "#495057", colors: ["#5c7a82", "#b38269", "#788560", "#a68894", "#7a6c5d"], levels: DEFAULT_LEVELS },
"business": { name: "💼 商务蓝调", rootBg: "#0b2545", colors: ["#1864ab", "#0b7285", "#087f5b", "#5f3dc4", "#c92a2a"], levels: DEFAULT_LEVELS },
"custom": { name: "⚙️ 自定义色", rootBg: "#1e1e1e", colors: ["#e03131", "#1971c2", "#099268", "#e8590c", "#9c36b5", "#fcc419"], levels: DEFAULT_LEVELS }
};
const FALLBACK_SETTINGS = {
direction: "LR", arrowType: "normal", defaultGap: 15, curveLength: 40, lengthBetweenElAndLine: 100, enableFixedPoint: true,
theme: "rainbow",
customTheme: { rootBg: FALLBACK_THEMES["custom"].rootBg, colors: [...FALLBACK_THEMES["custom"].colors], levels: JSON.parse(JSON.stringify(DEFAULT_LEVELS)) }
};
// --- Settings Initial Load (跨脚本共享读取) ---
let engineSettings = ExcalidrawAutomate.plugin.settings.scriptEngineSettings["Mindmap_Engine"] || {};
let mindmapConfig;
try {
if (engineSettings["Mindmap Configuration"] && engineSettings["Mindmap Configuration"].value) {
mindmapConfig = JSON.parse(engineSettings["Mindmap Configuration"].value);
} else {
mindmapConfig = { themes: FALLBACK_THEMES, defaultSettings: FALLBACK_SETTINGS };
}
} catch (e) {
mindmapConfig = { themes: FALLBACK_THEMES, defaultSettings: FALLBACK_SETTINGS };
}
const THEMES = mindmapConfig.themes || FALLBACK_THEMES;
let pluginSettings = mindmapConfig.defaultSettings || FALLBACK_SETTINGS;
// --------------------------------
function lightenHex(hex, percent) {
if (!hex || hex === "transparent") return hex;
let num = parseInt(hex.replace("#",""), 16);
if (isNaN(num)) return hex;
let amt = Math.round(2.55 * percent);
let R = (num >> 16) + amt; let G = (num >> 8 & 0x00FF) + amt; let B = (num & 0x0000FF) + amt;
return "#" + (0x1000000 + (R<255?R<1?0:R:255)*0x10000 + (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255)).toString(16).slice(1);
}
function resolveStyle(level, branchIndex, settings) {
const themeKey = settings?.theme || "rainbow";
let theme = THEMES[themeKey] || THEMES["rainbow"];
if (themeKey === "custom" && settings?.customTheme) theme = { ...theme, ...settings.customTheme };
const levels = theme.levels || DEFAULT_LEVELS;
const lvlConfig = level <= levels.length ? levels[level - 1] : levels[levels.length - 1];
let bg = "transparent";
if (level === 1) bg = theme.rootBg;
else {
const colors = theme.colors && theme.colors.length > 0 ? theme.colors : THEMES["rainbow"].colors;
const baseColor = colors[branchIndex % colors.length];
bg = level > 2 ? lightenHex(baseColor, Math.min((level - 2) * 15, 60)) : baseColor;
}
return { backgroundColor: bg, textColor: lvlConfig.textColor, strokeColor: lvlConfig.strokeColor, fontSize: lvlConfig.fontSize, strokeWidth: lvlConfig.strokeWidth };
}
let clipboardText = "";
try {
clipboardText = await navigator.clipboard.readText();
} catch (e) {
console.warn("navigator.clipboard.readText failed:", e);
}
if (!clipboardText || !clipboardText.trim()) {
if (utils && typeof utils.inputPrompt === "function") {
clipboardText = await utils.inputPrompt(t("prompt_paste_text"), "", "");
}
}
if (!clipboardText || !clipboardText.trim()) { new Notice(t("notice_empty")); return; }
function parseOutline(text) {
const lines = text.split('\n').filter(line => line.trim().length > 0);
const nodes = []; const parentStack = [];
lines.forEach((line, index) => {
let depth = 1; let textContent = line.trim();
const headingMatch = line.match(/^(#+)\s+(.*)/);
if (headingMatch) { depth = 0; textContent = headingMatch[2].trim(); }
else {
const match = line.match(/^([ \t]*)(?:[-*+]|\d+\.)?\s*(.*)/);
if (match) {
const indent = match[1]; const tabs = (indent.match(/\t/g) || []).length; const spaces = (indent.match(/ /g) || []).length;
depth = tabs + Math.floor(spaces / 2) + 1; textContent = match[2].trim() || "空白节点";
}
}
const node = { id: `temp_${index}`, depth: depth, text: textContent, children: [], eaId: null, parentId: null };
if (depth === 0) { parentStack[0] = node; nodes.push(node); }
else {
let parentDepth = depth - 1; while (parentDepth >= 0 && !parentStack[parentDepth]) parentDepth--;
const parent = parentStack[parentDepth];
if (parent) { node.parentId = parent.id; parent.children.push(node); } else { nodes.push(node); }
parentStack[depth] = node; parentStack.splice(depth + 1);
}
});
return nodes;
}
const parsedTrees = parseOutline(clipboardText);
if (parsedTrees.length === 0) { new Notice(t("notice_parse_fail")); return; }
const config = await (async () => {
return new Promise((resolve) => {
if (document.getElementById("ea-mindmap-import-panel")) return resolve(null);
const panel = document.createElement('div'); panel.id = "ea-mindmap-import-panel";
panel.style.cssText = `position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); width:min(320px, 92vw); max-height:85vh; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 8px 24px rgba(0,0,0,0.25); border-radius:8px; z-index:9999; display:flex; flex-direction:column; overflow:hidden;`;
const header = document.createElement('div'); header.style.cssText = `padding:10px 15px; background:var(--background-secondary); border-bottom:1px solid var(--background-modifier-border); border-radius: 8px 8px 0 0; font-weight:bold;`;
header.innerText = t("ui_title"); panel.appendChild(header);
const content = document.createElement('div'); content.style.cssText = `padding:15px; display:flex; flex-direction:column; gap:12px;`;
const createRow = (label, el) => {
const div = document.createElement('div'); div.style.cssText = "display:flex; justify-content:space-between; align-items:center;";
const lbl = document.createElement('label'); lbl.innerText = label; lbl.style.cssText = "font-size:12px; color:var(--text-muted);";
div.appendChild(lbl); div.appendChild(el); return div;
};
const themeSelect = document.createElement('select'); themeSelect.style.width = "120px";
Object.keys(THEMES).forEach(k => {
const o = document.createElement('option'); o.value = k; o.innerText = THEMES[k].name;
if (pluginSettings.theme === k) o.selected = true; themeSelect.appendChild(o);
});
const dirSelect = document.createElement('select'); dirSelect.style.width = "120px";
dirSelect.innerHTML = ``; dirSelect.value = pluginSettings.direction;
const arrowSelect = document.createElement('select'); arrowSelect.style.width = "120px";
arrowSelect.innerHTML = ``; arrowSelect.value = pluginSettings.arrowType;
const fixedPointSelect = document.createElement('select'); fixedPointSelect.style.width = "120px";
fixedPointSelect.innerHTML = ``;
fixedPointSelect.value = pluginSettings.enableFixedPoint ? "true" : "false";
content.appendChild(createRow(t("ui_theme"), themeSelect));
content.appendChild(createRow(t("ui_direction"), dirSelect));
content.appendChild(createRow(t("ui_arrow_style"), arrowSelect));
content.appendChild(createRow(t("ui_fixed_point"), fixedPointSelect));
panel.appendChild(content);
const footer = document.createElement('div'); footer.style.cssText = `padding:10px 15px; border-top:1px solid var(--background-modifier-border); display:flex; justify-content:flex-end; gap:10px;`;
const cancelBtn = document.createElement('button'); cancelBtn.innerText = t("ui_cancel"); cancelBtn.onclick = () => { panel.remove(); resolve(null); };
const confirmBtn = document.createElement('button'); confirmBtn.innerText = t("ui_confirm"); confirmBtn.className = "mod-cta";
confirmBtn.onclick = () => {
panel.remove();
resolve({
direction: dirSelect.value, arrowType: arrowSelect.value,
theme: themeSelect.value, customTheme: pluginSettings.customTheme,
enableFixedPoint: fixedPointSelect.value === "true"
});
};
footer.appendChild(cancelBtn); footer.appendChild(confirmBtn); panel.appendChild(footer); document.body.appendChild(panel);
});
})();
if (!config) return;
ea.clear();
const pointerPos = ea.targetView.currentPosition || { x: 0, y: 0 };
let currentYOffset = pointerPos.y;
const createdElementsData = [];
let globalBranchIndex = 0;
function buildElements(node, rootId, parentEaId, level, branchIndex = 0) {
if (level === 2) branchIndex = globalBranchIndex++;
ea.style.roundness = { type: 3 }; ea.style.fillStyle = "solid"; ea.style.roughness = 0;
const style = resolveStyle(level, branchIndex, config);
ea.style.backgroundColor = style.backgroundColor;
ea.style.strokeColor = style.strokeColor || "black";
ea.style.strokeWidth = style.strokeWidth || 1;
ea.style.fontSize = style.fontSize;
currentYOffset += 20;
const boxId = ea.addText(pointerPos.x + level * 200, currentYOffset, node.text, {
box: true, textAlign: "center", textVerticalAlign: "middle", boxStrokeColor: ea.style.strokeColor
});
node.eaId = boxId; const el = ea.getElement(boxId); const currentRootId = rootId || boxId;
el.customData = { mindmap: { status: "open", root: currentRootId, parent: parentEaId || currentRootId, level: level } };
if (level === 1) {
el.customData.mindmap.settings = {
direction: config.direction, arrowType: config.arrowType, theme: config.theme, customTheme: config.customTheme,
defaultGap: pluginSettings.defaultGap || 15, curveLength: 40, lengthBetweenElAndLine: 100, enableFixedPoint: config.enableFixedPoint
};
}
createdElementsData.push(el);
if (parentEaId) {
const arrowId = ea.connectObjects(parentEaId, "right", boxId, "left", { startArrowHead: null });
const arrowEl = ea.getElement(arrowId);
if(arrowEl) {
arrowEl.strokeColor = style.strokeColor || "black";
arrowEl.strokeWidth = style.strokeWidth || 1;
arrowEl.customData = arrowEl.customData || {};
// 根据配置注入 curveArrow 属性
if (config.arrowType === "curve") {
arrowEl.customData.curveArrow = true;
} else {
delete arrowEl.customData.curveArrow;
}
if (config.enableFixedPoint) {
if (arrowEl.startBinding) arrowEl.startBinding.mode = "inside";
if (arrowEl.endBinding) arrowEl.endBinding.mode = "inside";
} else {
if (arrowEl.startBinding) { delete arrowEl.startBinding.mode; delete arrowEl.startBinding.fixedPoint; }
if (arrowEl.endBinding) { delete arrowEl.endBinding.mode; delete arrowEl.endBinding.fixedPoint; }
}
}
createdElementsData.push(arrowEl);
}
node.children.forEach(child => buildElements(child, currentRootId, boxId, level + 1, branchIndex));
}
parsedTrees.forEach(treeRoot => buildElements(treeRoot, null, null, 1));
await ea.addElementsToView(false, false, true);
new Notice(t("notice_success"));
if (window.MindmapAPI && window.MindmapAPI.runLayout) {
setTimeout(async () => {
const elementsMap = new Map(); ea.getViewElements().forEach(el => elementsMap.set(el.id, el));
const allTreeEls = [];
createdElementsData.forEach(ce => {
const freshEl = elementsMap.get(ce.id);
if (freshEl) {
allTreeEls.push(freshEl);
if (freshEl.boundElements) {
freshEl.boundElements.forEach(b => {
if (b.type !== "arrow") { const textEl = elementsMap.get(b.id); if (textEl) allTreeEls.push(textEl); }
});
}
}
});
await window.MindmapAPI.runLayout(allTreeEls, true, ea);
}, 150);
} else {
new Notice(t("notice_no_engine"));
}