<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown to Freeplane Converter</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 20px auto;
padding: 20px;
}
.container {
display: flex;
gap: 20px;
height: 70vh;
}
textarea {
width: 100%;
height: 100%;
resize: none;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.input-area, .output-area {
flex: 1;
display: flex;
flex-direction: column;
}
.buttons {
margin: 10px 0;
display: flex;
gap: 10px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
#copyBtn {
background-color: #008CBA;
}
#copyBtn:hover {
background-color: #007399;
}
</style>
</head>
<body>
<h2>Markdown to Freeplane Converter</h2>
<div class="container">
<div class="input-area">
<h3>输入 Markdown</h3>
<textarea id="markdownInput" placeholder="请输入 Markdown 内容..."></textarea>
</div>
<div class="output-area">
<h3>Freeplane XML 输出</h3>
<textarea id="freeplaneOutput" readonly></textarea>
</div>
</div>
<div class="buttons">
<button onclick="convertToFreeplane()">转换为 Freeplane</button>
<button id="copyBtn" onclick="copyToClipboard()">复制到剪贴板</button>
</div>
<script>
function convertToFreeplane() {
const input = document.getElementById('markdownInput').value;
const lines = input.split('\n').filter(line => line.trim());
let output = '<?xml version="1.0" encoding="UTF-8"?>\n';
output += '<map version="freeplane 1.9.0">\n';
output += '<!--To view this file, download free mind mapping software Freeplane from https://www.freeplane.org -->\n';
// 根节点(取第一个标题或默认标题)
let rootText = "Converted Markdown";
const firstHeading = lines.find(line => line.startsWith('#'));
if (firstHeading) {
rootText = firstHeading.replace(/^#+/, '').trim();
}
// 生成根节点
output += `<node TEXT="${escapeXML(rootText)}" FOLDED="false" ID="ID_1" CREATED="${Date.now()}" MODIFIED="${Date.now()}">\n`;
// 添加默认样式(从示例中简化)
output += `<hook NAME="MapStyle">
<properties show_icon_for_attributes="false" show_notes_in_map="false" show_note_icons="false" fit_to_viewport="false;"/>
</hook>n`;
// 解析Markdown并生成节点结构
let currentLevel = 0;
let nodeStack = [0]; // 记录当前节点的层级
let nodeId = 2; // 从2开始,因为根节点是1
for (let line of lines) {
line = line.trim();
if (!line.startsWith('#')) continue; // 只处理标题
const match = line.match(/^(#+)\s*(.+)$/);
if (!match) continue;
const level = match[1].length;
const text = match[2].trim();
// 关闭之前的节点
while (nodeStack.length > level) {
output += '</node>\n';
nodeStack.pop();
}
// 如果当前层级比栈顶小,需要补齐层级
while (nodeStack.length < level) {
const parentLevel = nodeStack[nodeStack.length - 1];
output += `<node TEXT="Subtopic" FOLDED="false" ID="ID_${nodeId++}" CREATED="${Date.now()}" MODIFIED="${Date.now()}">\n`;
nodeStack.push(parentLevel + 1);
}
// 添加当前节点
output += `<node TEXT="${escapeXML(text)}" FOLDED="false" ID="ID_${nodeId++}" CREATED="${Date.now()}" MODIFIED="${Date.now()}">\n`;
nodeStack[nodeStack.length - 1] = level;
}
// 关闭所有打开的节点
while (nodeStack.length > 1) {
output += '</node>\n';
nodeStack.pop();
}
// 关闭根节点和map
output += '</node>\n';
output += '</map>';
document.getElementById('freeplaneOutput').value = output;
}
function escapeXML(unsafe) {
return unsafe
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function copyToClipboard() {
const output = document.getElementById('freeplaneOutput');
output.select();
try {
document.execCommand('copy');
alert('已复制到剪贴板!');
} catch (err) {
alert('复制失败,请手动复制!');
}
}
// 示例Markdown输入
document.getElementById('markdownInput').value = `# Main Topic
</script>
</body>
</html>