mdbub 0.3.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdbub/__init__.py +63 -0
- mdbub/cli.py +122 -0
- mdbub/commands/__init__.py +0 -0
- mdbub/commands/about.py +99 -0
- mdbub/commands/export.py +9 -0
- mdbub/commands/print_kv.py +59 -0
- mdbub/commands/print_links.py +24 -0
- mdbub/commands/print_tags.py +40 -0
- mdbub/commands/quick.py +1471 -0
- mdbub/commands/quickmode_config.py +141 -0
- mdbub/commands/tag_utils.py +0 -0
- mdbub/commands/version.py +67 -0
- mdbub/commands/view.py +9 -0
- mdbub/core/__init__.py +0 -0
- mdbub/core/mindmap.py +241 -0
- mdbub/core/mindmap_utils.py +33 -0
- mdbub/symbols.py +68 -0
- mdbub-0.3.7.dist-info/LICENSE +201 -0
- mdbub-0.3.7.dist-info/METADATA +182 -0
- mdbub-0.3.7.dist-info/RECORD +22 -0
- mdbub-0.3.7.dist-info/WHEEL +4 -0
- mdbub-0.3.7.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
"""
|
2
|
+
quickmode_config.py - Loads and manages Quick Mode UI configuration (colors, symbols, caps, etc.)
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import os
|
7
|
+
import sys
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Any, Dict, List, Optional
|
10
|
+
|
11
|
+
import toml
|
12
|
+
|
13
|
+
# Default UI constants
|
14
|
+
QUICKMODE_DEFAULTS = {
|
15
|
+
# Semantic ANSI color and highlight codes for terminal prints
|
16
|
+
# Semantic color roles (legacy single-value keys for backward compatibility)
|
17
|
+
"COLOR_PRINTS_CLEAR": "\033[H\033[J", # Clear screen
|
18
|
+
"COLOR_PRINTS_RESET": "\033[0m", # Reset
|
19
|
+
"COLOR_PRINTS_BLINK": "blink", # Rich blink style (not ANSI)
|
20
|
+
# Foreground/background split for all roles (preferred)
|
21
|
+
"COLOR_PRINTS_ACCENT_FG": "\033[36m",
|
22
|
+
"COLOR_PRINTS_ACCENT_BG": "",
|
23
|
+
"COLOR_PRINTS_HIGHLIGHT_FG": "",
|
24
|
+
"COLOR_PRINTS_HIGHLIGHT_BG": "\033[44m",
|
25
|
+
"COLOR_PRINTS_TEXT_FG": "\033[97m",
|
26
|
+
"COLOR_PRINTS_TEXT_BG": "",
|
27
|
+
"COLOR_PRINTS_DIM_FG": "\033[90m",
|
28
|
+
"COLOR_PRINTS_DIM_BG": "",
|
29
|
+
"COLOR_PRINTS_WARNING_FG": "\033[33m",
|
30
|
+
"COLOR_PRINTS_WARNING_BG": "",
|
31
|
+
"COLOR_PRINTS_SUCCESS_FG": "\033[32m",
|
32
|
+
"COLOR_PRINTS_SUCCESS_BG": "",
|
33
|
+
"COLOR_PRINTS_ERROR_FG": "\033[31m",
|
34
|
+
"COLOR_PRINTS_ERROR_BG": "",
|
35
|
+
"COLOR_PRINTS_STATUS_BAR_FG": "\u001b[93m",
|
36
|
+
"COLOR_PRINTS_STATUS_BAR_BG": "",
|
37
|
+
"COLOR_PRINTS_BREADCRUMB_BAR_FG": "\u001b[36m",
|
38
|
+
"COLOR_PRINTS_BREADCRUMB_BAR_BG": "",
|
39
|
+
"COLOR_PRINTS_CHILD_HIGHLIGHT_FG": "\u001b[90m",
|
40
|
+
"COLOR_PRINTS_CHILD_HIGHLIGHT_BG": "", # Child highlight (default: dim gray, no bg)
|
41
|
+
# UI timings
|
42
|
+
"STATUS_MESSAGE_TIMEOUT_SHORT": 1.0,
|
43
|
+
"STATUS_MESSAGE_TIMEOUT": 2.0,
|
44
|
+
"STATUS_MESSAGE_TIMEOUT_LONG": 4.0,
|
45
|
+
# Layout
|
46
|
+
"MAX_VISIBLE_CHILDREN": 4,
|
47
|
+
# Length caps
|
48
|
+
"MAX_NODE_LABEL_VIZ_LENGTH": 2048,
|
49
|
+
"MAX_BREADCRUMB_NODE_VIZ_LENGTH": 15,
|
50
|
+
"MAX_CHILDNODE_VIZ_LENGTH": 20,
|
51
|
+
# Colors
|
52
|
+
"COLOR_BREADCRUMBS": "dim white",
|
53
|
+
"COLOR_BREADCRUMBS_ROOT": "bold cyan",
|
54
|
+
"COLOR_BREADCRUMBS_CURRENT": "bold white",
|
55
|
+
"COLOR_CURRENT_NODE": "bold white",
|
56
|
+
"COLOR_SELECTED_CHILD": "grey23 on dim cyan",
|
57
|
+
"COLOR_CHILD": "dim cyan",
|
58
|
+
"COLOR_PAGINATION": "yellow",
|
59
|
+
"COLOR_POSITION": "dim white",
|
60
|
+
"COLOR_STATUS": "green",
|
61
|
+
"COLOR_HOTKEYS": "cyan",
|
62
|
+
"COLOR_ERROR": "red",
|
63
|
+
"COLOR_WARNING": "yellow",
|
64
|
+
"COLOR_SUCCESS": "green",
|
65
|
+
# Symbols
|
66
|
+
"SYMBOL_BULLET": "●",
|
67
|
+
"SYMBOL_BRANCH": "└─",
|
68
|
+
"SYMBOL_ROOT": "◉",
|
69
|
+
"SYMBOL_MORE_LEFT": "◀ more",
|
70
|
+
"SYMBOL_MORE_RIGHT": "more ▶",
|
71
|
+
"SYMBOL_CHILDNODE_OPENWRAP": "⦇",
|
72
|
+
"SYMBOL_CHILDNODE_CLOSEWRAP": "⦈",
|
73
|
+
"SYMBOL_BREADCRUMBNODE_OPENWRAP": "【",
|
74
|
+
"SYMBOL_BREADCRUMBNODE_CLOSEWRAP": "】",
|
75
|
+
}
|
76
|
+
|
77
|
+
CONFIG_FILENAME = "mdbub.toml"
|
78
|
+
SESSION_FILENAME = "session.json"
|
79
|
+
|
80
|
+
APPNAME = "mdbub"
|
81
|
+
|
82
|
+
|
83
|
+
def get_session_path() -> Path:
|
84
|
+
return get_xdg_config_path() / SESSION_FILENAME
|
85
|
+
|
86
|
+
|
87
|
+
def save_session(last_file: str, last_node_path: List[int]) -> None:
|
88
|
+
"""Persist the last opened file and selected node path to the session file."""
|
89
|
+
session_path = get_session_path()
|
90
|
+
session_path.parent.mkdir(parents=True, exist_ok=True)
|
91
|
+
session = {
|
92
|
+
"last_file": last_file,
|
93
|
+
"last_node_path": last_node_path,
|
94
|
+
}
|
95
|
+
try:
|
96
|
+
with open(session_path, "w", encoding="utf-8") as f:
|
97
|
+
json.dump(session, f)
|
98
|
+
except Exception as e:
|
99
|
+
print(
|
100
|
+
f"[mdbub] Failed to save session: {e}",
|
101
|
+
file=sys.stderr,
|
102
|
+
)
|
103
|
+
|
104
|
+
|
105
|
+
def load_session() -> Optional[Dict[str, Any]]:
|
106
|
+
"""Load the last session from the session file, if it exists."""
|
107
|
+
session_path = get_session_path()
|
108
|
+
if session_path.exists():
|
109
|
+
try:
|
110
|
+
with open(session_path, "r", encoding="utf-8") as f:
|
111
|
+
session: Dict[str, Any] = json.load(f)
|
112
|
+
return session
|
113
|
+
except Exception as e:
|
114
|
+
print(
|
115
|
+
f"[mdbub] Failed to load session: {e}",
|
116
|
+
file=sys.stderr,
|
117
|
+
)
|
118
|
+
return None
|
119
|
+
|
120
|
+
|
121
|
+
def get_xdg_config_path() -> Path:
|
122
|
+
# XDG_CONFIG_HOME or ~/.config/mdbub/
|
123
|
+
base = os.environ.get("XDG_CONFIG_HOME")
|
124
|
+
if base:
|
125
|
+
return Path(base) / APPNAME
|
126
|
+
return Path.home() / ".config" / APPNAME
|
127
|
+
|
128
|
+
|
129
|
+
def load_quickmode_config() -> Dict[str, Any]:
|
130
|
+
config_dir = get_xdg_config_path()
|
131
|
+
config_path = config_dir / CONFIG_FILENAME
|
132
|
+
config: Dict[str, Any] = QUICKMODE_DEFAULTS.copy()
|
133
|
+
if config_path.exists():
|
134
|
+
try:
|
135
|
+
user_cfg: Dict[str, Any] = toml.load(config_path)
|
136
|
+
for k, v in user_cfg.items():
|
137
|
+
if k in config:
|
138
|
+
config[k] = v
|
139
|
+
except Exception as e:
|
140
|
+
print(f"[mdbub] Failed to load quickmode config: {e}", file=sys.stderr)
|
141
|
+
return config # Always return a dict, never None
|
File without changes
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import os
|
2
|
+
import platform
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
import typer
|
7
|
+
|
8
|
+
from mdbub import BUILD_INFO, __version__
|
9
|
+
from mdbub.commands.quickmode_config import CONFIG_FILENAME, get_xdg_config_path
|
10
|
+
|
11
|
+
|
12
|
+
def main() -> None:
|
13
|
+
"""Print version, build info, and config path."""
|
14
|
+
version = __version__
|
15
|
+
build_info = BUILD_INFO
|
16
|
+
|
17
|
+
# Rich output with more details
|
18
|
+
typer.echo("🧠 mdbub - Terminal mindmap tool")
|
19
|
+
typer.echo("Warning: Pre-release version - data may be lost, expect bugs!")
|
20
|
+
typer.echo(f"Version: {version}")
|
21
|
+
typer.echo(f"Build: {build_info}")
|
22
|
+
typer.echo(f"Python: {sys.version.split()[0]} ({platform.python_implementation()})")
|
23
|
+
typer.echo(
|
24
|
+
f"Platform: {platform.system()} {platform.release()} ({platform.machine()})"
|
25
|
+
)
|
26
|
+
typer.echo("")
|
27
|
+
# Installation info
|
28
|
+
try:
|
29
|
+
import mdbub
|
30
|
+
|
31
|
+
install_path = Path(mdbub.__file__).parent.parent.parent
|
32
|
+
typer.echo(f"📦 Installed at: {install_path}")
|
33
|
+
except Exception:
|
34
|
+
typer.echo("📦 Installed at: unknown")
|
35
|
+
|
36
|
+
# check if config for mdbub exists and print path other print "using defaults"
|
37
|
+
config_dir = get_xdg_config_path()
|
38
|
+
config_path = config_dir / CONFIG_FILENAME
|
39
|
+
if config_path.exists():
|
40
|
+
typer.echo(f"🔧 Config: {config_path}")
|
41
|
+
else:
|
42
|
+
typer.echo(f"🔧 Config: {config_path}, Not found (using defaults)")
|
43
|
+
|
44
|
+
# Session info
|
45
|
+
import json
|
46
|
+
|
47
|
+
from mdbub.commands.quickmode_config import SESSION_FILENAME
|
48
|
+
|
49
|
+
session_path = config_dir / SESSION_FILENAME
|
50
|
+
if session_path.exists():
|
51
|
+
try:
|
52
|
+
with open(session_path, "r") as f:
|
53
|
+
session_data = json.load(f)
|
54
|
+
last_file = session_data.get("last_file", "-")
|
55
|
+
typer.echo(f"📝 Session: {session_path}")
|
56
|
+
if last_file != "-" and not (
|
57
|
+
Path(last_file).is_file() and os.access(last_file, os.R_OK)
|
58
|
+
):
|
59
|
+
typer.echo(f" Last file: {last_file} (missing or unreadable)")
|
60
|
+
else:
|
61
|
+
typer.echo(f" Last file: {last_file}")
|
62
|
+
# typer.echo(f" Last node path: {last_node_path}")
|
63
|
+
except Exception as e:
|
64
|
+
typer.echo(f"📝 Session: {session_path} (error reading: {e})")
|
65
|
+
else:
|
66
|
+
typer.echo(f"📝 Session: {session_path}, Not found")
|
67
|
+
typer.echo("")
|
mdbub/commands/view.py
ADDED
mdbub/core/__init__.py
ADDED
File without changes
|
mdbub/core/mindmap.py
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
import logging
|
2
|
+
import time
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
4
|
+
|
5
|
+
MAX_NODE_LABEL_LENGTH = 2048 # Maximum allowed characters for a node label
|
6
|
+
|
7
|
+
|
8
|
+
class MindMapNode:
|
9
|
+
def __init__(
|
10
|
+
self,
|
11
|
+
label: str,
|
12
|
+
children: Optional[List["MindMapNode"]] = None,
|
13
|
+
color: Optional[str] = None,
|
14
|
+
icon: Optional[str] = None,
|
15
|
+
metadata: Optional[Dict[str, Any]] = None,
|
16
|
+
) -> None:
|
17
|
+
if len(label) > MAX_NODE_LABEL_LENGTH:
|
18
|
+
logging.warning(
|
19
|
+
f"[mdbub] Node label exceeded {MAX_NODE_LABEL_LENGTH} chars and was "
|
20
|
+
"truncated."
|
21
|
+
)
|
22
|
+
label = label[:MAX_NODE_LABEL_LENGTH] + "... [truncated]"
|
23
|
+
self.label = label
|
24
|
+
self.children = children or []
|
25
|
+
self.color = color
|
26
|
+
self.icon = icon
|
27
|
+
self.metadata = metadata or {}
|
28
|
+
self.parent: Optional[
|
29
|
+
"MindMapNode"
|
30
|
+
] = None # Reference to parent node, useful for navigation
|
31
|
+
|
32
|
+
def add_child(self, child: "MindMapNode") -> None:
|
33
|
+
self.children.append(child)
|
34
|
+
child.parent = self # Set parent reference
|
35
|
+
|
36
|
+
def add_tag(self, tag: str) -> None:
|
37
|
+
"""Add a tag to this node's metadata."""
|
38
|
+
if "tags" not in self.metadata:
|
39
|
+
self.metadata["tags"] = []
|
40
|
+
if isinstance(self.metadata["tags"], list) and tag not in self.metadata["tags"]:
|
41
|
+
self.metadata["tags"].append(tag)
|
42
|
+
|
43
|
+
def remove_tag(self, tag: str) -> None:
|
44
|
+
"""Remove a tag from this node's metadata."""
|
45
|
+
if "tags" in self.metadata and isinstance(self.metadata["tags"], list):
|
46
|
+
if tag in self.metadata["tags"]:
|
47
|
+
self.metadata["tags"].remove(tag)
|
48
|
+
# Clean up empty tags list
|
49
|
+
if not self.metadata["tags"]:
|
50
|
+
del self.metadata["tags"]
|
51
|
+
|
52
|
+
def get_tags(self) -> List[str]:
|
53
|
+
"""Get all tags for this node."""
|
54
|
+
if "tags" in self.metadata and isinstance(self.metadata["tags"], list):
|
55
|
+
return self.metadata["tags"]
|
56
|
+
return []
|
57
|
+
|
58
|
+
def set_metadata(self, key: str, value: Any) -> None:
|
59
|
+
"""Set a metadata key-value pair."""
|
60
|
+
self.metadata[key] = value
|
61
|
+
|
62
|
+
def get_metadata(self, key: str, default: Any = None) -> Any:
|
63
|
+
"""Get a metadata value by key."""
|
64
|
+
return self.metadata.get(key, default)
|
65
|
+
|
66
|
+
def to_dict(self) -> Dict[str, Any]:
|
67
|
+
result: Dict[str, Any] = {
|
68
|
+
"label": self.label,
|
69
|
+
"children": [child.to_dict() for child in self.children],
|
70
|
+
}
|
71
|
+
# Only include non-empty values
|
72
|
+
if self.color:
|
73
|
+
result["color"] = self.color
|
74
|
+
if self.icon:
|
75
|
+
result["icon"] = self.icon
|
76
|
+
if self.metadata:
|
77
|
+
result["metadata"] = self.metadata
|
78
|
+
return result
|
79
|
+
|
80
|
+
@staticmethod
|
81
|
+
def from_dict(data: Dict[str, Any]) -> "MindMapNode":
|
82
|
+
node = MindMapNode(
|
83
|
+
label=data["label"],
|
84
|
+
color=data.get("color"),
|
85
|
+
icon=data.get("icon"),
|
86
|
+
metadata=data.get("metadata", {}),
|
87
|
+
)
|
88
|
+
for child_data in data.get("children", []):
|
89
|
+
child = MindMapNode.from_dict(child_data)
|
90
|
+
node.add_child(child)
|
91
|
+
return node
|
92
|
+
|
93
|
+
|
94
|
+
def parse_markdown_to_mindmap(md: str) -> MindMapNode:
|
95
|
+
"""Parse extended markdown into a MindMapNode tree with metadata.
|
96
|
+
|
97
|
+
Metadata format supports:
|
98
|
+
- Tags in the format: #tag1 #tag2
|
99
|
+
- Key-value pairs: @key=value
|
100
|
+
"""
|
101
|
+
start_time = time.time()
|
102
|
+
lines = [
|
103
|
+
line.rstrip()
|
104
|
+
for line in md.splitlines()
|
105
|
+
if line.strip() and not line.startswith("<!-- mdbub-format")
|
106
|
+
]
|
107
|
+
# NOTE: Loosen annotation for release: mypy cannot handle recursive tuple type here.
|
108
|
+
top_nodes: List[Any] = [] # List of (label, metadata, children_tuples, indent)
|
109
|
+
stack: List[Any] = [] # Stack for parent tracking
|
110
|
+
|
111
|
+
logging.info(f"[mdbub] Parsing {len(lines)} lines from markdown...")
|
112
|
+
for line in lines:
|
113
|
+
if not line.lstrip().startswith("-"):
|
114
|
+
continue
|
115
|
+
|
116
|
+
indent = len(line) - len(line.lstrip())
|
117
|
+
content = line.lstrip()[2:].strip()
|
118
|
+
|
119
|
+
# Parse metadata from the label
|
120
|
+
label, metadata = _parse_node_metadata(content)
|
121
|
+
|
122
|
+
if indent == 0:
|
123
|
+
top_nodes.append((label, metadata, [], 0))
|
124
|
+
stack = [top_nodes[-1]]
|
125
|
+
else:
|
126
|
+
# Attach as child to closest parent with lower indent
|
127
|
+
while stack and stack[-1][3] >= indent:
|
128
|
+
stack.pop()
|
129
|
+
if stack:
|
130
|
+
stack[-1][2].append((label, metadata, [], indent))
|
131
|
+
stack.append(stack[-1][2][-1])
|
132
|
+
|
133
|
+
def build_tree(
|
134
|
+
node_tuple: Any, # TODO: Recursive tuple type; loosened for mypy compatibility
|
135
|
+
depth: int = 0,
|
136
|
+
) -> MindMapNode:
|
137
|
+
label, metadata, children_tuples, _ = node_tuple
|
138
|
+
if len(label) > MAX_NODE_LABEL_LENGTH:
|
139
|
+
logging.warning(
|
140
|
+
f"[mdbub] Node label exceeded {MAX_NODE_LABEL_LENGTH} chars and was truncated during parsing."
|
141
|
+
)
|
142
|
+
label = label[:MAX_NODE_LABEL_LENGTH] + "... [truncated]"
|
143
|
+
if depth > 100:
|
144
|
+
logging.error(f"[mdbub] Maximum recursion depth reached at node '{label}'")
|
145
|
+
return MindMapNode(label, metadata=metadata)
|
146
|
+
|
147
|
+
node = MindMapNode(label, metadata=metadata)
|
148
|
+
|
149
|
+
# Process tags if present
|
150
|
+
if "tags" in metadata:
|
151
|
+
# Tags are already in the metadata dictionary
|
152
|
+
pass
|
153
|
+
|
154
|
+
for child_tuple in children_tuples:
|
155
|
+
node.add_child(build_tree(child_tuple, depth=depth + 1))
|
156
|
+
return node
|
157
|
+
|
158
|
+
if len(top_nodes) == 1:
|
159
|
+
result = build_tree(top_nodes[0])
|
160
|
+
elif len(top_nodes) > 1:
|
161
|
+
root = MindMapNode("SYNTHETIC ROOT")
|
162
|
+
for node_tuple in top_nodes:
|
163
|
+
root.add_child(build_tree(node_tuple))
|
164
|
+
|
165
|
+
def warn_multi_root() -> None:
|
166
|
+
root._multi_root_warning = True # type: ignore[attr-defined] # Custom attribute to signal warning
|
167
|
+
|
168
|
+
warn_multi_root()
|
169
|
+
result = root
|
170
|
+
else:
|
171
|
+
result = MindMapNode("") # Empty mindmap
|
172
|
+
elapsed = time.time() - start_time
|
173
|
+
logging.info(f"[mdbub] Mindmap parse complete in {elapsed:.3f} seconds.")
|
174
|
+
return result
|
175
|
+
|
176
|
+
|
177
|
+
def _parse_node_metadata(content: str) -> Tuple[str, Dict[str, Any]]:
|
178
|
+
"""Parse node metadata from content string.
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
tuple: (label, metadata_dict)
|
182
|
+
"""
|
183
|
+
import re
|
184
|
+
|
185
|
+
# Initialize metadata dictionary
|
186
|
+
metadata = {}
|
187
|
+
|
188
|
+
# Extract tags (format: #tag)
|
189
|
+
tags = re.findall(r"\s#([\w-]+)", content)
|
190
|
+
if tags:
|
191
|
+
metadata["tags"] = tags
|
192
|
+
# Remove tags from content
|
193
|
+
for tag in tags:
|
194
|
+
content = re.sub(r"\s#" + tag + "\b", "", content)
|
195
|
+
|
196
|
+
# Extract key-value pairs (format: @key=value)
|
197
|
+
kv_pairs = re.findall(r"\s@([\w-]+)=([^\s]+)", content)
|
198
|
+
for key, value in kv_pairs:
|
199
|
+
metadata[key] = value
|
200
|
+
# Remove key-value pair from content
|
201
|
+
content = re.sub(r"\s@" + key + "=" + value + "\b", "", content)
|
202
|
+
|
203
|
+
# Clean up any extra whitespace
|
204
|
+
label = content.strip()
|
205
|
+
|
206
|
+
return label, metadata
|
207
|
+
|
208
|
+
|
209
|
+
def mindmap_to_markdown(node: "MindMapNode", level: int = 0) -> str:
|
210
|
+
"""Serialize MindMapNode tree to markdown bullets with metadata."""
|
211
|
+
indent = " " * level
|
212
|
+
lines = []
|
213
|
+
# Build node content with metadata
|
214
|
+
content = node.label
|
215
|
+
|
216
|
+
# Only add tags that are NOT already inline in the label
|
217
|
+
label_lower = node.label.lower()
|
218
|
+
if "tags" in node.metadata and node.metadata["tags"]:
|
219
|
+
tags = node.metadata["tags"]
|
220
|
+
for tag in tags:
|
221
|
+
tag_str = f"#{tag.lower()}"
|
222
|
+
if tag_str not in label_lower:
|
223
|
+
content += f" #{tag}"
|
224
|
+
|
225
|
+
# Only add metadata that is NOT already inline in the label
|
226
|
+
for key, value in node.metadata.items():
|
227
|
+
if key != "tags" and value is not None:
|
228
|
+
if isinstance(value, (list, dict)):
|
229
|
+
continue # Skip complex structures for now
|
230
|
+
meta_str = f"@{key.lower()}={str(value).lower()}"
|
231
|
+
if meta_str not in label_lower:
|
232
|
+
content += f" @{key}={value}"
|
233
|
+
|
234
|
+
# Emit the current node with its metadata
|
235
|
+
lines.append(f"{indent}- {content}")
|
236
|
+
|
237
|
+
# Process all children recursively
|
238
|
+
for child in node.children:
|
239
|
+
lines.append(mindmap_to_markdown(child, level + 1))
|
240
|
+
|
241
|
+
return "\n".join([line for line in lines if line])
|
@@ -0,0 +1,33 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from .mindmap import MindMapNode
|
4
|
+
|
5
|
+
|
6
|
+
def get_node_by_path(root: MindMapNode, path: List[int]) -> MindMapNode:
|
7
|
+
node = root
|
8
|
+
for idx in path:
|
9
|
+
node = node.children[idx]
|
10
|
+
return node
|
11
|
+
|
12
|
+
|
13
|
+
def get_breadcrumbs(root: MindMapNode, path: List[int]) -> List[str]:
|
14
|
+
node = root
|
15
|
+
labels = [node.label]
|
16
|
+
for idx in path:
|
17
|
+
node = node.children[idx]
|
18
|
+
labels.append(node.label)
|
19
|
+
return labels
|
20
|
+
|
21
|
+
|
22
|
+
def add_child(node: MindMapNode, label: str = "New Child") -> MindMapNode:
|
23
|
+
child = MindMapNode(label)
|
24
|
+
node.children.append(child)
|
25
|
+
return child
|
26
|
+
|
27
|
+
|
28
|
+
def add_sibling(
|
29
|
+
parent: MindMapNode, idx: int, label: str = "New Sibling"
|
30
|
+
) -> MindMapNode:
|
31
|
+
sibling = MindMapNode(label)
|
32
|
+
parent.children.insert(idx + 1, sibling)
|
33
|
+
return sibling
|
mdbub/symbols.py
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
"""
|
2
|
+
# WRAPPER SYMBOL OPTIONS
|
3
|
+
# -------------------------------------------------------------------------------------
|
4
|
+
# These symbols can be used for SYMBOL_CHILDNODE_OPENWRAP, SYMBOL_CHILDNODE_CLOSEWRAP,
|
5
|
+
# SYMBOL_BREADCRUMBNODE_OPENWRAP, and SYMBOL_BREADCRUMBNODE_CLOSEWRAP
|
6
|
+
#
|
7
|
+
# TRADITIONAL BRACKETS
|
8
|
+
# -------------------------------------------------------------------------------------
|
9
|
+
# Parentheses: ( )
|
10
|
+
# Square brackets: [ ]
|
11
|
+
# Curly braces: { }
|
12
|
+
# Angle brackets: < >
|
13
|
+
#
|
14
|
+
# ASIAN STYLE BRACKETS
|
15
|
+
# -------------------------------------------------------------------------------------
|
16
|
+
# Japanese corner brackets: 「 」
|
17
|
+
# White corner brackets: 『 』
|
18
|
+
# Black lenticular brackets: 【 】
|
19
|
+
# White lenticular brackets: 〖 〗
|
20
|
+
# Tortoise shell brackets: 〔 〕
|
21
|
+
# Double angle brackets: 《 》
|
22
|
+
# CJK brackets: 〈 〉
|
23
|
+
# Fullwidth brackets: ( )
|
24
|
+
# Fullwidth curly braces: { }
|
25
|
+
# Fullwidth square brackets: [ ]
|
26
|
+
#
|
27
|
+
# MATHEMATICAL AND SPECIAL BRACKETS
|
28
|
+
# -------------------------------------------------------------------------------------
|
29
|
+
# Mathematical angle brackets: ⟨ ⟩
|
30
|
+
# Double square brackets: ⟦ ⟧
|
31
|
+
# Floor brackets: ⌊ ⌋
|
32
|
+
# Ceiling brackets: ⌈ ⌉
|
33
|
+
# Z notation: ⦇ ⦈
|
34
|
+
# Angle with dot: ⦑ ⦒
|
35
|
+
# Square with ticks: ⦋ ⦌
|
36
|
+
# Double-struck brackets: ⟦ ⟧
|
37
|
+
#
|
38
|
+
# DECORATIVE SYMBOLS (PAIRED)
|
39
|
+
# -------------------------------------------------------------------------------------
|
40
|
+
# Stars: ✧ ✧
|
41
|
+
# Black stars: ★ ★
|
42
|
+
# Arrows: → ←
|
43
|
+
# Heavy arrows: ➤ ➤
|
44
|
+
# Triangle arrows: ▶ ◀
|
45
|
+
# Dots/bullets: • •
|
46
|
+
# Diamonds: ◆ ◆
|
47
|
+
# White diamonds: ◇ ◇
|
48
|
+
# Circles: ○ ○
|
49
|
+
# Black circles: ● ●
|
50
|
+
# Flowers: ✿ ✿
|
51
|
+
# Cherry blossoms: ❀ ❀
|
52
|
+
# Sparkles: ✨ ✨
|
53
|
+
# Quotes: " "
|
54
|
+
# Typographic quotes: " "
|
55
|
+
# French quotes (guillemets): « »
|
56
|
+
# Single guillemets: ‹ ›
|
57
|
+
#
|
58
|
+
# REVERSED BRACKETS (RIGHT-TO-LEFT)
|
59
|
+
# -------------------------------------------------------------------------------------
|
60
|
+
# Reversed Japanese corner: 」 「
|
61
|
+
# Reversed white corner: 』 『
|
62
|
+
# Reversed double angle: 》 《
|
63
|
+
# Reversed black lenticular: 】 【
|
64
|
+
# Reversed white lenticular: 〗 〖
|
65
|
+
# Reversed square: ] [
|
66
|
+
# Reversed curly: } {
|
67
|
+
# Reversed parentheses: ) (
|
68
|
+
"""
|