markdown-to-confluence 0.1.12__py3-none-any.whl → 0.2.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.
- {markdown_to_confluence-0.1.12.dist-info → markdown_to_confluence-0.2.0.dist-info}/METADATA +68 -15
- markdown_to_confluence-0.2.0.dist-info/RECORD +17 -0
- {markdown_to_confluence-0.1.12.dist-info → markdown_to_confluence-0.2.0.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +31 -2
- md2conf/api.py +35 -8
- md2conf/application.py +14 -3
- md2conf/converter.py +258 -14
- md2conf/mermaid.py +54 -0
- md2conf/processor.py +2 -2
- markdown_to_confluence-0.1.12.dist-info/RECORD +0 -16
- {markdown_to_confluence-0.1.12.dist-info → markdown_to_confluence-0.2.0.dist-info}/LICENSE +0 -0
- {markdown_to_confluence-0.1.12.dist-info → markdown_to_confluence-0.2.0.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.1.12.dist-info → markdown_to_confluence-0.2.0.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.1.12.dist-info → markdown_to_confluence-0.2.0.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.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -21,13 +21,13 @@ Classifier: Typing :: Typed
|
|
|
21
21
|
Requires-Python: >=3.8
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: lxml
|
|
25
|
-
Requires-Dist: types-lxml
|
|
26
|
-
Requires-Dist: markdown
|
|
27
|
-
Requires-Dist: types-markdown
|
|
28
|
-
Requires-Dist: pymdown-extensions
|
|
29
|
-
Requires-Dist: requests
|
|
30
|
-
Requires-Dist: types-requests
|
|
24
|
+
Requires-Dist: lxml>=5.3
|
|
25
|
+
Requires-Dist: types-lxml>=2024.8.7
|
|
26
|
+
Requires-Dist: markdown>=3.6
|
|
27
|
+
Requires-Dist: types-markdown>=3.6
|
|
28
|
+
Requires-Dist: pymdown-extensions>=10.9
|
|
29
|
+
Requires-Dist: requests>=2.32
|
|
30
|
+
Requires-Dist: types-requests>=2.32
|
|
31
31
|
|
|
32
32
|
# Publish Markdown files to Confluence wiki
|
|
33
33
|
|
|
@@ -45,12 +45,29 @@ This Python package
|
|
|
45
45
|
|
|
46
46
|
* Sections and subsections
|
|
47
47
|
* Text with **bold**, *italic*, `monospace`, <ins>underline</ins> and ~~strikethrough~~
|
|
48
|
-
* Link to [external locations](http://example.com/)
|
|
48
|
+
* Link to [sections on the same page](#getting-started) or [external locations](http://example.com/)
|
|
49
49
|
* Ordered and unordered lists
|
|
50
50
|
* Code blocks (e.g. Python, JSON, XML)
|
|
51
51
|
* Image references (uploaded as Confluence page attachments)
|
|
52
|
-
*
|
|
53
|
-
* [
|
|
52
|
+
* Tables
|
|
53
|
+
* [Table of contents](https://docs.gitlab.com/ee/user/markdown.html#table-of-contents)
|
|
54
|
+
* [Admonitions](https://python-markdown.github.io/extensions/admonition/) and [alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts)
|
|
55
|
+
* [Collapsed sections](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections)
|
|
56
|
+
* [Mermaid diagrams](https://mermaid.live/) in code blocks (converted to images)
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
Install the core package from PyPI:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pip install markdown-to-confluence
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Converting code blocks of Mermaid diagrams into Confluence image attachments requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli):
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm install -g @mermaid-js/mermaid-cli
|
|
70
|
+
```
|
|
54
71
|
|
|
55
72
|
## Getting started
|
|
56
73
|
|
|
@@ -131,7 +148,7 @@ Alternatively, use the `--generated-by GENERATED_BY` option. The tag takes prece
|
|
|
131
148
|
|
|
132
149
|
You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
|
|
133
150
|
|
|
134
|
-
```
|
|
151
|
+
```sh
|
|
135
152
|
$ python3 -m md2conf sample/example.md
|
|
136
153
|
```
|
|
137
154
|
|
|
@@ -139,14 +156,15 @@ Use the `--help` switch to get a full list of supported command-line options:
|
|
|
139
156
|
|
|
140
157
|
```console
|
|
141
158
|
$ python3 -m md2conf --help
|
|
142
|
-
usage: md2conf [-h] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE] [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE]
|
|
143
|
-
[--no-generated-by] [--
|
|
159
|
+
usage: md2conf [-h] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE] [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE]
|
|
160
|
+
[--generated-by GENERATED_BY] [--no-generated-by] [--render-mermaid] [--no-render-mermaid]
|
|
161
|
+
[--render-mermaid-format {png,svg}] [--heading-anchors] [--ignore-invalid-url] [--local]
|
|
144
162
|
mdpath
|
|
145
163
|
|
|
146
164
|
positional arguments:
|
|
147
165
|
mdpath Path to Markdown file or directory to convert and publish.
|
|
148
166
|
|
|
149
|
-
|
|
167
|
+
options:
|
|
150
168
|
-h, --help show this help message and exit
|
|
151
169
|
-d DOMAIN, --domain DOMAIN
|
|
152
170
|
Confluence organization domain.
|
|
@@ -163,6 +181,41 @@ optional arguments:
|
|
|
163
181
|
--generated-by GENERATED_BY
|
|
164
182
|
Add prompt to pages (default: 'This page has been generated with a tool.').
|
|
165
183
|
--no-generated-by Do not add 'generated by a tool' prompt to pages.
|
|
184
|
+
--render-mermaid Render Mermaid diagrams as image files and add as attachments.
|
|
185
|
+
--no-render-mermaid Inline Mermaid diagram in Confluence page.
|
|
186
|
+
--render-mermaid-format {png,svg}
|
|
187
|
+
Format for rendering Mermaid diagrams (default: 'png').
|
|
188
|
+
--heading-anchors Place an anchor at each section heading with GitHub-style same-page identifiers.
|
|
166
189
|
--ignore-invalid-url Emit a warning but otherwise ignore relative URLs that point to ill-specified locations.
|
|
167
190
|
--local Write XHTML-based Confluence Storage Format files locally without invoking Confluence API.
|
|
168
191
|
```
|
|
192
|
+
|
|
193
|
+
### Using the docker container
|
|
194
|
+
|
|
195
|
+
You can run the docker container via `docker run` or via `Dockerfile`. Either can accept the environment variables or arguments similar to the Python options. The final argument `./` corresponds to `mdpath` in the command-line utility.
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
docker run --rm --name md2conf hunyadi/md2conf -d instructure.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s DAP ./
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Note that the entry point for the docker container's base image is `ENTRYPOINT ["python3", "-m", "md2conf"]`.
|
|
202
|
+
|
|
203
|
+
```Dockerfile
|
|
204
|
+
FROM hunyadi/md2conf:latest
|
|
205
|
+
|
|
206
|
+
ENV CONFLUENCE_DOMAIN='instructure.atlassian.net'
|
|
207
|
+
ENV CONFLUENCE_PATH='/wiki/'
|
|
208
|
+
ENV CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
|
|
209
|
+
ENV CONFLUENCE_API_KEY='0123456789abcdef'
|
|
210
|
+
ENV CONFLUENCE_SPACE_KEY='DAP'
|
|
211
|
+
|
|
212
|
+
CMD ["./"]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Alternatively,
|
|
216
|
+
|
|
217
|
+
```Dockerfile
|
|
218
|
+
FROM hunyadi/md2conf:latest
|
|
219
|
+
|
|
220
|
+
CMD ["-d", "instructure.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "DAP", "./"]
|
|
221
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
md2conf/__init__.py,sha256=1KRpqiilQTkQz-oL8-HFPnI_6_3-_H0dq-SxQxDw56s,402
|
|
2
|
+
md2conf/__main__.py,sha256=tWMEA_spxUTNNgViHtjsA85NzJixX-0G2zCq8BO3y_E,5230
|
|
3
|
+
md2conf/api.py,sha256=Oc4FAQBNs85U8s-lbY0XwLBUcjm3Sd0_W59N4H3XAnE,15768
|
|
4
|
+
md2conf/application.py,sha256=NnF84-cdW2cZUbU6VeHvuEg6g5NL5M9o2cpOSU7uv7o,5548
|
|
5
|
+
md2conf/converter.py,sha256=XY7D8zpsVS7_PZzywciQ5YT2SHH5t1udPU5s2aPsmqs,27040
|
|
6
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
7
|
+
md2conf/mermaid.py,sha256=3zawPXHXkCDhEK-WNtCH-gTqsLBDRzLrmlSo8ZW-Ii8,1371
|
|
8
|
+
md2conf/processor.py,sha256=3JZkbFtMjbtnQLEm6wFum96ldjZ9xNJuL8JjFadyGmg,3084
|
|
9
|
+
md2conf/properties.py,sha256=oXvtPssbougM1BTE9ytcD_1Yjc3nd7DDSHqEr0QoZAU,1811
|
|
10
|
+
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
markdown_to_confluence-0.2.0.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
12
|
+
markdown_to_confluence-0.2.0.dist-info/METADATA,sha256=nxwG4F2TX1do0lk38BCFIUMwqv6y2edErd2_5M-4la4,10023
|
|
13
|
+
markdown_to_confluence-0.2.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
14
|
+
markdown_to_confluence-0.2.0.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
15
|
+
markdown_to_confluence-0.2.0.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
16
|
+
markdown_to_confluence-0.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
17
|
+
markdown_to_confluence-0.2.0.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.
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2024, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
md2conf/__main__.py
CHANGED
|
@@ -2,7 +2,6 @@ import argparse
|
|
|
2
2
|
import logging
|
|
3
3
|
import os.path
|
|
4
4
|
import sys
|
|
5
|
-
import typing
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Optional
|
|
8
7
|
|
|
@@ -24,6 +23,7 @@ class Arguments(argparse.Namespace):
|
|
|
24
23
|
space: str
|
|
25
24
|
loglevel: str
|
|
26
25
|
ignore_invalid_url: bool
|
|
26
|
+
heading_anchors: bool
|
|
27
27
|
generated_by: Optional[str]
|
|
28
28
|
|
|
29
29
|
|
|
@@ -52,7 +52,7 @@ def main() -> None:
|
|
|
52
52
|
"-l",
|
|
53
53
|
"--loglevel",
|
|
54
54
|
choices=[
|
|
55
|
-
|
|
55
|
+
logging.getLevelName(level).lower()
|
|
56
56
|
for level in (
|
|
57
57
|
logging.DEBUG,
|
|
58
58
|
logging.INFO,
|
|
@@ -81,6 +81,32 @@ def main() -> None:
|
|
|
81
81
|
const=None,
|
|
82
82
|
help="Do not add 'generated by a tool' prompt to pages.",
|
|
83
83
|
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--render-mermaid",
|
|
86
|
+
dest="render_mermaid",
|
|
87
|
+
action="store_true",
|
|
88
|
+
default=True,
|
|
89
|
+
help="Render Mermaid diagrams as image files and add as attachments.",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--no-render-mermaid",
|
|
93
|
+
dest="render_mermaid",
|
|
94
|
+
action="store_false",
|
|
95
|
+
help="Inline Mermaid diagram in Confluence page.",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--render-mermaid-format",
|
|
99
|
+
dest="diagram_output_format",
|
|
100
|
+
choices=["png", "svg"],
|
|
101
|
+
default="png",
|
|
102
|
+
help="Format for rendering Mermaid diagrams (default: 'png').",
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--heading-anchors",
|
|
106
|
+
action="store_true",
|
|
107
|
+
default=False,
|
|
108
|
+
help="Place an anchor at each section heading with GitHub-style same-page identifiers.",
|
|
109
|
+
)
|
|
84
110
|
parser.add_argument(
|
|
85
111
|
"--ignore-invalid-url",
|
|
86
112
|
action="store_true",
|
|
@@ -107,9 +133,12 @@ def main() -> None:
|
|
|
107
133
|
)
|
|
108
134
|
|
|
109
135
|
options = ConfluenceDocumentOptions(
|
|
136
|
+
heading_anchors=args.heading_anchors,
|
|
110
137
|
ignore_invalid_url=args.ignore_invalid_url,
|
|
111
138
|
generated_by=args.generated_by,
|
|
112
139
|
root_page_id=args.root_page,
|
|
140
|
+
render_mermaid=args.render_mermaid,
|
|
141
|
+
diagram_output_format=args.diagram_output_format,
|
|
113
142
|
)
|
|
114
143
|
properties = ConfluenceProperties(
|
|
115
144
|
args.domain, args.path, args.username, args.apikey, args.space
|
md2conf/api.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
import mimetypes
|
|
@@ -177,7 +178,8 @@ class ConfluenceSession:
|
|
|
177
178
|
extensions = typing.cast(Dict[str, JsonType], result["extensions"])
|
|
178
179
|
media_type = typing.cast(str, extensions["mediaType"])
|
|
179
180
|
file_size = typing.cast(int, extensions["fileSize"])
|
|
180
|
-
comment =
|
|
181
|
+
comment = extensions.get("comment", "")
|
|
182
|
+
comment = typing.cast(str, comment)
|
|
181
183
|
return ConfluenceAttachment(id, media_type, file_size, comment)
|
|
182
184
|
|
|
183
185
|
def upload_attachment(
|
|
@@ -185,6 +187,7 @@ class ConfluenceSession:
|
|
|
185
187
|
page_id: str,
|
|
186
188
|
attachment_path: Path,
|
|
187
189
|
attachment_name: str,
|
|
190
|
+
raw_data: Optional[bytes] = None,
|
|
188
191
|
comment: Optional[str] = None,
|
|
189
192
|
*,
|
|
190
193
|
space_key: Optional[str] = None,
|
|
@@ -192,7 +195,7 @@ class ConfluenceSession:
|
|
|
192
195
|
) -> None:
|
|
193
196
|
content_type = mimetypes.guess_type(attachment_path, strict=True)[0]
|
|
194
197
|
|
|
195
|
-
if not attachment_path.is_file():
|
|
198
|
+
if not raw_data and not attachment_path.is_file():
|
|
196
199
|
raise ConfluenceError(f"file not found: {attachment_path}")
|
|
197
200
|
|
|
198
201
|
try:
|
|
@@ -200,9 +203,14 @@ class ConfluenceSession:
|
|
|
200
203
|
page_id, attachment_name, space_key=space_key
|
|
201
204
|
)
|
|
202
205
|
|
|
203
|
-
if not
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
if not raw_data:
|
|
207
|
+
if not force and attachment.file_size == attachment_path.stat().st_size:
|
|
208
|
+
LOGGER.info("Up-to-date attachment: %s", attachment_name)
|
|
209
|
+
return
|
|
210
|
+
else:
|
|
211
|
+
if not force and attachment.file_size == len(raw_data):
|
|
212
|
+
LOGGER.info("Up-to-date embedded image: %s", attachment_name)
|
|
213
|
+
return
|
|
206
214
|
|
|
207
215
|
id = removeprefix(attachment.id, "att")
|
|
208
216
|
path = f"/content/{page_id}/child/attachment/{id}/data"
|
|
@@ -212,17 +220,36 @@ class ConfluenceSession:
|
|
|
212
220
|
|
|
213
221
|
url = self._build_url(path)
|
|
214
222
|
|
|
215
|
-
|
|
223
|
+
if not raw_data:
|
|
224
|
+
with open(attachment_path, "rb") as attachment_file:
|
|
225
|
+
file_to_upload = {
|
|
226
|
+
"comment": comment,
|
|
227
|
+
"file": (
|
|
228
|
+
attachment_name, # will truncate path component
|
|
229
|
+
attachment_file,
|
|
230
|
+
content_type,
|
|
231
|
+
{"Expires": "0"},
|
|
232
|
+
),
|
|
233
|
+
}
|
|
234
|
+
LOGGER.info("Uploading attachment: %s", attachment_name)
|
|
235
|
+
response = self.session.post(
|
|
236
|
+
url,
|
|
237
|
+
files=file_to_upload, # type: ignore
|
|
238
|
+
headers={"X-Atlassian-Token": "no-check"},
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
LOGGER.info("Uploading raw data: %s", attachment_name)
|
|
242
|
+
|
|
216
243
|
file_to_upload = {
|
|
217
244
|
"comment": comment,
|
|
218
245
|
"file": (
|
|
219
246
|
attachment_name, # will truncate path component
|
|
220
|
-
|
|
247
|
+
io.BytesIO(raw_data), # type: ignore
|
|
221
248
|
content_type,
|
|
222
249
|
{"Expires": "0"},
|
|
223
250
|
),
|
|
224
251
|
}
|
|
225
|
-
|
|
252
|
+
|
|
226
253
|
response = self.session.post(
|
|
227
254
|
url,
|
|
228
255
|
files=file_to_upload, # type: ignore
|
md2conf/application.py
CHANGED
|
@@ -94,7 +94,7 @@ class Application:
|
|
|
94
94
|
"""
|
|
95
95
|
|
|
96
96
|
# parse file
|
|
97
|
-
with open(absolute_path, "r") as f:
|
|
97
|
+
with open(absolute_path, "r", encoding="utf-8") as f:
|
|
98
98
|
document = f.read()
|
|
99
99
|
|
|
100
100
|
qualified_id, document = extract_qualified_id(document)
|
|
@@ -131,9 +131,20 @@ class Application:
|
|
|
131
131
|
)
|
|
132
132
|
|
|
133
133
|
def _update_document(self, document: ConfluenceDocument, base_path: Path) -> None:
|
|
134
|
+
|
|
134
135
|
for image in document.images:
|
|
135
136
|
self.api.upload_attachment(
|
|
136
|
-
document.id.page_id,
|
|
137
|
+
document.id.page_id,
|
|
138
|
+
base_path / image,
|
|
139
|
+
attachment_name(image),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
for image, data in document.embedded_images.items():
|
|
143
|
+
self.api.upload_attachment(
|
|
144
|
+
document.id.page_id,
|
|
145
|
+
Path("EMB") / image,
|
|
146
|
+
attachment_name(image),
|
|
147
|
+
raw_data=data,
|
|
137
148
|
)
|
|
138
149
|
|
|
139
150
|
content = document.xhtml()
|
|
@@ -147,7 +158,7 @@ class Application:
|
|
|
147
158
|
page_id: str,
|
|
148
159
|
space_key: Optional[str],
|
|
149
160
|
) -> None:
|
|
150
|
-
with open(path, "w") as file:
|
|
161
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
151
162
|
file.write(f"<!-- confluence-page-id: {page_id} -->\n")
|
|
152
163
|
if space_key:
|
|
153
164
|
file.write(f"<!-- confluence-space-key: {space_key} -->\n")
|
md2conf/converter.py
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
# mypy: disable-error-code="dict-item"
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
1
4
|
import importlib.resources as resources
|
|
2
5
|
import logging
|
|
3
6
|
import os.path
|
|
4
7
|
import pathlib
|
|
5
8
|
import re
|
|
6
9
|
import sys
|
|
10
|
+
import uuid
|
|
7
11
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Dict, List, Optional, Tuple
|
|
12
|
+
from typing import Dict, List, Literal, Optional, Tuple
|
|
9
13
|
from urllib.parse import ParseResult, urlparse, urlunparse
|
|
10
14
|
|
|
11
15
|
import lxml.etree as ET
|
|
12
16
|
import markdown
|
|
13
17
|
from lxml.builder import ElementMaker
|
|
14
18
|
|
|
19
|
+
from . import mermaid
|
|
20
|
+
|
|
15
21
|
namespaces = {
|
|
16
22
|
"ac": "http://atlassian.com/content",
|
|
17
23
|
"ri": "http://atlassian.com/resource/identifier",
|
|
@@ -19,7 +25,6 @@ namespaces = {
|
|
|
19
25
|
for key, value in namespaces.items():
|
|
20
26
|
ET.register_namespace(key, value)
|
|
21
27
|
|
|
22
|
-
|
|
23
28
|
HTML = ElementMaker()
|
|
24
29
|
AC = ElementMaker(namespace=namespaces["ac"])
|
|
25
30
|
RI = ElementMaker(namespace=namespaces["ri"])
|
|
@@ -51,6 +56,7 @@ def markdown_to_html(content: str) -> str:
|
|
|
51
56
|
"pymdownx.magiclink",
|
|
52
57
|
"pymdownx.tilde",
|
|
53
58
|
"sane_lists",
|
|
59
|
+
"md_in_html",
|
|
54
60
|
],
|
|
55
61
|
)
|
|
56
62
|
|
|
@@ -100,6 +106,10 @@ def elements_from_strings(items: List[str]) -> ET._Element:
|
|
|
100
106
|
return _elements_from_strings(dtd_path, items)
|
|
101
107
|
|
|
102
108
|
|
|
109
|
+
def elements_from_string(content: str) -> ET._Element:
|
|
110
|
+
return elements_from_strings([content])
|
|
111
|
+
|
|
112
|
+
|
|
103
113
|
_languages = [
|
|
104
114
|
"abap",
|
|
105
115
|
"actionscript3",
|
|
@@ -139,6 +149,7 @@ _languages = [
|
|
|
139
149
|
"kotlin",
|
|
140
150
|
"livescript",
|
|
141
151
|
"lua",
|
|
152
|
+
"mermaid",
|
|
142
153
|
"mathematica",
|
|
143
154
|
"matlab",
|
|
144
155
|
"objectivec",
|
|
@@ -192,6 +203,8 @@ class ConfluencePageMetadata:
|
|
|
192
203
|
|
|
193
204
|
class NodeVisitor:
|
|
194
205
|
def visit(self, node: ET._Element) -> None:
|
|
206
|
+
"Recursively visits all descendants of this node."
|
|
207
|
+
|
|
195
208
|
if len(node) < 1:
|
|
196
209
|
return
|
|
197
210
|
|
|
@@ -207,6 +220,15 @@ class NodeVisitor:
|
|
|
207
220
|
pass
|
|
208
221
|
|
|
209
222
|
|
|
223
|
+
def title_to_identifier(title: str) -> str:
|
|
224
|
+
"Converts a section heading title to a GitHub-style Markdown same-page anchor."
|
|
225
|
+
|
|
226
|
+
s = title.strip().lower()
|
|
227
|
+
s = re.sub("[^ A-Za-z0-9]", "", s)
|
|
228
|
+
s = s.replace(" ", "-")
|
|
229
|
+
return s
|
|
230
|
+
|
|
231
|
+
|
|
210
232
|
@dataclass
|
|
211
233
|
class ConfluenceConverterOptions:
|
|
212
234
|
"""
|
|
@@ -214,9 +236,16 @@ class ConfluenceConverterOptions:
|
|
|
214
236
|
|
|
215
237
|
:param ignore_invalid_url: When true, ignore invalid URLs in input, emit a warning and replace the anchor with
|
|
216
238
|
plain text; when false, raise an exception.
|
|
239
|
+
:param heading_anchors: When true, emit a structured macro *anchor* for each section heading using GitHub
|
|
240
|
+
conversion rules for the identifier.
|
|
241
|
+
:param render_mermaid: Whether to pre-render Mermaid diagrams into PNG/SVG images.
|
|
242
|
+
:param diagram_output_format: Target image format for diagrams.
|
|
217
243
|
"""
|
|
218
244
|
|
|
219
245
|
ignore_invalid_url: bool = False
|
|
246
|
+
heading_anchors: bool = False
|
|
247
|
+
render_mermaid: bool = False
|
|
248
|
+
diagram_output_format: Literal["png", "svg"] = "png"
|
|
220
249
|
|
|
221
250
|
|
|
222
251
|
class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
@@ -227,6 +256,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
227
256
|
base_path: pathlib.Path
|
|
228
257
|
links: List[str]
|
|
229
258
|
images: List[str]
|
|
259
|
+
embedded_images: Dict[str, bytes]
|
|
230
260
|
page_metadata: Dict[pathlib.Path, ConfluencePageMetadata]
|
|
231
261
|
|
|
232
262
|
def __init__(
|
|
@@ -241,8 +271,33 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
241
271
|
self.base_path = path.parent
|
|
242
272
|
self.links = []
|
|
243
273
|
self.images = []
|
|
274
|
+
self.embedded_images = {}
|
|
244
275
|
self.page_metadata = page_metadata
|
|
245
276
|
|
|
277
|
+
def _transform_heading(self, heading: ET._Element) -> None:
|
|
278
|
+
title = "".join(heading.itertext()).strip()
|
|
279
|
+
|
|
280
|
+
for e in heading:
|
|
281
|
+
self.visit(e)
|
|
282
|
+
|
|
283
|
+
anchor = AC(
|
|
284
|
+
"structured-macro",
|
|
285
|
+
{
|
|
286
|
+
ET.QName(namespaces["ac"], "name"): "anchor",
|
|
287
|
+
ET.QName(namespaces["ac"], "schema-version"): "1",
|
|
288
|
+
},
|
|
289
|
+
AC(
|
|
290
|
+
"parameter",
|
|
291
|
+
{ET.QName(namespaces["ac"], "name"): ""},
|
|
292
|
+
title_to_identifier(title),
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# insert anchor as first child, pushing any text nodes
|
|
297
|
+
heading.insert(0, anchor)
|
|
298
|
+
anchor.tail = heading.text
|
|
299
|
+
heading.text = None
|
|
300
|
+
|
|
246
301
|
def _transform_link(self, anchor: ET._Element) -> None:
|
|
247
302
|
url = anchor.attrib["href"]
|
|
248
303
|
if is_absolute_url(url):
|
|
@@ -344,20 +399,89 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
344
399
|
language = "none"
|
|
345
400
|
content: str = code.text or ""
|
|
346
401
|
content = content.rstrip()
|
|
402
|
+
|
|
403
|
+
if language == "mermaid":
|
|
404
|
+
return self._transform_mermaid(content)
|
|
405
|
+
|
|
347
406
|
return AC(
|
|
348
407
|
"structured-macro",
|
|
349
408
|
{
|
|
350
409
|
ET.QName(namespaces["ac"], "name"): "code",
|
|
351
410
|
ET.QName(namespaces["ac"], "schema-version"): "1",
|
|
352
411
|
},
|
|
353
|
-
AC("parameter", {ET.QName(namespaces["ac"], "name"): "theme"}, "Midnight"),
|
|
354
|
-
AC("parameter", {ET.QName(namespaces["ac"], "name"): "language"}, language),
|
|
355
412
|
AC(
|
|
356
|
-
"parameter",
|
|
413
|
+
"parameter",
|
|
414
|
+
{ET.QName(namespaces["ac"], "name"): "theme"},
|
|
415
|
+
"Midnight",
|
|
416
|
+
),
|
|
417
|
+
AC(
|
|
418
|
+
"parameter",
|
|
419
|
+
{ET.QName(namespaces["ac"], "name"): "language"},
|
|
420
|
+
language,
|
|
421
|
+
),
|
|
422
|
+
AC(
|
|
423
|
+
"parameter",
|
|
424
|
+
{ET.QName(namespaces["ac"], "name"): "linenumbers"},
|
|
425
|
+
"true",
|
|
357
426
|
),
|
|
358
427
|
AC("plain-text-body", ET.CDATA(content)),
|
|
359
428
|
)
|
|
360
429
|
|
|
430
|
+
def _transform_mermaid(self, content: str) -> ET._Element:
|
|
431
|
+
"Transforms a Mermaid diagram code block."
|
|
432
|
+
|
|
433
|
+
if self.options.render_mermaid:
|
|
434
|
+
image_data = mermaid.render(content, self.options.diagram_output_format)
|
|
435
|
+
image_hash = hashlib.md5(image_data).hexdigest()
|
|
436
|
+
image_filename = attachment_name(
|
|
437
|
+
f"embedded_{image_hash}.{self.options.diagram_output_format}"
|
|
438
|
+
)
|
|
439
|
+
self.embedded_images[image_filename] = image_data
|
|
440
|
+
return AC(
|
|
441
|
+
"image",
|
|
442
|
+
{
|
|
443
|
+
ET.QName(namespaces["ac"], "align"): "center",
|
|
444
|
+
ET.QName(namespaces["ac"], "layout"): "center",
|
|
445
|
+
},
|
|
446
|
+
RI(
|
|
447
|
+
"attachment",
|
|
448
|
+
{ET.QName(namespaces["ri"], "filename"): image_filename},
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
local_id = str(uuid.uuid4())
|
|
453
|
+
macro_id = str(uuid.uuid4())
|
|
454
|
+
return AC(
|
|
455
|
+
"structured-macro",
|
|
456
|
+
{
|
|
457
|
+
ET.QName(namespaces["ac"], "name"): "macro-diagram",
|
|
458
|
+
ET.QName(namespaces["ac"], "schema-version"): "1",
|
|
459
|
+
ET.QName(namespaces["ac"], "data-layout"): "default",
|
|
460
|
+
ET.QName(namespaces["ac"], "local-id"): local_id,
|
|
461
|
+
ET.QName(namespaces["ac"], "macro-id"): macro_id,
|
|
462
|
+
},
|
|
463
|
+
AC(
|
|
464
|
+
"parameter",
|
|
465
|
+
{ET.QName(namespaces["ac"], "name"): "sourceType"},
|
|
466
|
+
"MacroBody",
|
|
467
|
+
),
|
|
468
|
+
AC(
|
|
469
|
+
"parameter",
|
|
470
|
+
{ET.QName(namespaces["ac"], "name"): "attachmentPageId"},
|
|
471
|
+
),
|
|
472
|
+
AC(
|
|
473
|
+
"parameter",
|
|
474
|
+
{ET.QName(namespaces["ac"], "name"): "syntax"},
|
|
475
|
+
"Mermaid",
|
|
476
|
+
),
|
|
477
|
+
AC(
|
|
478
|
+
"parameter",
|
|
479
|
+
{ET.QName(namespaces["ac"], "name"): "attachmentId"},
|
|
480
|
+
),
|
|
481
|
+
AC("parameter", {ET.QName(namespaces["ac"], "name"): "url"}),
|
|
482
|
+
AC("plain-text-body", ET.CDATA(content)),
|
|
483
|
+
)
|
|
484
|
+
|
|
361
485
|
def _transform_toc(self, code: ET._Element) -> ET._Element:
|
|
362
486
|
return AC(
|
|
363
487
|
"structured-macro",
|
|
@@ -371,10 +495,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
371
495
|
|
|
372
496
|
def _transform_admonition(self, elem: ET._Element) -> ET._Element:
|
|
373
497
|
"""
|
|
374
|
-
Creates an info, tip, note or warning panel.
|
|
498
|
+
Creates an info, tip, note or warning panel from a Markdown admonition.
|
|
375
499
|
|
|
376
|
-
Transforms [Python-Markdown admonition](https://python-markdown.github.io/extensions/admonition/)
|
|
377
|
-
into Confluence structured
|
|
500
|
+
Transforms [Python-Markdown admonition](https://python-markdown.github.io/extensions/admonition/)
|
|
501
|
+
syntax into one of the Confluence structured macros *info*, *tip*, *note*, or *warning*.
|
|
378
502
|
"""
|
|
379
503
|
|
|
380
504
|
# <div class="admonition note">
|
|
@@ -401,7 +525,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
401
525
|
AC(
|
|
402
526
|
"parameter",
|
|
403
527
|
{ET.QName(namespaces["ac"], "name"): "title"},
|
|
404
|
-
elem[0].text,
|
|
528
|
+
elem[0].text or "",
|
|
405
529
|
),
|
|
406
530
|
AC("rich-text-body", {}, *list(elem[1:])),
|
|
407
531
|
]
|
|
@@ -417,6 +541,87 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
417
541
|
*content,
|
|
418
542
|
)
|
|
419
543
|
|
|
544
|
+
def _transform_alert(self, elem: ET._Element) -> ET._Element:
|
|
545
|
+
"""
|
|
546
|
+
Creates an info, tip, note or warning panel from a GitHub alert.
|
|
547
|
+
|
|
548
|
+
Transforms
|
|
549
|
+
[GitHub alert](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) # noqa: E501 # no way to make this link shorter
|
|
550
|
+
syntax into one of the Confluence structured macros *info*, *tip*, *note*, or *warning*.
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
pattern = re.compile(r"^\[!([A-Z]+)\]\s*")
|
|
554
|
+
|
|
555
|
+
content = elem[0]
|
|
556
|
+
if content.text is None:
|
|
557
|
+
raise DocumentError("empty content")
|
|
558
|
+
|
|
559
|
+
match = pattern.match(content.text)
|
|
560
|
+
if match is None:
|
|
561
|
+
raise DocumentError("not an alert")
|
|
562
|
+
alert = match.group(1)
|
|
563
|
+
|
|
564
|
+
if alert == "NOTE":
|
|
565
|
+
class_name = "note"
|
|
566
|
+
elif alert == "TIP":
|
|
567
|
+
class_name = "tip"
|
|
568
|
+
elif alert == "IMPORTANT":
|
|
569
|
+
class_name = "tip"
|
|
570
|
+
elif alert == "WARNING":
|
|
571
|
+
class_name = "warning"
|
|
572
|
+
elif alert == "CAUTION":
|
|
573
|
+
class_name = "warning"
|
|
574
|
+
else:
|
|
575
|
+
raise DocumentError(f"unsupported alert: {alert}")
|
|
576
|
+
|
|
577
|
+
for e in elem:
|
|
578
|
+
self.visit(e)
|
|
579
|
+
|
|
580
|
+
content.text = pattern.sub("", content.text, count=1)
|
|
581
|
+
return AC(
|
|
582
|
+
"structured-macro",
|
|
583
|
+
{
|
|
584
|
+
ET.QName(namespaces["ac"], "name"): class_name,
|
|
585
|
+
ET.QName(namespaces["ac"], "schema-version"): "1",
|
|
586
|
+
},
|
|
587
|
+
AC("rich-text-body", {}, *list(elem)),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
def _transform_section(self, elem: ET._Element) -> ET._Element:
|
|
591
|
+
"""
|
|
592
|
+
Creates a collapsed section.
|
|
593
|
+
|
|
594
|
+
Transforms
|
|
595
|
+
[GitHub collapsed section](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections) # noqa: E501 # no way to make this link shorter
|
|
596
|
+
syntax into the Confluence structured macro *expand*.
|
|
597
|
+
"""
|
|
598
|
+
|
|
599
|
+
if elem[0].tag != "summary":
|
|
600
|
+
raise DocumentError(
|
|
601
|
+
"expected: `<summary>` as first direct child of `<details>`"
|
|
602
|
+
)
|
|
603
|
+
if elem[0].tail is not None:
|
|
604
|
+
raise DocumentError('expected: attribute `markdown="1"` on `<details>`')
|
|
605
|
+
|
|
606
|
+
summary = "".join(elem[0].itertext()).strip()
|
|
607
|
+
elem.remove(elem[0])
|
|
608
|
+
|
|
609
|
+
self.visit(elem)
|
|
610
|
+
|
|
611
|
+
return AC(
|
|
612
|
+
"structured-macro",
|
|
613
|
+
{
|
|
614
|
+
ET.QName(namespaces["ac"], "name"): "expand",
|
|
615
|
+
ET.QName(namespaces["ac"], "schema-version"): "1",
|
|
616
|
+
},
|
|
617
|
+
AC(
|
|
618
|
+
"parameter",
|
|
619
|
+
{ET.QName(namespaces["ac"], "name"): "title"},
|
|
620
|
+
summary,
|
|
621
|
+
),
|
|
622
|
+
AC("rich-text-body", {}, *list(elem)),
|
|
623
|
+
)
|
|
624
|
+
|
|
420
625
|
def transform(self, child: ET._Element) -> Optional[ET._Element]:
|
|
421
626
|
# normalize line breaks to regular space in element text
|
|
422
627
|
if child.text:
|
|
@@ -426,6 +631,13 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
426
631
|
tail: str = child.tail
|
|
427
632
|
child.tail = tail.replace("\n", " ")
|
|
428
633
|
|
|
634
|
+
if self.options.heading_anchors:
|
|
635
|
+
# <h1>...</h1>
|
|
636
|
+
# <h2>...</h2> ...
|
|
637
|
+
if re.match(r"^h[1-6]$", child.tag, flags=re.IGNORECASE) is not None:
|
|
638
|
+
self._transform_heading(child)
|
|
639
|
+
return None
|
|
640
|
+
|
|
429
641
|
# <p><img src="..." /></p>
|
|
430
642
|
if child.tag == "p" and len(child) == 1 and child[0].tag == "img":
|
|
431
643
|
return self._transform_image(child[0])
|
|
@@ -448,6 +660,26 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
448
660
|
elif child.tag == "div" and "admonition" in child.attrib.get("class", ""):
|
|
449
661
|
return self._transform_admonition(child)
|
|
450
662
|
|
|
663
|
+
# Alerts in GitHub
|
|
664
|
+
# <blockquote>
|
|
665
|
+
# <p>[!TIP] ...</p>
|
|
666
|
+
# </blockquote>
|
|
667
|
+
elif (
|
|
668
|
+
child.tag == "blockquote"
|
|
669
|
+
and len(child) > 0
|
|
670
|
+
and child[0].tag == "p"
|
|
671
|
+
and child[0].text is not None
|
|
672
|
+
and child[0].text.startswith("[!")
|
|
673
|
+
):
|
|
674
|
+
return self._transform_alert(child)
|
|
675
|
+
|
|
676
|
+
# <details markdown="1">
|
|
677
|
+
# <summary>...</summary>
|
|
678
|
+
# ...
|
|
679
|
+
# </details>
|
|
680
|
+
elif child.tag == "details" and len(child) > 1 and child[0].tag == "summary":
|
|
681
|
+
return self._transform_section(child)
|
|
682
|
+
|
|
451
683
|
# <img src="..." alt="..." />
|
|
452
684
|
elif child.tag == "img":
|
|
453
685
|
return self._transform_image(child)
|
|
@@ -516,12 +748,20 @@ class ConfluenceDocumentOptions:
|
|
|
516
748
|
|
|
517
749
|
:param ignore_invalid_url: When true, ignore invalid URLs in input, emit a warning and replace the anchor with
|
|
518
750
|
plain text; when false, raise an exception.
|
|
751
|
+
:param heading_anchors: When true, emit a structured macro *anchor* for each section heading using GitHub
|
|
752
|
+
conversion rules for the identifier.
|
|
753
|
+
:param generated_by: Text to use as the generated-by prompt.
|
|
519
754
|
:param show_generated: Whether to display a prompt "This page has been generated with a tool."
|
|
755
|
+
:param render_mermaid: Whether to pre-render Mermaid diagrams into PNG/SVG images.
|
|
756
|
+
:param diagram_output_format: Target image format for diagrams.
|
|
520
757
|
"""
|
|
521
758
|
|
|
522
759
|
ignore_invalid_url: bool = False
|
|
760
|
+
heading_anchors: bool = False
|
|
523
761
|
generated_by: Optional[str] = "This page has been generated with a tool."
|
|
524
762
|
root_page_id: Optional[str] = None
|
|
763
|
+
render_mermaid: bool = False
|
|
764
|
+
diagram_output_format: Literal["png", "svg"] = "png"
|
|
525
765
|
|
|
526
766
|
|
|
527
767
|
class ConfluenceDocument:
|
|
@@ -541,7 +781,7 @@ class ConfluenceDocument:
|
|
|
541
781
|
self.options = options
|
|
542
782
|
path = path.absolute()
|
|
543
783
|
|
|
544
|
-
with open(path, "r") as f:
|
|
784
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
545
785
|
text = f.read()
|
|
546
786
|
|
|
547
787
|
# extract Confluence page ID
|
|
@@ -579,7 +819,10 @@ class ConfluenceDocument:
|
|
|
579
819
|
|
|
580
820
|
converter = ConfluenceStorageFormatConverter(
|
|
581
821
|
ConfluenceConverterOptions(
|
|
582
|
-
ignore_invalid_url=self.options.ignore_invalid_url
|
|
822
|
+
ignore_invalid_url=self.options.ignore_invalid_url,
|
|
823
|
+
heading_anchors=self.options.heading_anchors,
|
|
824
|
+
render_mermaid=self.options.render_mermaid,
|
|
825
|
+
diagram_output_format=self.options.diagram_output_format,
|
|
583
826
|
),
|
|
584
827
|
path,
|
|
585
828
|
page_metadata,
|
|
@@ -587,9 +830,10 @@ class ConfluenceDocument:
|
|
|
587
830
|
converter.visit(self.root)
|
|
588
831
|
self.links = converter.links
|
|
589
832
|
self.images = converter.images
|
|
833
|
+
self.embedded_images = converter.embedded_images
|
|
590
834
|
|
|
591
835
|
def xhtml(self) -> str:
|
|
592
|
-
return
|
|
836
|
+
return elements_to_string(self.root)
|
|
593
837
|
|
|
594
838
|
|
|
595
839
|
def attachment_name(name: str) -> str:
|
|
@@ -612,10 +856,10 @@ def sanitize_confluence(html: str) -> str:
|
|
|
612
856
|
|
|
613
857
|
root = elements_from_strings([html])
|
|
614
858
|
ConfluenceStorageFormatCleaner().visit(root)
|
|
615
|
-
return
|
|
859
|
+
return elements_to_string(root)
|
|
616
860
|
|
|
617
861
|
|
|
618
|
-
def
|
|
862
|
+
def elements_to_string(root: ET._Element) -> str:
|
|
619
863
|
xml = ET.tostring(root, encoding="utf8", method="xml").decode("utf8")
|
|
620
864
|
m = re.match(r"^<root\s+[^>]*>(.*)</root>\s*$", xml, re.DOTALL)
|
|
621
865
|
if m:
|
md2conf/mermaid.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import os.path
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def has_mmdc() -> bool:
|
|
9
|
+
"True if Mermaid diagram converter is available on the OS."
|
|
10
|
+
|
|
11
|
+
if os.name == "nt":
|
|
12
|
+
executable = "mmdc.cmd"
|
|
13
|
+
else:
|
|
14
|
+
executable = "mmdc"
|
|
15
|
+
return shutil.which(executable) is not None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def render(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
|
|
19
|
+
"Generates a PNG or SVG image from a Mermaid diagram source."
|
|
20
|
+
|
|
21
|
+
filename = f"tmp_mermaid.{output_format}"
|
|
22
|
+
|
|
23
|
+
if os.name == "nt":
|
|
24
|
+
executable = "mmdc.cmd"
|
|
25
|
+
else:
|
|
26
|
+
executable = "mmdc"
|
|
27
|
+
try:
|
|
28
|
+
cmd = [
|
|
29
|
+
executable,
|
|
30
|
+
"--input",
|
|
31
|
+
"-",
|
|
32
|
+
"--output",
|
|
33
|
+
filename,
|
|
34
|
+
"--outputFormat",
|
|
35
|
+
output_format,
|
|
36
|
+
]
|
|
37
|
+
proc = subprocess.Popen(
|
|
38
|
+
cmd,
|
|
39
|
+
stdout=subprocess.PIPE,
|
|
40
|
+
stdin=subprocess.PIPE,
|
|
41
|
+
stderr=subprocess.PIPE,
|
|
42
|
+
text=False,
|
|
43
|
+
)
|
|
44
|
+
proc.communicate(input=source.encode("utf-8"))
|
|
45
|
+
if proc.returncode:
|
|
46
|
+
raise RuntimeError(
|
|
47
|
+
f"failed to convert Mermaid diagram; exit code: {proc.returncode}"
|
|
48
|
+
)
|
|
49
|
+
with open(filename, "rb") as image:
|
|
50
|
+
return image.read()
|
|
51
|
+
|
|
52
|
+
finally:
|
|
53
|
+
if os.path.exists(filename):
|
|
54
|
+
os.remove(filename)
|
md2conf/processor.py
CHANGED
|
@@ -69,13 +69,13 @@ class Processor:
|
|
|
69
69
|
|
|
70
70
|
document = ConfluenceDocument(path, self.options, page_metadata)
|
|
71
71
|
content = document.xhtml()
|
|
72
|
-
with open(path.with_suffix(".csf"), "w") as f:
|
|
72
|
+
with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
|
|
73
73
|
f.write(content)
|
|
74
74
|
|
|
75
75
|
def _get_page(self, absolute_path: Path) -> ConfluencePageMetadata:
|
|
76
76
|
"Extracts metadata from a Markdown file."
|
|
77
77
|
|
|
78
|
-
with open(absolute_path, "r") as f:
|
|
78
|
+
with open(absolute_path, "r", encoding="utf-8") as f:
|
|
79
79
|
document = f.read()
|
|
80
80
|
|
|
81
81
|
qualified_id, document = extract_qualified_id(document)
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
md2conf/__init__.py,sha256=SS79zE1Jss2UJQH39HX6zMvAPO2MGjq3jjHny4L6dvY,403
|
|
2
|
-
md2conf/__main__.py,sha256=L0XaHfV3GOPnyceWxUGamZXPfQUL3Dfsf_VpWeF08Qo,4246
|
|
3
|
-
md2conf/api.py,sha256=xYqQbdy-0d_mqv-zDT0DCcLs17HYLdY2LfWoY2jhMB8,14738
|
|
4
|
-
md2conf/application.py,sha256=ksWeDQGQs6xvAS8wdghwBMgWsGI6KMQiwlH5qaKXGv4,5221
|
|
5
|
-
md2conf/converter.py,sha256=xiaf5hQHXPE3psTTk_pHVip4NDfeU-hos3Kkl1Pju50,18236
|
|
6
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
7
|
-
md2conf/processor.py,sha256=f0JG8jqjsGq2sbxMdHNz6EyZ1KQ92nCKKYU3fxP6C0o,3048
|
|
8
|
-
md2conf/properties.py,sha256=oXvtPssbougM1BTE9ytcD_1Yjc3nd7DDSHqEr0QoZAU,1811
|
|
9
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
markdown_to_confluence-0.1.12.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
11
|
-
markdown_to_confluence-0.1.12.dist-info/METADATA,sha256=HCmOX5YhQuc6v2szBgQa_VuGXaeZNKKBy8Q_Fi1GreQ,7875
|
|
12
|
-
markdown_to_confluence-0.1.12.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
|
|
13
|
-
markdown_to_confluence-0.1.12.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
14
|
-
markdown_to_confluence-0.1.12.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
15
|
-
markdown_to_confluence-0.1.12.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
16
|
-
markdown_to_confluence-0.1.12.dist-info/RECORD,,
|
|
File without changes
|
{markdown_to_confluence-0.1.12.dist-info → markdown_to_confluence-0.2.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.1.12.dist-info → markdown_to_confluence-0.2.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|