markdown-to-confluence 0.1.13__py3-none-any.whl → 0.2.1__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.
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/LICENSE +21 -21
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/METADATA +226 -168
- markdown_to_confluence-0.2.1.dist-info/RECORD +17 -0
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/WHEEL +1 -1
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/zip-safe +1 -1
- md2conf/__init__.py +13 -13
- md2conf/__main__.py +211 -139
- md2conf/api.py +517 -459
- md2conf/application.py +231 -154
- md2conf/converter.py +965 -626
- md2conf/entities.dtd +537 -537
- md2conf/mermaid.py +54 -0
- md2conf/processor.py +113 -91
- md2conf/properties.py +53 -52
- markdown_to_confluence-0.1.13.dist-info/RECORD +0 -16
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/top_level.txt +0 -0
md2conf/application.py
CHANGED
|
@@ -1,154 +1,231 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os.path
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Dict, Optional
|
|
5
|
-
|
|
6
|
-
from .api import ConfluenceSession
|
|
7
|
-
from .converter import (
|
|
8
|
-
ConfluenceDocument,
|
|
9
|
-
ConfluenceDocumentOptions,
|
|
10
|
-
ConfluencePageMetadata,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
self
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
self,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
1
|
+
import logging
|
|
2
|
+
import os.path
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from .api import ConfluencePage, ConfluenceSession
|
|
7
|
+
from .converter import (
|
|
8
|
+
ConfluenceDocument,
|
|
9
|
+
ConfluenceDocumentOptions,
|
|
10
|
+
ConfluencePageMetadata,
|
|
11
|
+
ConfluenceQualifiedID,
|
|
12
|
+
attachment_name,
|
|
13
|
+
extract_qualified_id,
|
|
14
|
+
read_qualified_id,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
LOGGER = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Application:
|
|
21
|
+
"The entry point for Markdown to Confluence conversion."
|
|
22
|
+
|
|
23
|
+
api: ConfluenceSession
|
|
24
|
+
options: ConfluenceDocumentOptions
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, api: ConfluenceSession, options: ConfluenceDocumentOptions
|
|
28
|
+
) -> None:
|
|
29
|
+
self.api = api
|
|
30
|
+
self.options = options
|
|
31
|
+
|
|
32
|
+
def synchronize(self, path: Path) -> None:
|
|
33
|
+
"Synchronizes a single Markdown page or a directory of Markdown pages."
|
|
34
|
+
|
|
35
|
+
if path.is_dir():
|
|
36
|
+
self.synchronize_directory(path)
|
|
37
|
+
elif path.is_file():
|
|
38
|
+
self.synchronize_page(path)
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError(f"expected: valid file or directory path; got: {path}")
|
|
41
|
+
|
|
42
|
+
def synchronize_page(self, page_path: Path) -> None:
|
|
43
|
+
"Synchronizes a single Markdown page with Confluence."
|
|
44
|
+
|
|
45
|
+
self._synchronize_page(page_path, {})
|
|
46
|
+
|
|
47
|
+
def synchronize_directory(self, local_dir: Path) -> None:
|
|
48
|
+
"Synchronizes a directory of Markdown pages with Confluence."
|
|
49
|
+
|
|
50
|
+
LOGGER.info(f"Synchronizing directory: {local_dir}")
|
|
51
|
+
|
|
52
|
+
# Step 1: build index of all page metadata
|
|
53
|
+
page_metadata: Dict[Path, ConfluencePageMetadata] = {}
|
|
54
|
+
root_id = (
|
|
55
|
+
ConfluenceQualifiedID(self.options.root_page_id, self.api.space_key)
|
|
56
|
+
if self.options.root_page_id
|
|
57
|
+
else None
|
|
58
|
+
)
|
|
59
|
+
self._index_directory(local_dir, root_id, page_metadata)
|
|
60
|
+
LOGGER.info(f"indexed {len(page_metadata)} page(s)")
|
|
61
|
+
|
|
62
|
+
# Step 2: convert each page
|
|
63
|
+
for page_path in page_metadata.keys():
|
|
64
|
+
self._synchronize_page(page_path, page_metadata)
|
|
65
|
+
|
|
66
|
+
def _synchronize_page(
|
|
67
|
+
self,
|
|
68
|
+
page_path: Path,
|
|
69
|
+
page_metadata: Dict[Path, ConfluencePageMetadata],
|
|
70
|
+
) -> None:
|
|
71
|
+
base_path = page_path.parent
|
|
72
|
+
|
|
73
|
+
LOGGER.info(f"Synchronizing page: {page_path}")
|
|
74
|
+
document = ConfluenceDocument(page_path, self.options, page_metadata)
|
|
75
|
+
|
|
76
|
+
if document.id.space_key:
|
|
77
|
+
with self.api.switch_space(document.id.space_key):
|
|
78
|
+
self._update_document(document, base_path)
|
|
79
|
+
else:
|
|
80
|
+
self._update_document(document, base_path)
|
|
81
|
+
|
|
82
|
+
def _index_directory(
|
|
83
|
+
self,
|
|
84
|
+
local_dir: Path,
|
|
85
|
+
root_id: Optional[ConfluenceQualifiedID],
|
|
86
|
+
page_metadata: Dict[Path, ConfluencePageMetadata],
|
|
87
|
+
) -> None:
|
|
88
|
+
"Indexes Markdown files in a directory recursively."
|
|
89
|
+
|
|
90
|
+
LOGGER.info(f"Indexing directory: {local_dir}")
|
|
91
|
+
|
|
92
|
+
files: List[Path] = []
|
|
93
|
+
directories: List[Path] = []
|
|
94
|
+
for entry in os.scandir(local_dir):
|
|
95
|
+
if entry.is_file():
|
|
96
|
+
if entry.name.endswith(".md"):
|
|
97
|
+
# skip non-markdown files
|
|
98
|
+
files.append((Path(local_dir) / entry.name).absolute())
|
|
99
|
+
elif entry.is_dir():
|
|
100
|
+
if not entry.name.startswith("."):
|
|
101
|
+
directories.append((Path(local_dir) / entry.name).absolute())
|
|
102
|
+
|
|
103
|
+
# make page act as parent node in Confluence
|
|
104
|
+
parent_id: Optional[ConfluenceQualifiedID] = None
|
|
105
|
+
if "index.md" in files:
|
|
106
|
+
parent_id = read_qualified_id(Path(local_dir) / "index.md")
|
|
107
|
+
elif "README.md" in files:
|
|
108
|
+
parent_id = read_qualified_id(Path(local_dir) / "README.md")
|
|
109
|
+
|
|
110
|
+
if parent_id is None:
|
|
111
|
+
parent_id = root_id
|
|
112
|
+
|
|
113
|
+
for doc in files:
|
|
114
|
+
metadata = self._get_or_create_page(doc, parent_id)
|
|
115
|
+
LOGGER.debug(f"indexed {doc} with metadata: {metadata}")
|
|
116
|
+
page_metadata[doc] = metadata
|
|
117
|
+
|
|
118
|
+
for directory in directories:
|
|
119
|
+
self._index_directory(Path(local_dir) / directory, parent_id, page_metadata)
|
|
120
|
+
|
|
121
|
+
def _get_or_create_page(
|
|
122
|
+
self,
|
|
123
|
+
absolute_path: Path,
|
|
124
|
+
parent_id: Optional[ConfluenceQualifiedID],
|
|
125
|
+
*,
|
|
126
|
+
title: Optional[str] = None,
|
|
127
|
+
) -> ConfluencePageMetadata:
|
|
128
|
+
"""
|
|
129
|
+
Creates a new Confluence page if no page is linked in the Markdown document.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
# parse file
|
|
133
|
+
with open(absolute_path, "r", encoding="utf-8") as f:
|
|
134
|
+
document = f.read()
|
|
135
|
+
|
|
136
|
+
qualified_id, document = extract_qualified_id(document)
|
|
137
|
+
if qualified_id is not None:
|
|
138
|
+
confluence_page = self.api.get_page(
|
|
139
|
+
qualified_id.page_id, space_key=qualified_id.space_key
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
if parent_id is None:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"expected: Confluence page ID to act as parent for Markdown files with no linked Confluence page"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
confluence_page = self._create_page(
|
|
148
|
+
absolute_path, document, title, parent_id
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return ConfluencePageMetadata(
|
|
152
|
+
domain=self.api.domain,
|
|
153
|
+
base_path=self.api.base_path,
|
|
154
|
+
page_id=confluence_page.id,
|
|
155
|
+
space_key=confluence_page.space_key or self.api.space_key,
|
|
156
|
+
title=confluence_page.title or "",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _create_page(
|
|
160
|
+
self,
|
|
161
|
+
absolute_path: Path,
|
|
162
|
+
document: str,
|
|
163
|
+
title: Optional[str],
|
|
164
|
+
parent_id: ConfluenceQualifiedID,
|
|
165
|
+
) -> ConfluencePage:
|
|
166
|
+
"Creates a new Confluence page when Markdown file doesn't have an embedded page ID yet."
|
|
167
|
+
|
|
168
|
+
# use file name without extension if no title is supplied
|
|
169
|
+
if title is None:
|
|
170
|
+
title = absolute_path.stem
|
|
171
|
+
|
|
172
|
+
confluence_page = self.api.get_or_create_page(
|
|
173
|
+
title, parent_id.page_id, space_key=parent_id.space_key
|
|
174
|
+
)
|
|
175
|
+
self._update_markdown(
|
|
176
|
+
absolute_path,
|
|
177
|
+
document,
|
|
178
|
+
confluence_page.id,
|
|
179
|
+
confluence_page.space_key,
|
|
180
|
+
)
|
|
181
|
+
return confluence_page
|
|
182
|
+
|
|
183
|
+
def _update_document(self, document: ConfluenceDocument, base_path: Path) -> None:
|
|
184
|
+
"Saves a new version of a Confluence document."
|
|
185
|
+
|
|
186
|
+
for image in document.images:
|
|
187
|
+
self.api.upload_attachment(
|
|
188
|
+
document.id.page_id,
|
|
189
|
+
base_path / image,
|
|
190
|
+
attachment_name(image),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
for image, data in document.embedded_images.items():
|
|
194
|
+
self.api.upload_attachment(
|
|
195
|
+
document.id.page_id,
|
|
196
|
+
Path("EMB") / image,
|
|
197
|
+
attachment_name(image),
|
|
198
|
+
raw_data=data,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
content = document.xhtml()
|
|
202
|
+
LOGGER.debug(f"generated Confluence Storage Format document:\n{content}")
|
|
203
|
+
self.api.update_page(document.id.page_id, content)
|
|
204
|
+
|
|
205
|
+
def _update_markdown(
|
|
206
|
+
self,
|
|
207
|
+
path: Path,
|
|
208
|
+
document: str,
|
|
209
|
+
page_id: str,
|
|
210
|
+
space_key: Optional[str],
|
|
211
|
+
) -> None:
|
|
212
|
+
"Writes the Confluence page ID and space key at the beginning of the Markdown file."
|
|
213
|
+
|
|
214
|
+
content: List[str] = []
|
|
215
|
+
|
|
216
|
+
# check if the file has frontmatter
|
|
217
|
+
index = 0
|
|
218
|
+
if document.startswith("---\n"):
|
|
219
|
+
index = document.find("\n---\n", 4) + 4
|
|
220
|
+
|
|
221
|
+
# insert the Confluence keys after the frontmatter
|
|
222
|
+
content.append(document[:index])
|
|
223
|
+
|
|
224
|
+
content.append(f"<!-- confluence-page-id: {page_id} -->")
|
|
225
|
+
if space_key:
|
|
226
|
+
content.append(f"<!-- confluence-space-key: {space_key} -->")
|
|
227
|
+
|
|
228
|
+
content.append(document[index:])
|
|
229
|
+
|
|
230
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
231
|
+
file.write("\n".join(content))
|