Task 401: .MM File Format

Task 401: .MM File Format

.MM File Format Specifications

The .MM file format (typically lowercase .mm) refers to the FreeMind mind map format, which is an XML-based text format used for storing hierarchical mind maps. It is not a binary format but a structured XML document without an XML declaration header. The format defines a tree structure starting with a root <map> element, containing nested <node> elements representing mind map nodes, along with various child elements and attributes for styling, links, and connections. The specification is detailed in the FreeMind documentation, with a schema available in freemind.xsd for versions like 1.0.1. FreeMind ignores XML declarations if present and focuses on the custom elements below.

1. List of All Properties Intrinsic to This File Format

These properties are derived from the XML structure and attributes in the .mm format (based on FreeMind 0.7.1 specification, with notes on later extensions). They define the file's content, hierarchy, and styling. Properties are grouped by element for clarity:

Map (root element):

  • version (string, e.g., "0.7.1" or "1.0.1")

Node:

  • id (string, unique identifier)
  • text (string, node content)
  • link (string, hyperlink URL)
  • folded (boolean, "true" or "false")
  • color (string, hex color like "#FF0000")
  • position (string, "left" or "right" for root children)

Edge:

  • style (string, e.g., "bezier", "linear")
  • color (string, hex color)
  • width (string, e.g., "thin", "1", "2")

Font:

  • name (string, font family like "SansSerif")
  • size (integer, font size)
  • bold (string, "true")
  • italic (string, "true")

Icon:

  • builtin (string, icon name like "idea", "button_ok")

Cloud:

  • color (string, hex color for background cloud)

Arrowlink:

  • color (string, hex color)
  • destination (string, target node ID)
  • startarrow (string, arrow style like "none", "default")
  • endarrow (string, arrow style like "none", "default")
  • startinclination (string, angle like "100;50;")
  • endinclination (string, angle like "100;50;")

Extensions in later versions (e.g., 0.9.0+) include:

  • richcontent (for HTML content in nodes)
  • attribute (key-value pairs on nodes)
  • hook (for extensions like equations or external objects)

The file is hierarchical, with nodes nested to represent parent-child relationships. There is no fixed "magic number" or binary header; files start directly with <map version="...">.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .MM File Dump

This is a self-contained HTML page with embedded JavaScript that can be embedded in a Ghost blog (or any HTML context). It provides a drag-and-drop area. When a .mm file is dropped, it reads the file, parses the XML, traverses the structure, and dumps all properties from the list above to the screen (in a pre-formatted output div).

.MM File Properties Dumper
Drag and drop a .mm file here

Note: Attribute names in FreeMind XML are often uppercase (e.g., 'TEXT' instead of 'text'). This code uses uppercase for compatibility.

4. Python Class for .MM File

This Python class uses xml.etree.ElementTree to open, parse (decode/read), print properties, and write (encode/write) the file. It traverses the XML tree recursively to print all properties.

import xml.etree.ElementTree as ET

class MMFile:
    def __init__(self, filename):
        self.tree = ET.parse(filename)
        self.root = self.tree.getroot()

    def print_properties(self):
        print("Dumping .MM File Properties:\n")
        if self.root.tag == 'map':
            print(f"Map version: {self.root.get('version', 'N/A')}\n")

        def traverse_node(node, depth=0):
            indent = '  ' * depth
            print(f"{indent}Node:")
            print(f"{indent}  id: {node.get('ID', 'N/A')}")
            print(f"{indent}  text: {node.get('TEXT', 'N/A')}")
            print(f"{indent}  link: {node.get('LINK', 'N/A')}")
            print(f"{indent}  folded: {node.get('FOLDED', 'N/A')}")
            print(f"{indent}  color: {node.get('COLOR', 'N/A')}")
            print(f"{indent}  position: {node.get('POSITION', 'N/A')}")

            edge = node.find('edge')
            if edge is not None:
                print(f"{indent}  Edge style: {edge.get('STYLE', 'N/A')}")
                print(f"{indent}  Edge color: {edge.get('COLOR', 'N/A')}")
                print(f"{indent}  Edge width: {edge.get('WIDTH', 'N/A')}")

            font = node.find('font')
            if font is not None:
                print(f"{indent}  Font name: {font.get('NAME', 'N/A')}")
                print(f"{indent}  Font size: {font.get('SIZE', 'N/A')}")
                print(f"{indent}  Font bold: {font.get('BOLD', 'N/A')}")
                print(f"{indent}  Font italic: {font.get('ITALIC', 'N/A')}")

            for icon in node.findall('icon'):
                print(f"{indent}  Icon builtin: {icon.get('BUILTIN', 'N/A')}")

            cloud = node.find('cloud')
            if cloud is not None:
                print(f"{indent}  Cloud color: {cloud.get('COLOR', 'N/A')}")

            for al in node.findall('arrowlink'):
                print(f"{indent}  Arrowlink color: {al.get('COLOR', 'N/A')}")
                print(f"{indent}  Arrowlink destination: {al.get('DESTINATION', 'N/A')}")
                print(f"{indent}  Arrowlink startarrow: {al.get('STARTARROW', 'N/A')}")
                print(f"{indent}  Arrowlink endarrow: {al.get('ENDARROW', 'N/A')}")
                print(f"{indent}  Arrowlink startinclination: {al.get('STARTINCLINATION', 'N/A')}")
                print(f"{indent}  Arrowlink endinclination: {al.get('ENDINCLINATION', 'N/A')}")

            for child in node.findall('node'):
                traverse_node(child, depth + 1)

        root_node = self.root.find('node')
        if root_node is not None:
            traverse_node(root_node)

    def write(self, filename):
        self.tree.write(filename, encoding='utf-8', xml_declaration=False)  # FreeMind style, no declaration

# Example usage:
# mm = MMFile('example.mm')
# mm.print_properties()
# mm.write('output.mm')

5. Java Class for .MM File

This Java class uses javax.xml.parsers.DocumentBuilder to parse/read, print properties, and write the file using Transformer.

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;

public class MMFile {
    private Document document;

    public MMFile(String filename) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        this.document = builder.parse(new File(filename));
    }

    public void printProperties() {
        System.out.println("Dumping .MM File Properties:\n");
        Element root = document.getDocumentElement();
        if (root.getTagName().equals("map")) {
            System.out.println("Map version: " + root.getAttribute("version"));
            System.out.println();
        }

        traverseNode(root.getElementsByTagName("node").item(0), 0);
    }

    private void traverseNode(org.w3c.dom.Node node, int depth) {
        if (node == null) return;
        Element elem = (Element) node;
        String indent = "  ".repeat(depth);
        System.out.println(indent + "Node:");
        System.out.println(indent + "  id: " + elem.getAttribute("ID"));
        System.out.println(indent + "  text: " + elem.getAttribute("TEXT"));
        System.out.println(indent + "  link: " + elem.getAttribute("LINK"));
        System.out.println(indent + "  folded: " + elem.getAttribute("FOLDED"));
        System.out.println(indent + "  color: " + elem.getAttribute("COLOR"));
        System.out.println(indent + "  position: " + elem.getAttribute("POSITION"));

        Element edge = (Element) elem.getElementsByTagName("edge").item(0);
        if (edge != null) {
            System.out.println(indent + "  Edge style: " + edge.getAttribute("STYLE"));
            System.out.println(indent + "  Edge color: " + edge.getAttribute("COLOR"));
            System.out.println(indent + "  Edge width: " + edge.getAttribute("WIDTH"));
        }

        Element font = (Element) elem.getElementsByTagName("font").item(0);
        if (font != null) {
            System.out.println(indent + "  Font name: " + font.getAttribute("NAME"));
            System.out.println(indent + "  Font size: " + font.getAttribute("SIZE"));
            System.out.println(indent + "  Font bold: " + font.getAttribute("BOLD"));
            System.out.println(indent + "  Font italic: " + font.getAttribute("ITALIC"));
        }

        NodeList icons = elem.getElementsByTagName("icon");
        for (int i = 0; i < icons.getLength(); i++) {
            Element icon = (Element) icons.item(i);
            System.out.println(indent + "  Icon builtin: " + icon.getAttribute("BUILTIN"));
        }

        Element cloud = (Element) elem.getElementsByTagName("cloud").item(0);
        if (cloud != null) {
            System.out.println(indent + "  Cloud color: " + cloud.getAttribute("COLOR"));
        }

        NodeList arrowlinks = elem.getElementsByTagName("arrowlink");
        for (int i = 0; i < arrowlinks.getLength(); i++) {
            Element al = (Element) arrowlinks.item(i);
            System.out.println(indent + "  Arrowlink color: " + al.getAttribute("COLOR"));
            System.out.println(indent + "  Arrowlink destination: " + al.getAttribute("DESTINATION"));
            System.out.println(indent + "  Arrowlink startarrow: " + al.getAttribute("STARTARROW"));
            System.out.println(indent + "  Arrowlink endarrow: " + al.getAttribute("ENDARROW"));
            System.out.println(indent + "  Arrowlink startinclination: " + al.getAttribute("STARTINCLINATION"));
            System.out.println(indent + "  Arrowlink endinclination: " + al.getAttribute("ENDINCLINATION"));
        }

        NodeList children = elem.getElementsByTagName("node");
        for (int i = 0; i < children.getLength(); i++) {
            traverseNode(children.item(i), depth + 1);
        }
    }

    public void write(String filename) throws Exception {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty("omit-xml-declaration", "yes"); // FreeMind style
        DOMSource source = new DOMSource(document);
        StreamResult result = new StreamResult(new File(filename));
        transformer.transform(source, result);
    }

    // Example usage:
    // public static void main(String[] args) throws Exception {
    //     MMFile mm = new MMFile("example.mm");
    //     mm.printProperties();
    //     mm.write("output.mm");
    // }
}

6. JavaScript Class for .MM File

This JavaScript class (for Node.js) uses fs to read/write and xmldom (assume installed or polyfill) to parse. For browser, adapt with FileReader.

const fs = require('fs');
const { DOMParser } = require('xmldom'); // npm install xmldom

class MMFile {
    constructor(filename) {
        this.xmlText = fs.readFileSync(filename, 'utf-8');
        const parser = new DOMParser();
        this.document = parser.parseFromString(this.xmlText, 'text/xml');
    }

    printProperties() {
        console.log('Dumping .MM File Properties:\n');
        const root = this.document.documentElement;
        if (root.tagName === 'map') {
            console.log(`Map version: ${root.getAttribute('version') || 'N/A'}\n`);
        }

        const traverseNode = (node, depth = 0) => {
            if (!node) return;
            const indent = '  '.repeat(depth);
            console.log(`${indent}Node:`);
            console.log(`${indent}  id: ${node.getAttribute('ID') || 'N/A'}`);
            console.log(`${indent}  text: ${node.getAttribute('TEXT') || 'N/A'}`);
            console.log(`${indent}  link: ${node.getAttribute('LINK') || 'N/A'}`);
            console.log(`${indent}  folded: ${node.getAttribute('FOLDED') || 'N/A'}`);
            console.log(`${indent}  color: ${node.getAttribute('COLOR') || 'N/A'}`);
            console.log(`${indent}  position: ${node.getAttribute('POSITION') || 'N/A'}`);

            const edge = node.getElementsByTagName('edge')[0];
            if (edge) {
                console.log(`${indent}  Edge style: ${edge.getAttribute('STYLE') || 'N/A'}`);
                console.log(`${indent}  Edge color: ${edge.getAttribute('COLOR') || 'N/A'}`);
                console.log(`${indent}  Edge width: ${edge.getAttribute('WIDTH') || 'N/A'}`);
            }

            const font = node.getElementsByTagName('font')[0];
            if (font) {
                console.log(`${indent}  Font name: ${font.getAttribute('NAME') || 'N/A'}`);
                console.log(`${indent}  Font size: ${font.getAttribute('SIZE') || 'N/A'}`);
                console.log(`${indent}  Font bold: ${font.getAttribute('BOLD') || 'N/A'}`);
                console.log(`${indent}  Font italic: ${font.getAttribute('ITALIC') || 'N/A'}`);
            }

            const icons = node.getElementsByTagName('icon');
            for (let i = 0; i < icons.length; i++) {
                console.log(`${indent}  Icon builtin: ${icons[i].getAttribute('BUILTIN') || 'N/A'}`);
            }

            const cloud = node.getElementsByTagName('cloud')[0];
            if (cloud) {
                console.log(`${indent}  Cloud color: ${cloud.getAttribute('COLOR') || 'N/A'}`);
            }

            const arrowlinks = node.getElementsByTagName('arrowlink');
            for (let i = 0; i < arrowlinks.length; i++) {
                const al = arrowlinks[i];
                console.log(`${indent}  Arrowlink color: ${al.getAttribute('COLOR') || 'N/A'}`);
                console.log(`${indent}  Arrowlink destination: ${al.getAttribute('DESTINATION') || 'N/A'}`);
                console.log(`${indent}  Arrowlink startarrow: ${al.getAttribute('STARTARROW') || 'N/A'}`);
                console.log(`${indent}  Arrowlink endarrow: ${al.getAttribute('ENDARROW') || 'N/A'}`);
                console.log(`${indent}  Arrowlink startinclination: ${al.getAttribute('STARTINCLINATION') || 'N/A'}`);
                console.log(`${indent}  Arrowlink endinclination: ${al.getAttribute('ENDINCLINATION') || 'N/A'}`);
            }

            const children = node.getElementsByTagName('node');
            for (let i = 0; i < children.length; i++) {
                traverseNode(children[i], depth + 1);
            }
        };

        const rootNode = this.document.getElementsByTagName('node')[0];
        if (rootNode) {
            traverseNode(rootNode);
        }
    }

    write(filename) {
        fs.writeFileSync(filename, this.document.toString().replace(/<\?xml.*\?>/, '')); // Remove declaration if present
    }
}

// Example usage:
// const mm = new MMFile('example.mm');
// mm.printProperties();
// mm.write('output.mm');

7. C "Class" for .MM File

C doesn't have classes, so this uses a struct with functions. It uses libxml2 for XML parsing (assume libxml2 installed; include <libxml/parser.h>). Compile with gcc -o mmfile mmfile.c pkg-config --cflags --libs libxml-2.0``.

#include <stdio.h>
#include <string.h>
#include <libxml/parser.h>
#include <libxml/tree.h>

typedef struct {
    xmlDocPtr doc;
} MMFile;

MMFile* mmfile_open(const char* filename) {
    MMFile* mm = malloc(sizeof(MMFile));
    mm->doc = xmlReadFile(filename, NULL, 0);
    if (mm->doc == NULL) {
        fprintf(stderr, "Failed to parse %s\n", filename);
        free(mm);
        return NULL;
    }
    return mm;
}

void mmfile_print_properties(MMFile* mm) {
    if (mm == NULL || mm->doc == NULL) return;

    printf("Dumping .MM File Properties:\n\n");
    xmlNode* root = xmlDocGetRootElement(mm->doc);
    if (root && xmlStrcmp(root->name, (const xmlChar*)"map") == 0) {
        xmlChar* version = xmlGetProp(root, (const xmlChar*)"version");
        printf("Map version: %s\n\n", version ? (char*)version : "N/A");
        xmlFree(version);
    }

    void traverse_node(xmlNode* node, int depth) {
        if (node == NULL) return;
        char indent[256] = {0};
        memset(indent, ' ', depth * 2);
        indent[depth * 2] = '\0';

        printf("%sNode:\n", indent);
        xmlChar* id = xmlGetProp(node, (const xmlChar*)"ID");
        printf("%s  id: %s\n", indent, id ? (char*)id : "N/A");
        xmlFree(id);
        xmlChar* text = xmlGetProp(node, (const xmlChar*)"TEXT");
        printf("%s  text: %s\n", indent, text ? (char*)text : "N/A");
        xmlFree(text);
        xmlChar* link = xmlGetProp(node, (const xmlChar*)"LINK");
        printf("%s  link: %s\n", indent, link ? (char*)link : "N/A");
        xmlFree(link);
        xmlChar* folded = xmlGetProp(node, (const xmlChar*)"FOLDED");
        printf("%s  folded: %s\n", indent, folded ? (char*)folded : "N/A");
        xmlFree(folded);
        xmlChar* color = xmlGetProp(node, (const xmlChar*)"COLOR");
        printf("%s  color: %s\n", indent, color ? (char*)color : "N/A");
        xmlFree(color);
        xmlChar* position = xmlGetProp(node, (const xmlChar*)"POSITION");
        printf("%s  position: %s\n", indent, position ? (char*)position : "N/A");
        xmlFree(position);

        xmlNode* child = node->children;
        while (child) {
            if (xmlStrcmp(child->name, (const xmlChar*)"edge") == 0) {
                xmlChar* style = xmlGetProp(child, (const xmlChar*)"STYLE");
                printf("%s  Edge style: %s\n", indent, style ? (char*)style : "N/A");
                xmlFree(style);
                xmlChar* ecolor = xmlGetProp(child, (const xmlChar*)"COLOR");
                printf("%s  Edge color: %s\n", indent, ecolor ? (char*)ecolor : "N/A");
                xmlFree(ecolor);
                xmlChar* width = xmlGetProp(child, (const xmlChar*)"WIDTH");
                printf("%s  Edge width: %s\n", indent, width ? (char*)width : "N/A");
                xmlFree(width);
            } else if (xmlStrcmp(child->name, (const xmlChar*)"font") == 0) {
                xmlChar* name = xmlGetProp(child, (const xmlChar*)"NAME");
                printf("%s  Font name: %s\n", indent, name ? (char*)name : "N/A");
                xmlFree(name);
                xmlChar* size = xmlGetProp(child, (const xmlChar*)"SIZE");
                printf("%s  Font size: %s\n", indent, size ? (char*)size : "N/A");
                xmlFree(size);
                xmlChar* bold = xmlGetProp(child, (const xmlChar*)"BOLD");
                printf("%s  Font bold: %s\n", indent, bold ? (char*)bold : "N/A");
                xmlFree(bold);
                xmlChar* italic = xmlGetProp(child, (const xmlChar*)"ITALIC");
                printf("%s  Font italic: %s\n", indent, italic ? (char*)italic : "N/A");
                xmlFree(italic);
            } else if (xmlStrcmp(child->name, (const xmlChar*)"icon") == 0) {
                xmlChar* builtin = xmlGetProp(child, (const xmlChar*)"BUILTIN");
                printf("%s  Icon builtin: %s\n", indent, builtin ? (char*)builtin : "N/A");
                xmlFree(builtin);
            } else if (xmlStrcmp(child->name, (const xmlChar*)"cloud") == 0) {
                xmlChar* ccolor = xmlGetProp(child, (const xmlChar*)"COLOR");
                printf("%s  Cloud color: %s\n", indent, ccolor ? (char*)ccolor : "N/A");
                xmlFree(ccolor);
            } else if (xmlStrcmp(child->name, (const xmlChar*)"arrowlink") == 0) {
                xmlChar* acolor = xmlGetProp(child, (const xmlChar*)"COLOR");
                printf("%s  Arrowlink color: %s\n", indent, acolor ? (char*)acolor : "N/A");
                xmlFree(acolor);
                xmlChar* dest = xmlGetProp(child, (const xmlChar*)"DESTINATION");
                printf("%s  Arrowlink destination: %s\n", indent, dest ? (char*)dest : "N/A");
                xmlFree(dest);
                xmlChar* startarrow = xmlGetProp(child, (const xmlChar*)"STARTARROW");
                printf("%s  Arrowlink startarrow: %s\n", indent, startarrow ? (char*)startarrow : "N/A");
                xmlFree(startarrow);
                xmlChar* endarrow = xmlGetProp(child, (const xmlChar*)"ENDARROW");
                printf("%s  Arrowlink endarrow: %s\n", indent, endarrow ? (char*)endarrow : "N/A");
                xmlFree(endarrow);
                xmlChar* startincl = xmlGetProp(child, (const xmlChar*)"STARTINCLINATION");
                printf("%s  Arrowlink startinclination: %s\n", indent, startincl ? (char*)startincl : "N/A");
                xmlFree(startincl);
                xmlChar* endincl = xmlGetProp(child, (const xmlChar*)"ENDINCLINATION");
                printf("%s  Arrowlink endinclination: %s\n", indent, endincl ? (char*)endincl : "N/A");
                xmlFree(endincl);
            } else if (xmlStrcmp(child->name, (const xmlChar*)"node") == 0) {
                traverse_node(child, depth + 1);
            }
            child = child->next;
        }
    }

    xmlNode* root_node = root->children;
    while (root_node) {
        if (xmlStrcmp(root_node->name, (const xmlChar*)"node") == 0) {
            traverse_node(root_node, 0);
            break;
        }
        root_node = root_node->next;
    }
}

void mmfile_write(MMFile* mm, const char* filename) {
    if (mm == NULL || mm->doc == NULL) return;
    xmlSaveFormatFileEnc(filename, mm->doc, "UTF-8", 0); // No declaration
}

void mmfile_close(MMFile* mm) {
    if (mm) {
        xmlFreeDoc(mm->doc);
        free(mm);
    }
}

// Example usage:
// int main() {
//     MMFile* mm = mmfile_open("example.mm");
//     if (mm) {
//         mmfile_print_properties(mm);
//         mmfile_write(mm, "output.mm");
//         mmfile_close(mm);
//     }
//     return 0;
// }