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,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Frontmatter parser extension.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FrontmatterParser:
|
|
10
|
+
"""
|
|
11
|
+
Parser for YAML frontmatter in Markdown files.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def extract(content: str) -> Tuple[Dict[str, Any], str]:
|
|
16
|
+
"""
|
|
17
|
+
Extract YAML frontmatter from content.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
content: Markdown content with frontmatter
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Tuple of (frontmatter dict, content without frontmatter)
|
|
24
|
+
"""
|
|
25
|
+
frontmatter_pattern = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
|
26
|
+
match = frontmatter_pattern.match(content)
|
|
27
|
+
|
|
28
|
+
if match:
|
|
29
|
+
try:
|
|
30
|
+
import yaml
|
|
31
|
+
|
|
32
|
+
frontmatter = yaml.safe_load(match.group(1)) or {}
|
|
33
|
+
# Remove frontmatter from content
|
|
34
|
+
remaining_content = content[match.end() :]
|
|
35
|
+
return frontmatter, remaining_content
|
|
36
|
+
except (yaml.YAMLError, ImportError):
|
|
37
|
+
# Invalid YAML or yaml module not available
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
return {}, content
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def get_title(frontmatter: Dict[str, Any], default: Optional[str] = None) -> Optional[str]:
|
|
44
|
+
"""
|
|
45
|
+
Get title from frontmatter.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
frontmatter: Frontmatter dictionary
|
|
49
|
+
default: Default value if title not found
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Title or default value
|
|
53
|
+
"""
|
|
54
|
+
return frontmatter.get("connie-title", frontmatter.get("title", default))
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def get_page_id(frontmatter: Dict[str, Any]) -> Optional[str]:
|
|
58
|
+
"""
|
|
59
|
+
Get Confluence page ID from frontmatter.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
frontmatter: Frontmatter dictionary
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Page ID or None
|
|
66
|
+
"""
|
|
67
|
+
return frontmatter.get("connie-page-id")
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def should_publish(frontmatter: Dict[str, Any]) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Check if page should be published based on frontmatter.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
frontmatter: Frontmatter dictionary
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if page should be published, False otherwise
|
|
79
|
+
"""
|
|
80
|
+
return frontmatter.get("connie-publish", True)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mermaid diagram parser extension.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from docspan.backends.confluence.markdown.ast import MarkdownNode, MermaidNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MermaidParser:
|
|
13
|
+
"""
|
|
14
|
+
Parser for Mermaid diagram code blocks.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def parse(content: str) -> List[Tuple[str, MarkdownNode]]:
|
|
19
|
+
"""
|
|
20
|
+
Parse Mermaid diagrams in content and return list of replacements.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
content: Markdown content to parse
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of tuples (original text, MermaidNode)
|
|
27
|
+
"""
|
|
28
|
+
pattern = r"```mermaid\n([\s\S]+?)\n```"
|
|
29
|
+
matches = re.finditer(pattern, content)
|
|
30
|
+
|
|
31
|
+
replacements = []
|
|
32
|
+
for match in matches:
|
|
33
|
+
original = match.group(0)
|
|
34
|
+
code = match.group(1).strip()
|
|
35
|
+
|
|
36
|
+
node = MermaidNode(code=code)
|
|
37
|
+
node.attrs["id"] = str(uuid.uuid4())
|
|
38
|
+
|
|
39
|
+
replacements.append((original, node))
|
|
40
|
+
|
|
41
|
+
return replacements
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def render_diagram(code: str, output_path: Optional[str] = None) -> Optional[str]:
|
|
45
|
+
"""
|
|
46
|
+
Render a Mermaid diagram to an image file.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
code: Mermaid diagram code
|
|
50
|
+
output_path: Path to save the rendered image (generated if None)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Path to the rendered image or None if rendering failed
|
|
54
|
+
|
|
55
|
+
Note:
|
|
56
|
+
This is a placeholder. Implementation would typically use a library like
|
|
57
|
+
node-mermaid or puppeteer to render the diagram.
|
|
58
|
+
"""
|
|
59
|
+
# TODO: Implement actual rendering logic
|
|
60
|
+
# For now, just return a dummy path
|
|
61
|
+
if not output_path:
|
|
62
|
+
output_path = f"diagram_{uuid.uuid4()}.png"
|
|
63
|
+
|
|
64
|
+
return output_path
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wikilinks parser extension.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from docspan.backends.confluence.markdown.ast import LinkNode, MarkdownNode, TextNode, WikiLinkNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WikilinksParser:
|
|
12
|
+
"""
|
|
13
|
+
Parser for wiki-style links ([[page]] or [[page|title]]).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
PATTERN = r"\[\[([^\]|]+)(?:\|([^\]]+))?\]\]"
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def parse(cls, content: str) -> List[Tuple[str, MarkdownNode]]:
|
|
20
|
+
"""
|
|
21
|
+
Parse wiki links in content and return list of replacements.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
content: Text content to parse
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of tuples (original text, WikiLinkNode)
|
|
28
|
+
"""
|
|
29
|
+
pattern = cls.PATTERN
|
|
30
|
+
matches = re.finditer(pattern, content)
|
|
31
|
+
|
|
32
|
+
replacements = []
|
|
33
|
+
for match in matches:
|
|
34
|
+
original = match.group(0)
|
|
35
|
+
target = match.group(1).strip()
|
|
36
|
+
display = match.group(2).strip() if match.group(2) else None
|
|
37
|
+
|
|
38
|
+
node = WikiLinkNode(target=target, display=display)
|
|
39
|
+
replacements.append((original, node))
|
|
40
|
+
|
|
41
|
+
return replacements
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def replace_wikilinks(
|
|
45
|
+
cls, text_node: TextNode, page_map: Dict[str, Any] = None
|
|
46
|
+
) -> List[MarkdownNode]:
|
|
47
|
+
"""
|
|
48
|
+
Replace wiki links in a text node with link nodes.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
text_node: Text node to process
|
|
52
|
+
page_map: Optional mapping of page names to page IDs
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of nodes with wiki links replaced
|
|
56
|
+
"""
|
|
57
|
+
if not text_node.content:
|
|
58
|
+
return [text_node]
|
|
59
|
+
|
|
60
|
+
content = text_node.content
|
|
61
|
+
pattern = cls.PATTERN
|
|
62
|
+
|
|
63
|
+
# Find all wikilink matches
|
|
64
|
+
matches = list(re.finditer(pattern, content))
|
|
65
|
+
if not matches:
|
|
66
|
+
return [text_node]
|
|
67
|
+
|
|
68
|
+
# Create result nodes
|
|
69
|
+
result = []
|
|
70
|
+
last_end = 0
|
|
71
|
+
|
|
72
|
+
for match in matches:
|
|
73
|
+
start, end = match.span()
|
|
74
|
+
|
|
75
|
+
# Add text before the link
|
|
76
|
+
if start > last_end:
|
|
77
|
+
result.append(TextNode(content=content[last_end:start]))
|
|
78
|
+
|
|
79
|
+
# Process the link
|
|
80
|
+
target = match.group(1).strip()
|
|
81
|
+
display = match.group(2).strip() if match.group(2) else target
|
|
82
|
+
|
|
83
|
+
# Resolve link target if page map provided
|
|
84
|
+
page_id = None
|
|
85
|
+
if page_map:
|
|
86
|
+
page_id = cls.resolve_link(target, page_map)
|
|
87
|
+
|
|
88
|
+
# Create a wiki link node or standard link node
|
|
89
|
+
if page_id:
|
|
90
|
+
# Create regular link to Confluence page
|
|
91
|
+
result.append(LinkNode(url=f"#page-{page_id}", title=target))
|
|
92
|
+
link_node = result[-1]
|
|
93
|
+
link_node.children.append(TextNode(content=display))
|
|
94
|
+
else:
|
|
95
|
+
# Create wiki link node
|
|
96
|
+
result.append(WikiLinkNode(target=target, display=display))
|
|
97
|
+
|
|
98
|
+
last_end = end
|
|
99
|
+
|
|
100
|
+
# Add remaining text
|
|
101
|
+
if last_end < len(content):
|
|
102
|
+
result.append(TextNode(content=content[last_end:]))
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def resolve_link(cls, target: str, page_map: Dict[str, Any]) -> Optional[str]:
|
|
108
|
+
"""
|
|
109
|
+
Resolve wiki link target to a Confluence page ID.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
target: Link target (page name)
|
|
113
|
+
page_map: Dictionary mapping page names to page IDs
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Page ID or None if not found
|
|
117
|
+
"""
|
|
118
|
+
# Check direct match
|
|
119
|
+
if target in page_map:
|
|
120
|
+
return page_map[target]
|
|
121
|
+
|
|
122
|
+
# Try case-insensitive match
|
|
123
|
+
target_lower = target.lower()
|
|
124
|
+
for name, page_id in page_map.items():
|
|
125
|
+
if name.lower() == target_lower:
|
|
126
|
+
return page_id
|
|
127
|
+
|
|
128
|
+
# Try with different separators
|
|
129
|
+
normalized_target = cls._normalize_page_name(target)
|
|
130
|
+
for name, page_id in page_map.items():
|
|
131
|
+
normalized_name = cls._normalize_page_name(name)
|
|
132
|
+
if normalized_name == normalized_target:
|
|
133
|
+
return page_id
|
|
134
|
+
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _normalize_page_name(name: str) -> str:
|
|
139
|
+
"""
|
|
140
|
+
Normalize a page name for comparison.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
name: Page name to normalize
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Normalized page name
|
|
147
|
+
"""
|
|
148
|
+
# Replace spaces, hyphens, and underscores with a common separator
|
|
149
|
+
result = re.sub(r"[ \-_]+", "-", name.lower())
|
|
150
|
+
# Remove any non-alphanumeric chars (except dashes)
|
|
151
|
+
result = re.sub(r"[^\w\-]", "", result, flags=re.ASCII)
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def build_page_map(cls, pages: List[Dict[str, Any]]) -> Dict[str, str]:
|
|
156
|
+
"""
|
|
157
|
+
Build a mapping of page titles to page IDs.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
pages: List of page data dictionaries
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary mapping page titles to page IDs
|
|
164
|
+
"""
|
|
165
|
+
result = {}
|
|
166
|
+
|
|
167
|
+
for page in pages:
|
|
168
|
+
title = page.get("title")
|
|
169
|
+
page_id = page.get("id")
|
|
170
|
+
|
|
171
|
+
if title and page_id:
|
|
172
|
+
result[title] = page_id
|
|
173
|
+
|
|
174
|
+
# Also add normalized version
|
|
175
|
+
normalized = cls._normalize_page_name(title)
|
|
176
|
+
if normalized != title.lower():
|
|
177
|
+
result[normalized] = page_id
|
|
178
|
+
|
|
179
|
+
return result
|