markdown-to-confluence 0.3.1__py3-none-any.whl → 0.3.3__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.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info}/METADATA +25 -7
- markdown_to_confluence-0.3.3.dist-info/RECORD +20 -0
- {markdown_to_confluence-0.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +36 -11
- md2conf/api.py +48 -18
- md2conf/application.py +34 -18
- md2conf/converter.py +115 -26
- md2conf/emoji.py +3 -1
- md2conf/mermaid.py +1 -1
- md2conf/processor.py +11 -10
- md2conf/properties.py +40 -16
- markdown_to_confluence-0.3.1.dist-info/RECORD +0 -20
- {markdown_to_confluence-0.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info/licenses}/LICENSE +0 -0
- {markdown_to_confluence-0.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info}/zip-safe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -30,6 +30,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
30
30
|
Requires-Dist: types-PyYAML>=6.0
|
|
31
31
|
Requires-Dist: requests>=2.32
|
|
32
32
|
Requires-Dist: types-requests>=2.32
|
|
33
|
+
Dynamic: license-file
|
|
33
34
|
|
|
34
35
|
# Publish Markdown files to Confluence wiki
|
|
35
36
|
|
|
@@ -166,7 +167,7 @@ The concepts above are illustrated in the following sections.
|
|
|
166
167
|
|
|
167
168
|
#### File-system directory hierarchy
|
|
168
169
|
|
|
169
|
-
The title of each Markdown file (either the text of the
|
|
170
|
+
The title of each Markdown file (either the text of the topmost unique heading (`#`), or the title specified in front-matter) is shown next to the file name.
|
|
170
171
|
|
|
171
172
|
```
|
|
172
173
|
.
|
|
@@ -197,12 +198,30 @@ root
|
|
|
197
198
|
└── Mean vs. median
|
|
198
199
|
```
|
|
199
200
|
|
|
201
|
+
### Publishing images
|
|
202
|
+
|
|
203
|
+
Local images referenced in a Markdown file are automatically published to Confluence as attachments to the page.
|
|
204
|
+
|
|
205
|
+
Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
|
|
206
|
+
|
|
207
|
+
External images referenced with an absolute URL retain the original URL.
|
|
208
|
+
|
|
200
209
|
### Ignoring files
|
|
201
210
|
|
|
202
211
|
Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
|
|
203
212
|
|
|
204
213
|
Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
|
|
205
214
|
|
|
215
|
+
### Page title
|
|
216
|
+
|
|
217
|
+
*md2conf* makes a best-effort attempt at setting the Confluence wiki page title when it publishes a Markdown document the first time. The following are probed in this order:
|
|
218
|
+
|
|
219
|
+
1. The `title` attribute set in the [front-matter](https://daily-dev-tips.com/posts/what-exactly-is-frontmatter/). Front-matter is a block delimited by `---` at the beginning of a Markdown document. Currently, only YAML syntax is supported.
|
|
220
|
+
2. The text of the topmost unique Markdown heading (`#`). For example, if a document has a single first-level heading (e.g. `# My document`), its text is used. However, if there are multiple first-level headings, this step is skipped.
|
|
221
|
+
3. The file name (without the extension `.md`).
|
|
222
|
+
|
|
223
|
+
If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
|
|
224
|
+
|
|
206
225
|
### Running the tool
|
|
207
226
|
|
|
208
227
|
You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
|
|
@@ -215,10 +234,8 @@ Use the `--help` switch to get a full list of supported command-line options:
|
|
|
215
234
|
|
|
216
235
|
```console
|
|
217
236
|
$ python3 -m md2conf --help
|
|
218
|
-
usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE]
|
|
219
|
-
[-
|
|
220
|
-
[--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors]
|
|
221
|
-
[--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
|
|
237
|
+
usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE] [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE] [--keep-hierarchy] [--generated-by GENERATED_BY] [--no-generated-by]
|
|
238
|
+
[--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors] [--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
|
|
222
239
|
mdpath
|
|
223
240
|
|
|
224
241
|
positional arguments:
|
|
@@ -239,6 +256,7 @@ options:
|
|
|
239
256
|
-l {debug,info,warning,error,critical}, --loglevel {debug,info,warning,error,critical}
|
|
240
257
|
Use this option to set the log verbosity.
|
|
241
258
|
-r ROOT_PAGE Root Confluence page to create new pages. If omitted, will raise exception when creating new pages.
|
|
259
|
+
--keep-hierarchy Maintain source directory structure when exporting to Confluence.
|
|
242
260
|
--generated-by GENERATED_BY
|
|
243
261
|
Add prompt to pages (default: 'This page has been generated with a tool.').
|
|
244
262
|
--no-generated-by Do not add 'generated by a tool' prompt to pages.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
markdown_to_confluence-0.3.3.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
2
|
+
md2conf/__init__.py,sha256=NHoSu8tHMVLytWmla4BA_Uzkl-04rV_O8YkkFxUkT_E,402
|
|
3
|
+
md2conf/__main__.py,sha256=aTRiXcvoIYMkwCGejL6MUriHXBo3qVP2Acr2I-XzMyg,7947
|
|
4
|
+
md2conf/api.py,sha256=S5IB7j48wE9MHSj1jodHYmTE6scSXb80faULW6-5RjU,20376
|
|
5
|
+
md2conf/application.py,sha256=FkJ9zYBLwYcCRkd_WiX6JI6nlw4QMETmrOXHeSzCwCE,9735
|
|
6
|
+
md2conf/converter.py,sha256=B4Z8afTmhea6nSXhzDVxN55GfMvlY34tGqCLspQ_p5g,38983
|
|
7
|
+
md2conf/emoji.py,sha256=48QJtOD0F3Be1laYLvAOwe0GxrJS-vcfjtCdiBsNcAc,1960
|
|
8
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
9
|
+
md2conf/matcher.py,sha256=FgMFPvGiOqGezCs8OyerfsVo-iIHFoI6LRMzdcjM5UY,3693
|
|
10
|
+
md2conf/mermaid.py,sha256=un_KHBDpG5Zad_QD3HN1uBwUxp4I-HVJYhNKbH7KwcA,2312
|
|
11
|
+
md2conf/processor.py,sha256=9jPswgPewh2glLSHdgxyXesGxkcxPVa_h7oUhM9EsA4,4740
|
|
12
|
+
md2conf/properties.py,sha256=TOCXLdTfYkKjRwZaMgvXw0mNCI4opEUwpBXro2Kv2B4,2467
|
|
13
|
+
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
14
|
+
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
markdown_to_confluence-0.3.3.dist-info/METADATA,sha256=SiOfBvA3jMCn3Hjd_Let9R-DqcMuPG48xP-1x2pg_JI,16495
|
|
16
|
+
markdown_to_confluence-0.3.3.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
|
17
|
+
markdown_to_confluence-0.3.3.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
18
|
+
markdown_to_confluence-0.3.3.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
19
|
+
markdown_to_confluence-0.3.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
20
|
+
markdown_to_confluence-0.3.3.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.3.
|
|
8
|
+
__version__ = "0.3.3"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2025, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
md2conf/__main__.py
CHANGED
|
@@ -22,18 +22,22 @@ import requests
|
|
|
22
22
|
from . import __version__
|
|
23
23
|
from .api import ConfluenceAPI
|
|
24
24
|
from .application import Application
|
|
25
|
-
from .converter import ConfluenceDocumentOptions
|
|
25
|
+
from .converter import ConfluenceDocumentOptions, ConfluenceSiteMetadata
|
|
26
26
|
from .processor import Processor
|
|
27
|
-
from .properties import
|
|
27
|
+
from .properties import (
|
|
28
|
+
ArgumentError,
|
|
29
|
+
ConfluenceConnectionProperties,
|
|
30
|
+
ConfluenceSiteProperties,
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
class Arguments(argparse.Namespace):
|
|
31
35
|
mdpath: Path
|
|
32
|
-
domain: str
|
|
33
|
-
path: str
|
|
34
|
-
username: str
|
|
35
|
-
apikey: str
|
|
36
|
-
space: str
|
|
36
|
+
domain: Optional[str]
|
|
37
|
+
path: Optional[str]
|
|
38
|
+
username: Optional[str]
|
|
39
|
+
apikey: Optional[str]
|
|
40
|
+
space: Optional[str]
|
|
37
41
|
loglevel: str
|
|
38
42
|
ignore_invalid_url: bool
|
|
39
43
|
heading_anchors: bool
|
|
@@ -201,12 +205,33 @@ def main() -> None:
|
|
|
201
205
|
diagram_output_format=args.diagram_output_format,
|
|
202
206
|
webui_links=args.webui_links,
|
|
203
207
|
)
|
|
204
|
-
properties = ConfluenceProperties(
|
|
205
|
-
args.domain, args.path, args.username, args.apikey, args.space, args.headers
|
|
206
|
-
)
|
|
207
208
|
if args.local:
|
|
208
|
-
|
|
209
|
+
try:
|
|
210
|
+
site_properties = ConfluenceSiteProperties(
|
|
211
|
+
domain=args.domain,
|
|
212
|
+
base_path=args.path,
|
|
213
|
+
space_key=args.space,
|
|
214
|
+
)
|
|
215
|
+
except ArgumentError as e:
|
|
216
|
+
parser.error(str(e))
|
|
217
|
+
site_metadata = ConfluenceSiteMetadata(
|
|
218
|
+
domain=site_properties.domain,
|
|
219
|
+
base_path=site_properties.base_path,
|
|
220
|
+
space_key=site_properties.space_key,
|
|
221
|
+
)
|
|
222
|
+
Processor(options, site_metadata).process(args.mdpath)
|
|
209
223
|
else:
|
|
224
|
+
try:
|
|
225
|
+
properties = ConfluenceConnectionProperties(
|
|
226
|
+
args.domain,
|
|
227
|
+
args.path,
|
|
228
|
+
args.username,
|
|
229
|
+
args.apikey,
|
|
230
|
+
args.space,
|
|
231
|
+
args.headers,
|
|
232
|
+
)
|
|
233
|
+
except ArgumentError as e:
|
|
234
|
+
parser.error(str(e))
|
|
210
235
|
try:
|
|
211
236
|
with ConfluenceAPI(properties) as api:
|
|
212
237
|
Application(
|
md2conf/api.py
CHANGED
|
@@ -21,7 +21,12 @@ from urllib.parse import urlencode, urlparse, urlunparse
|
|
|
21
21
|
import requests
|
|
22
22
|
|
|
23
23
|
from .converter import ParseError, sanitize_confluence
|
|
24
|
-
from .properties import
|
|
24
|
+
from .properties import (
|
|
25
|
+
ArgumentError,
|
|
26
|
+
ConfluenceConnectionProperties,
|
|
27
|
+
ConfluenceError,
|
|
28
|
+
PageError,
|
|
29
|
+
)
|
|
25
30
|
|
|
26
31
|
# a JSON type with possible `null` values
|
|
27
32
|
JsonType = Union[
|
|
@@ -40,6 +45,18 @@ class ConfluenceVersion(enum.Enum):
|
|
|
40
45
|
VERSION_2 = "api/v2"
|
|
41
46
|
|
|
42
47
|
|
|
48
|
+
class ConfluencePageParentContentType(enum.Enum):
|
|
49
|
+
"""
|
|
50
|
+
Content types that can be a parent to a Confluence page
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
PAGE = "page"
|
|
54
|
+
WHITEBOARD = "whiteboard"
|
|
55
|
+
DATABASE = "database"
|
|
56
|
+
EMBED = "embed"
|
|
57
|
+
FOLDER = "folder"
|
|
58
|
+
|
|
59
|
+
|
|
43
60
|
def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
44
61
|
"Builds a URL with scheme, host, port, path and query string parameters."
|
|
45
62
|
|
|
@@ -71,17 +88,21 @@ class ConfluenceAttachment:
|
|
|
71
88
|
class ConfluencePage:
|
|
72
89
|
id: str
|
|
73
90
|
space_id: str
|
|
91
|
+
parent_id: str
|
|
92
|
+
parent_type: Optional[ConfluencePageParentContentType]
|
|
74
93
|
title: str
|
|
75
94
|
version: int
|
|
76
95
|
content: str
|
|
77
96
|
|
|
78
97
|
|
|
79
98
|
class ConfluenceAPI:
|
|
80
|
-
properties:
|
|
99
|
+
properties: ConfluenceConnectionProperties
|
|
81
100
|
session: Optional["ConfluenceSession"] = None
|
|
82
101
|
|
|
83
|
-
def __init__(
|
|
84
|
-
self
|
|
102
|
+
def __init__(
|
|
103
|
+
self, properties: Optional[ConfluenceConnectionProperties] = None
|
|
104
|
+
) -> None:
|
|
105
|
+
self.properties = properties or ConfluenceConnectionProperties()
|
|
85
106
|
|
|
86
107
|
def __enter__(self) -> "ConfluenceSession":
|
|
87
108
|
session = requests.Session()
|
|
@@ -128,7 +149,7 @@ class ConfluenceSession:
|
|
|
128
149
|
session: requests.Session,
|
|
129
150
|
domain: str,
|
|
130
151
|
base_path: str,
|
|
131
|
-
space_key: Optional[str],
|
|
152
|
+
space_key: Optional[str] = None,
|
|
132
153
|
) -> None:
|
|
133
154
|
self.session = session
|
|
134
155
|
self.domain = domain
|
|
@@ -170,11 +191,9 @@ class ConfluenceSession:
|
|
|
170
191
|
|
|
171
192
|
url = self._build_url(version, path, query)
|
|
172
193
|
response = self.session.get(url)
|
|
173
|
-
response.
|
|
174
|
-
if len(response.text) > 240:
|
|
175
|
-
LOGGER.debug("Received HTTP payload (truncated):\n%.240s...", response.text)
|
|
176
|
-
else:
|
|
194
|
+
if response.text:
|
|
177
195
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
196
|
+
response.raise_for_status()
|
|
178
197
|
return response.json()
|
|
179
198
|
|
|
180
199
|
def _save(self, version: ConfluenceVersion, path: str, data: dict) -> None:
|
|
@@ -184,6 +203,8 @@ class ConfluenceSession:
|
|
|
184
203
|
data=json.dumps(data),
|
|
185
204
|
headers={"Content-Type": "application/json"},
|
|
186
205
|
)
|
|
206
|
+
if response.text:
|
|
207
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
187
208
|
response.raise_for_status()
|
|
188
209
|
|
|
189
210
|
def space_id_to_key(self, id: str) -> str:
|
|
@@ -194,7 +215,7 @@ class ConfluenceSession:
|
|
|
194
215
|
payload = self._invoke(
|
|
195
216
|
ConfluenceVersion.VERSION_2,
|
|
196
217
|
"/spaces",
|
|
197
|
-
{"ids": id, "
|
|
218
|
+
{"ids": id, "status": "current"},
|
|
198
219
|
)
|
|
199
220
|
payload = typing.cast(dict[str, JsonType], payload)
|
|
200
221
|
results = typing.cast(list[JsonType], payload["results"])
|
|
@@ -216,7 +237,7 @@ class ConfluenceSession:
|
|
|
216
237
|
payload = self._invoke(
|
|
217
238
|
ConfluenceVersion.VERSION_2,
|
|
218
239
|
"/spaces",
|
|
219
|
-
{"keys": key, "
|
|
240
|
+
{"keys": key, "status": "current"},
|
|
220
241
|
)
|
|
221
242
|
payload = typing.cast(dict[str, JsonType], payload)
|
|
222
243
|
results = typing.cast(list[JsonType], payload["results"])
|
|
@@ -261,12 +282,11 @@ class ConfluenceSession:
|
|
|
261
282
|
comment: Optional[str] = None,
|
|
262
283
|
force: bool = False,
|
|
263
284
|
) -> None:
|
|
264
|
-
|
|
265
285
|
if attachment_path is None and raw_data is None:
|
|
266
|
-
raise
|
|
286
|
+
raise ArgumentError("required: `attachment_path` or `raw_data`")
|
|
267
287
|
|
|
268
288
|
if attachment_path is not None and raw_data is not None:
|
|
269
|
-
raise
|
|
289
|
+
raise ArgumentError("expected: either `attachment_path` or `raw_data`")
|
|
270
290
|
|
|
271
291
|
if content_type is None:
|
|
272
292
|
if attachment_path is not None:
|
|
@@ -276,7 +296,7 @@ class ConfluenceSession:
|
|
|
276
296
|
content_type, _ = mimetypes.guess_type(name, strict=True)
|
|
277
297
|
|
|
278
298
|
if attachment_path is not None and not attachment_path.is_file():
|
|
279
|
-
raise
|
|
299
|
+
raise PageError(f"file not found: {attachment_path}")
|
|
280
300
|
|
|
281
301
|
try:
|
|
282
302
|
attachment = self.get_attachment_by_name(page_id, attachment_name)
|
|
@@ -422,6 +442,12 @@ class ConfluenceSession:
|
|
|
422
442
|
return ConfluencePage(
|
|
423
443
|
id=page_id,
|
|
424
444
|
space_id=typing.cast(str, data["spaceId"]),
|
|
445
|
+
parent_id=typing.cast(str, data["parentId"]),
|
|
446
|
+
parent_type=(
|
|
447
|
+
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
448
|
+
if data["parentType"] is not None
|
|
449
|
+
else None
|
|
450
|
+
),
|
|
425
451
|
title=typing.cast(str, data["title"]),
|
|
426
452
|
version=typing.cast(int, version["number"]),
|
|
427
453
|
content=typing.cast(str, storage["value"]),
|
|
@@ -493,9 +519,7 @@ class ConfluenceSession:
|
|
|
493
519
|
|
|
494
520
|
coalesced_space_key = space_key or self.space_key
|
|
495
521
|
if coalesced_space_key is None:
|
|
496
|
-
raise
|
|
497
|
-
"Confluence space key required for creating a new page"
|
|
498
|
-
)
|
|
522
|
+
raise ArgumentError("Confluence space key required for creating a new page")
|
|
499
523
|
|
|
500
524
|
path = "/pages/"
|
|
501
525
|
query = {
|
|
@@ -524,6 +548,12 @@ class ConfluenceSession:
|
|
|
524
548
|
return ConfluencePage(
|
|
525
549
|
id=typing.cast(str, data["id"]),
|
|
526
550
|
space_id=typing.cast(str, data["spaceId"]),
|
|
551
|
+
parent_id=typing.cast(str, data["parentId"]),
|
|
552
|
+
parent_type=(
|
|
553
|
+
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
554
|
+
if data["parentType"] is not None
|
|
555
|
+
else None
|
|
556
|
+
),
|
|
527
557
|
title=typing.cast(str, data["title"]),
|
|
528
558
|
version=typing.cast(int, version["number"]),
|
|
529
559
|
content=typing.cast(str, storage["value"]),
|
md2conf/application.py
CHANGED
|
@@ -6,8 +6,9 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import hashlib
|
|
9
10
|
import logging
|
|
10
|
-
import os
|
|
11
|
+
import os
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Optional
|
|
13
14
|
|
|
@@ -17,12 +18,14 @@ from .converter import (
|
|
|
17
18
|
ConfluenceDocumentOptions,
|
|
18
19
|
ConfluencePageMetadata,
|
|
19
20
|
ConfluenceQualifiedID,
|
|
21
|
+
ConfluenceSiteMetadata,
|
|
20
22
|
attachment_name,
|
|
21
23
|
extract_frontmatter_title,
|
|
22
24
|
extract_qualified_id,
|
|
23
25
|
read_qualified_id,
|
|
24
26
|
)
|
|
25
27
|
from .matcher import Matcher, MatcherOptions
|
|
28
|
+
from .properties import ArgumentError, PageError
|
|
26
29
|
|
|
27
30
|
LOGGER = logging.getLogger(__name__)
|
|
28
31
|
|
|
@@ -48,7 +51,7 @@ class Application:
|
|
|
48
51
|
elif path.is_file():
|
|
49
52
|
self.synchronize_page(path)
|
|
50
53
|
else:
|
|
51
|
-
raise
|
|
54
|
+
raise ArgumentError(f"expected: valid file or directory path; got: {path}")
|
|
52
55
|
|
|
53
56
|
def synchronize_page(
|
|
54
57
|
self, page_path: Path, root_dir: Optional[Path] = None
|
|
@@ -83,7 +86,7 @@ class Application:
|
|
|
83
86
|
if self.options.root_page_id
|
|
84
87
|
else None
|
|
85
88
|
)
|
|
86
|
-
self._index_directory(local_dir, root_id, page_metadata)
|
|
89
|
+
self._index_directory(local_dir, root_dir, root_id, page_metadata)
|
|
87
90
|
LOGGER.info("Indexed %d page(s)", len(page_metadata))
|
|
88
91
|
|
|
89
92
|
# Step 2: convert each page
|
|
@@ -99,12 +102,21 @@ class Application:
|
|
|
99
102
|
base_path = page_path.parent
|
|
100
103
|
|
|
101
104
|
LOGGER.info("Synchronizing page: %s", page_path)
|
|
102
|
-
|
|
105
|
+
site_metadata = ConfluenceSiteMetadata(
|
|
106
|
+
domain=self.api.domain,
|
|
107
|
+
base_path=self.api.base_path,
|
|
108
|
+
space_key=self.api.space_key,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
document = ConfluenceDocument.create(
|
|
112
|
+
page_path, self.options, root_dir, site_metadata, page_metadata
|
|
113
|
+
)
|
|
103
114
|
self._update_document(document, base_path)
|
|
104
115
|
|
|
105
116
|
def _index_directory(
|
|
106
117
|
self,
|
|
107
118
|
local_dir: Path,
|
|
119
|
+
root_dir: Path,
|
|
108
120
|
root_id: Optional[ConfluenceQualifiedID],
|
|
109
121
|
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
110
122
|
) -> None:
|
|
@@ -144,7 +156,7 @@ class Application:
|
|
|
144
156
|
if parent_doc is not None:
|
|
145
157
|
files.remove(parent_doc)
|
|
146
158
|
|
|
147
|
-
metadata = self._get_or_create_page(parent_doc, root_id)
|
|
159
|
+
metadata = self._get_or_create_page(parent_doc, root_dir, root_id)
|
|
148
160
|
LOGGER.debug("Indexed parent %s with metadata: %s", parent_doc, metadata)
|
|
149
161
|
page_metadata[parent_doc] = metadata
|
|
150
162
|
|
|
@@ -153,16 +165,17 @@ class Application:
|
|
|
153
165
|
parent_id = root_id
|
|
154
166
|
|
|
155
167
|
for doc in files:
|
|
156
|
-
metadata = self._get_or_create_page(doc, parent_id)
|
|
168
|
+
metadata = self._get_or_create_page(doc, root_dir, parent_id)
|
|
157
169
|
LOGGER.debug("Indexed %s with metadata: %s", doc, metadata)
|
|
158
170
|
page_metadata[doc] = metadata
|
|
159
171
|
|
|
160
172
|
for directory in directories:
|
|
161
|
-
self._index_directory(directory, parent_id, page_metadata)
|
|
173
|
+
self._index_directory(directory, root_dir, parent_id, page_metadata)
|
|
162
174
|
|
|
163
175
|
def _get_or_create_page(
|
|
164
176
|
self,
|
|
165
177
|
absolute_path: Path,
|
|
178
|
+
root_dir: Path,
|
|
166
179
|
parent_id: Optional[ConfluenceQualifiedID],
|
|
167
180
|
*,
|
|
168
181
|
title: Optional[str] = None,
|
|
@@ -176,19 +189,28 @@ class Application:
|
|
|
176
189
|
document = f.read()
|
|
177
190
|
|
|
178
191
|
qualified_id, document = extract_qualified_id(document)
|
|
179
|
-
frontmatter_title, _ = extract_frontmatter_title(document)
|
|
180
192
|
|
|
181
193
|
if qualified_id is not None:
|
|
182
194
|
confluence_page = self.api.get_page(qualified_id.page_id)
|
|
183
195
|
else:
|
|
184
196
|
if parent_id is None:
|
|
185
|
-
raise
|
|
197
|
+
raise PageError(
|
|
186
198
|
f"expected: parent page ID for Markdown file with no linked Confluence page: {absolute_path}"
|
|
187
199
|
)
|
|
188
200
|
|
|
189
|
-
# assign title from
|
|
201
|
+
# assign title from front-matter if present
|
|
202
|
+
if title is None:
|
|
203
|
+
title, _ = extract_frontmatter_title(document)
|
|
204
|
+
|
|
205
|
+
# use file name (without extension) and path hash if no title is supplied
|
|
206
|
+
if title is None:
|
|
207
|
+
relative_path = absolute_path.relative_to(root_dir)
|
|
208
|
+
hash = hashlib.md5(relative_path.as_posix().encode("utf-8"))
|
|
209
|
+
digest = "".join(f"{c:x}" for c in hash.digest())
|
|
210
|
+
title = f"{absolute_path.stem} [{digest}]"
|
|
211
|
+
|
|
190
212
|
confluence_page = self._create_page(
|
|
191
|
-
absolute_path, document, title
|
|
213
|
+
absolute_path, document, title, parent_id
|
|
192
214
|
)
|
|
193
215
|
|
|
194
216
|
space_key = (
|
|
@@ -198,8 +220,6 @@ class Application:
|
|
|
198
220
|
)
|
|
199
221
|
|
|
200
222
|
return ConfluencePageMetadata(
|
|
201
|
-
domain=self.api.domain,
|
|
202
|
-
base_path=self.api.base_path,
|
|
203
223
|
page_id=confluence_page.id,
|
|
204
224
|
space_key=space_key,
|
|
205
225
|
title=confluence_page.title or "",
|
|
@@ -209,15 +229,11 @@ class Application:
|
|
|
209
229
|
self,
|
|
210
230
|
absolute_path: Path,
|
|
211
231
|
document: str,
|
|
212
|
-
title:
|
|
232
|
+
title: str,
|
|
213
233
|
parent_id: ConfluenceQualifiedID,
|
|
214
234
|
) -> ConfluencePage:
|
|
215
235
|
"Creates a new Confluence page when Markdown file doesn't have an embedded page ID yet."
|
|
216
236
|
|
|
217
|
-
# use file name without extension if no title is supplied
|
|
218
|
-
if title is None:
|
|
219
|
-
title = absolute_path.stem
|
|
220
|
-
|
|
221
237
|
confluence_page = self.api.get_or_create_page(
|
|
222
238
|
title, parent_id.page_id, space_key=parent_id.space_key
|
|
223
239
|
)
|
md2conf/converter.py
CHANGED
|
@@ -25,7 +25,8 @@ import markdown
|
|
|
25
25
|
import yaml
|
|
26
26
|
from lxml.builder import ElementMaker
|
|
27
27
|
|
|
28
|
-
from . import
|
|
28
|
+
from .mermaid import render_diagram
|
|
29
|
+
from .properties import PageError
|
|
29
30
|
|
|
30
31
|
namespaces = {
|
|
31
32
|
"ac": "http://atlassian.com/content",
|
|
@@ -91,9 +92,11 @@ def markdown_to_html(content: str) -> str:
|
|
|
91
92
|
extensions=[
|
|
92
93
|
"admonition",
|
|
93
94
|
"markdown.extensions.tables",
|
|
94
|
-
"markdown.extensions.fenced_code",
|
|
95
|
+
# "markdown.extensions.fenced_code",
|
|
95
96
|
"pymdownx.emoji",
|
|
97
|
+
"pymdownx.highlight", # required by `pymdownx.superfences`
|
|
96
98
|
"pymdownx.magiclink",
|
|
99
|
+
"pymdownx.superfences",
|
|
97
100
|
"pymdownx.tilde",
|
|
98
101
|
"sane_lists",
|
|
99
102
|
"md_in_html",
|
|
@@ -101,7 +104,10 @@ def markdown_to_html(content: str) -> str:
|
|
|
101
104
|
extension_configs={
|
|
102
105
|
"pymdownx.emoji": {
|
|
103
106
|
"emoji_generator": emoji_generator,
|
|
104
|
-
}
|
|
107
|
+
},
|
|
108
|
+
"pymdownx.highlight": {
|
|
109
|
+
"use_pygments": False,
|
|
110
|
+
},
|
|
105
111
|
},
|
|
106
112
|
)
|
|
107
113
|
|
|
@@ -235,9 +241,14 @@ _languages = [
|
|
|
235
241
|
|
|
236
242
|
|
|
237
243
|
@dataclass
|
|
238
|
-
class
|
|
244
|
+
class ConfluenceSiteMetadata:
|
|
239
245
|
domain: str
|
|
240
246
|
base_path: str
|
|
247
|
+
space_key: Optional[str]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclass
|
|
251
|
+
class ConfluencePageMetadata:
|
|
241
252
|
page_id: str
|
|
242
253
|
space_key: Optional[str]
|
|
243
254
|
title: str
|
|
@@ -271,6 +282,53 @@ def title_to_identifier(title: str) -> str:
|
|
|
271
282
|
return s
|
|
272
283
|
|
|
273
284
|
|
|
285
|
+
def element_to_text(node: ET._Element) -> str:
|
|
286
|
+
"Returns all text contained in an element as a concatenated string."
|
|
287
|
+
|
|
288
|
+
return "".join(node.itertext()).strip()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@dataclass
|
|
292
|
+
class TableOfContentsEntry:
|
|
293
|
+
level: int
|
|
294
|
+
text: str
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TableOfContents:
|
|
298
|
+
"Builds a table of contents from Markdown headings."
|
|
299
|
+
|
|
300
|
+
headings: list[TableOfContentsEntry]
|
|
301
|
+
|
|
302
|
+
def __init__(self) -> None:
|
|
303
|
+
self.headings = []
|
|
304
|
+
|
|
305
|
+
def add(self, level: int, text: str) -> None:
|
|
306
|
+
"""
|
|
307
|
+
Adds a heading to the table of contents.
|
|
308
|
+
|
|
309
|
+
:param level: Markdown heading level (e.g. `1` for first-level heading).
|
|
310
|
+
:param text: Markdown heading text.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
self.headings.append(TableOfContentsEntry(level, text))
|
|
314
|
+
|
|
315
|
+
def get_title(self) -> Optional[str]:
|
|
316
|
+
"""
|
|
317
|
+
Returns a proposed document title (if unique).
|
|
318
|
+
|
|
319
|
+
:returns: Title text, or `None` if no unique title can be inferred.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
for level in range(1, 7):
|
|
323
|
+
try:
|
|
324
|
+
(title,) = (item.text for item in self.headings if item.level == level)
|
|
325
|
+
return title
|
|
326
|
+
except ValueError:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
274
332
|
@dataclass
|
|
275
333
|
class ConfluenceConverterOptions:
|
|
276
334
|
"""
|
|
@@ -299,9 +357,11 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
299
357
|
path: Path
|
|
300
358
|
base_dir: Path
|
|
301
359
|
root_dir: Path
|
|
360
|
+
toc: TableOfContents
|
|
302
361
|
links: list[str]
|
|
303
362
|
images: list[Path]
|
|
304
363
|
embedded_images: dict[str, bytes]
|
|
364
|
+
site_metadata: ConfluenceSiteMetadata
|
|
305
365
|
page_metadata: dict[Path, ConfluencePageMetadata]
|
|
306
366
|
|
|
307
367
|
def __init__(
|
|
@@ -309,6 +369,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
309
369
|
options: ConfluenceConverterOptions,
|
|
310
370
|
path: Path,
|
|
311
371
|
root_dir: Path,
|
|
372
|
+
site_metadata: ConfluenceSiteMetadata,
|
|
312
373
|
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
313
374
|
) -> None:
|
|
314
375
|
super().__init__()
|
|
@@ -316,14 +377,14 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
316
377
|
self.path = path
|
|
317
378
|
self.base_dir = path.parent
|
|
318
379
|
self.root_dir = root_dir
|
|
380
|
+
self.toc = TableOfContents()
|
|
319
381
|
self.links = []
|
|
320
382
|
self.images = []
|
|
321
383
|
self.embedded_images = {}
|
|
384
|
+
self.site_metadata = site_metadata
|
|
322
385
|
self.page_metadata = page_metadata
|
|
323
386
|
|
|
324
387
|
def _transform_heading(self, heading: ET._Element) -> None:
|
|
325
|
-
title = "".join(heading.itertext()).strip()
|
|
326
|
-
|
|
327
388
|
for e in heading:
|
|
328
389
|
self.visit(e)
|
|
329
390
|
|
|
@@ -336,7 +397,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
336
397
|
AC(
|
|
337
398
|
"parameter",
|
|
338
399
|
{ET.QName(namespaces["ac"], "name"): ""},
|
|
339
|
-
title_to_identifier(
|
|
400
|
+
title_to_identifier(element_to_text(heading)),
|
|
340
401
|
),
|
|
341
402
|
)
|
|
342
403
|
|
|
@@ -409,13 +470,20 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
409
470
|
self.links.append(url)
|
|
410
471
|
|
|
411
472
|
if self.options.webui_links:
|
|
412
|
-
page_url = f"{
|
|
473
|
+
page_url = f"{self.site_metadata.base_path}pages/viewpage.action?pageId={link_metadata.page_id}"
|
|
413
474
|
else:
|
|
414
|
-
|
|
475
|
+
space_key = link_metadata.space_key or self.site_metadata.space_key
|
|
476
|
+
|
|
477
|
+
if space_key is None:
|
|
478
|
+
raise DocumentError(
|
|
479
|
+
"Confluence space key required for building full web URLs"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
page_url = f"{self.site_metadata.base_path}spaces/{space_key}/pages/{link_metadata.page_id}/{link_metadata.title}"
|
|
415
483
|
|
|
416
484
|
components = ParseResult(
|
|
417
485
|
scheme="https",
|
|
418
|
-
netloc=
|
|
486
|
+
netloc=self.site_metadata.domain,
|
|
419
487
|
path=page_url,
|
|
420
488
|
params="",
|
|
421
489
|
query="",
|
|
@@ -527,11 +595,6 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
527
595
|
{ET.QName(namespaces["ac"], "name"): "language"},
|
|
528
596
|
language,
|
|
529
597
|
),
|
|
530
|
-
AC(
|
|
531
|
-
"parameter",
|
|
532
|
-
{ET.QName(namespaces["ac"], "name"): "linenumbers"},
|
|
533
|
-
"true",
|
|
534
|
-
),
|
|
535
598
|
AC("plain-text-body", ET.CDATA(content)),
|
|
536
599
|
)
|
|
537
600
|
|
|
@@ -539,7 +602,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
539
602
|
"Transforms a Mermaid diagram code block."
|
|
540
603
|
|
|
541
604
|
if self.options.render_mermaid:
|
|
542
|
-
image_data =
|
|
605
|
+
image_data = render_diagram(content, self.options.diagram_output_format)
|
|
543
606
|
image_hash = hashlib.md5(image_data).hexdigest()
|
|
544
607
|
image_filename = attachment_name(
|
|
545
608
|
f"embedded_{image_hash}.{self.options.diagram_output_format}"
|
|
@@ -799,10 +862,15 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
799
862
|
if not isinstance(child.tag, str):
|
|
800
863
|
return None
|
|
801
864
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
865
|
+
# <h1>...</h1>
|
|
866
|
+
# <h2>...</h2> ...
|
|
867
|
+
m = re.match(r"^h([1-6])$", child.tag, flags=re.IGNORECASE)
|
|
868
|
+
if m is not None:
|
|
869
|
+
level = int(m.group(1))
|
|
870
|
+
title = element_to_text(child)
|
|
871
|
+
self.toc.add(level, title)
|
|
872
|
+
|
|
873
|
+
if self.options.heading_anchors:
|
|
806
874
|
self._transform_heading(child)
|
|
807
875
|
return None
|
|
808
876
|
|
|
@@ -891,7 +959,7 @@ class ConfluenceStorageFormatCleaner(NodeVisitor):
|
|
|
891
959
|
|
|
892
960
|
|
|
893
961
|
class DocumentError(RuntimeError):
|
|
894
|
-
|
|
962
|
+
"Raised when a converted Markdown document has an unexpected element or attribute."
|
|
895
963
|
|
|
896
964
|
|
|
897
965
|
def extract_value(pattern: str, text: str) -> tuple[Optional[str], str]:
|
|
@@ -996,14 +1064,15 @@ class ConfluenceDocument:
|
|
|
996
1064
|
options: ConfluenceDocumentOptions
|
|
997
1065
|
root: ET._Element
|
|
998
1066
|
|
|
999
|
-
|
|
1000
|
-
|
|
1067
|
+
@classmethod
|
|
1068
|
+
def create(
|
|
1069
|
+
cls,
|
|
1001
1070
|
path: Path,
|
|
1002
1071
|
options: ConfluenceDocumentOptions,
|
|
1003
1072
|
root_dir: Path,
|
|
1073
|
+
site_metadata: ConfluenceSiteMetadata,
|
|
1004
1074
|
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
1005
|
-
) ->
|
|
1006
|
-
self.options = options
|
|
1075
|
+
) -> "ConfluenceDocument":
|
|
1007
1076
|
path = path.resolve(True)
|
|
1008
1077
|
|
|
1009
1078
|
with open(path, "r", encoding="utf-8") as f:
|
|
@@ -1019,7 +1088,23 @@ class ConfluenceDocument:
|
|
|
1019
1088
|
metadata.page_id, metadata.space_key
|
|
1020
1089
|
)
|
|
1021
1090
|
if qualified_id is None:
|
|
1022
|
-
raise
|
|
1091
|
+
raise PageError("missing Confluence page ID")
|
|
1092
|
+
|
|
1093
|
+
return ConfluenceDocument(
|
|
1094
|
+
path, text, qualified_id, options, root_dir, site_metadata, page_metadata
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
def __init__(
|
|
1098
|
+
self,
|
|
1099
|
+
path: Path,
|
|
1100
|
+
text: str,
|
|
1101
|
+
qualified_id: ConfluenceQualifiedID,
|
|
1102
|
+
options: ConfluenceDocumentOptions,
|
|
1103
|
+
root_dir: Path,
|
|
1104
|
+
site_metadata: ConfluenceSiteMetadata,
|
|
1105
|
+
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
1106
|
+
) -> None:
|
|
1107
|
+
self.options = options
|
|
1023
1108
|
self.id = qualified_id
|
|
1024
1109
|
|
|
1025
1110
|
# extract 'generated-by' tag text
|
|
@@ -1059,6 +1144,7 @@ class ConfluenceDocument:
|
|
|
1059
1144
|
),
|
|
1060
1145
|
path,
|
|
1061
1146
|
root_dir,
|
|
1147
|
+
site_metadata,
|
|
1062
1148
|
page_metadata,
|
|
1063
1149
|
)
|
|
1064
1150
|
converter.visit(self.root)
|
|
@@ -1066,6 +1152,9 @@ class ConfluenceDocument:
|
|
|
1066
1152
|
self.images = converter.images
|
|
1067
1153
|
self.embedded_images = converter.embedded_images
|
|
1068
1154
|
|
|
1155
|
+
if self.title is None:
|
|
1156
|
+
self.title = converter.toc.get_title()
|
|
1157
|
+
|
|
1069
1158
|
def xhtml(self) -> str:
|
|
1070
1159
|
return elements_to_string(self.root)
|
|
1071
1160
|
|
md2conf/emoji.py
CHANGED
|
@@ -10,6 +10,8 @@ import pathlib
|
|
|
10
10
|
|
|
11
11
|
import pymdownx.emoji1_db as emoji_db
|
|
12
12
|
|
|
13
|
+
EMOJI_PAGE_ID = "86918529216"
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def generate_source(path: pathlib.Path) -> None:
|
|
15
17
|
"Generates a source Markdown document for testing emojis."
|
|
@@ -17,7 +19,7 @@ def generate_source(path: pathlib.Path) -> None:
|
|
|
17
19
|
emojis = emoji_db.emoji
|
|
18
20
|
|
|
19
21
|
with open(path, "w") as f:
|
|
20
|
-
print("<!-- confluence-page-id:
|
|
22
|
+
print(f"<!-- confluence-page-id: {EMOJI_PAGE_ID} -->", file=f)
|
|
21
23
|
print("<!-- This file has been generated by a script. -->", file=f)
|
|
22
24
|
print(file=f)
|
|
23
25
|
print("## Emoji", file=f)
|
md2conf/mermaid.py
CHANGED
|
@@ -47,7 +47,7 @@ def has_mmdc() -> bool:
|
|
|
47
47
|
return shutil.which(executable) is not None
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def
|
|
50
|
+
def render_diagram(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
|
|
51
51
|
"Generates a PNG or SVG image from a Mermaid diagram source."
|
|
52
52
|
|
|
53
53
|
filename = f"tmp_mermaid.{output_format}"
|
md2conf/processor.py
CHANGED
|
@@ -17,23 +17,24 @@ from .converter import (
|
|
|
17
17
|
ConfluenceDocumentOptions,
|
|
18
18
|
ConfluencePageMetadata,
|
|
19
19
|
ConfluenceQualifiedID,
|
|
20
|
+
ConfluenceSiteMetadata,
|
|
20
21
|
extract_qualified_id,
|
|
21
22
|
)
|
|
22
23
|
from .matcher import Matcher, MatcherOptions
|
|
23
|
-
from .properties import
|
|
24
|
+
from .properties import ArgumentError
|
|
24
25
|
|
|
25
26
|
LOGGER = logging.getLogger(__name__)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class Processor:
|
|
29
30
|
options: ConfluenceDocumentOptions
|
|
30
|
-
|
|
31
|
+
site_metadata: ConfluenceSiteMetadata
|
|
31
32
|
|
|
32
33
|
def __init__(
|
|
33
|
-
self, options: ConfluenceDocumentOptions,
|
|
34
|
+
self, options: ConfluenceDocumentOptions, site_metadata: ConfluenceSiteMetadata
|
|
34
35
|
) -> None:
|
|
35
36
|
self.options = options
|
|
36
|
-
self.
|
|
37
|
+
self.site_metadata = site_metadata
|
|
37
38
|
|
|
38
39
|
def process(self, path: Path) -> None:
|
|
39
40
|
"Processes a single Markdown file or a directory of Markdown files."
|
|
@@ -44,7 +45,7 @@ class Processor:
|
|
|
44
45
|
elif path.is_file():
|
|
45
46
|
self.process_page(path)
|
|
46
47
|
else:
|
|
47
|
-
raise
|
|
48
|
+
raise ArgumentError(f"expected: valid file or directory path; got: {path}")
|
|
48
49
|
|
|
49
50
|
def process_directory(
|
|
50
51
|
self, local_dir: Path, root_dir: Optional[Path] = None
|
|
@@ -87,7 +88,9 @@ class Processor:
|
|
|
87
88
|
) -> None:
|
|
88
89
|
"Processes a single Markdown file."
|
|
89
90
|
|
|
90
|
-
document = ConfluenceDocument(
|
|
91
|
+
document = ConfluenceDocument.create(
|
|
92
|
+
path, self.options, root_dir, self.site_metadata, page_metadata
|
|
93
|
+
)
|
|
91
94
|
content = document.xhtml()
|
|
92
95
|
with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
|
|
93
96
|
f.write(content)
|
|
@@ -136,12 +139,10 @@ class Processor:
|
|
|
136
139
|
LOGGER.info("Identifier %s assigned to page: %s", digest, absolute_path)
|
|
137
140
|
qualified_id = ConfluenceQualifiedID(digest)
|
|
138
141
|
else:
|
|
139
|
-
raise
|
|
142
|
+
raise ArgumentError("required: page ID for local output")
|
|
140
143
|
|
|
141
144
|
return ConfluencePageMetadata(
|
|
142
|
-
domain=self.properties.domain,
|
|
143
|
-
base_path=self.properties.base_path,
|
|
144
145
|
page_id=qualified_id.page_id,
|
|
145
|
-
space_key=qualified_id.space_key
|
|
146
|
+
space_key=qualified_id.space_key,
|
|
146
147
|
title="",
|
|
147
148
|
)
|
md2conf/properties.py
CHANGED
|
@@ -10,50 +10,74 @@ import os
|
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class ArgumentError(ValueError):
|
|
14
|
+
"Raised when wrong arguments are passed to a function call."
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PageError(ValueError):
|
|
18
|
+
"Raised in case there is an issue with a Confluence page."
|
|
19
|
+
|
|
20
|
+
|
|
13
21
|
class ConfluenceError(RuntimeError):
|
|
14
|
-
|
|
22
|
+
"Raised when a Confluence API call fails."
|
|
15
23
|
|
|
16
24
|
|
|
17
|
-
class
|
|
25
|
+
class ConfluenceSiteProperties:
|
|
18
26
|
domain: str
|
|
19
27
|
base_path: str
|
|
20
28
|
space_key: Optional[str]
|
|
21
|
-
user_name: Optional[str]
|
|
22
|
-
api_key: str
|
|
23
|
-
headers: Optional[dict[str, str]]
|
|
24
29
|
|
|
25
30
|
def __init__(
|
|
26
31
|
self,
|
|
27
32
|
domain: Optional[str] = None,
|
|
28
33
|
base_path: Optional[str] = None,
|
|
29
|
-
user_name: Optional[str] = None,
|
|
30
|
-
api_key: Optional[str] = None,
|
|
31
34
|
space_key: Optional[str] = None,
|
|
32
|
-
headers: Optional[dict[str, str]] = None,
|
|
33
35
|
) -> None:
|
|
34
36
|
opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
|
|
35
37
|
opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
|
|
36
|
-
opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
|
|
37
|
-
opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
|
|
38
38
|
opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
|
|
39
39
|
|
|
40
40
|
if not opt_domain:
|
|
41
|
-
raise
|
|
41
|
+
raise ArgumentError("Confluence domain not specified")
|
|
42
42
|
if not opt_base_path:
|
|
43
43
|
opt_base_path = "/wiki/"
|
|
44
|
-
if not opt_api_key:
|
|
45
|
-
raise ConfluenceError("Confluence API key not specified")
|
|
46
44
|
|
|
47
45
|
if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
|
|
48
|
-
raise
|
|
46
|
+
raise ArgumentError(
|
|
49
47
|
"Confluence domain looks like a URL; only host name required"
|
|
50
48
|
)
|
|
51
49
|
if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
|
|
52
|
-
raise
|
|
50
|
+
raise ArgumentError("Confluence base path must start and end with a '/'")
|
|
53
51
|
|
|
54
52
|
self.domain = opt_domain
|
|
55
53
|
self.base_path = opt_base_path
|
|
54
|
+
self.space_key = opt_space_key
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ConfluenceConnectionProperties(ConfluenceSiteProperties):
|
|
58
|
+
"Properties related to connecting to Confluence."
|
|
59
|
+
|
|
60
|
+
user_name: Optional[str]
|
|
61
|
+
api_key: str
|
|
62
|
+
headers: Optional[dict[str, str]]
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
domain: Optional[str] = None,
|
|
67
|
+
base_path: Optional[str] = None,
|
|
68
|
+
user_name: Optional[str] = None,
|
|
69
|
+
api_key: Optional[str] = None,
|
|
70
|
+
space_key: Optional[str] = None,
|
|
71
|
+
headers: Optional[dict[str, str]] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
super().__init__(domain, base_path, space_key)
|
|
74
|
+
|
|
75
|
+
opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
|
|
76
|
+
opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
|
|
77
|
+
|
|
78
|
+
if not opt_api_key:
|
|
79
|
+
raise ArgumentError("Confluence API key not specified")
|
|
80
|
+
|
|
56
81
|
self.user_name = opt_user_name
|
|
57
82
|
self.api_key = opt_api_key
|
|
58
|
-
self.space_key = opt_space_key
|
|
59
83
|
self.headers = headers
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
md2conf/__init__.py,sha256=AtPkcrgEezF8jnJ14ALB3VdF6UAWPR9EPSYtoi6y5Nc,402
|
|
2
|
-
md2conf/__main__.py,sha256=ypjV_5mE0smlIRBFrpikgzXq18as2hY43HJxMLpzGp4,7145
|
|
3
|
-
md2conf/api.py,sha256=uwIR_wBSQqvZ9XZ2m2009Hf8B5w7T5PUXJ88BU8CJmA,19520
|
|
4
|
-
md2conf/application.py,sha256=5K-nCPHJZfIahjubrLtXTwI-zsTiD140fdYXDnh3GSk,9161
|
|
5
|
-
md2conf/converter.py,sha256=MoGbXqh5rE4qkdxxY8RHcnoZ5mz0aEuFz9nmUnt0WdM,36397
|
|
6
|
-
md2conf/emoji.py,sha256=IZeguWqcboeOyJkGLTVONDMO4ZXfYXPgfkp56PTI-hE,1924
|
|
7
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
8
|
-
md2conf/matcher.py,sha256=FgMFPvGiOqGezCs8OyerfsVo-iIHFoI6LRMzdcjM5UY,3693
|
|
9
|
-
md2conf/mermaid.py,sha256=Alzkv0BY-lju4ojtBdW2qtCLZ59MO9kaS2RpQO6Kyfk,2304
|
|
10
|
-
md2conf/processor.py,sha256=G-MIh1jGq9jjgogHnlnRUSrNgiV6_xO6Fy7ct9alqgM,4769
|
|
11
|
-
md2conf/properties.py,sha256=WaVVOYSck7drVQfcBJmBMa7Mb0KVOZl9UZHvLS1Du8U,1892
|
|
12
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
13
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
markdown_to_confluence-0.3.1.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
15
|
-
markdown_to_confluence-0.3.1.dist-info/METADATA,sha256=pTnAvuTg_rgETAUZbsN_5HYbOwXE7qVpDGvhaXMwB2Y,14936
|
|
16
|
-
markdown_to_confluence-0.3.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
17
|
-
markdown_to_confluence-0.3.1.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
18
|
-
markdown_to_confluence-0.3.1.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
19
|
-
markdown_to_confluence-0.3.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
20
|
-
markdown_to_confluence-0.3.1.dist-info/RECORD,,
|
{markdown_to_confluence-0.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|
{markdown_to_confluence-0.3.1.dist-info → markdown_to_confluence-0.3.3.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|