开发者

使用Go语言实现xmind文件转换为markdown

目录
  • 解锁思维导图新姿势
  • 一、认识Xmind结构
  • 二、核心转换流程详解
    • 1.解压XMind文件(ZIP处理)
    • 2.解析jsON数据结构
    • 3:递归转换树形结构
    • 4:Markdown层级生成逻辑
  • 三、完整代码

    解锁思维导图新姿势

    将XMind转为结构化Markdown

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

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

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

    一、认识Xmind结构

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

    ├── Thumbnails/      # 缩略图

    ├── content.json     # 核心内容

    ├── content.XML     

    ├── manifest.json

    ├── metadata.json    # 元数据

    ├── Revisions/       # 修订历史

    └── resources/       # 附件资源

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

    二、核心转换流程详解

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

    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结构体:

    // 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 {http://www.devze.com
                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:递归转换树形结构

    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)
        }
    }
    

    递归策略:

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

    4:Markdown层级生成逻辑

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

    # 思维导图名称        // H1

    ## 中心主题          // H2

    ### 主要分支         // H3

    - 子主题1           // 无序列表

      - 子子主题         // 缩进列表

    这种结构完美保留了:

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

    三、完整代码

    /*
    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:"posiSjKKution"`
    	Children struct {
    		Attached []Topic `jsohttp://www.devze.comn:"attached"`
    	} `json:"children"`
    	Branch  string `json:"branch"`
    	Markers []struct {
    		MarkerID string `json:"markerId"`
    	} `json:"markers"`
    	Summaries []struct {
    		Range   string `jsonSjKKu:"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) errjavascriptor {
    	// 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)
    }
    

    到此这篇关于使用Go语言实现xmind文件转换为markdown的文章就介绍到这了,更多相关Go xmind转markdown内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜