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)
}
相关推荐
DemonAvenger14 小时前
Go并发编程:内存同步与竞态处理
性能优化·架构·go
程序员爱钓鱼17 小时前
Go 并发编程基础:通道(Channel)的使用
后端·google·go
fashia17 小时前
Java转Go日记(六十):gin其他常用知识
开发语言·后端·golang·go·gin
余厌厌厌1 天前
go语言学习 第4章:流程控制
go
DemonAvenger2 天前
Go 大对象与小对象分配策略优化
性能优化·架构·go
seth2 天前
一个基于 Go 语言 开发的命令行版 V2EX 客户端,支持在终端内快速浏览主题、查看评论、切换节点及基础配置管理
go·iterm·v2ex
fashia2 天前
Java转Go日记(五十七):gin 中间件
开发语言·后端·golang·go·gin
余厌厌厌2 天前
go语言学习 第5章:函数
开发语言·学习·golang·go
程序员爱钓鱼2 天前
Go语言 并发编程基础:Goroutine 的创建与调度
后端·go·排序算法