markdown-to-confluence 0.1.13__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/LICENSE +21 -21
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/METADATA +226 -168
- markdown_to_confluence-0.2.1.dist-info/RECORD +17 -0
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/WHEEL +1 -1
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/zip-safe +1 -1
- md2conf/__init__.py +13 -13
- md2conf/__main__.py +211 -139
- md2conf/api.py +517 -459
- md2conf/application.py +231 -154
- md2conf/converter.py +965 -626
- md2conf/entities.dtd +537 -537
- md2conf/mermaid.py +54 -0
- md2conf/processor.py +113 -91
- md2conf/properties.py +53 -52
- markdown_to_confluence-0.1.13.dist-info/RECORD +0 -16
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/top_level.txt +0 -0
md2conf/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
|
@@ -1,91 +1,113 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
self
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
self.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
) -> None:
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
from .converter import (
|
|
8
|
+
ConfluenceDocument,
|
|
9
|
+
ConfluenceDocumentOptions,
|
|
10
|
+
ConfluencePageMetadata,
|
|
11
|
+
ConfluenceQualifiedID,
|
|
12
|
+
extract_qualified_id,
|
|
13
|
+
)
|
|
14
|
+
from .properties import ConfluenceProperties
|
|
15
|
+
|
|
16
|
+
LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Processor:
|
|
20
|
+
options: ConfluenceDocumentOptions
|
|
21
|
+
properties: ConfluenceProperties
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, options: ConfluenceDocumentOptions, properties: ConfluenceProperties
|
|
25
|
+
) -> None:
|
|
26
|
+
self.options = options
|
|
27
|
+
self.properties = properties
|
|
28
|
+
|
|
29
|
+
def process(self, path: Path) -> None:
|
|
30
|
+
"Processes a single Markdown file or a directory of Markdown files."
|
|
31
|
+
|
|
32
|
+
if path.is_dir():
|
|
33
|
+
self.process_directory(path)
|
|
34
|
+
elif path.is_file():
|
|
35
|
+
self.process_page(path, {})
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError(f"expected: valid file or directory path; got: {path}")
|
|
38
|
+
|
|
39
|
+
def process_directory(self, local_dir: Path) -> None:
|
|
40
|
+
"Recursively scans a directory hierarchy for Markdown files."
|
|
41
|
+
|
|
42
|
+
LOGGER.info(f"Synchronizing directory: {local_dir}")
|
|
43
|
+
|
|
44
|
+
# Step 1: build index of all page metadata
|
|
45
|
+
page_metadata: Dict[Path, ConfluencePageMetadata] = {}
|
|
46
|
+
self._index_directory(local_dir, page_metadata)
|
|
47
|
+
LOGGER.info(f"indexed {len(page_metadata)} page(s)")
|
|
48
|
+
|
|
49
|
+
# Step 2: convert each page
|
|
50
|
+
for page_path in page_metadata.keys():
|
|
51
|
+
self.process_page(page_path, page_metadata)
|
|
52
|
+
|
|
53
|
+
def process_page(
|
|
54
|
+
self, path: Path, page_metadata: Dict[Path, ConfluencePageMetadata]
|
|
55
|
+
) -> None:
|
|
56
|
+
"Processes a single Markdown file."
|
|
57
|
+
|
|
58
|
+
document = ConfluenceDocument(path, self.options, page_metadata)
|
|
59
|
+
content = document.xhtml()
|
|
60
|
+
with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
|
|
61
|
+
f.write(content)
|
|
62
|
+
|
|
63
|
+
def _index_directory(
|
|
64
|
+
self,
|
|
65
|
+
local_dir: Path,
|
|
66
|
+
page_metadata: Dict[Path, ConfluencePageMetadata],
|
|
67
|
+
) -> None:
|
|
68
|
+
"Indexes Markdown files in a directory recursively."
|
|
69
|
+
|
|
70
|
+
LOGGER.info(f"Indexing directory: {local_dir}")
|
|
71
|
+
|
|
72
|
+
files: List[Path] = []
|
|
73
|
+
directories: List[Path] = []
|
|
74
|
+
for entry in os.scandir(local_dir):
|
|
75
|
+
if entry.is_file():
|
|
76
|
+
if entry.name.endswith(".md"):
|
|
77
|
+
# skip non-markdown files
|
|
78
|
+
files.append((Path(local_dir) / entry.name).absolute())
|
|
79
|
+
elif entry.is_dir():
|
|
80
|
+
if not entry.name.startswith("."):
|
|
81
|
+
directories.append((Path(local_dir) / entry.name).absolute())
|
|
82
|
+
|
|
83
|
+
for doc in files:
|
|
84
|
+
metadata = self._get_page(doc)
|
|
85
|
+
LOGGER.debug(f"indexed {doc} with metadata: {metadata}")
|
|
86
|
+
page_metadata[doc] = metadata
|
|
87
|
+
|
|
88
|
+
for directory in directories:
|
|
89
|
+
self._index_directory(Path(local_dir) / directory, page_metadata)
|
|
90
|
+
|
|
91
|
+
def _get_page(self, absolute_path: Path) -> ConfluencePageMetadata:
|
|
92
|
+
"Extracts metadata from a Markdown file."
|
|
93
|
+
|
|
94
|
+
with open(absolute_path, "r", encoding="utf-8") as f:
|
|
95
|
+
document = f.read()
|
|
96
|
+
|
|
97
|
+
qualified_id, document = extract_qualified_id(document)
|
|
98
|
+
if qualified_id is None:
|
|
99
|
+
if self.options.root_page_id is not None:
|
|
100
|
+
hash = hashlib.md5(document.encode("utf-8"))
|
|
101
|
+
digest = "".join(f"{c:x}" for c in hash.digest())
|
|
102
|
+
LOGGER.info(f"Identifier '{digest}' assigned to page: {absolute_path}")
|
|
103
|
+
qualified_id = ConfluenceQualifiedID(digest)
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError("required: page ID for local output")
|
|
106
|
+
|
|
107
|
+
return ConfluencePageMetadata(
|
|
108
|
+
domain=self.properties.domain,
|
|
109
|
+
base_path=self.properties.base_path,
|
|
110
|
+
page_id=qualified_id.page_id,
|
|
111
|
+
space_key=qualified_id.space_key or self.properties.space_key,
|
|
112
|
+
title="",
|
|
113
|
+
)
|
md2conf/properties.py
CHANGED
|
@@ -1,52 +1,53 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class ConfluenceError(RuntimeError):
|
|
6
|
-
pass
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class ConfluenceProperties:
|
|
10
|
-
domain: str
|
|
11
|
-
base_path: str
|
|
12
|
-
space_key: str
|
|
13
|
-
user_name: Optional[str]
|
|
14
|
-
api_key: str
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if not
|
|
33
|
-
|
|
34
|
-
if not
|
|
35
|
-
|
|
36
|
-
if not
|
|
37
|
-
raise ConfluenceError("Confluence
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
self.
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
51
|
-
self.
|
|
52
|
-
self.space_key = opt_space_key
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfluenceError(RuntimeError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfluenceProperties:
|
|
10
|
+
domain: str
|
|
11
|
+
base_path: str
|
|
12
|
+
space_key: str
|
|
13
|
+
user_name: Optional[str]
|
|
14
|
+
api_key: str
|
|
15
|
+
headers: Optional[Dict[str, str]]
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
domain: Optional[str] = None,
|
|
20
|
+
base_path: Optional[str] = None,
|
|
21
|
+
user_name: Optional[str] = None,
|
|
22
|
+
api_key: Optional[str] = None,
|
|
23
|
+
space_key: Optional[str] = None,
|
|
24
|
+
headers: Optional[Dict[str, str]] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
|
|
27
|
+
opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
|
|
28
|
+
opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
|
|
29
|
+
opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
|
|
30
|
+
opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
|
|
31
|
+
|
|
32
|
+
if not opt_domain:
|
|
33
|
+
raise ConfluenceError("Confluence domain not specified")
|
|
34
|
+
if not opt_base_path:
|
|
35
|
+
opt_base_path = "/wiki/"
|
|
36
|
+
if not opt_api_key:
|
|
37
|
+
raise ConfluenceError("Confluence API key not specified")
|
|
38
|
+
if not opt_space_key:
|
|
39
|
+
raise ConfluenceError("Confluence space key not specified")
|
|
40
|
+
|
|
41
|
+
if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
|
|
42
|
+
raise ConfluenceError(
|
|
43
|
+
"Confluence domain looks like a URL; only host name required"
|
|
44
|
+
)
|
|
45
|
+
if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
|
|
46
|
+
raise ConfluenceError("Confluence base path must start and end with a '/'")
|
|
47
|
+
|
|
48
|
+
self.domain = opt_domain
|
|
49
|
+
self.base_path = opt_base_path
|
|
50
|
+
self.user_name = opt_user_name
|
|
51
|
+
self.api_key = opt_api_key
|
|
52
|
+
self.space_key = opt_space_key
|
|
53
|
+
self.headers = headers
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
md2conf/__init__.py,sha256=yAKxptdEAiSXE55X3TxhUYa2iMkbW9fo1CWVRX8FNUY,416
|
|
2
|
-
md2conf/__main__.py,sha256=-ZztZII6QYeuukxlOkVm8Mh9M1_ksA-5I2e2o5mvKLI,4353
|
|
3
|
-
md2conf/api.py,sha256=ghLdaQtltRJ2-SUjhOZNkohL-RJ0Y1h6SfT7RiLPPZM,15231
|
|
4
|
-
md2conf/application.py,sha256=BuIWLWrU8yaMbQhK0n3SUEPjxc7XJ0RIUdE3ALw9umE,5411
|
|
5
|
-
md2conf/converter.py,sha256=_wfhDG-8Q3iAtR28Hp_ja-jyRMeb4HOvjCBsg1YMSZM,18926
|
|
6
|
-
md2conf/entities.dtd,sha256=6and_fCiCHKhC7XXX0Fs-AaC0k_YN6csRQ7RiqbwyuM,30752
|
|
7
|
-
md2conf/processor.py,sha256=TRsDbrNswNUjkdP7vXEUPYZvC07P6QN92ddwTGXVljg,3175
|
|
8
|
-
md2conf/properties.py,sha256=zjtOQ73gQa5clPC6Ep4WHVjYIjTwPfrAtGDK7CUqbyc,1863
|
|
9
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
markdown_to_confluence-0.1.13.dist-info/LICENSE,sha256=d0Kt_fnS_LlDYrLPPI5xUSDGXRWRZNv81uKvmHa3lZM,1098
|
|
11
|
-
markdown_to_confluence-0.1.13.dist-info/METADATA,sha256=NcsPNl4S4PsKF_lM8tbxNbOjdpf1Be3OFec9AmdJrSs,8035
|
|
12
|
-
markdown_to_confluence-0.1.13.dist-info/WHEEL,sha256=nCVcAvsfA9TDtwGwhYaRrlPhTLV9m-Ga6mdyDtuwK18,91
|
|
13
|
-
markdown_to_confluence-0.1.13.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
14
|
-
markdown_to_confluence-0.1.13.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
15
|
-
markdown_to_confluence-0.1.13.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
16
|
-
markdown_to_confluence-0.1.13.dist-info/RECORD,,
|
{markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|