from graphviz import Digraph import re import random def parse_markdown_to_dict(md_text): lines = md_text.strip().splitlines() mindmap = {} stack = [] for line in lines: heading_match = re.match(r'^(#{1,6})\s+(.*)', line) bullet_match = re.match(r'^\s*-\s+(.*)', line) if heading_match: level = len(heading_match.group(1)) title = heading_match.group(2).strip() node = {'title': title, 'children': []} while len(stack) >= level: stack.pop() if stack: stack[-1]['children'].append(node) else: mindmap = node stack.append(node) elif bullet_match and stack: stack[-1]['children'].append({'title': bullet_match.group(1), 'children': []}) return mindmap generated_colors = set() def generate_random_color(): """Generate a random color that hasn't been generated before.""" while True: # Generate a random color in hex format color = "#{:02x}{:02x}{:02x}".format(random.randint(128, 255), random.randint(128, 255), random.randint(128, 255)) # If the color is not in the set, it's unique if color not in generated_colors: generated_colors.add(color) # Add the color to the set of generated colors return color # Return the unique color else: continue # Try again def brighten_color(color, factor=0.15): """Brighten the color by a certain factor (default 10%)""" # Remove the '#' symbol color = color.lstrip('#') # Convert hex to RGB r, g, b = [int(color[i:i+2], 16) for i in (0, 2, 4)] # Increase each component by the factor, but clamp to 255 r = min(255, int(r * (1 + factor))) g = min(255, int(g * (1 + factor))) b = min(255, int(b * (1 + factor))) # Convert back to hex return "#{:02x}{:02x}{:02x}".format(r, g, b) def add_nodes_to_graph(graph, node, parent_id=None, font_size=9, parent_color=None): node_id = str(id(node)) title = node['title'] if parent_color is None: node_color = "#ADD8E6" # Light Blue for the main heading border_color = "#000000" # Dark Blue border for the main heading parent_color = "#ADD8E6" elif parent_color == "#ADD8E6": node_color = generate_random_color() border_color = "#808080" parent_color = node_color else: # Child node and its descendants with the same random color node_color = brighten_color(parent_color, factor=0.15) border_color = "#808080" # Check for markdown links url_match = re.search(r'\[(.*?)\]\((.*?)\)', title) if url_match: prefix_text = title[:url_match.start()].strip() display_text = url_match.group(1) url = url_match.group(2) label = f'{prefix_text} {display_text}' graph.node(node_id, label=label, shape="box", style="rounded,filled", color=border_color, fontcolor="black", fillcolor=node_color, href=url, tooltip=title, fontsize=str(font_size)) else: graph.node(node_id, title, shape="box", style="rounded,filled", color=border_color, fontcolor="black", fillcolor=node_color, tooltip=title, fontsize=str(font_size)) if parent_id: graph.edge(parent_id, node_id) # Recurse to children, passing down color for the child and its descendants for child in node.get('children', []): # Assign a random color to each child node (no inheritance from parent) add_nodes_to_graph(graph, child, node_id, font_size=max(8, font_size - 1), parent_color=parent_color) def generate_mindmap_svg(md_text): mindmap_dict = parse_markdown_to_dict(md_text) root_title = mindmap_dict.get('title', 'Mindmap') sanitized_title = re.sub(r'[^a-zA-Z0-9_\-]', '', root_title.replace(" ", "")) if output_filename is None: output_filename = sanitized_title graph = Digraph(format='svg') graph.attr(rankdir='LR', size='10,10!', pad="0.5", margin="0.2", ratio="auto") graph.attr('node', fontname="Arial", fontsize="9") add_nodes_to_graph(graph, mindmap_dict) svg_content = graph.pipe(format='svg').decode('utf-8') # Replace %3 with the sanitized filename in the SVG content svg_content = svg_content.replace("%3", root_title) # Save the modified SVG content to a file with open(f'{output_filename}.svg', 'w') as f: f.write(svg_content) return f"{output_filename}".svg