markdown-to-confluence 0.2.5__py3-none-any.whl → 0.2.6__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.2.5.dist-info → markdown_to_confluence-0.2.6.dist-info}/METADATA +23 -15
- markdown_to_confluence-0.2.6.dist-info/RECORD +21 -0
- {markdown_to_confluence-0.2.5.dist-info → markdown_to_confluence-0.2.6.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/api.py +28 -10
- md2conf/application.py +24 -10
- md2conf/converter.py +38 -23
- md2conf/mermaid.py +4 -0
- md2conf/processor.py +29 -9
- markdown_to_confluence-0.2.5.dist-info/RECORD +0 -21
- {markdown_to_confluence-0.2.5.dist-info → markdown_to_confluence-0.2.6.dist-info}/LICENSE +0 -0
- {markdown_to_confluence-0.2.5.dist-info → markdown_to_confluence-0.2.6.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.2.5.dist-info → markdown_to_confluence-0.2.6.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.2.5.dist-info → markdown_to_confluence-0.2.6.dist-info}/zip-safe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -22,10 +22,10 @@ Requires-Python: >=3.8
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: lxml>=5.3
|
|
25
|
-
Requires-Dist: types-lxml>=2024.8
|
|
26
|
-
Requires-Dist: markdown>=3.
|
|
27
|
-
Requires-Dist: types-markdown>=3.
|
|
28
|
-
Requires-Dist: pymdown-extensions>=10.
|
|
25
|
+
Requires-Dist: types-lxml>=2024.11.8
|
|
26
|
+
Requires-Dist: markdown>=3.7
|
|
27
|
+
Requires-Dist: types-markdown>=3.7
|
|
28
|
+
Requires-Dist: pymdown-extensions>=10.12
|
|
29
29
|
Requires-Dist: pyyaml>=6.0
|
|
30
30
|
Requires-Dist: types-PyYAML>=6.0
|
|
31
31
|
Requires-Dist: requests>=2.32
|
|
@@ -75,11 +75,11 @@ npm install -g @mermaid-js/mermaid-cli
|
|
|
75
75
|
|
|
76
76
|
In order to get started, you will need
|
|
77
77
|
|
|
78
|
-
* your organization domain name (e.g. `
|
|
78
|
+
* your organization domain name (e.g. `example.atlassian.net`),
|
|
79
79
|
* base path for Confluence wiki (typically `/wiki/` for managed Confluence, `/` for on-premise)
|
|
80
80
|
* your Confluence username (e.g. `levente.hunyadi@instructure.com`) (only if required by your deployment),
|
|
81
81
|
* a Confluence API token (a string of alphanumeric characters), and
|
|
82
|
-
* the space key in Confluence (e.g. `
|
|
82
|
+
* the space key in Confluence (e.g. `SPACE`) you are publishing content to.
|
|
83
83
|
|
|
84
84
|
### Obtaining an API token
|
|
85
85
|
|
|
@@ -93,11 +93,11 @@ In order to get started, you will need
|
|
|
93
93
|
Confluence organization domain, base path, username, API token and space key can be specified at runtime or set as Confluence environment variables (e.g. add to your `~/.profile` on Linux, or `~/.bash_profile` or `~/.zshenv` on MacOS):
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
export CONFLUENCE_DOMAIN='
|
|
96
|
+
export CONFLUENCE_DOMAIN='example.atlassian.net'
|
|
97
97
|
export CONFLUENCE_PATH='/wiki/'
|
|
98
98
|
export CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
|
|
99
99
|
export CONFLUENCE_API_KEY='0123456789abcdef'
|
|
100
|
-
export CONFLUENCE_SPACE_KEY='
|
|
100
|
+
export CONFLUENCE_SPACE_KEY='SPACE'
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
On Windows, these can be set via system properties.
|
|
@@ -129,7 +129,7 @@ The above tells the tool to synchronize the Markdown file with the given Conflue
|
|
|
129
129
|
If you work in an environment where there are multiple Confluence spaces, and some Markdown pages may go into one space, whereas other pages may go into another, you can set the target space on a per-document basis:
|
|
130
130
|
|
|
131
131
|
```markdown
|
|
132
|
-
<!-- confluence-space-key:
|
|
132
|
+
<!-- confluence-space-key: SPACE -->
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
This overrides the default space set via command-line arguments or environment variables.
|
|
@@ -146,9 +146,17 @@ Provide generated-by prompt text in the Markdown file with a tag:
|
|
|
146
146
|
|
|
147
147
|
Alternatively, use the `--generated-by GENERATED_BY` option. The tag takes precedence.
|
|
148
148
|
|
|
149
|
+
### Publishing a single page
|
|
150
|
+
|
|
151
|
+
*md2conf* has two modes of operation: *single-page mode* and *directory mode*.
|
|
152
|
+
|
|
153
|
+
In single-page mode, you specify a single Markdown file as the source, which can contain absolute links to external locations (e.g. `https://example.com`) but not relative links to other pages (e.g. `local.md`). In other words, the page must be stand-alone.
|
|
154
|
+
|
|
149
155
|
### Publishing a directory
|
|
150
156
|
|
|
151
|
-
*md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file if you pass a directory as
|
|
157
|
+
*md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file in *directory mode* if you pass a directory as the source. This will traverse the specified directory recursively, and synchronize each Markdown file.
|
|
158
|
+
|
|
159
|
+
First, *md2conf* builds an index of pages in the directory hierarchy. The index maps each Markdown file path to a Confluence page ID. Whenever a relative link is encountered in a Markdown file, the relative link is replaced with a Confluence URL to the referenced page with the help of the index. All relative links must point to Markdown files that are located in the directory hierarchy.
|
|
152
160
|
|
|
153
161
|
If a Markdown file doesn't yet pair up with a Confluence page, *md2conf* creates a new page and assigns a parent. Parent-child relationships are reflected in the navigation panel in Confluence. You can set a root page ID with the command-line option `-r`, which constitutes the topmost parent. (This could correspond to the landing page of your Confluence space. The Confluence page ID is always revealed when you edit a page.) Whenever a directory contains the file `index.md` or `README.md`, this page becomes the future parent page, and all Markdown files in this directory (and possibly nested directories) become its child pages (unless they already have a page ID). However, if an `index.md` or `README.md` file is subsequently found in one of the nested directories, it becomes the parent page of that directory, and any of its subdirectories.
|
|
154
162
|
|
|
@@ -216,7 +224,7 @@ You can run the Docker container via `docker run` or via `Dockerfile`. Either ca
|
|
|
216
224
|
With `docker run`, you can pass Confluence domain, user, API and space key directly to `docker run`:
|
|
217
225
|
|
|
218
226
|
```sh
|
|
219
|
-
docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d
|
|
227
|
+
docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d example.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s SPACE ./
|
|
220
228
|
```
|
|
221
229
|
|
|
222
230
|
Alternatively, you can use a separate file `.env` to pass these parameters as environment variables:
|
|
@@ -234,11 +242,11 @@ With the `Dockerfile` approach, you can extend the base image:
|
|
|
234
242
|
```Dockerfile
|
|
235
243
|
FROM leventehunyadi/md2conf:latest
|
|
236
244
|
|
|
237
|
-
ENV CONFLUENCE_DOMAIN='
|
|
245
|
+
ENV CONFLUENCE_DOMAIN='example.atlassian.net'
|
|
238
246
|
ENV CONFLUENCE_PATH='/wiki/'
|
|
239
247
|
ENV CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
|
|
240
248
|
ENV CONFLUENCE_API_KEY='0123456789abcdef'
|
|
241
|
-
ENV CONFLUENCE_SPACE_KEY='
|
|
249
|
+
ENV CONFLUENCE_SPACE_KEY='SPACE'
|
|
242
250
|
|
|
243
251
|
CMD ["./"]
|
|
244
252
|
```
|
|
@@ -248,5 +256,5 @@ Alternatively,
|
|
|
248
256
|
```Dockerfile
|
|
249
257
|
FROM leventehunyadi/md2conf:latest
|
|
250
258
|
|
|
251
|
-
CMD ["-d", "
|
|
259
|
+
CMD ["-d", "example.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "SPACE", "./"]
|
|
252
260
|
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
md2conf/__init__.py,sha256=aD2z2fkqyEVbUDQvLSJxfFUOpwMYt5lAZIUAQocULuM,402
|
|
2
|
+
md2conf/__main__.py,sha256=6iOI28W_d71tlnCMFpZwvkBmBt5-HazlZsz69gS4Oak,6894
|
|
3
|
+
md2conf/api.py,sha256=T-g_VS_cVahcYOs2jBVW38J7MSS94JxzMXlqohd_Sfw,17326
|
|
4
|
+
md2conf/application.py,sha256=DSnqBx5hOWWVopnjo1iK_tbQg_7H8MhNPx_SAC3ovXQ,9157
|
|
5
|
+
md2conf/converter.py,sha256=5cxxHnI9ux0pi-VW-CArBCGcpMClb8qEJZQd--NyrdY,35042
|
|
6
|
+
md2conf/emoji.py,sha256=w9oiOIxzObAE7HTo3f6aETT1_D3t3yZwr88ynU4ENm0,1924
|
|
7
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
8
|
+
md2conf/matcher.py,sha256=mYMltZOLypK4O-SJugLgicOwUMem67hiNLg_kPFoJkU,3583
|
|
9
|
+
md2conf/mermaid.py,sha256=gqA6Hg6WcPDdR7JOClezAgNZj2Gq4pXJSgmOUlUt6Dk,2192
|
|
10
|
+
md2conf/processor.py,sha256=E-Na-a8tNp4CaoRPA5etcXdHXNRdgyMrf6bfKa9P7O4,4781
|
|
11
|
+
md2conf/properties.py,sha256=iVIc0h0XtS3Y2LCywX1C9cvmVQ0WljOMt8pl2MDMVCI,1990
|
|
12
|
+
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
13
|
+
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
md2conf/util.py,sha256=ftf60MiW7S7rW45ipWX6efP_Sv2F2qpyIDHrGA0cBiw,743
|
|
15
|
+
markdown_to_confluence-0.2.6.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
16
|
+
markdown_to_confluence-0.2.6.dist-info/METADATA,sha256=kRQoSGz7LUCqHS7YhX9xsGae9OTql11hMWEnNP0ZRAw,13540
|
|
17
|
+
markdown_to_confluence-0.2.6.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
18
|
+
markdown_to_confluence-0.2.6.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
19
|
+
markdown_to_confluence-0.2.6.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
20
|
+
markdown_to_confluence-0.2.6.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
21
|
+
markdown_to_confluence-0.2.6.dist-info/RECORD,,
|
md2conf/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ Parses Markdown files, converts Markdown content into the Confluence Storage For
|
|
|
5
5
|
Confluence API endpoints to upload images and content.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.2.
|
|
8
|
+
__version__ = "0.2.6"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2024, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
md2conf/api.py
CHANGED
|
@@ -178,17 +178,30 @@ class ConfluenceSession:
|
|
|
178
178
|
def upload_attachment(
|
|
179
179
|
self,
|
|
180
180
|
page_id: str,
|
|
181
|
-
attachment_path: Path,
|
|
182
181
|
attachment_name: str,
|
|
182
|
+
*,
|
|
183
|
+
attachment_path: Optional[Path] = None,
|
|
183
184
|
raw_data: Optional[bytes] = None,
|
|
185
|
+
content_type: Optional[str] = None,
|
|
184
186
|
comment: Optional[str] = None,
|
|
185
|
-
*,
|
|
186
187
|
space_key: Optional[str] = None,
|
|
187
188
|
force: bool = False,
|
|
188
189
|
) -> None:
|
|
189
|
-
content_type = mimetypes.guess_type(attachment_path, strict=True)[0]
|
|
190
190
|
|
|
191
|
-
if
|
|
191
|
+
if attachment_path is None and raw_data is None:
|
|
192
|
+
raise ConfluenceError("required: `attachment_path` or `raw_data`")
|
|
193
|
+
|
|
194
|
+
if attachment_path is not None and raw_data is not None:
|
|
195
|
+
raise ConfluenceError("expected: either `attachment_path` or `raw_data`")
|
|
196
|
+
|
|
197
|
+
if content_type is None:
|
|
198
|
+
if attachment_path is not None:
|
|
199
|
+
name = str(attachment_path)
|
|
200
|
+
else:
|
|
201
|
+
name = attachment_name
|
|
202
|
+
content_type, _ = mimetypes.guess_type(name, strict=True)
|
|
203
|
+
|
|
204
|
+
if attachment_path is not None and not attachment_path.is_file():
|
|
192
205
|
raise ConfluenceError(f"file not found: {attachment_path}")
|
|
193
206
|
|
|
194
207
|
try:
|
|
@@ -196,14 +209,16 @@ class ConfluenceSession:
|
|
|
196
209
|
page_id, attachment_name, space_key=space_key
|
|
197
210
|
)
|
|
198
211
|
|
|
199
|
-
if not
|
|
212
|
+
if attachment_path is not None:
|
|
200
213
|
if not force and attachment.file_size == attachment_path.stat().st_size:
|
|
201
214
|
LOGGER.info("Up-to-date attachment: %s", attachment_name)
|
|
202
215
|
return
|
|
203
|
-
|
|
216
|
+
elif raw_data is not None:
|
|
204
217
|
if not force and attachment.file_size == len(raw_data):
|
|
205
218
|
LOGGER.info("Up-to-date embedded image: %s", attachment_name)
|
|
206
219
|
return
|
|
220
|
+
else:
|
|
221
|
+
raise NotImplementedError("never occurs")
|
|
207
222
|
|
|
208
223
|
id = removeprefix(attachment.id, "att")
|
|
209
224
|
path = f"/content/{page_id}/child/attachment/{id}/data"
|
|
@@ -213,7 +228,7 @@ class ConfluenceSession:
|
|
|
213
228
|
|
|
214
229
|
url = self._build_url(path)
|
|
215
230
|
|
|
216
|
-
if not
|
|
231
|
+
if attachment_path is not None:
|
|
217
232
|
with open(attachment_path, "rb") as attachment_file:
|
|
218
233
|
file_to_upload = {
|
|
219
234
|
"comment": comment,
|
|
@@ -230,24 +245,27 @@ class ConfluenceSession:
|
|
|
230
245
|
files=file_to_upload, # type: ignore
|
|
231
246
|
headers={"X-Atlassian-Token": "no-check"},
|
|
232
247
|
)
|
|
233
|
-
|
|
248
|
+
elif raw_data is not None:
|
|
234
249
|
LOGGER.info("Uploading raw data: %s", attachment_name)
|
|
235
250
|
|
|
251
|
+
raw_file = io.BytesIO(raw_data)
|
|
252
|
+
raw_file.name = attachment_name
|
|
236
253
|
file_to_upload = {
|
|
237
254
|
"comment": comment,
|
|
238
255
|
"file": (
|
|
239
256
|
attachment_name, # will truncate path component
|
|
240
|
-
|
|
257
|
+
raw_file, # type: ignore
|
|
241
258
|
content_type,
|
|
242
259
|
{"Expires": "0"},
|
|
243
260
|
),
|
|
244
261
|
}
|
|
245
|
-
|
|
246
262
|
response = self.session.post(
|
|
247
263
|
url,
|
|
248
264
|
files=file_to_upload, # type: ignore
|
|
249
265
|
headers={"X-Atlassian-Token": "no-check"},
|
|
250
266
|
)
|
|
267
|
+
else:
|
|
268
|
+
raise NotImplementedError("never occurs")
|
|
251
269
|
|
|
252
270
|
response.raise_for_status()
|
|
253
271
|
data = response.json()
|
md2conf/application.py
CHANGED
|
@@ -52,17 +52,31 @@ class Application:
|
|
|
52
52
|
else:
|
|
53
53
|
raise ValueError(f"expected: valid file or directory path; got: {path}")
|
|
54
54
|
|
|
55
|
-
def synchronize_page(
|
|
55
|
+
def synchronize_page(
|
|
56
|
+
self, page_path: Path, root_dir: Optional[Path] = None
|
|
57
|
+
) -> None:
|
|
56
58
|
"Synchronizes a single Markdown page with Confluence."
|
|
57
59
|
|
|
58
60
|
page_path = page_path.resolve(True)
|
|
59
|
-
|
|
61
|
+
if root_dir is None:
|
|
62
|
+
root_dir = page_path.parent
|
|
63
|
+
else:
|
|
64
|
+
root_dir = root_dir.resolve(True)
|
|
65
|
+
|
|
66
|
+
self._synchronize_page(page_path, root_dir, {})
|
|
60
67
|
|
|
61
|
-
def synchronize_directory(
|
|
68
|
+
def synchronize_directory(
|
|
69
|
+
self, local_dir: Path, root_dir: Optional[Path] = None
|
|
70
|
+
) -> None:
|
|
62
71
|
"Synchronizes a directory of Markdown pages with Confluence."
|
|
63
72
|
|
|
64
|
-
LOGGER.info("Synchronizing directory: %s", local_dir)
|
|
65
73
|
local_dir = local_dir.resolve(True)
|
|
74
|
+
if root_dir is None:
|
|
75
|
+
root_dir = local_dir
|
|
76
|
+
else:
|
|
77
|
+
root_dir = root_dir.resolve(True)
|
|
78
|
+
|
|
79
|
+
LOGGER.info("Synchronizing directory: %s", local_dir)
|
|
66
80
|
|
|
67
81
|
# Step 1: build index of all page metadata
|
|
68
82
|
page_metadata: Dict[Path, ConfluencePageMetadata] = {}
|
|
@@ -76,17 +90,18 @@ class Application:
|
|
|
76
90
|
|
|
77
91
|
# Step 2: convert each page
|
|
78
92
|
for page_path in page_metadata.keys():
|
|
79
|
-
self._synchronize_page(page_path, page_metadata)
|
|
93
|
+
self._synchronize_page(page_path, root_dir, page_metadata)
|
|
80
94
|
|
|
81
95
|
def _synchronize_page(
|
|
82
96
|
self,
|
|
83
97
|
page_path: Path,
|
|
98
|
+
root_dir: Path,
|
|
84
99
|
page_metadata: Dict[Path, ConfluencePageMetadata],
|
|
85
100
|
) -> None:
|
|
86
101
|
base_path = page_path.parent
|
|
87
102
|
|
|
88
103
|
LOGGER.info("Synchronizing page: %s", page_path)
|
|
89
|
-
document = ConfluenceDocument(page_path, self.options, page_metadata)
|
|
104
|
+
document = ConfluenceDocument(page_path, self.options, root_dir, page_metadata)
|
|
90
105
|
|
|
91
106
|
if document.id.space_key:
|
|
92
107
|
with self.api.switch_space(document.id.space_key):
|
|
@@ -221,15 +236,14 @@ class Application:
|
|
|
221
236
|
for image in document.images:
|
|
222
237
|
self.api.upload_attachment(
|
|
223
238
|
document.id.page_id,
|
|
224
|
-
base_path / image,
|
|
225
239
|
attachment_name(image),
|
|
240
|
+
attachment_path=base_path / image,
|
|
226
241
|
)
|
|
227
242
|
|
|
228
|
-
for
|
|
243
|
+
for name, data in document.embedded_images.items():
|
|
229
244
|
self.api.upload_attachment(
|
|
230
245
|
document.id.page_id,
|
|
231
|
-
|
|
232
|
-
attachment_name(image),
|
|
246
|
+
name,
|
|
233
247
|
raw_data=data,
|
|
234
248
|
)
|
|
235
249
|
|
md2conf/converter.py
CHANGED
|
@@ -18,7 +18,7 @@ import uuid
|
|
|
18
18
|
import xml.etree.ElementTree
|
|
19
19
|
from dataclasses import dataclass
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
21
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
|
22
22
|
from urllib.parse import ParseResult, urlparse, urlunparse
|
|
23
23
|
|
|
24
24
|
import lxml.etree as ET
|
|
@@ -301,9 +301,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
301
301
|
|
|
302
302
|
options: ConfluenceConverterOptions
|
|
303
303
|
path: Path
|
|
304
|
-
|
|
304
|
+
base_dir: Path
|
|
305
|
+
root_dir: Path
|
|
305
306
|
links: List[str]
|
|
306
|
-
images: List[
|
|
307
|
+
images: List[Path]
|
|
307
308
|
embedded_images: Dict[str, bytes]
|
|
308
309
|
page_metadata: Dict[Path, ConfluencePageMetadata]
|
|
309
310
|
|
|
@@ -311,12 +312,14 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
311
312
|
self,
|
|
312
313
|
options: ConfluenceConverterOptions,
|
|
313
314
|
path: Path,
|
|
315
|
+
root_dir: Path,
|
|
314
316
|
page_metadata: Dict[Path, ConfluencePageMetadata],
|
|
315
317
|
) -> None:
|
|
316
318
|
super().__init__()
|
|
317
319
|
self.options = options
|
|
318
320
|
self.path = path
|
|
319
|
-
self.
|
|
321
|
+
self.base_dir = path.parent
|
|
322
|
+
self.root_dir = root_dir
|
|
320
323
|
self.links = []
|
|
321
324
|
self.images = []
|
|
322
325
|
self.embedded_images = {}
|
|
@@ -383,9 +386,9 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
383
386
|
# convert the relative URL to absolute URL based on the base path value, then look up
|
|
384
387
|
# the absolute path in the page metadata dictionary to discover the relative path
|
|
385
388
|
# within Confluence that should be used
|
|
386
|
-
absolute_path = (self.
|
|
387
|
-
if not str(absolute_path).startswith(str(self.
|
|
388
|
-
msg = f"relative URL {url} points to outside
|
|
389
|
+
absolute_path = (self.base_dir / relative_url.path).resolve(True)
|
|
390
|
+
if not str(absolute_path).startswith(str(self.root_dir)):
|
|
391
|
+
msg = f"relative URL {url} points to outside root path: {self.root_dir}"
|
|
389
392
|
if self.options.ignore_invalid_url:
|
|
390
393
|
LOGGER.warning(msg)
|
|
391
394
|
anchor.attrib.pop("href")
|
|
@@ -393,8 +396,6 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
393
396
|
else:
|
|
394
397
|
raise DocumentError(msg)
|
|
395
398
|
|
|
396
|
-
relative_path = os.path.relpath(absolute_path, self.base_path)
|
|
397
|
-
|
|
398
399
|
link_metadata = self.page_metadata.get(absolute_path)
|
|
399
400
|
if link_metadata is None:
|
|
400
401
|
msg = f"unable to find matching page for URL: {url}"
|
|
@@ -405,6 +406,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
405
406
|
else:
|
|
406
407
|
raise DocumentError(msg)
|
|
407
408
|
|
|
409
|
+
relative_path = os.path.relpath(absolute_path, self.base_dir)
|
|
408
410
|
LOGGER.debug(
|
|
409
411
|
"found link to page %s with metadata: %s", relative_path, link_metadata
|
|
410
412
|
)
|
|
@@ -432,17 +434,24 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
432
434
|
def _transform_image(self, image: ET._Element) -> ET._Element:
|
|
433
435
|
path: str = image.attrib["src"]
|
|
434
436
|
|
|
437
|
+
if not path:
|
|
438
|
+
raise DocumentError("image lacks `src` attribute")
|
|
439
|
+
|
|
440
|
+
if is_absolute_url(path):
|
|
441
|
+
# images whose `src` attribute is an absolute URL cannot be converted into an `ac:image`;
|
|
442
|
+
# Confluence images are expected to refer to an uploaded attachment
|
|
443
|
+
raise DocumentError("image has a `src` attribute that is an absolute URL")
|
|
444
|
+
|
|
445
|
+
relative_path = Path(path)
|
|
446
|
+
|
|
435
447
|
# prefer PNG over SVG; Confluence displays SVG in wrong size, and text labels are truncated
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
):
|
|
442
|
-
path = str(relative_path.with_suffix(".png"))
|
|
443
|
-
|
|
444
|
-
self.images.append(path)
|
|
448
|
+
png_file = relative_path.with_suffix(".png")
|
|
449
|
+
if relative_path.suffix == ".svg" and (self.base_dir / png_file).exists():
|
|
450
|
+
relative_path = png_file
|
|
451
|
+
|
|
452
|
+
self.images.append(relative_path)
|
|
445
453
|
caption = image.attrib["alt"]
|
|
454
|
+
image_name = attachment_name(relative_path)
|
|
446
455
|
return AC(
|
|
447
456
|
"image",
|
|
448
457
|
{
|
|
@@ -451,7 +460,8 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
451
460
|
},
|
|
452
461
|
RI(
|
|
453
462
|
"attachment",
|
|
454
|
-
|
|
463
|
+
# refers to an attachment uploaded alongside the page
|
|
464
|
+
{ET.QName(namespaces["ri"], "filename"): image_name},
|
|
455
465
|
),
|
|
456
466
|
AC("caption", HTML.p(caption)),
|
|
457
467
|
)
|
|
@@ -757,6 +767,9 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
757
767
|
tail: str = child.tail
|
|
758
768
|
child.tail = tail.replace("\n", " ")
|
|
759
769
|
|
|
770
|
+
if not isinstance(child.tag, str):
|
|
771
|
+
return None
|
|
772
|
+
|
|
760
773
|
if self.options.heading_anchors:
|
|
761
774
|
# <h1>...</h1>
|
|
762
775
|
# <h2>...</h2> ...
|
|
@@ -932,7 +945,7 @@ class ConfluenceDocumentOptions:
|
|
|
932
945
|
class ConfluenceDocument:
|
|
933
946
|
id: ConfluenceQualifiedID
|
|
934
947
|
links: List[str]
|
|
935
|
-
images: List[
|
|
948
|
+
images: List[Path]
|
|
936
949
|
|
|
937
950
|
options: ConfluenceDocumentOptions
|
|
938
951
|
root: ET._Element
|
|
@@ -941,10 +954,11 @@ class ConfluenceDocument:
|
|
|
941
954
|
self,
|
|
942
955
|
path: Path,
|
|
943
956
|
options: ConfluenceDocumentOptions,
|
|
957
|
+
root_dir: Path,
|
|
944
958
|
page_metadata: Dict[Path, ConfluencePageMetadata],
|
|
945
959
|
) -> None:
|
|
946
960
|
self.options = options
|
|
947
|
-
path = path.
|
|
961
|
+
path = path.resolve(True)
|
|
948
962
|
|
|
949
963
|
with open(path, "r", encoding="utf-8") as f:
|
|
950
964
|
text = f.read()
|
|
@@ -998,6 +1012,7 @@ class ConfluenceDocument:
|
|
|
998
1012
|
webui_links=self.options.webui_links,
|
|
999
1013
|
),
|
|
1000
1014
|
path,
|
|
1015
|
+
root_dir,
|
|
1001
1016
|
page_metadata,
|
|
1002
1017
|
)
|
|
1003
1018
|
converter.visit(self.root)
|
|
@@ -1009,7 +1024,7 @@ class ConfluenceDocument:
|
|
|
1009
1024
|
return elements_to_string(self.root)
|
|
1010
1025
|
|
|
1011
1026
|
|
|
1012
|
-
def attachment_name(name: str) -> str:
|
|
1027
|
+
def attachment_name(name: Union[Path, str]) -> str:
|
|
1013
1028
|
"""
|
|
1014
1029
|
Safe name for use with attachment uploads.
|
|
1015
1030
|
|
|
@@ -1018,7 +1033,7 @@ def attachment_name(name: str) -> str:
|
|
|
1018
1033
|
* Special characters: hyphen (-), underscore (_), period (.)
|
|
1019
1034
|
"""
|
|
1020
1035
|
|
|
1021
|
-
return re.sub(r"[^\-0-9A-Za-z_.]", "_", name)
|
|
1036
|
+
return re.sub(r"[^\-0-9A-Za-z_.]", "_", str(name))
|
|
1022
1037
|
|
|
1023
1038
|
|
|
1024
1039
|
def sanitize_confluence(html: str) -> str:
|
md2conf/mermaid.py
CHANGED
|
@@ -56,6 +56,10 @@ def render(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
|
|
|
56
56
|
filename,
|
|
57
57
|
"--outputFormat",
|
|
58
58
|
output_format,
|
|
59
|
+
"--backgroundColor",
|
|
60
|
+
"transparent",
|
|
61
|
+
"--scale",
|
|
62
|
+
"2",
|
|
59
63
|
]
|
|
60
64
|
root = os.path.dirname(__file__)
|
|
61
65
|
if is_docker():
|
md2conf/processor.py
CHANGED
|
@@ -10,7 +10,7 @@ import hashlib
|
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Dict, List
|
|
13
|
+
from typing import Dict, List, Optional
|
|
14
14
|
|
|
15
15
|
from .converter import (
|
|
16
16
|
ConfluenceDocument,
|
|
@@ -42,15 +42,22 @@ class Processor:
|
|
|
42
42
|
if path.is_dir():
|
|
43
43
|
self.process_directory(path)
|
|
44
44
|
elif path.is_file():
|
|
45
|
-
self.process_page(path
|
|
45
|
+
self.process_page(path)
|
|
46
46
|
else:
|
|
47
47
|
raise ValueError(f"expected: valid file or directory path; got: {path}")
|
|
48
48
|
|
|
49
|
-
def process_directory(
|
|
49
|
+
def process_directory(
|
|
50
|
+
self, local_dir: Path, root_dir: Optional[Path] = None
|
|
51
|
+
) -> None:
|
|
50
52
|
"Recursively scans a directory hierarchy for Markdown files."
|
|
51
53
|
|
|
52
|
-
LOGGER.info("Synchronizing directory: %s", local_dir)
|
|
53
54
|
local_dir = local_dir.resolve(True)
|
|
55
|
+
if root_dir is None:
|
|
56
|
+
root_dir = local_dir
|
|
57
|
+
else:
|
|
58
|
+
root_dir = root_dir.resolve(True)
|
|
59
|
+
|
|
60
|
+
LOGGER.info("Synchronizing directory: %s", local_dir)
|
|
54
61
|
|
|
55
62
|
# Step 1: build index of all page metadata
|
|
56
63
|
page_metadata: Dict[Path, ConfluencePageMetadata] = {}
|
|
@@ -59,15 +66,28 @@ class Processor:
|
|
|
59
66
|
|
|
60
67
|
# Step 2: convert each page
|
|
61
68
|
for page_path in page_metadata.keys():
|
|
62
|
-
self.
|
|
69
|
+
self._process_page(page_path, root_dir, page_metadata)
|
|
63
70
|
|
|
64
|
-
def process_page(
|
|
65
|
-
self, path: Path, page_metadata: Dict[Path, ConfluencePageMetadata]
|
|
66
|
-
) -> None:
|
|
71
|
+
def process_page(self, path: Path, root_dir: Optional[Path] = None) -> None:
|
|
67
72
|
"Processes a single Markdown file."
|
|
68
73
|
|
|
69
74
|
path = path.resolve(True)
|
|
70
|
-
|
|
75
|
+
if root_dir is None:
|
|
76
|
+
root_dir = path.parent
|
|
77
|
+
else:
|
|
78
|
+
root_dir = root_dir.resolve(True)
|
|
79
|
+
|
|
80
|
+
self._process_page(path, root_dir, {})
|
|
81
|
+
|
|
82
|
+
def _process_page(
|
|
83
|
+
self,
|
|
84
|
+
path: Path,
|
|
85
|
+
root_dir: Path,
|
|
86
|
+
page_metadata: Dict[Path, ConfluencePageMetadata],
|
|
87
|
+
) -> None:
|
|
88
|
+
"Processes a single Markdown file."
|
|
89
|
+
|
|
90
|
+
document = ConfluenceDocument(path, self.options, root_dir, page_metadata)
|
|
71
91
|
content = document.xhtml()
|
|
72
92
|
with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
|
|
73
93
|
f.write(content)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
md2conf/__init__.py,sha256=0eak9lvskuCqGJnGeno6SHoCiBFAX5IQLHVBx1LV0w8,402
|
|
2
|
-
md2conf/__main__.py,sha256=6iOI28W_d71tlnCMFpZwvkBmBt5-HazlZsz69gS4Oak,6894
|
|
3
|
-
md2conf/api.py,sha256=EZSHbuH5O9fPyW7iLAX0Fqw8njXmvd6sEbgseP-eUUc,16498
|
|
4
|
-
md2conf/application.py,sha256=hmfLiofGulN8zUw2uXuueohCkDh978sqLkoUot928qM,8796
|
|
5
|
-
md2conf/converter.py,sha256=8X8tNELqwAaZYSVvczJl_ZpJL9tu2ImCBXaQBQvGgeM,34413
|
|
6
|
-
md2conf/emoji.py,sha256=w9oiOIxzObAE7HTo3f6aETT1_D3t3yZwr88ynU4ENm0,1924
|
|
7
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
8
|
-
md2conf/matcher.py,sha256=mYMltZOLypK4O-SJugLgicOwUMem67hiNLg_kPFoJkU,3583
|
|
9
|
-
md2conf/mermaid.py,sha256=Tsibd1aOn4hRYv6emQg0hrZMPTkflIeXHVbZ7nQ5lSc,2108
|
|
10
|
-
md2conf/processor.py,sha256=tUt5D4_D3uhofg2Bn23owBJmkVHj4tSll0zI95J6cdk,4243
|
|
11
|
-
md2conf/properties.py,sha256=iVIc0h0XtS3Y2LCywX1C9cvmVQ0WljOMt8pl2MDMVCI,1990
|
|
12
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
13
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
md2conf/util.py,sha256=ftf60MiW7S7rW45ipWX6efP_Sv2F2qpyIDHrGA0cBiw,743
|
|
15
|
-
markdown_to_confluence-0.2.5.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
16
|
-
markdown_to_confluence-0.2.5.dist-info/METADATA,sha256=E7j_aFJ7rT4SOpoUIa40G2QJL_7PjuXBA5JvdANRIdc,12764
|
|
17
|
-
markdown_to_confluence-0.2.5.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
18
|
-
markdown_to_confluence-0.2.5.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
19
|
-
markdown_to_confluence-0.2.5.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
20
|
-
markdown_to_confluence-0.2.5.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
21
|
-
markdown_to_confluence-0.2.5.dist-info/RECORD,,
|
|
File without changes
|
{markdown_to_confluence-0.2.5.dist-info → markdown_to_confluence-0.2.6.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.2.5.dist-info → markdown_to_confluence-0.2.6.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|