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