xmind转换为markdown

文章目录

解锁思维导图新姿势:将XMind转为结构化Markdown

你是否曾遇到过这些场景?

  • 精心设计的XMind思维导图需要分享给只支持Markdown的协作者
  • 想将思维导图发布到支持Markdown的博客平台
  • 需要版本化管理思维导图内容

今天我们将深入探讨如何用Go语言构建一个强大的命令行工具,实现XMind到Markdown的无损转换


一、认识Xmind结构

和docx等格式一样,xmind本质上来说是一个压缩包,将节点信息压缩在文件内。

text 复制代码
├── Thumbnails/      # 缩略图
├── content.json     # 核心内容
├── content.xml     
├── manifest.json
├── metadata.json    # 元数据
├── Revisions/       # 修订历史
└── resources/       # 附件资源

其中最关键的是content.json文件,它用JSON格式存储了完整的思维导图数据结构。我们的转换工具需要精准解析这个文件。


二、核心转换流程详解

1.解压XMind文件(ZIP处理)

go 复制代码
r, err := zip.OpenReader(inputPath)
defer r.Close()

for _, f := range r.File {
    if f.Name == "content.json" {
        // 读取文件内容
    }
}

这里使用标准库archive/zip读取压缩包,精准定位核心JSON文件。异常处理是关键点:

  • 检查是否为有效ZIP文件
  • 确保content.json存在
  • 处理文件读取错误

2.解析JSON数据结构

我们定义了精准映射JSON的Go结构体:

go 复制代码
// XMindContent represents the structure of content.json
type XMindContent []struct {
	ID        string `json:"id"`
	Class     string `json:"class"`
	Title     string `json:"title"`
	RootTopic struct {
		ID             string `json:"id"`
		Class          string `json:"class"`
		Title          string `json:"title"`
		Href           string `json:"href"`
		StructureClass string `json:"structureClass"`
		Children       struct {
			Attached []Topic `json:"attached"`
		} `json:"children"`
	} `json:"rootTopic"`
}

type Topic struct {
	Title    string `json:"title"`
	ID       string `json:"id"`
	Href     string `json:"href"`
	Position struct {
		X float64 `json:"x"`
		Y float64 `json:"y"`
	} `json:"position"`
	Children struct {
		Attached []Topic `json:"attached"`
	} `json:"children"`
	Branch  string `json:"branch"`
	Markers []struct {
		MarkerID string `json:"markerId"`
	} `json:"markers"`
	Summaries []struct {
		Range   string `json:"range"`
		TopicID string `json:"topicId"`
	} `json:"summaries"`
	Image struct {
		Src   string `json:"src"`
		Align string `json:"align"`
	} `json:"image"`
	AttributedTitle []struct {
		Text string `json:"text"`
	} `json:"attributedTitle"`
}

核心

  • 嵌套结构匹配XMind的树形数据
  • Attached字段处理多分支结构
  • 支持标记(markers)和超链接(href)解析

3:递归转换树形结构

go 复制代码
func printTopic(topic Topic, level int, output *os.File) {
    // 动态计算缩进
    fmt.Fprintf(output, "%s- ", strings.Repeat("  ", level))
    
    // 处理超链接
    if topic.Href != "" {
        fmt.Fprintf(output, "[%s](%s)", topic.Title, topic.Href)
    } else {
        fmt.Fprint(output, topic.Title)
    }
    
    // 添加标记图标
    if len(topic.Markers) > 0 {
        fmt.Fprint(output, " [")
        for i, m := range topic.Markers {
            if i > 0 { fmt.Print(", ") }
            fmt.Fprint(output, m.MarkerID)
        }
        fmt.Print("]")
    }
    fmt.Println()
    
    // 递归处理子节点
    for _, child := range topic.Children.Attached {
        printTopic(child, level+1, output)
    }
}

递归策略

  1. 每个节点根据层级生成对应缩进
  2. 动态处理超链接和标记
  3. 深度优先遍历确保结构正确性

4:Markdown层级生成逻辑

采用清晰的标题层级映射:

go 复制代码
# 思维导图名称        // H1
## 中心主题          // H2
### 主要分支         // H3
- 子主题1           // 无序列表
  - 子子主题         // 缩进列表

这种结构完美保留了:

  • 原始信息的层次关系
  • 超链接资源
  • 优先级标记(旗帜/星标等)

三、完整代码

go 复制代码
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"

	"github.com/spf13/cobra"
)

// XMindContent represents the structure of content.json
type XMindContent []struct {
	ID        string `json:"id"`
	Class     string `json:"class"`
	Title     string `json:"title"`
	RootTopic struct {
		ID             string `json:"id"`
		Class          string `json:"class"`
		Title          string `json:"title"`
		Href           string `json:"href"`
		StructureClass string `json:"structureClass"`
		Children       struct {
			Attached []Topic `json:"attached"`
		} `json:"children"`
	} `json:"rootTopic"`
}

type Topic struct {
	Title    string `json:"title"`
	ID       string `json:"id"`
	Href     string `json:"href"`
	Position struct {
		X float64 `json:"x"`
		Y float64 `json:"y"`
	} `json:"position"`
	Children struct {
		Attached []Topic `json:"attached"`
	} `json:"children"`
	Branch  string `json:"branch"`
	Markers []struct {
		MarkerID string `json:"markerId"`
	} `json:"markers"`
	Summaries []struct {
		Range   string `json:"range"`
		TopicID string `json:"topicId"`
	} `json:"summaries"`
}

func generateMarkdown(sheets XMindContent, outputPath string) error {
	// Create output file
	outputFile, err := os.Create(outputPath)
	if err != nil {
		return fmt.Errorf("failed to create output file: %v", err)
	}
	defer outputFile.Close()

	// Generate Markdown for each sheet
	for _, sheet := range sheets {
		// Sheet title as H1
		fmt.Fprintf(outputFile, "# %s\n\n", sheet.Title)

		// Root topic title as H2
		fmt.Fprintf(outputFile, "## %s\n", sheet.RootTopic.Title)
		if sheet.RootTopic.Href != "" {
			fmt.Fprintf(outputFile, "[%s](%s)\n", sheet.RootTopic.Title, sheet.RootTopic.Href)
		}
		fmt.Fprintln(outputFile)

		// First level topics as H3
		for _, topic := range sheet.RootTopic.Children.Attached {
			fmt.Fprintf(outputFile, "### %s\n", topic.Title)
			if topic.Href != "" {
				fmt.Fprintf(outputFile, "[%s](%s)\n", topic.Title, topic.Href)
			}

			// Print markers if present
			if len(topic.Markers) > 0 {
				fmt.Fprint(outputFile, "Markers: ")
				for i, marker := range topic.Markers {
					if i > 0 {
						fmt.Fprint(outputFile, ", ")
					}
					fmt.Fprint(outputFile, marker.MarkerID)
				}
				fmt.Fprintln(outputFile)
			}

			// Deeper levels as lists
			for _, child := range topic.Children.Attached {
				printTopic(child, 0, outputFile)
			}
			fmt.Fprintln(outputFile) // Add extra space between topics
		}
	}

	return nil
}

func printTopic(topic Topic, level int, output *os.File) {
	// Print topic title with indentation
	fmt.Fprintf(output, "%s- ", getIndent(level))

	// Handle title with or without href
	if topic.Href != "" {
		fmt.Fprintf(output, "[%s](%s)", topic.Title, topic.Href)
	} else {
		fmt.Fprint(output, topic.Title)
	}

	// Show markers if present
	if len(topic.Markers) > 0 {
		fmt.Fprint(output, " [")
		for i, marker := range topic.Markers {
			if i > 0 {
				fmt.Fprint(output, ", ")
			}
			fmt.Fprint(output, marker.MarkerID)
		}
		fmt.Fprint(output, "]")
	}
	fmt.Fprintln(output)

	// Recursively print subtopics
	for _, child := range topic.Children.Attached {
		printTopic(child, level+1, output)
	}
}

func getIndent(level int) string {
	indent := ""
	for i := 0; i < level; i++ {
		indent += "  "
	}
	return indent
}

func Convert(inputPath, outputPath string) error {
	// 1. Unzip XMind file
	r, err := zip.OpenReader(inputPath)
	if err != nil {
		return fmt.Errorf("failed to open XMind file: %v", err)
	}
	defer r.Close()

	// 2. Read content.json
	var content []byte
	for _, f := range r.File {
		if f.Name == "content.json" {
			rc, err := f.Open()
			if err != nil {
				return fmt.Errorf("failed to open content.json: %v", err)
			}
			defer rc.Close()
			content, err = ioutil.ReadAll(rc)
			if err != nil {
				return fmt.Errorf("failed to read content.json: %v", err)
			}
			break
		}
	}
	if content == nil {
		return fmt.Errorf("content.json not found in XMind file")
	}

	// 3. Parse content.json
	var xmindContent XMindContent
	err = json.Unmarshal(content, &xmindContent)
	if err != nil {
		return fmt.Errorf("failed to parse JSON: %v", err)
	}

	// 4. Generate Markdown
	return generateMarkdown(xmindContent, outputPath)
}

// xmind2mdCmd represents the xmind2md command
var xmind2mdCmd = &cobra.Command{
	Use:   "xmind2md",
	Short: "Convert XMind to Markdown",
	Long:  `Transform XMind mind maps to Markdown format`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) == 0 {
			fmt.Println("Please provide an input XMind file")
			return
		}
		inputFile := args[0]
		outputFile, _ := cmd.Flags().GetString("output")
		if outputFile == "" {
			// 去除.xmind后缀
			if len(inputFile) > 6 && inputFile[len(inputFile)-6:] == ".xmind" {
				outputFile = inputFile[:len(inputFile)-6] + ".md"
			} else {
				outputFile = inputFile + ".md"
			}

		}
		fmt.Printf("Converting %s to %s\n", inputFile, outputFile)
		err := Convert(inputFile, outputFile)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
		} else {
			fmt.Printf("Successfully converted to %s\n", outputFile)
		}
	},
}

func init() {
	xmind2mdCmd.Flags().StringP("output", "o", "", "output file")
	rootCmd.AddCommand(xmind2mdCmd)
}
相关推荐
lekami_兰16 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘20 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤20 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo8 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go