mindmap.serialization module

mindmap.serialization.build_mapping(topic, guid_mapping)

Build a mapping of GUIDs to numeric IDs for an entire mindmap.

Parameters:
  • topic (MindmapTopic) – Root topic of the mindmap

  • guid_mapping (dict) – Dictionary to store GUID to ID mappings

Returns:

The mapping is updated in-place

Return type:

None

mindmap.serialization.deserialize_mermaid_full(mermaid_text: str, guid_mapping: dict) MindmapTopic

Convert Mermaid text with metadata to a Mindmap structure.

Parameters:
  • mermaid_text (str) – Mermaid formatted string with JSON metadata to parse

  • guid_mapping (dict) – Dictionary mapping numeric IDs to GUIDs

Returns:

Root topic of the fully deserialized mindmap with all attributes

Return type:

MindmapTopic

mindmap.serialization.deserialize_mermaid_with_id(mermaid_text: str, guid_mapping: dict) MindmapTopic

Convert Mermaid text with id to a Mindmap structure.

Parameters:
  • mermaid_text (str) – Mermaid formatted string to parse

  • guid_mapping (dict) – Dictionary mapping numeric IDs to GUIDs

Returns:

Root topic of the deserialized mindmap

Return type:

MindmapTopic

mindmap.serialization.serialize_mindmap(root_topic, guid_mapping, id_only=False)

Serialize a mindmap to valid Mermaid format including id and all other attributes (optional).

Parameters:
  • root_topic (MindmapTopic) – Root topic of the mindmap

  • guid_mapping (dict) – Dictionary mapping GUIDs to numeric IDs

  • id_only (bool, optional) – If True, only include IDs without detailed attributes. Defaults to False.

Returns:

Mermaid formatted string representing the mindmap

Return type:

str

mindmap.serialization.serialize_mindmap_markdown(root_topic, include_notes=True)

Serialize a mindmap to markdown including notes (optional).

Parameters:
  • root_topic (MindmapTopic) – Root topic of the mindmap

  • include_notes (bool, optional) – If True, notes are included

Returns:

Markdown formatted string representing the mindmap

Return type:

str

mindmap.serialization.serialize_object(obj, guid_mapping, name='', visited=None, ignore_rtf=True)

Serialize an object recursively, handling special fields and mapping GUIDs to IDs.

Parameters:
  • obj – The object to serialize

  • guid_mapping – Dictionary mapping GUIDs to numeric IDs

  • name (str, optional) – The name of the attribute being serialized. Defaults to ‘’.

  • visited (set, optional) – Set of object IDs that have been visited to prevent cycles. Defaults to None.

  • ignore_rtf (bool, optional) – Whether to ignore RTF content. Defaults to True.

Returns:

Serialized representation of the input object to be exported at JSON

Return type:

object

mindmap.serialization.serialize_object_simple(obj, name='', visited=None, ignore_rtf=True)

Serialize an object recursively without GUID mapping.

Parameters:
  • obj – The object to serialize

  • name (str, optional) – The name of the attribute being serialized. Defaults to ‘’.

  • visited (set, optional) – Set of object IDs that have been visited to prevent cycles. Defaults to None.

  • ignore_rtf (bool, optional) – Whether to ignore RTF content. Defaults to True.

Returns:

Simplified serialized representation of the input object to be exported at JSON

Return type:

object

Source code for serialization.py
  1from mindmap.mindmap import *
  2from mindmap import helpers
  3
  4import json
  5import re
  6import uuid
  7import html2text
  8
  9IGNORE_RTF = True
 10
 11def serialize_object(obj, guid_mapping, name='', visited=None, ignore_rtf=True):
 12    """Serialize an object recursively, handling special fields and mapping GUIDs to IDs.
 13    
 14    Args:
 15        obj: The object to serialize
 16        guid_mapping: Dictionary mapping GUIDs to numeric IDs
 17        name (str, optional): The name of the attribute being serialized. Defaults to ''.
 18        visited (set, optional): Set of object IDs that have been visited to prevent cycles. Defaults to None.
 19        ignore_rtf (bool, optional): Whether to ignore RTF content. Defaults to True.
 20        
 21    Returns:
 22        object: Serialized representation of the input object to be exported at JSON
 23    """
 24    if visited is None:
 25        visited = set()
 26    if name == 'topic':
 27        if id(obj) in visited:
 28            return None
 29        visited.add(id(obj))
 30    visited.add(id(obj))
 31    if isinstance(obj, (str, int, float, bool, type(None))):
 32        return obj
 33    if isinstance(obj, list):
 34        attr_name = 'topic' if name == 'subtopics' else ''
 35        return [serialize_object(item, guid_mapping, attr_name, visited) for item in obj]
 36    if isinstance(obj, dict):
 37        return {str(k): serialize_object(v, guid_mapping, visited=visited) for k, v in obj.items()}
 38    if hasattr(obj, '__dict__'):
 39        serialized = {}
 40        for attr_name, attr_value in vars(obj).items():
 41            if attr_name in ["parent", "level", "selected"]: 
 42                continue
 43            if attr_name in ["rtf"]:
 44                if ignore_rtf == True:
 45                    continue
 46            if attr_value is None or attr_value == "" or attr_value == []:
 47                continue
 48            new_attr_name = attr_name
 49            if new_attr_name in ["guid", "guid_1", "guid_2"]:
 50                if new_attr_name == "guid":
 51                    new_attr_name = "id"
 52                elif new_attr_name == "guid_1":
 53                    new_attr_name = "id_1"
 54                elif new_attr_name == "guid_2":
 55                    new_attr_name = "id_2"
 56                serialized[new_attr_name] = guid_mapping[attr_value]
 57            else:
 58                dict_val = serialize_object(attr_value, guid_mapping, attr_name, visited)
 59                if dict_val != {}:
 60                    serialized[new_attr_name] = dict_val
 61        return serialized
 62    return str(obj)
 63
 64def serialize_object_simple(obj, name='', visited=None, ignore_rtf=True):
 65    """Serialize an object recursively without GUID mapping.
 66    
 67    Args:
 68        obj: The object to serialize
 69        name (str, optional): The name of the attribute being serialized. Defaults to ''.
 70        visited (set, optional): Set of object IDs that have been visited to prevent cycles. Defaults to None.
 71        ignore_rtf (bool, optional): Whether to ignore RTF content. Defaults to True.
 72        
 73    Returns:
 74        object: Simplified serialized representation of the input object to be exported at JSON
 75    """
 76    if visited is None:
 77        visited = set()
 78    if name == 'topic':
 79        if id(obj) in visited:
 80            return None
 81        visited.add(id(obj))
 82    if isinstance(obj, (str, int, float, bool, type(None))):
 83        return obj
 84    if isinstance(obj, list):
 85        attr_name = 'topic' if name == 'subtopics' else ''
 86        return [serialize_object_simple(item, attr_name, visited) for item in obj]
 87    if isinstance(obj, dict):
 88        return {str(k): serialize_object_simple(v, k, visited) for k, v in obj.items()}
 89    if hasattr(obj, '__dict__'):
 90        serialized = {}
 91        for attr_name, attr_value in vars(obj).items():
 92            if attr_name in ["parent", "level", "selected"]:
 93                continue
 94            if attr_name in ["rtf"]:
 95                if ignore_rtf == True:
 96                    continue
 97            if attr_value is None or attr_value == "" or attr_value == []:
 98                continue
 99            dict_val = serialize_object_simple(attr_value, attr_name, visited)
100            if dict_val != {}:
101                serialized[attr_name] = dict_val
102        return serialized
103    return str(obj)
104
105def serialize_mindmap(root_topic, guid_mapping, id_only=False):
106    """Serialize a mindmap to valid Mermaid format including id and all other attributes (optional).
107    
108    Args:
109        root_topic (MindmapTopic): Root topic of the mindmap
110        guid_mapping (dict): Dictionary mapping GUIDs to numeric IDs
111        id_only (bool, optional): If True, only include IDs without detailed attributes. Defaults to False.
112        
113    Returns:
114        str: Mermaid formatted string representing the mindmap
115    """
116    lines = ["mindmap"]
117
118    def serialize_topic_attributes(topic, guid_mapping, ignore_rtf=True):
119        """Extract and serialize the attributes of a MindmapTopic.
120        
121        Args:
122            topic (MindmapTopic): The topic to serialize
123            guid_mapping (dict): Dictionary mapping GUIDs to numeric IDs
124            
125        Returns:
126            dict: Dictionary containing serialized topic attributes
127        """
128        d = {}
129        d["id"] = guid_mapping.get(topic.guid, topic.guid)
130        #d["text"] = topic.text
131        if topic.rtf != topic.text and not ignore_rtf == True:
132            d["rtf"] = topic.rtf
133        if topic.selected == True:
134            d["selected"] = topic.selected
135        if topic.links:
136            d["links"] = []
137            for link in topic.links:
138                l = {}
139                if link.text:
140                    l["text"] = link.text
141                if link.url:
142                    l["url"] = link.url
143                if link.guid:
144                    l["id"] = guid_mapping.get(link.guid, link.guid)
145                d["links"].append(l)
146        if topic.image:
147            d["image"] = {"text": topic.image.text}
148        if topic.icons:
149            d["icons"] = []
150            for icon in topic.icons:
151                i = {}
152                if icon.text:
153                    i["text"] = icon.text
154                if icon.is_stock_icon is not None:
155                    i["is_stock_icon"] = icon.is_stock_icon
156                if icon.index is not None:
157                    i["index"] = icon.index
158                if icon.signature:
159                    i["signature"] = icon.signature
160                if icon.path:
161                    i["path"] = icon.path
162                if icon.group:
163                    i["group"] = icon.group
164                d["icons"].append(i)
165        if topic.notes and (topic.notes.text or topic.notes.xhtml or topic.notes.rtf):
166            notes = {}
167            if topic.notes.text:
168                notes["text"] = topic.notes.text
169            if topic.notes.xhtml:
170                notes["xhtml"] = topic.notes.xhtml
171            if topic.notes.rtf:
172                notes["rtf"] = topic.notes.rtf
173            if notes != {}:
174                d["notes"] = notes
175        if topic.tags:
176            d["tags"] = [tag.text for tag in topic.tags]
177        if topic.references:
178            d["references"] = []
179            for ref in topic.references:
180                r = {}
181                if ref.guid_1:
182                    r["id_1"] = guid_mapping.get(ref.guid_1, ref.guid_1)
183                if ref.guid_2:
184                    r["id_2"] = guid_mapping.get(ref.guid_2, ref.guid_2)
185                if ref.direction:
186                    r["direction"] = ref.direction
187                if ref.label:
188                    r["label"] = ref.label
189                d["references"].append(r)
190        d = helpers.replace_unicode_in_obj(d)
191        return d
192    
193    def traverse(topic, indent):
194        indent_str = "  " * indent
195        node_text = helpers.escape_mermaid_text(topic.text)
196        if id_only:
197            id = guid_mapping.get(topic.guid, topic.guid)
198            line = f"{indent_str}id{id}[{node_text}]"
199            #line = f"{indent_str}({node_text})"
200            #topic_attrs = {"id": id}
201        else:
202            line = f"{indent_str}[{node_text}]"
203            topic_attrs = serialize_topic_attributes(topic, guid_mapping, ignore_rtf=IGNORE_RTF)
204            json_comment = json.dumps(topic_attrs, ensure_ascii=True)
205            line += f" %% {json_comment}"
206        lines.append(line)
207        for sub in topic.subtopics:
208            traverse(sub, indent + 1)
209
210    traverse(root_topic, 1)
211    return "\n".join(lines)
212
213
214def serialize_mindmap_markdown(root_topic, include_notes=True):
215    """Serialize a mindmap to markdown including notes (optional).
216    
217    Args:
218        root_topic (MindmapTopic): Root topic of the mindmap
219        include_notes (bool, optional): If True, notes are included
220        
221    Returns:
222        str: Markdown formatted string representing the mindmap
223    """
224
225    lines = []
226    
227    def traverse(topic, lines, level, prefix, index):
228        text = topic.text
229        notes_text = ""
230        notes_xhtml = ""
231        notes_rtf = ""
232
233        if level > 0:
234            if prefix == '':
235                prefix = str(index)
236            else:
237                prefix = f"{prefix}.{index}"
238        
239        if topic.notes:
240            if topic.notes.text or topic.notes.xhtml or topic.notes.rtf:
241                if topic.notes.text:
242                    notes_text = topic.notes.text
243                if topic.notes.xhtml:
244                    xhtml = topic.notes.xhtml
245                    root_match = re.search(r'<(?:root|body)[^>]*>(.*?)</(?:root|body)>', xhtml, re.DOTALL | re.IGNORECASE)
246                    if root_match:
247                        xhtml = root_match.group(1)
248                    xhtml = re.sub(r'<\?xml[^>]*\?>', '', xhtml)
249                    xhtml = re.sub(r'<!DOCTYPE[^>]*>', '', xhtml)
250                    try:
251                        h = html2text.HTML2Text()
252                        h.ignore_links = False
253                        h.ignore_images = False
254                        h.body_width = 0  # Don't wrap lines
255                        notes_xhtml = h.handle(xhtml).strip()
256                    except ImportError:
257                        notes_xhtml = re.sub(r'<[^>]*>', '', xhtml).strip()
258                if topic.notes.rtf:
259                    # not implemented due to bad results
260                    pass
261
262        if include_notes and (notes_text or notes_xhtml or notes_rtf):
263            notes_content = notes_text
264            if notes_rtf:
265                notes_content = notes_rtf
266            if notes_xhtml:
267                notes_content = notes_xhtml
268            notes = f"Notes: {notes_content}  "
269        else:
270            notes = ""
271        
272        if topic.subtopics:
273            line = f"{(level + 1) * '#'} {prefix if level > 0 else ''} {text}  "
274            lines.append(line)
275            if notes:
276                lines.append(notes)
277
278            sub_index = 0
279            for sub in topic.subtopics:
280                sub_index += 1
281                traverse(sub, lines, level + 1, prefix, sub_index)
282        else:
283            line = f"- {text}  "
284            lines.append(line)
285            if notes:
286                lines.append(notes)
287
288    traverse(root_topic, lines, 0, '', 0)
289    return "\n".join(lines)
290
291
292def deserialize_mermaid_with_id(mermaid_text: str, guid_mapping: dict) -> MindmapTopic:
293    """Convert Mermaid text with id to a Mindmap structure.
294    
295    Args:
296        mermaid_text (str): Mermaid formatted string to parse
297        guid_mapping (dict): Dictionary mapping numeric IDs to GUIDs
298        
299    Returns:
300        MindmapTopic: Root topic of the deserialized mindmap
301    """
302    id_to_guid = {id_num: guid for guid, id_num in guid_mapping.items()}
303    lines = [line for line in mermaid_text.splitlines() if line.strip()]
304    if lines and lines[0].strip().lower() == "mindmap":
305        lines = lines[1:]
306    node_pattern = re.compile(r"^(id(\d+))\[(.*)\]$")
307    root = None
308    stack = []
309    for line in lines:
310        indent = len(line) - len(line.lstrip(" "))
311        level = indent // 2
312        stripped = line.lstrip(" ")
313        match = node_pattern.match(stripped)
314        if not match:
315            continue        
316        node_id_str = match.group(1)
317        id_number = int(match.group(2))
318        node_text = match.group(3)        
319        if id_number in id_to_guid:
320            guid = id_to_guid[id_number]
321        else:
322            guid = str(uuid.uuid4())
323            id_to_guid[id_number] = guid
324        node = MindmapTopic(guid=guid, text=node_text, level=level)
325        if root is None:
326            root = node
327            stack.append((level, node))
328            continue
329        while stack and stack[-1][0] >= level:
330            stack.pop()
331        if stack:
332            parent = stack[-1][1]
333            node.parent = parent
334            parent.subtopics.append(node)
335        else:
336            root.subtopics.append(node)
337            node.parent = root
338        stack.append((level, node))
339    return root
340
341def deserialize_mermaid_full(mermaid_text: str, guid_mapping: dict) -> MindmapTopic:
342    """Convert Mermaid text with metadata to a Mindmap structure.
343    
344    Args:
345        mermaid_text (str): Mermaid formatted string with JSON metadata to parse
346        guid_mapping (dict): Dictionary mapping numeric IDs to GUIDs
347        
348    Returns:
349        MindmapTopic: Root topic of the fully deserialized mindmap with all attributes
350    """
351    id_to_guid = {v: k for k, v in guid_mapping.items()}
352    lines = [line for line in mermaid_text.splitlines() if line.strip()]
353    if lines and lines[0].strip().lower() == "mindmap":
354        lines = lines[1:]
355    pattern = re.compile(r"^( *)(\[.*?\])\s*%%\s*(\{.*\})\s*$")
356    root = None
357    stack = []
358    
359    def restore_guid(numeric_id):
360        try:
361            num = int(numeric_id)
362        except:
363            return str(uuid.uuid4())
364        if num in id_to_guid:
365            return id_to_guid[num]
366        else:
367            new_guid = str(uuid.uuid4())
368            id_to_guid[num] = new_guid
369            return new_guid
370
371    def process_subobject(field_dict: dict, id_field: str) -> dict:
372        if id_field in field_dict:
373            field_dict[id_field] = restore_guid(field_dict[id_field])
374        return field_dict
375
376    for line in lines:
377        m = pattern.match(line)
378        if not m:
379            continue
380        indent_str, bracket_part, json_part = m.groups()
381        level = len(indent_str) // 2
382        fallback_text = bracket_part.strip()[1:-1]
383        try:
384            attrs = json.loads(json_part)
385        except Exception as e:
386            attrs = {}
387        if "id" in attrs:
388            node_guid = restore_guid(attrs["id"])
389        else:
390            node_guid = str(uuid.uuid4())
391        node_text = attrs.get("text", fallback_text)
392        node_rtf = attrs.get("rtf", "")
393        selected = attrs.get("selected", False)
394        links = []
395        if "links" in attrs and isinstance(attrs["links"], list):
396            for link_dict in attrs["links"]:
397                ld = dict(link_dict)
398                ld = process_subobject(ld, "id")
399                link_text = ld.get("text", "")
400                link_url = ld.get("url", "")
401                link_guid = ld.get("id", "")
402                links.append(MindmapLink(text=link_text, url=link_url, guid=link_guid))
403        image_obj = None
404        if "image" in attrs and isinstance(attrs["image"], dict):
405            image_obj = MindmapImage(text=attrs["image"].get("text", ""))
406        icons = []
407        if "icons" in attrs and isinstance(attrs["icons"], list):
408            for icon_dict in attrs["icons"]:
409                idict = dict(icon_dict)
410                icons.append(MindmapIcon(
411                    text=idict.get("text", ""),
412                    is_stock_icon=idict.get("is_stock_icon", True),
413                    index=idict.get("index", 1),
414                    signature=idict.get("signature", ""),
415                    path=idict.get("path", ""),
416                    group=idict.get("group", "")
417                ))
418        notes_obj = None
419        if "notes" in attrs:
420            if isinstance(attrs["notes"], dict):
421                notes_obj = MindmapNotes(
422                    text=attrs["notes"].get("text", ""),
423                    xhtml=attrs["notes"].get("xhtml", ""),
424                    rtf=attrs["notes"].get("rtf", "")
425                )
426            elif isinstance(attrs["notes"], str):
427                notes_obj = MindmapNotes(text=attrs["notes"])
428        tags = []
429        if "tags" in attrs and isinstance(attrs["tags"], list):
430            for tag_item in attrs["tags"]:
431                if isinstance(tag_item, dict):
432                    tag_text = tag_item.get("text", "")
433                else:
434                    tag_text = str(tag_item)
435                tags.append(MindmapTag(text=tag_text))
436        references = []
437        if "references" in attrs and isinstance(attrs["references"], list):
438            for ref_dict in attrs["references"]:
439                rd = dict(ref_dict)
440                rd = process_subobject(rd, "id_1")
441                rd = process_subobject(rd, "id_2")
442                direction = rd.get("direction", None)
443                label = rd.get("label", "")
444                references.append(MindmapReference(
445                    guid_1=rd.get("id_1", ""),
446                    guid_2=rd.get("id_2", ""),
447                    direction=direction,
448                    label=label
449                ))
450        node = MindmapTopic(guid=node_guid, text=node_text, rtf=node_rtf, level=level, selected=selected)
451        node.links = links
452        node.image = image_obj
453        node.icons = icons
454        node.notes = notes_obj
455        node.tags = tags
456        node.references = references
457        while stack and stack[-1][0] >= level:
458            stack.pop()
459        if stack:
460            parent = stack[-1][1]
461            node.parent = parent
462            parent.subtopics.append(node)
463        else:
464            root = node
465        stack.append((level, node))
466    return root
467
468def build_mapping(topic, guid_mapping):
469    """Build a mapping of GUIDs to numeric IDs for an entire mindmap.
470    
471    Args:
472        topic (MindmapTopic): Root topic of the mindmap
473        guid_mapping (dict): Dictionary to store GUID to ID mappings
474        
475    Returns:
476        None: The mapping is updated in-place
477    """
478    if topic.guid not in guid_mapping:
479        guid_mapping[topic.guid] = len(guid_mapping) + 1
480    for sub in topic.subtopics:
481        build_mapping(sub, guid_mapping)