markdown-to-confluence 0.1.13__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.13.dist-info → markdown_to_confluence-0.2.0.dist-info}/LICENSE +21 -21
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.0.dist-info}/METADATA +221 -168
- markdown_to_confluence-0.2.0.dist-info/RECORD +17 -0
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.0.dist-info}/WHEEL +1 -1
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.0.dist-info}/zip-safe +1 -1
- md2conf/__init__.py +13 -13
- md2conf/__main__.py +169 -139
- md2conf/api.py +485 -459
- md2conf/application.py +165 -154
- md2conf/converter.py +868 -626
- md2conf/entities.dtd +537 -537
- md2conf/mermaid.py +54 -0
- md2conf/processor.py +91 -91
- md2conf/properties.py +52 -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.0.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.0.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,91 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Dict
|
|
5
|
-
|
|
6
|
-
from .converter import (
|
|
7
|
-
ConfluenceDocument,
|
|
8
|
-
ConfluenceDocumentOptions,
|
|
9
|
-
ConfluencePageMetadata,
|
|
10
|
-
extract_qualified_id,
|
|
11
|
-
)
|
|
12
|
-
from .properties import ConfluenceProperties
|
|
13
|
-
|
|
14
|
-
LOGGER = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class Processor:
|
|
18
|
-
options: ConfluenceDocumentOptions
|
|
19
|
-
properties: ConfluenceProperties
|
|
20
|
-
|
|
21
|
-
def __init__(
|
|
22
|
-
self, options: ConfluenceDocumentOptions, properties: ConfluenceProperties
|
|
23
|
-
) -> None:
|
|
24
|
-
self.options = options
|
|
25
|
-
self.properties = properties
|
|
26
|
-
|
|
27
|
-
def process(self, path: Path) -> None:
|
|
28
|
-
"Processes a single Markdown file or a directory of Markdown files."
|
|
29
|
-
|
|
30
|
-
if path.is_dir():
|
|
31
|
-
self.process_directory(path)
|
|
32
|
-
elif path.is_file():
|
|
33
|
-
self.process_page(path, {})
|
|
34
|
-
else:
|
|
35
|
-
raise ValueError(f"expected: valid file or directory path; got: {path}")
|
|
36
|
-
|
|
37
|
-
def process_directory(self, local_dir: Path) -> None:
|
|
38
|
-
"Recursively scans a directory hierarchy for Markdown files."
|
|
39
|
-
|
|
40
|
-
page_metadata: Dict[Path, ConfluencePageMetadata] = {}
|
|
41
|
-
LOGGER.info(f"Synchronizing directory: {local_dir}")
|
|
42
|
-
|
|
43
|
-
# Step 1: build index of all page metadata
|
|
44
|
-
# NOTE: Pathlib.walk() is implemented only in Python 3.12+
|
|
45
|
-
# so sticking for old os.walk
|
|
46
|
-
for root, directories, files in os.walk(local_dir):
|
|
47
|
-
for file_name in files:
|
|
48
|
-
# Reconstitute Path object back
|
|
49
|
-
docfile = (Path(root) / file_name).absolute()
|
|
50
|
-
|
|
51
|
-
# Skip non-markdown files
|
|
52
|
-
if docfile.suffix.lower() != ".md":
|
|
53
|
-
continue
|
|
54
|
-
|
|
55
|
-
metadata = self._get_page(docfile)
|
|
56
|
-
LOGGER.debug(f"indexed {docfile} with metadata: {metadata}")
|
|
57
|
-
page_metadata[docfile] = metadata
|
|
58
|
-
|
|
59
|
-
LOGGER.info(f"indexed {len(page_metadata)} pages")
|
|
60
|
-
|
|
61
|
-
# Step 2: Convert each page
|
|
62
|
-
for page_path in page_metadata.keys():
|
|
63
|
-
self.process_page(page_path, page_metadata)
|
|
64
|
-
|
|
65
|
-
def process_page(
|
|
66
|
-
self, path: Path, page_metadata: Dict[Path, ConfluencePageMetadata]
|
|
67
|
-
) -> None:
|
|
68
|
-
"Processes a single Markdown file."
|
|
69
|
-
|
|
70
|
-
document = ConfluenceDocument(path, self.options, page_metadata)
|
|
71
|
-
content = document.xhtml()
|
|
72
|
-
with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
|
|
73
|
-
f.write(content)
|
|
74
|
-
|
|
75
|
-
def _get_page(self, absolute_path: Path) -> ConfluencePageMetadata:
|
|
76
|
-
"Extracts metadata from a Markdown file."
|
|
77
|
-
|
|
78
|
-
with open(absolute_path, "r", encoding="utf-8") as f:
|
|
79
|
-
document = f.read()
|
|
80
|
-
|
|
81
|
-
qualified_id, document = extract_qualified_id(document)
|
|
82
|
-
if qualified_id is None:
|
|
83
|
-
raise ValueError("required: page ID for local output")
|
|
84
|
-
|
|
85
|
-
return ConfluencePageMetadata(
|
|
86
|
-
domain=self.properties.domain,
|
|
87
|
-
base_path=self.properties.base_path,
|
|
88
|
-
page_id=qualified_id.page_id,
|
|
89
|
-
space_key=qualified_id.space_key or self.properties.space_key,
|
|
90
|
-
title="",
|
|
91
|
-
)
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from .converter import (
|
|
7
|
+
ConfluenceDocument,
|
|
8
|
+
ConfluenceDocumentOptions,
|
|
9
|
+
ConfluencePageMetadata,
|
|
10
|
+
extract_qualified_id,
|
|
11
|
+
)
|
|
12
|
+
from .properties import ConfluenceProperties
|
|
13
|
+
|
|
14
|
+
LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Processor:
|
|
18
|
+
options: ConfluenceDocumentOptions
|
|
19
|
+
properties: ConfluenceProperties
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, options: ConfluenceDocumentOptions, properties: ConfluenceProperties
|
|
23
|
+
) -> None:
|
|
24
|
+
self.options = options
|
|
25
|
+
self.properties = properties
|
|
26
|
+
|
|
27
|
+
def process(self, path: Path) -> None:
|
|
28
|
+
"Processes a single Markdown file or a directory of Markdown files."
|
|
29
|
+
|
|
30
|
+
if path.is_dir():
|
|
31
|
+
self.process_directory(path)
|
|
32
|
+
elif path.is_file():
|
|
33
|
+
self.process_page(path, {})
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(f"expected: valid file or directory path; got: {path}")
|
|
36
|
+
|
|
37
|
+
def process_directory(self, local_dir: Path) -> None:
|
|
38
|
+
"Recursively scans a directory hierarchy for Markdown files."
|
|
39
|
+
|
|
40
|
+
page_metadata: Dict[Path, ConfluencePageMetadata] = {}
|
|
41
|
+
LOGGER.info(f"Synchronizing directory: {local_dir}")
|
|
42
|
+
|
|
43
|
+
# Step 1: build index of all page metadata
|
|
44
|
+
# NOTE: Pathlib.walk() is implemented only in Python 3.12+
|
|
45
|
+
# so sticking for old os.walk
|
|
46
|
+
for root, directories, files in os.walk(local_dir):
|
|
47
|
+
for file_name in files:
|
|
48
|
+
# Reconstitute Path object back
|
|
49
|
+
docfile = (Path(root) / file_name).absolute()
|
|
50
|
+
|
|
51
|
+
# Skip non-markdown files
|
|
52
|
+
if docfile.suffix.lower() != ".md":
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
metadata = self._get_page(docfile)
|
|
56
|
+
LOGGER.debug(f"indexed {docfile} with metadata: {metadata}")
|
|
57
|
+
page_metadata[docfile] = metadata
|
|
58
|
+
|
|
59
|
+
LOGGER.info(f"indexed {len(page_metadata)} pages")
|
|
60
|
+
|
|
61
|
+
# Step 2: Convert each page
|
|
62
|
+
for page_path in page_metadata.keys():
|
|
63
|
+
self.process_page(page_path, page_metadata)
|
|
64
|
+
|
|
65
|
+
def process_page(
|
|
66
|
+
self, path: Path, page_metadata: Dict[Path, ConfluencePageMetadata]
|
|
67
|
+
) -> None:
|
|
68
|
+
"Processes a single Markdown file."
|
|
69
|
+
|
|
70
|
+
document = ConfluenceDocument(path, self.options, page_metadata)
|
|
71
|
+
content = document.xhtml()
|
|
72
|
+
with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
|
|
73
|
+
f.write(content)
|
|
74
|
+
|
|
75
|
+
def _get_page(self, absolute_path: Path) -> ConfluencePageMetadata:
|
|
76
|
+
"Extracts metadata from a Markdown file."
|
|
77
|
+
|
|
78
|
+
with open(absolute_path, "r", encoding="utf-8") as f:
|
|
79
|
+
document = f.read()
|
|
80
|
+
|
|
81
|
+
qualified_id, document = extract_qualified_id(document)
|
|
82
|
+
if qualified_id is None:
|
|
83
|
+
raise ValueError("required: page ID for local output")
|
|
84
|
+
|
|
85
|
+
return ConfluencePageMetadata(
|
|
86
|
+
domain=self.properties.domain,
|
|
87
|
+
base_path=self.properties.base_path,
|
|
88
|
+
page_id=qualified_id.page_id,
|
|
89
|
+
space_key=qualified_id.space_key or self.properties.space_key,
|
|
90
|
+
title="",
|
|
91
|
+
)
|
md2conf/properties.py
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
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
|
-
def __init__(
|
|
17
|
-
self,
|
|
18
|
-
domain: Optional[str] = None,
|
|
19
|
-
base_path: Optional[str] = None,
|
|
20
|
-
user_name: Optional[str] = None,
|
|
21
|
-
api_key: Optional[str] = None,
|
|
22
|
-
space_key: Optional[str] = None,
|
|
23
|
-
) -> None:
|
|
24
|
-
opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
|
|
25
|
-
opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
|
|
26
|
-
opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
|
|
27
|
-
opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
|
|
28
|
-
opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
|
|
29
|
-
|
|
30
|
-
if not opt_domain:
|
|
31
|
-
raise ConfluenceError("Confluence domain not specified")
|
|
32
|
-
if not opt_base_path:
|
|
33
|
-
opt_base_path = "/wiki/"
|
|
34
|
-
if not opt_api_key:
|
|
35
|
-
raise ConfluenceError("Confluence API key not specified")
|
|
36
|
-
if not opt_space_key:
|
|
37
|
-
raise ConfluenceError("Confluence space key not specified")
|
|
38
|
-
|
|
39
|
-
if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
|
|
40
|
-
raise ConfluenceError(
|
|
41
|
-
"Confluence domain looks like a URL; only host name required"
|
|
42
|
-
)
|
|
43
|
-
if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
|
|
44
|
-
raise ConfluenceError("Confluence base path must start and end with a '/'")
|
|
45
|
-
|
|
46
|
-
self.domain = opt_domain
|
|
47
|
-
self.base_path = opt_base_path
|
|
48
|
-
self.user_name = opt_user_name
|
|
49
|
-
self.api_key = opt_api_key
|
|
50
|
-
self.space_key = opt_space_key
|
|
51
|
-
self.space_key = opt_space_key
|
|
52
|
-
self.space_key = opt_space_key
|
|
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
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
domain: Optional[str] = None,
|
|
19
|
+
base_path: Optional[str] = None,
|
|
20
|
+
user_name: Optional[str] = None,
|
|
21
|
+
api_key: Optional[str] = None,
|
|
22
|
+
space_key: Optional[str] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
|
|
25
|
+
opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
|
|
26
|
+
opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
|
|
27
|
+
opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
|
|
28
|
+
opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
|
|
29
|
+
|
|
30
|
+
if not opt_domain:
|
|
31
|
+
raise ConfluenceError("Confluence domain not specified")
|
|
32
|
+
if not opt_base_path:
|
|
33
|
+
opt_base_path = "/wiki/"
|
|
34
|
+
if not opt_api_key:
|
|
35
|
+
raise ConfluenceError("Confluence API key not specified")
|
|
36
|
+
if not opt_space_key:
|
|
37
|
+
raise ConfluenceError("Confluence space key not specified")
|
|
38
|
+
|
|
39
|
+
if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
|
|
40
|
+
raise ConfluenceError(
|
|
41
|
+
"Confluence domain looks like a URL; only host name required"
|
|
42
|
+
)
|
|
43
|
+
if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
|
|
44
|
+
raise ConfluenceError("Confluence base path must start and end with a '/'")
|
|
45
|
+
|
|
46
|
+
self.domain = opt_domain
|
|
47
|
+
self.base_path = opt_base_path
|
|
48
|
+
self.user_name = opt_user_name
|
|
49
|
+
self.api_key = opt_api_key
|
|
50
|
+
self.space_key = opt_space_key
|
|
51
|
+
self.space_key = opt_space_key
|
|
52
|
+
self.space_key = opt_space_key
|
|
@@ -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.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.1.13.dist-info → markdown_to_confluence-0.2.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|