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:
- mindmap.serialization.deserialize_mermaid_simple(mermaid_text: str) MindmapTopic¶
Convert a simple indentation-based Mermaid mindmap to a Mindmap structure.
- Parameters:
mermaid_text (str) – Mermaid formatted string containing only textual nodes expressed via indentation (no IDs or metadata)
- Returns:
Root topic of the deserialized mindmap
- Return type:
- 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:
- 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_mindmap_simple(root_topic: MindmapTopic) str¶
Serialize a mindmap to a simplified Mermaid format with indentation-only nodes.
- Parameters:
root_topic (MindmapTopic) – Root topic of the mindmap
- Returns:
Mermaid formatted string (text and indentation only)
- 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
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
213def serialize_mindmap_simple(root_topic: MindmapTopic) -> str:
214 """Serialize a mindmap to a simplified Mermaid format with indentation-only nodes.
215
216 Args:
217 root_topic (MindmapTopic): Root topic of the mindmap
218
219 Returns:
220 str: Mermaid formatted string (text and indentation only)
221 """
222 lines = ["mindmap"]
223
224 def traverse(topic, indent):
225 indent_str = " " * indent
226 text = topic.text if topic.text is not None else ""
227 lines.append(f"{indent_str}{text}")
228 for sub in topic.subtopics:
229 traverse(sub, indent + 1)
230
231 traverse(root_topic, 1)
232 return "\n".join(lines)
233
234def serialize_mindmap_markdown(root_topic, include_notes=True):
235 """Serialize a mindmap to markdown including notes (optional).
236
237 Args:
238 root_topic (MindmapTopic): Root topic of the mindmap
239 include_notes (bool, optional): If True, notes are included
240
241 Returns:
242 str: Markdown formatted string representing the mindmap
243 """
244
245 lines = []
246
247 def traverse(topic, lines, level, prefix, index):
248 text = topic.text
249 notes_text = ""
250 notes_xhtml = ""
251 notes_rtf = ""
252
253 if level > 0:
254 if prefix == '':
255 prefix = str(index)
256 else:
257 prefix = f"{prefix}.{index}"
258
259 if topic.notes:
260 if topic.notes.text or topic.notes.xhtml or topic.notes.rtf:
261 if topic.notes.text:
262 notes_text = topic.notes.text
263 if topic.notes.xhtml:
264 xhtml = topic.notes.xhtml
265 root_match = re.search(r'<(?:root|body)[^>]*>(.*?)</(?:root|body)>', xhtml, re.DOTALL | re.IGNORECASE)
266 if root_match:
267 xhtml = root_match.group(1)
268 xhtml = re.sub(r'<\?xml[^>]*\?>', '', xhtml)
269 xhtml = re.sub(r'<!DOCTYPE[^>]*>', '', xhtml)
270 try:
271 h = html2text.HTML2Text()
272 h.ignore_links = False
273 h.ignore_images = False
274 h.body_width = 0 # Don't wrap lines
275 notes_xhtml = h.handle(xhtml).strip()
276 except ImportError:
277 notes_xhtml = re.sub(r'<[^>]*>', '', xhtml).strip()
278 if topic.notes.rtf:
279 # not implemented due to bad results
280 pass
281
282 if include_notes and (notes_text or notes_xhtml or notes_rtf):
283 notes_content = notes_text
284 if notes_rtf:
285 notes_content = notes_rtf
286 if notes_xhtml:
287 notes_content = notes_xhtml
288 notes = f"Notes: {notes_content} "
289 else:
290 notes = ""
291
292 if topic.subtopics:
293 line = f"{(level + 1) * '#'} {prefix if level > 0 else ''} {text} "
294 lines.append(line)
295 if notes:
296 lines.append(notes)
297
298 sub_index = 0
299 for sub in topic.subtopics:
300 sub_index += 1
301 traverse(sub, lines, level + 1, prefix, sub_index)
302 else:
303 line = f"- {text} "
304 lines.append(line)
305 if notes:
306 lines.append(notes)
307
308 traverse(root_topic, lines, 0, '', 0)
309 return "\n".join(lines)
310
311
312def deserialize_mermaid_with_id(mermaid_text: str, guid_mapping: dict) -> MindmapTopic:
313 """Convert Mermaid text with id to a Mindmap structure.
314
315 Args:
316 mermaid_text (str): Mermaid formatted string to parse
317 guid_mapping (dict): Dictionary mapping numeric IDs to GUIDs
318
319 Returns:
320 MindmapTopic: Root topic of the deserialized mindmap
321 """
322 id_to_guid = {id_num: guid for guid, id_num in guid_mapping.items()}
323 lines = [line for line in mermaid_text.splitlines() if line.strip()]
324 if lines and lines[0].strip().lower() == "mindmap":
325 lines = lines[1:]
326 node_pattern = re.compile(r"^(id(\d+))\[(.*)\]$")
327 root = None
328 stack = []
329 for line in lines:
330 indent = len(line) - len(line.lstrip(" "))
331 level = indent // 2
332 stripped = line.lstrip(" ")
333 match = node_pattern.match(stripped)
334 if not match:
335 continue
336 node_id_str = match.group(1)
337 id_number = int(match.group(2))
338 node_text = match.group(3)
339 if id_number in id_to_guid:
340 guid = id_to_guid[id_number]
341 else:
342 guid = str(uuid.uuid4())
343 id_to_guid[id_number] = guid
344 node = MindmapTopic(guid=guid, text=node_text, level=level)
345 if root is None:
346 root = node
347 stack.append((level, node))
348 continue
349 while stack and stack[-1][0] >= level:
350 stack.pop()
351 if stack:
352 parent = stack[-1][1]
353 node.parent = parent
354 parent.subtopics.append(node)
355 else:
356 root.subtopics.append(node)
357 node.parent = root
358 stack.append((level, node))
359 return root
360
361def deserialize_mermaid_simple(mermaid_text: str) -> MindmapTopic:
362 """Convert a simple indentation-based Mermaid mindmap to a Mindmap structure.
363
364 Args:
365 mermaid_text (str): Mermaid formatted string containing only textual nodes
366 expressed via indentation (no IDs or metadata)
367
368 Returns:
369 MindmapTopic: Root topic of the deserialized mindmap
370 """
371 lines = [line for line in mermaid_text.splitlines() if line.strip()]
372 if lines and lines[0].strip().lower() == "mindmap":
373 lines = lines[1:]
374 root = None
375 stack = []
376 for raw_line in lines:
377 # Allow optional inline comments and convert tabs to spaces for consistent indentation
378 line = raw_line.expandtabs(2)
379 line = line.split("%%", 1)[0]
380 stripped = line.strip()
381 if not stripped:
382 continue
383 if stripped.startswith("[") and stripped.endswith("]"):
384 stripped = stripped[1:-1]
385 indent = len(line) - len(line.lstrip(" "))
386 level = indent // 2
387 node = MindmapTopic(guid=str(uuid.uuid4()), text=stripped, level=level)
388 if root is None:
389 root = node
390 stack = [(level, node)]
391 continue
392 while stack and stack[-1][0] >= level:
393 stack.pop()
394 if stack:
395 parent = stack[-1][1]
396 node.parent = parent
397 parent.subtopics.append(node)
398 else:
399 # Fallback for malformed inputs that contain multiple root-level nodes
400 if root:
401 node.parent = root
402 root.subtopics.append(node)
403 stack.append((level, node))
404 return root
405
406def deserialize_mermaid_full(mermaid_text: str, guid_mapping: dict) -> MindmapTopic:
407 """Convert Mermaid text with metadata to a Mindmap structure.
408
409 Args:
410 mermaid_text (str): Mermaid formatted string with JSON metadata to parse
411 guid_mapping (dict): Dictionary mapping numeric IDs to GUIDs
412
413 Returns:
414 MindmapTopic: Root topic of the fully deserialized mindmap with all attributes
415 """
416 id_to_guid = {v: k for k, v in guid_mapping.items()}
417 lines = [line for line in mermaid_text.splitlines() if line.strip()]
418 if lines and lines[0].strip().lower() == "mindmap":
419 lines = lines[1:]
420 pattern = re.compile(r"^( *)(\[.*?\])\s*%%\s*(\{.*\})\s*$")
421 root = None
422 stack = []
423
424 def restore_guid(numeric_id):
425 try:
426 num = int(numeric_id)
427 except:
428 return str(uuid.uuid4())
429 if num in id_to_guid:
430 return id_to_guid[num]
431 else:
432 new_guid = str(uuid.uuid4())
433 id_to_guid[num] = new_guid
434 return new_guid
435
436 def process_subobject(field_dict: dict, id_field: str) -> dict:
437 if id_field in field_dict:
438 field_dict[id_field] = restore_guid(field_dict[id_field])
439 return field_dict
440
441 for line in lines:
442 m = pattern.match(line)
443 if not m:
444 continue
445 indent_str, bracket_part, json_part = m.groups()
446 level = len(indent_str) // 2
447 fallback_text = bracket_part.strip()[1:-1]
448 try:
449 attrs = json.loads(json_part)
450 except json.JSONDecodeError as e:
451 if "Invalid \\escape" in e.msg:
452 repaired_json = re.sub(r'(?<!\\)\\(?![\\\"/bfnrtu])', r'\\\\', json_part)
453 try:
454 attrs = json.loads(repaired_json)
455 except Exception as inner:
456 print(f"Failed to parse Mermaid JSON metadata even after repairing backslashes: {inner} (original: {json_part!r})")
457 attrs = {}
458 else:
459 print(f"Failed to parse Mermaid JSON metadata: {e.msg} at pos {e.pos} in {json_part!r}")
460 attrs = {}
461 except Exception as e:
462 print(f"Unexpected error while parsing Mermaid JSON metadata {json_part!r}: {e}")
463 attrs = {}
464 if "id" in attrs:
465 node_guid = restore_guid(attrs["id"])
466 else:
467 node_guid = str(uuid.uuid4())
468 node_text = attrs.get("text", fallback_text)
469 node_rtf = attrs.get("rtf", "")
470 selected = attrs.get("selected", False)
471 links = []
472 if "links" in attrs and isinstance(attrs["links"], list):
473 for link_dict in attrs["links"]:
474 ld = dict(link_dict)
475 ld = process_subobject(ld, "id")
476 link_text = ld.get("text", "")
477 link_url = ld.get("url", "")
478 link_guid = ld.get("id", "")
479 links.append(MindmapLink(text=link_text, url=link_url, guid=link_guid))
480 image_obj = None
481 if "image" in attrs and isinstance(attrs["image"], dict):
482 image_obj = MindmapImage(text=attrs["image"].get("text", ""))
483 icons = []
484 if "icons" in attrs and isinstance(attrs["icons"], list):
485 for icon_dict in attrs["icons"]:
486 idict = dict(icon_dict)
487 icons.append(MindmapIcon(
488 text=idict.get("text", ""),
489 is_stock_icon=idict.get("is_stock_icon", True),
490 index=idict.get("index", 1),
491 signature=idict.get("signature", ""),
492 path=idict.get("path", ""),
493 group=idict.get("group", "")
494 ))
495 notes_obj = None
496 if "notes" in attrs:
497 if isinstance(attrs["notes"], dict):
498 notes_obj = MindmapNotes(
499 text=attrs["notes"].get("text", ""),
500 xhtml=attrs["notes"].get("xhtml", ""),
501 rtf=attrs["notes"].get("rtf", "")
502 )
503 elif isinstance(attrs["notes"], str):
504 notes_obj = MindmapNotes(text=attrs["notes"])
505 tags = []
506 if "tags" in attrs and isinstance(attrs["tags"], list):
507 for tag_item in attrs["tags"]:
508 if isinstance(tag_item, dict):
509 tag_text = tag_item.get("text", "")
510 else:
511 tag_text = str(tag_item)
512 tags.append(MindmapTag(text=tag_text))
513 references = []
514 if "references" in attrs and isinstance(attrs["references"], list):
515 for ref_dict in attrs["references"]:
516 rd = dict(ref_dict)
517 rd = process_subobject(rd, "id_1")
518 rd = process_subobject(rd, "id_2")
519 direction = rd.get("direction", None)
520 label = rd.get("label", "")
521 references.append(MindmapReference(
522 guid_1=rd.get("id_1", ""),
523 guid_2=rd.get("id_2", ""),
524 direction=direction,
525 label=label
526 ))
527 node = MindmapTopic(guid=node_guid, text=node_text, rtf=node_rtf, level=level, selected=selected)
528 node.links = links
529 node.image = image_obj
530 node.icons = icons
531 node.notes = notes_obj
532 node.tags = tags
533 node.references = references
534 while stack and stack[-1][0] >= level:
535 stack.pop()
536 if stack:
537 parent = stack[-1][1]
538 node.parent = parent
539 parent.subtopics.append(node)
540 else:
541 root = node
542 stack.append((level, node))
543 return root
544
545def build_mapping(topic, guid_mapping):
546 """Build a mapping of GUIDs to numeric IDs for an entire mindmap.
547
548 Args:
549 topic (MindmapTopic): Root topic of the mindmap
550 guid_mapping (dict): Dictionary to store GUID to ID mappings
551
552 Returns:
553 None: The mapping is updated in-place
554 """
555 if topic.guid not in guid_mapping:
556 guid_mapping[topic.guid] = len(guid_mapping) + 1
557 for sub in topic.subtopics:
558 build_mapping(sub, guid_mapping)