docspan 0.1.0__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.
- docspan/__init__.py +3 -0
- docspan/__main__.py +0 -0
- docspan/backends/__init__.py +19 -0
- docspan/backends/base.py +85 -0
- docspan/backends/confluence/__init__.py +0 -0
- docspan/backends/confluence/adf/__init__.py +14 -0
- docspan/backends/confluence/adf/comparator.py +427 -0
- docspan/backends/confluence/adf/converter.py +119 -0
- docspan/backends/confluence/adf/converters.py +1449 -0
- docspan/backends/confluence/adf/interfaces.py +191 -0
- docspan/backends/confluence/adf/nodes.py +2085 -0
- docspan/backends/confluence/adf/parser.py +400 -0
- docspan/backends/confluence/adf/validators.py +161 -0
- docspan/backends/confluence/adf/visitors.py +495 -0
- docspan/backends/confluence/backend.py +227 -0
- docspan/backends/confluence/client.py +44 -0
- docspan/backends/confluence/config/__init__.py +21 -0
- docspan/backends/confluence/config/loader.py +107 -0
- docspan/backends/confluence/config/models.py +167 -0
- docspan/backends/confluence/config/validation.py +297 -0
- docspan/backends/confluence/markdown/__init__.py +22 -0
- docspan/backends/confluence/markdown/ast.py +819 -0
- docspan/backends/confluence/markdown/extensions/__init__.py +5 -0
- docspan/backends/confluence/markdown/extensions/frontmatter.py +80 -0
- docspan/backends/confluence/markdown/extensions/mermaid.py +64 -0
- docspan/backends/confluence/markdown/extensions/wikilinks.py +179 -0
- docspan/backends/confluence/markdown/inline_parser.py +495 -0
- docspan/backends/confluence/markdown/parser.py +1006 -0
- docspan/backends/confluence/models/__init__.py +18 -0
- docspan/backends/confluence/models/markdown_file.py +402 -0
- docspan/backends/confluence/models/page.py +212 -0
- docspan/backends/confluence/models/path_utils.py +34 -0
- docspan/backends/confluence/models/results.py +28 -0
- docspan/backends/confluence/models/sync_status.py +382 -0
- docspan/backends/confluence/services/__init__.py +0 -0
- docspan/backends/confluence/services/confluence/__init__.py +40 -0
- docspan/backends/confluence/services/confluence/attachment_client.py +147 -0
- docspan/backends/confluence/services/confluence/base_client.py +420 -0
- docspan/backends/confluence/services/confluence/client.py +376 -0
- docspan/backends/confluence/services/confluence/comment_client.py +682 -0
- docspan/backends/confluence/services/confluence/crawler.py +587 -0
- docspan/backends/confluence/services/confluence/label_client.py +130 -0
- docspan/backends/confluence/services/confluence/page_client.py +1288 -0
- docspan/backends/confluence/services/confluence/space_client.py +179 -0
- docspan/backends/confluence/services/confluence/url_parser.py +106 -0
- docspan/backends/google_docs/__init__.py +0 -0
- docspan/backends/google_docs/auth.py +143 -0
- docspan/backends/google_docs/backend.py +140 -0
- docspan/backends/google_docs/client.py +665 -0
- docspan/backends/google_docs/converter.py +471 -0
- docspan/backends/google_docs/docs_request_builder.py +232 -0
- docspan/backends/google_docs/docs_structure_parser.py +120 -0
- docspan/backends/google_docs/markdown_to_paragraph_parser.py +145 -0
- docspan/cli/__init__.py +0 -0
- docspan/cli/main.py +408 -0
- docspan/config.py +62 -0
- docspan/core/__init__.py +49 -0
- docspan/core/merge.py +30 -0
- docspan/core/orchestrator.py +332 -0
- docspan/core/paths.py +8 -0
- docspan/core/state.py +53 -0
- docspan-0.1.0.dist-info/METADATA +273 -0
- docspan-0.1.0.dist-info/RECORD +65 -0
- docspan-0.1.0.dist-info/WHEEL +4 -0
- docspan-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inline element parser for Markdown.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, List, Match
|
|
7
|
+
|
|
8
|
+
from docspan.backends.confluence.markdown.ast import (
|
|
9
|
+
ColoredTextNode,
|
|
10
|
+
DateNode,
|
|
11
|
+
EmojiNode,
|
|
12
|
+
HighlightedTextNode,
|
|
13
|
+
ImageNode,
|
|
14
|
+
InlineCodeNode,
|
|
15
|
+
LinkNode,
|
|
16
|
+
MarkdownNode,
|
|
17
|
+
MentionNode,
|
|
18
|
+
StatusBadgeNode,
|
|
19
|
+
StatusMacroNode,
|
|
20
|
+
TextNode,
|
|
21
|
+
TocNode,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InlineParser:
|
|
26
|
+
"""
|
|
27
|
+
Parser for inline Markdown elements.
|
|
28
|
+
|
|
29
|
+
This parser handles inline formatting like bold, italic, links,
|
|
30
|
+
inline code, and other elements that appear within block-level elements.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Regex patterns for inline elements
|
|
34
|
+
PATTERNS = {
|
|
35
|
+
"link": r'\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)', # [text](url "title")
|
|
36
|
+
"image": r'!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?(?:\s+=(\d+)(?:x(\d+))?)?\)', # 
|
|
37
|
+
"bold": r"\*\*([^*]+)\*\*|\b__([^_]+)__\b", # **text** or __text__
|
|
38
|
+
"italic": r"\*([^*]+)\*|\b_([^_]+)_\b", # *text* or _text_
|
|
39
|
+
"bold_italic": r"\*\*\*([^*]+)\*\*\*|\b___([^_]+)___\b", # ***text*** or ___text___
|
|
40
|
+
"inline_code": r"`([^`]+)`", # `code`
|
|
41
|
+
"strikethrough": r"~~([^~]+)~~", # ~~text~~
|
|
42
|
+
"emoji": r":([a-z0-9_+-]+):", # :emoji_name:
|
|
43
|
+
"mention": r"@([a-zA-Z0-9._-]+)", # @username
|
|
44
|
+
"status_badge": r"\[!([A-Z_]+)\]", # [!STATUS]
|
|
45
|
+
"highlighted": r"==([^=]+)==", # ==text==
|
|
46
|
+
"colored_text": r'<mark[^>]*style="color:([^"]+)"[^>]*>([^<]+)</mark>', # <mark style="color:#ff0000">text</mark>
|
|
47
|
+
"iso_date": r"\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2})?", # 2025-10-29 or 2025-10-29T14:00:00
|
|
48
|
+
"inline_macro": r"\{([a-z][a-z0-9-]*)(?::([^}]+))?\}", # {toc} or {toc:maxLevel=3} or {status:colour=Green|title=Active}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
"""Initialize the parser."""
|
|
53
|
+
self.compiled_patterns = {
|
|
54
|
+
name: re.compile(pattern) for name, pattern in self.PATTERNS.items()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def parse(self, text: str) -> List[MarkdownNode]:
|
|
58
|
+
"""
|
|
59
|
+
Parse inline elements in text.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
text: Text content to parse
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of MarkdownNode objects
|
|
66
|
+
"""
|
|
67
|
+
if not text:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
# Find all inline element matches
|
|
71
|
+
matches = []
|
|
72
|
+
for elem_type, pattern in self.compiled_patterns.items():
|
|
73
|
+
for match in pattern.finditer(text):
|
|
74
|
+
start, end = match.span()
|
|
75
|
+
matches.append((start, end, elem_type, match))
|
|
76
|
+
|
|
77
|
+
# If no matches, return a single text node
|
|
78
|
+
if not matches:
|
|
79
|
+
return [TextNode(type="text", content=text)]
|
|
80
|
+
|
|
81
|
+
# Sort matches by start position
|
|
82
|
+
matches.sort(key=lambda x: x[0])
|
|
83
|
+
|
|
84
|
+
# Create result nodes
|
|
85
|
+
result = []
|
|
86
|
+
last_end = 0
|
|
87
|
+
|
|
88
|
+
for start, end, elem_type, match in matches:
|
|
89
|
+
# Handle overlapping matches by checking if this match starts
|
|
90
|
+
# after the end of the previous match
|
|
91
|
+
if start < last_end:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Add text before the match
|
|
95
|
+
if start > last_end:
|
|
96
|
+
result.append(TextNode(type="text", content=text[last_end:start]))
|
|
97
|
+
|
|
98
|
+
# Process the match based on its type
|
|
99
|
+
if elem_type == "link":
|
|
100
|
+
node = self._create_link_node(match)
|
|
101
|
+
result.append(node)
|
|
102
|
+
elif elem_type == "image":
|
|
103
|
+
node = self._create_image_node(match)
|
|
104
|
+
result.append(node)
|
|
105
|
+
elif elem_type == "inline_code":
|
|
106
|
+
node = self._create_inline_code_node(match)
|
|
107
|
+
result.append(node)
|
|
108
|
+
elif elem_type == "emoji":
|
|
109
|
+
node = self._create_emoji_node(match)
|
|
110
|
+
result.append(node)
|
|
111
|
+
elif elem_type == "mention":
|
|
112
|
+
node = self._create_mention_node(match)
|
|
113
|
+
result.append(node)
|
|
114
|
+
elif elem_type == "status_badge":
|
|
115
|
+
node = self._create_status_badge_node(match)
|
|
116
|
+
result.append(node)
|
|
117
|
+
elif elem_type == "highlighted":
|
|
118
|
+
node = self._create_highlighted_text_node(match)
|
|
119
|
+
result.append(node)
|
|
120
|
+
elif elem_type == "colored_text":
|
|
121
|
+
node = self._create_colored_text_node(match)
|
|
122
|
+
result.append(node)
|
|
123
|
+
elif elem_type == "iso_date":
|
|
124
|
+
node = self._create_date_node(match)
|
|
125
|
+
result.append(node)
|
|
126
|
+
elif elem_type == "inline_macro":
|
|
127
|
+
node = self._create_inline_macro_node(match)
|
|
128
|
+
result.append(node)
|
|
129
|
+
else:
|
|
130
|
+
# Handle formatting marks (bold, italic, etc.)
|
|
131
|
+
# This returns a list of nodes (to support nested elements like links in bold)
|
|
132
|
+
nodes = self._create_formatted_text_node(match, elem_type)
|
|
133
|
+
result.extend(nodes)
|
|
134
|
+
|
|
135
|
+
last_end = end
|
|
136
|
+
|
|
137
|
+
# Add any remaining text
|
|
138
|
+
if last_end < len(text):
|
|
139
|
+
result.append(TextNode(type="text", content=text[last_end:]))
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def _create_link_node(self, match: Match) -> LinkNode:
|
|
144
|
+
"""
|
|
145
|
+
Create a link node from a match.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
match: Regex match for a link
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
LinkNode
|
|
152
|
+
"""
|
|
153
|
+
text = match.group(1)
|
|
154
|
+
url = match.group(2)
|
|
155
|
+
title = match.group(3) if match.lastindex >= 3 else None
|
|
156
|
+
|
|
157
|
+
link = LinkNode(type="link", url=url, title=title)
|
|
158
|
+
link.children.append(TextNode(type="text", content=text))
|
|
159
|
+
|
|
160
|
+
return link
|
|
161
|
+
|
|
162
|
+
def _create_image_node(self, match: Match) -> ImageNode:
|
|
163
|
+
"""
|
|
164
|
+
Create an image node from a match.
|
|
165
|
+
|
|
166
|
+
Supports extended syntax for image dimensions:
|
|
167
|
+
-  - basic image
|
|
168
|
+
-  - with title
|
|
169
|
+
-  - with width only
|
|
170
|
+
-  - with width and height
|
|
171
|
+
-  - with title and dimensions
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
match: Regex match for an image
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
ImageNode with optional width and height
|
|
178
|
+
"""
|
|
179
|
+
alt = match.group(1) or ""
|
|
180
|
+
src = match.group(2)
|
|
181
|
+
title = match.group(3) if match.lastindex >= 3 and match.group(3) else None
|
|
182
|
+
|
|
183
|
+
# Parse dimensions if present (groups 4 and 5)
|
|
184
|
+
width = None
|
|
185
|
+
height = None
|
|
186
|
+
|
|
187
|
+
if match.lastindex >= 4 and match.group(4):
|
|
188
|
+
try:
|
|
189
|
+
width = int(match.group(4))
|
|
190
|
+
except (ValueError, TypeError):
|
|
191
|
+
pass # Ignore invalid width values
|
|
192
|
+
|
|
193
|
+
if match.lastindex >= 5 and match.group(5):
|
|
194
|
+
try:
|
|
195
|
+
height = int(match.group(5))
|
|
196
|
+
except (ValueError, TypeError):
|
|
197
|
+
pass # Ignore invalid height values
|
|
198
|
+
|
|
199
|
+
return ImageNode(
|
|
200
|
+
type="image",
|
|
201
|
+
src=src,
|
|
202
|
+
alt=alt,
|
|
203
|
+
title=title,
|
|
204
|
+
width=width,
|
|
205
|
+
height=height
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def _create_inline_code_node(self, match: Match) -> InlineCodeNode:
|
|
209
|
+
"""
|
|
210
|
+
Create an inline code node from a match.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
match: Regex match for inline code
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
InlineCodeNode
|
|
217
|
+
"""
|
|
218
|
+
code = match.group(1)
|
|
219
|
+
node = InlineCodeNode(type="inlineCode", content=code)
|
|
220
|
+
return node
|
|
221
|
+
|
|
222
|
+
def _create_emoji_node(self, match: Match) -> EmojiNode:
|
|
223
|
+
"""
|
|
224
|
+
Create an emoji node from a match.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
match: Regex match for emoji (:emoji_name:)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
EmojiNode
|
|
231
|
+
"""
|
|
232
|
+
short_name = match.group(1)
|
|
233
|
+
# EmojiNode will auto-initialize text with :short_name: format
|
|
234
|
+
node = EmojiNode(short_name=short_name)
|
|
235
|
+
return node
|
|
236
|
+
|
|
237
|
+
def _create_mention_node(self, match: Match) -> MentionNode:
|
|
238
|
+
"""
|
|
239
|
+
Create a mention node from a match.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
match: Regex match for mention (@username)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
MentionNode
|
|
246
|
+
"""
|
|
247
|
+
username = match.group(1)
|
|
248
|
+
# MentionNode will auto-initialize text with @username format
|
|
249
|
+
node = MentionNode(username=username)
|
|
250
|
+
return node
|
|
251
|
+
|
|
252
|
+
def _create_status_badge_node(self, match: Match) -> StatusBadgeNode:
|
|
253
|
+
"""
|
|
254
|
+
Create a status badge node from a match.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
match: Regex match for status badge ([!STATUS])
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
StatusBadgeNode
|
|
261
|
+
"""
|
|
262
|
+
status_text = match.group(1)
|
|
263
|
+
|
|
264
|
+
# Status color mapping
|
|
265
|
+
STATUS_COLOR_MAP = {
|
|
266
|
+
# Green - Success states
|
|
267
|
+
"DONE": "green",
|
|
268
|
+
"COMPLETE": "green",
|
|
269
|
+
"SUCCESS": "green",
|
|
270
|
+
"PASSED": "green",
|
|
271
|
+
"APPROVED": "green",
|
|
272
|
+
# Red - Error states
|
|
273
|
+
"ERROR": "red",
|
|
274
|
+
"FAILED": "red",
|
|
275
|
+
"BLOCKED": "red",
|
|
276
|
+
"REJECTED": "red",
|
|
277
|
+
# Yellow - Warning states
|
|
278
|
+
"WARNING": "yellow",
|
|
279
|
+
"PENDING": "yellow",
|
|
280
|
+
"REVIEW": "yellow",
|
|
281
|
+
# Blue - Info states
|
|
282
|
+
"INFO": "blue",
|
|
283
|
+
"IN_PROGRESS": "blue",
|
|
284
|
+
"RUNNING": "blue",
|
|
285
|
+
# Purple - Special states
|
|
286
|
+
"OPTIONAL": "purple",
|
|
287
|
+
# Neutral - Default
|
|
288
|
+
"TODO": "neutral",
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
color = STATUS_COLOR_MAP.get(status_text, "neutral")
|
|
292
|
+
node = StatusBadgeNode(text=status_text, color=color)
|
|
293
|
+
return node
|
|
294
|
+
|
|
295
|
+
def _create_highlighted_text_node(self, match: Match) -> HighlightedTextNode:
|
|
296
|
+
"""
|
|
297
|
+
Create a highlighted text node from a match.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
match: Regex match for highlighted text (==text==)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
HighlightedTextNode
|
|
304
|
+
"""
|
|
305
|
+
text = match.group(1)
|
|
306
|
+
node = HighlightedTextNode(content=text, bg_color="#ffff00")
|
|
307
|
+
return node
|
|
308
|
+
|
|
309
|
+
def _create_colored_text_node(self, match: Match) -> ColoredTextNode:
|
|
310
|
+
"""
|
|
311
|
+
Create a colored text node from a match.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
match: Regex match for colored text (<mark style="color:...">text</mark>)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
ColoredTextNode
|
|
318
|
+
"""
|
|
319
|
+
color = match.group(1)
|
|
320
|
+
text = match.group(2)
|
|
321
|
+
node = ColoredTextNode(content=text, color=color)
|
|
322
|
+
return node
|
|
323
|
+
|
|
324
|
+
def _create_date_node(self, match: Match) -> DateNode:
|
|
325
|
+
"""
|
|
326
|
+
Create a date node from a match.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
match: Regex match for ISO date (2025-10-29 or 2025-10-29T14:00:00)
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
DateNode
|
|
333
|
+
"""
|
|
334
|
+
from datetime import datetime
|
|
335
|
+
|
|
336
|
+
date_string = match.group(0)
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
# Try parsing as datetime first
|
|
340
|
+
if 'T' in date_string:
|
|
341
|
+
dt = datetime.fromisoformat(date_string)
|
|
342
|
+
else:
|
|
343
|
+
# Parse as date only and set time to midnight
|
|
344
|
+
dt = datetime.strptime(date_string, "%Y-%m-%d")
|
|
345
|
+
|
|
346
|
+
# Convert to milliseconds timestamp
|
|
347
|
+
timestamp = int(dt.timestamp() * 1000)
|
|
348
|
+
node = DateNode(timestamp=timestamp, date_string=date_string)
|
|
349
|
+
except (ValueError, AttributeError):
|
|
350
|
+
# If parsing fails, create a date node with timestamp 0
|
|
351
|
+
node = DateNode(timestamp=0, date_string=date_string)
|
|
352
|
+
|
|
353
|
+
return node
|
|
354
|
+
|
|
355
|
+
def _create_inline_macro_node(self, match: Match) -> MarkdownNode:
|
|
356
|
+
"""
|
|
357
|
+
Create a macro node from a match.
|
|
358
|
+
|
|
359
|
+
Supports curly brace macro syntax:
|
|
360
|
+
- {toc} - simple macro
|
|
361
|
+
- {toc:maxLevel=3} - with parameters
|
|
362
|
+
- {status:colour=Green|title=Active} - status with pipe separator
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
match: Regex match for macro ({macro-name:params})
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Appropriate extension node (TocNode, StatusMacroNode, etc.)
|
|
369
|
+
"""
|
|
370
|
+
macro_name = match.group(1)
|
|
371
|
+
params_str = match.group(2) if match.lastindex >= 2 and match.group(2) else None
|
|
372
|
+
|
|
373
|
+
# Parse parameters
|
|
374
|
+
params = self._parse_macro_parameters(params_str) if params_str else {}
|
|
375
|
+
|
|
376
|
+
# Create appropriate node based on macro name
|
|
377
|
+
if macro_name == "toc":
|
|
378
|
+
# Parse TOC-specific parameters
|
|
379
|
+
max_level = int(params.get("maxLevel", "7"))
|
|
380
|
+
min_level = int(params.get("minLevel", "1"))
|
|
381
|
+
include = params.get("include")
|
|
382
|
+
exclude = params.get("exclude")
|
|
383
|
+
toc_type = params.get("type", "list")
|
|
384
|
+
printable = params.get("printable", "true").lower() == "true"
|
|
385
|
+
separator = params.get("separator", "dots")
|
|
386
|
+
|
|
387
|
+
return TocNode(
|
|
388
|
+
max_level=max_level,
|
|
389
|
+
min_level=min_level,
|
|
390
|
+
include=include,
|
|
391
|
+
exclude=exclude,
|
|
392
|
+
toc_type=toc_type,
|
|
393
|
+
printable=printable,
|
|
394
|
+
separator=separator
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
elif macro_name == "status":
|
|
398
|
+
# Parse status-specific parameters
|
|
399
|
+
# Support both 'title' and direct text
|
|
400
|
+
status_text = params.get("title", "")
|
|
401
|
+
color = params.get("colour", params.get("color", "Grey")) # Support both spellings
|
|
402
|
+
subtle = params.get("subtle", "false").lower() == "true"
|
|
403
|
+
|
|
404
|
+
return StatusMacroNode(
|
|
405
|
+
status_text=status_text,
|
|
406
|
+
color=color,
|
|
407
|
+
subtle=subtle
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
else:
|
|
411
|
+
# For other macros, create a generic extension node placeholder
|
|
412
|
+
# This will be handled by block parser if it's a block macro
|
|
413
|
+
# For now, treat as text node
|
|
414
|
+
return TextNode(type="text", content=match.group(0))
|
|
415
|
+
|
|
416
|
+
def _parse_macro_parameters(self, params_str: str) -> Dict[str, str]:
|
|
417
|
+
"""
|
|
418
|
+
Parse macro parameters from parameter string.
|
|
419
|
+
|
|
420
|
+
Supports both comma-separated and pipe-separated parameters:
|
|
421
|
+
- maxLevel=3,minLevel=1
|
|
422
|
+
- colour=Green|title=Active
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
params_str: Parameter string (e.g., "maxLevel=3,minLevel=1")
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Dictionary of parameter key-value pairs
|
|
429
|
+
"""
|
|
430
|
+
params = {}
|
|
431
|
+
|
|
432
|
+
# Determine separator (pipe for status, comma for others)
|
|
433
|
+
if "|" in params_str:
|
|
434
|
+
separator = "|"
|
|
435
|
+
else:
|
|
436
|
+
separator = ","
|
|
437
|
+
|
|
438
|
+
# Split by separator and parse key=value pairs
|
|
439
|
+
for pair in params_str.split(separator):
|
|
440
|
+
pair = pair.strip()
|
|
441
|
+
if "=" in pair:
|
|
442
|
+
key, value = pair.split("=", 1)
|
|
443
|
+
params[key.strip()] = value.strip()
|
|
444
|
+
|
|
445
|
+
return params
|
|
446
|
+
|
|
447
|
+
def _create_formatted_text_node(self, match: Match, format_type: str) -> List[MarkdownNode]:
|
|
448
|
+
"""
|
|
449
|
+
Create formatted text nodes from a match, recursively parsing nested inline elements.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
match: Regex match for formatted text
|
|
453
|
+
format_type: Type of formatting (bold, italic, etc.)
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
List of MarkdownNode objects with appropriate marks applied
|
|
457
|
+
"""
|
|
458
|
+
# Get the captured text content from the match
|
|
459
|
+
text = match.group(1) if match.group(1) else match.group(2)
|
|
460
|
+
|
|
461
|
+
# Determine which marks to apply
|
|
462
|
+
marks = []
|
|
463
|
+
if format_type == "bold":
|
|
464
|
+
marks.append({"type": "strong"})
|
|
465
|
+
elif format_type == "italic":
|
|
466
|
+
marks.append({"type": "em"})
|
|
467
|
+
elif format_type == "bold_italic":
|
|
468
|
+
marks.append({"type": "strong"})
|
|
469
|
+
marks.append({"type": "em"})
|
|
470
|
+
elif format_type == "strikethrough":
|
|
471
|
+
marks.append({"type": "strike"})
|
|
472
|
+
|
|
473
|
+
# Recursively parse the content for nested inline elements (like links)
|
|
474
|
+
nested_nodes = self.parse(text)
|
|
475
|
+
|
|
476
|
+
# Apply marks to all text nodes in the result
|
|
477
|
+
self._apply_marks_to_nodes(nested_nodes, marks)
|
|
478
|
+
|
|
479
|
+
return nested_nodes
|
|
480
|
+
|
|
481
|
+
def _apply_marks_to_nodes(self, nodes: List[MarkdownNode], marks: List[Dict[str, Any]]) -> None:
|
|
482
|
+
"""
|
|
483
|
+
Recursively apply marks to all TextNode objects in a list of nodes.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
nodes: List of nodes to process
|
|
487
|
+
marks: Marks to apply to text nodes
|
|
488
|
+
"""
|
|
489
|
+
for node in nodes:
|
|
490
|
+
if isinstance(node, TextNode):
|
|
491
|
+
# Add marks to this text node
|
|
492
|
+
node.marks.extend(marks)
|
|
493
|
+
elif hasattr(node, 'children') and node.children:
|
|
494
|
+
# Recursively apply to children (e.g., text inside links)
|
|
495
|
+
self._apply_marks_to_nodes(node.children, marks)
|