markdown-to-confluence 0.4.8__py3-none-any.whl → 0.5.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.4.8.dist-info → markdown_to_confluence-0.5.1.dist-info}/METADATA +11 -11
- markdown_to_confluence-0.5.1.dist-info/RECORD +35 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +13 -13
- md2conf/api.py +55 -71
- md2conf/collection.py +2 -2
- md2conf/converter.py +22 -22
- md2conf/domain.py +3 -3
- md2conf/drawio.py +1 -1
- md2conf/environment.py +22 -22
- md2conf/latex.py +9 -9
- md2conf/local.py +5 -6
- md2conf/markdown.py +7 -7
- md2conf/matcher.py +5 -5
- md2conf/mermaid.py +3 -3
- md2conf/metadata.py +1 -2
- md2conf/processor.py +12 -12
- md2conf/publisher.py +1 -2
- md2conf/scanner.py +33 -41
- md2conf/serializer.py +64 -0
- md2conf/toc.py +2 -3
- md2conf/xml.py +4 -6
- markdown_to_confluence-0.4.8.dist-info/RECORD +0 -34
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.1.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.1.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.1.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.1.dist-info}/zip-safe +0 -0
md2conf/domain.py
CHANGED
|
@@ -7,7 +7,7 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Literal
|
|
10
|
+
from typing import Literal
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@dataclass
|
|
@@ -39,8 +39,8 @@ class ConfluenceDocumentOptions:
|
|
|
39
39
|
|
|
40
40
|
ignore_invalid_url: bool = False
|
|
41
41
|
heading_anchors: bool = False
|
|
42
|
-
generated_by:
|
|
43
|
-
root_page_id:
|
|
42
|
+
generated_by: str | None = "This page has been generated with a tool."
|
|
43
|
+
root_page_id: ConfluencePageID | None = None
|
|
44
44
|
keep_hierarchy: bool = False
|
|
45
45
|
prefer_raster: bool = True
|
|
46
46
|
render_drawio: bool = False
|
md2conf/drawio.py
CHANGED
|
@@ -51,7 +51,7 @@ def inflate(data: bytes) -> bytes:
|
|
|
51
51
|
return zlib.decompress(data, -zlib.MAX_WBITS)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def decompress_diagram(xml_data:
|
|
54
|
+
def decompress_diagram(xml_data: bytes | str) -> ElementType:
|
|
55
55
|
"""
|
|
56
56
|
Decompresses the text content of the `<diagram>` element in a draw.io XML document.
|
|
57
57
|
|
md2conf/environment.py
CHANGED
|
@@ -7,7 +7,7 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import overload
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class ArgumentError(ValueError):
|
|
@@ -27,10 +27,10 @@ def _validate_domain(domain: str) -> str: ...
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@overload
|
|
30
|
-
def _validate_domain(domain:
|
|
30
|
+
def _validate_domain(domain: str | None) -> str | None: ...
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def _validate_domain(domain:
|
|
33
|
+
def _validate_domain(domain: str | None) -> str | None:
|
|
34
34
|
if domain is None:
|
|
35
35
|
return None
|
|
36
36
|
|
|
@@ -45,10 +45,10 @@ def _validate_base_path(base_path: str) -> str: ...
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
@overload
|
|
48
|
-
def _validate_base_path(base_path:
|
|
48
|
+
def _validate_base_path(base_path: str | None) -> str | None: ...
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def _validate_base_path(base_path:
|
|
51
|
+
def _validate_base_path(base_path: str | None) -> str | None:
|
|
52
52
|
if base_path is None:
|
|
53
53
|
return None
|
|
54
54
|
|
|
@@ -61,13 +61,13 @@ def _validate_base_path(base_path: Optional[str]) -> Optional[str]:
|
|
|
61
61
|
class ConfluenceSiteProperties:
|
|
62
62
|
domain: str
|
|
63
63
|
base_path: str
|
|
64
|
-
space_key:
|
|
64
|
+
space_key: str | None
|
|
65
65
|
|
|
66
66
|
def __init__(
|
|
67
67
|
self,
|
|
68
|
-
domain:
|
|
69
|
-
base_path:
|
|
70
|
-
space_key:
|
|
68
|
+
domain: str | None = None,
|
|
69
|
+
base_path: str | None = None,
|
|
70
|
+
space_key: str | None = None,
|
|
71
71
|
) -> None:
|
|
72
72
|
opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
|
|
73
73
|
opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
|
|
@@ -93,24 +93,24 @@ class ConfluenceConnectionProperties:
|
|
|
93
93
|
:param headers: Additional HTTP headers to pass to Confluence REST API calls.
|
|
94
94
|
"""
|
|
95
95
|
|
|
96
|
-
domain:
|
|
97
|
-
base_path:
|
|
98
|
-
space_key:
|
|
99
|
-
api_url:
|
|
100
|
-
user_name:
|
|
96
|
+
domain: str | None
|
|
97
|
+
base_path: str | None
|
|
98
|
+
space_key: str | None
|
|
99
|
+
api_url: str | None
|
|
100
|
+
user_name: str | None
|
|
101
101
|
api_key: str
|
|
102
|
-
headers:
|
|
102
|
+
headers: dict[str, str] | None
|
|
103
103
|
|
|
104
104
|
def __init__(
|
|
105
105
|
self,
|
|
106
106
|
*,
|
|
107
|
-
api_url:
|
|
108
|
-
domain:
|
|
109
|
-
base_path:
|
|
110
|
-
user_name:
|
|
111
|
-
api_key:
|
|
112
|
-
space_key:
|
|
113
|
-
headers:
|
|
107
|
+
api_url: str | None = None,
|
|
108
|
+
domain: str | None = None,
|
|
109
|
+
base_path: str | None = None,
|
|
110
|
+
user_name: str | None = None,
|
|
111
|
+
api_key: str | None = None,
|
|
112
|
+
space_key: str | None = None,
|
|
113
|
+
headers: dict[str, str] | None = None,
|
|
114
114
|
) -> None:
|
|
115
115
|
opt_api_url = api_url or os.getenv("CONFLUENCE_API_URL")
|
|
116
116
|
opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
|
md2conf/latex.py
CHANGED
|
@@ -10,7 +10,7 @@ import importlib.util
|
|
|
10
10
|
from io import BytesIO
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from struct import unpack
|
|
13
|
-
from typing import BinaryIO, Iterable, Literal,
|
|
13
|
+
from typing import BinaryIO, Iterable, Literal, overload
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def render_latex(expression: str, *, format: Literal["png", "svg"] = "png", dpi: int = 100, font_size: int = 12) -> bytes:
|
|
@@ -71,10 +71,10 @@ def get_png_dimensions(*, data: bytes) -> tuple[int, int]: ...
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
@overload
|
|
74
|
-
def get_png_dimensions(*, path:
|
|
74
|
+
def get_png_dimensions(*, path: str | Path) -> tuple[int, int]: ...
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def get_png_dimensions(*, data:
|
|
77
|
+
def get_png_dimensions(*, data: bytes | None = None, path: str | Path | None = None) -> tuple[int, int]:
|
|
78
78
|
"""
|
|
79
79
|
Returns the width and height of a PNG image inspecting its header.
|
|
80
80
|
|
|
@@ -100,20 +100,20 @@ def remove_png_chunks(names: Iterable[str], *, source_data: bytes) -> bytes: ...
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
@overload
|
|
103
|
-
def remove_png_chunks(names: Iterable[str], *, source_path:
|
|
103
|
+
def remove_png_chunks(names: Iterable[str], *, source_path: str | Path) -> bytes: ...
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
@overload
|
|
107
|
-
def remove_png_chunks(names: Iterable[str], *, source_data: bytes, target_path:
|
|
107
|
+
def remove_png_chunks(names: Iterable[str], *, source_data: bytes, target_path: str | Path) -> None: ...
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
@overload
|
|
111
|
-
def remove_png_chunks(names: Iterable[str], *, source_path:
|
|
111
|
+
def remove_png_chunks(names: Iterable[str], *, source_path: str | Path, target_path: str | Path) -> None: ...
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
def remove_png_chunks(
|
|
115
|
-
names: Iterable[str], *, source_data:
|
|
116
|
-
) ->
|
|
115
|
+
names: Iterable[str], *, source_data: bytes | None = None, source_path: str | Path | None = None, target_path: str | Path | None = None
|
|
116
|
+
) -> bytes | None:
|
|
117
117
|
"""
|
|
118
118
|
Rewrites a PNG file by removing chunks with the specified names.
|
|
119
119
|
|
|
@@ -168,7 +168,7 @@ def _read_signature(f: BinaryIO) -> None:
|
|
|
168
168
|
raise ValueError("not a valid PNG file")
|
|
169
169
|
|
|
170
170
|
|
|
171
|
-
def _read_chunk(f: BinaryIO) ->
|
|
171
|
+
def _read_chunk(f: BinaryIO) -> _Chunk | None:
|
|
172
172
|
"Reads and parses a PNG chunk such as `IHDR` or `tEXt`."
|
|
173
173
|
|
|
174
174
|
length_bytes = f.read(4)
|
md2conf/local.py
CHANGED
|
@@ -9,7 +9,6 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Optional
|
|
13
12
|
|
|
14
13
|
from .converter import ConfluenceDocument
|
|
15
14
|
from .domain import ConfluenceDocumentOptions, ConfluencePageID
|
|
@@ -30,7 +29,7 @@ class LocalProcessor(Processor):
|
|
|
30
29
|
options: ConfluenceDocumentOptions,
|
|
31
30
|
site: ConfluenceSiteMetadata,
|
|
32
31
|
*,
|
|
33
|
-
out_dir:
|
|
32
|
+
out_dir: Path | None,
|
|
34
33
|
root_dir: Path,
|
|
35
34
|
) -> None:
|
|
36
35
|
"""
|
|
@@ -46,7 +45,7 @@ class LocalProcessor(Processor):
|
|
|
46
45
|
self.out_dir = out_dir or root_dir
|
|
47
46
|
|
|
48
47
|
@override
|
|
49
|
-
def _synchronize_tree(self, root: DocumentNode, root_id:
|
|
48
|
+
def _synchronize_tree(self, root: DocumentNode, root_id: ConfluencePageID | None) -> None:
|
|
50
49
|
"""
|
|
51
50
|
Creates the cross-reference index.
|
|
52
51
|
|
|
@@ -89,13 +88,13 @@ class LocalProcessor(Processor):
|
|
|
89
88
|
|
|
90
89
|
|
|
91
90
|
class LocalProcessorFactory(ProcessorFactory):
|
|
92
|
-
out_dir:
|
|
91
|
+
out_dir: Path | None
|
|
93
92
|
|
|
94
93
|
def __init__(
|
|
95
94
|
self,
|
|
96
95
|
options: ConfluenceDocumentOptions,
|
|
97
96
|
site: ConfluenceSiteMetadata,
|
|
98
|
-
out_dir:
|
|
97
|
+
out_dir: Path | None = None,
|
|
99
98
|
) -> None:
|
|
100
99
|
super().__init__(options, site)
|
|
101
100
|
self.out_dir = out_dir
|
|
@@ -113,6 +112,6 @@ class LocalConverter(Converter):
|
|
|
113
112
|
self,
|
|
114
113
|
options: ConfluenceDocumentOptions,
|
|
115
114
|
site: ConfluenceSiteMetadata,
|
|
116
|
-
out_dir:
|
|
115
|
+
out_dir: Path | None = None,
|
|
117
116
|
) -> None:
|
|
118
117
|
super().__init__(LocalProcessorFactory(options, site, out_dir))
|
md2conf/markdown.py
CHANGED
|
@@ -7,7 +7,7 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import xml.etree.ElementTree
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
import markdown
|
|
13
13
|
|
|
@@ -15,11 +15,11 @@ import markdown
|
|
|
15
15
|
def _emoji_generator(
|
|
16
16
|
index: str,
|
|
17
17
|
shortname: str,
|
|
18
|
-
alias:
|
|
19
|
-
uc:
|
|
18
|
+
alias: str | None,
|
|
19
|
+
uc: str | None,
|
|
20
20
|
alt: str,
|
|
21
|
-
title:
|
|
22
|
-
category:
|
|
21
|
+
title: str | None,
|
|
22
|
+
category: str | None,
|
|
23
23
|
options: dict[str, Any],
|
|
24
24
|
md: markdown.Markdown,
|
|
25
25
|
) -> xml.etree.ElementTree.Element:
|
|
@@ -46,9 +46,9 @@ def _verbatim_formatter(
|
|
|
46
46
|
css_class: str,
|
|
47
47
|
options: dict[str, Any],
|
|
48
48
|
md: markdown.Markdown,
|
|
49
|
-
classes:
|
|
49
|
+
classes: list[str] | None = None,
|
|
50
50
|
id_value: str = "",
|
|
51
|
-
attrs:
|
|
51
|
+
attrs: dict[str, str] | None = None,
|
|
52
52
|
**kwargs: Any,
|
|
53
53
|
) -> str:
|
|
54
54
|
"""
|
md2conf/matcher.py
CHANGED
|
@@ -10,7 +10,7 @@ import os.path
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from fnmatch import fnmatch
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Iterable,
|
|
13
|
+
from typing import Iterable, final, overload
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass(frozen=True, eq=True)
|
|
@@ -95,14 +95,14 @@ class MatcherOptions:
|
|
|
95
95
|
"""
|
|
96
96
|
|
|
97
97
|
source: str
|
|
98
|
-
extension:
|
|
98
|
+
extension: str | None = None
|
|
99
99
|
|
|
100
100
|
def __post_init__(self) -> None:
|
|
101
101
|
if self.extension is not None and not self.extension.startswith("."):
|
|
102
102
|
self.extension = f".{self.extension}"
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def _entry_name_dir(entry:
|
|
105
|
+
def _entry_name_dir(entry: Entry | os.DirEntry[str]) -> tuple[str, bool]:
|
|
106
106
|
if isinstance(entry, Entry):
|
|
107
107
|
return entry.name, entry.is_dir
|
|
108
108
|
else:
|
|
@@ -155,7 +155,7 @@ class Matcher:
|
|
|
155
155
|
|
|
156
156
|
...
|
|
157
157
|
|
|
158
|
-
def is_excluded(self, entry:
|
|
158
|
+
def is_excluded(self, entry: Entry | os.DirEntry[str]) -> bool:
|
|
159
159
|
name, is_dir = _entry_name_dir(entry)
|
|
160
160
|
|
|
161
161
|
# skip hidden files and directories
|
|
@@ -192,7 +192,7 @@ class Matcher:
|
|
|
192
192
|
"""
|
|
193
193
|
...
|
|
194
194
|
|
|
195
|
-
def is_included(self, entry:
|
|
195
|
+
def is_included(self, entry: Entry | os.DirEntry[str]) -> bool:
|
|
196
196
|
return not self.is_excluded(entry)
|
|
197
197
|
|
|
198
198
|
def filter(self, entries: Iterable[Entry]) -> list[Entry]:
|
md2conf/mermaid.py
CHANGED
|
@@ -12,7 +12,7 @@ import os.path
|
|
|
12
12
|
import shutil
|
|
13
13
|
import subprocess
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
-
from typing import Literal
|
|
15
|
+
from typing import Literal
|
|
16
16
|
|
|
17
17
|
LOGGER = logging.getLogger(__name__)
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@ class MermaidConfigProperties:
|
|
|
25
25
|
:param scale: Scaling factor for the rendered diagram.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
scale:
|
|
28
|
+
scale: float | None = None
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def is_docker() -> bool:
|
|
@@ -56,7 +56,7 @@ def has_mmdc() -> bool:
|
|
|
56
56
|
return shutil.which(executable) is not None
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def render_diagram(source: str, output_format: Literal["png", "svg"] = "png", config:
|
|
59
|
+
def render_diagram(source: str, output_format: Literal["png", "svg"] = "png", config: MermaidConfigProperties | None = None) -> bytes:
|
|
60
60
|
"Generates a PNG or SVG image from a Mermaid diagram source."
|
|
61
61
|
|
|
62
62
|
if config is None:
|
md2conf/metadata.py
CHANGED
|
@@ -7,7 +7,6 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Optional
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
@dataclass
|
|
@@ -22,7 +21,7 @@ class ConfluenceSiteMetadata:
|
|
|
22
21
|
|
|
23
22
|
domain: str
|
|
24
23
|
base_path: str
|
|
25
|
-
space_key:
|
|
24
|
+
space_key: str | None
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
@dataclass
|
md2conf/processor.py
CHANGED
|
@@ -11,7 +11,7 @@ import logging
|
|
|
11
11
|
import os
|
|
12
12
|
from abc import abstractmethod
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Iterable
|
|
14
|
+
from typing import Iterable
|
|
15
15
|
|
|
16
16
|
from .collection import ConfluencePageCollection
|
|
17
17
|
from .converter import ConfluenceDocument
|
|
@@ -26,9 +26,9 @@ LOGGER = logging.getLogger(__name__)
|
|
|
26
26
|
|
|
27
27
|
class DocumentNode:
|
|
28
28
|
absolute_path: Path
|
|
29
|
-
page_id:
|
|
30
|
-
space_key:
|
|
31
|
-
title:
|
|
29
|
+
page_id: str | None
|
|
30
|
+
space_key: str | None
|
|
31
|
+
title: str | None
|
|
32
32
|
synchronized: bool
|
|
33
33
|
|
|
34
34
|
_children: list["DocumentNode"]
|
|
@@ -36,9 +36,9 @@ class DocumentNode:
|
|
|
36
36
|
def __init__(
|
|
37
37
|
self,
|
|
38
38
|
absolute_path: Path,
|
|
39
|
-
page_id:
|
|
40
|
-
space_key:
|
|
41
|
-
title:
|
|
39
|
+
page_id: str | None,
|
|
40
|
+
space_key: str | None,
|
|
41
|
+
title: str | None,
|
|
42
42
|
synchronized: bool,
|
|
43
43
|
):
|
|
44
44
|
self.absolute_path = absolute_path
|
|
@@ -140,7 +140,7 @@ class Processor:
|
|
|
140
140
|
self._update_page(page_id, document, path)
|
|
141
141
|
|
|
142
142
|
@abstractmethod
|
|
143
|
-
def _synchronize_tree(self, root: DocumentNode, root_id:
|
|
143
|
+
def _synchronize_tree(self, root: DocumentNode, root_id: ConfluencePageID | None) -> None:
|
|
144
144
|
"""
|
|
145
145
|
Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
|
|
146
146
|
|
|
@@ -157,7 +157,7 @@ class Processor:
|
|
|
157
157
|
"""
|
|
158
158
|
...
|
|
159
159
|
|
|
160
|
-
def _index_directory(self, local_dir: Path, parent:
|
|
160
|
+
def _index_directory(self, local_dir: Path, parent: DocumentNode | None) -> DocumentNode:
|
|
161
161
|
"""
|
|
162
162
|
Indexes Markdown files in a directory hierarchy recursively.
|
|
163
163
|
"""
|
|
@@ -181,7 +181,7 @@ class Processor:
|
|
|
181
181
|
directories.sort()
|
|
182
182
|
|
|
183
183
|
# make page act as parent node
|
|
184
|
-
parent_doc:
|
|
184
|
+
parent_doc: Path | None = None
|
|
185
185
|
if FileEntry("index.md") in files:
|
|
186
186
|
parent_doc = local_dir / "index.md"
|
|
187
187
|
elif FileEntry("README.md") in files:
|
|
@@ -277,7 +277,7 @@ class Converter:
|
|
|
277
277
|
else:
|
|
278
278
|
raise ArgumentError(f"expected: valid file or directory path; got: {path}")
|
|
279
279
|
|
|
280
|
-
def process_directory(self, local_dir: Path, root_dir:
|
|
280
|
+
def process_directory(self, local_dir: Path, root_dir: Path | None = None) -> None:
|
|
281
281
|
"""
|
|
282
282
|
Recursively scans a directory hierarchy for Markdown files, and processes each, resolving cross-references.
|
|
283
283
|
"""
|
|
@@ -290,7 +290,7 @@ class Converter:
|
|
|
290
290
|
|
|
291
291
|
self.factory.create(root_dir).process_directory(local_dir)
|
|
292
292
|
|
|
293
|
-
def process_page(self, path: Path, root_dir:
|
|
293
|
+
def process_page(self, path: Path, root_dir: Path | None = None) -> None:
|
|
294
294
|
"""
|
|
295
295
|
Processes a single Markdown file.
|
|
296
296
|
"""
|
md2conf/publisher.py
CHANGED
|
@@ -8,7 +8,6 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Optional
|
|
12
11
|
|
|
13
12
|
from .api import ConfluenceContentProperty, ConfluenceLabel, ConfluenceSession, ConfluenceStatus
|
|
14
13
|
from .converter import ConfluenceDocument, attachment_name, get_volatile_attributes, get_volatile_elements
|
|
@@ -43,7 +42,7 @@ class SynchronizingProcessor(Processor):
|
|
|
43
42
|
self.api = api
|
|
44
43
|
|
|
45
44
|
@override
|
|
46
|
-
def _synchronize_tree(self, root: DocumentNode, root_id:
|
|
45
|
+
def _synchronize_tree(self, root: DocumentNode, root_id: ConfluencePageID | None) -> None:
|
|
47
46
|
"""
|
|
48
47
|
Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
|
|
49
48
|
|
md2conf/scanner.py
CHANGED
|
@@ -10,25 +10,17 @@ import re
|
|
|
10
10
|
import typing
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Literal,
|
|
13
|
+
from typing import Any, Literal, TypeVar
|
|
14
14
|
|
|
15
15
|
import yaml
|
|
16
|
-
from strong_typing.core import JsonType
|
|
17
|
-
from strong_typing.serialization import DeserializerOptions, json_to_object
|
|
18
16
|
|
|
19
17
|
from .mermaid import MermaidConfigProperties
|
|
18
|
+
from .serializer import JsonType, json_to_object
|
|
20
19
|
|
|
21
20
|
T = TypeVar("T")
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
def
|
|
25
|
-
typ: type[T],
|
|
26
|
-
data: JsonType,
|
|
27
|
-
) -> T:
|
|
28
|
-
return json_to_object(typ, data, options=DeserializerOptions(skip_unassigned=True))
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def extract_value(pattern: str, text: str) -> tuple[Optional[str], str]:
|
|
23
|
+
def extract_value(pattern: str, text: str) -> tuple[str | None, str]:
|
|
32
24
|
values: list[str] = []
|
|
33
25
|
|
|
34
26
|
def _repl_func(matchobj: re.Match[str]) -> str:
|
|
@@ -40,18 +32,18 @@ def extract_value(pattern: str, text: str) -> tuple[Optional[str], str]:
|
|
|
40
32
|
return value, text
|
|
41
33
|
|
|
42
34
|
|
|
43
|
-
def extract_frontmatter_block(text: str) -> tuple[
|
|
35
|
+
def extract_frontmatter_block(text: str) -> tuple[str | None, str]:
|
|
44
36
|
"Extracts the front-matter from a Markdown document as a blob of unparsed text."
|
|
45
37
|
|
|
46
38
|
return extract_value(r"(?ms)\A---$(.+?)^---$", text)
|
|
47
39
|
|
|
48
40
|
|
|
49
|
-
def extract_frontmatter_properties(text: str) -> tuple[
|
|
41
|
+
def extract_frontmatter_properties(text: str) -> tuple[dict[str, JsonType] | None, str]:
|
|
50
42
|
"Extracts the front-matter from a Markdown document as a dictionary."
|
|
51
43
|
|
|
52
44
|
block, text = extract_frontmatter_block(text)
|
|
53
45
|
|
|
54
|
-
properties:
|
|
46
|
+
properties: dict[str, Any] | None = None
|
|
55
47
|
if block is not None:
|
|
56
48
|
data = yaml.safe_load(block)
|
|
57
49
|
if isinstance(data, dict):
|
|
@@ -77,16 +69,16 @@ class DocumentProperties:
|
|
|
77
69
|
:param alignment: Alignment for block-level images and formulas.
|
|
78
70
|
"""
|
|
79
71
|
|
|
80
|
-
page_id:
|
|
81
|
-
space_key:
|
|
82
|
-
confluence_page_id:
|
|
83
|
-
confluence_space_key:
|
|
84
|
-
generated_by:
|
|
85
|
-
title:
|
|
86
|
-
tags:
|
|
87
|
-
synchronized:
|
|
88
|
-
properties:
|
|
89
|
-
alignment:
|
|
72
|
+
page_id: str | None = None
|
|
73
|
+
space_key: str | None = None
|
|
74
|
+
confluence_page_id: str | None = None
|
|
75
|
+
confluence_space_key: str | None = None
|
|
76
|
+
generated_by: str | None = None
|
|
77
|
+
title: str | None = None
|
|
78
|
+
tags: list[str] | None = None
|
|
79
|
+
synchronized: bool | None = None
|
|
80
|
+
properties: dict[str, JsonType] | None = None
|
|
81
|
+
alignment: Literal["center", "left", "right"] | None = None
|
|
90
82
|
|
|
91
83
|
|
|
92
84
|
@dataclass
|
|
@@ -105,14 +97,14 @@ class ScannedDocument:
|
|
|
105
97
|
:param text: Text that remains after front-matter and inline properties have been extracted.
|
|
106
98
|
"""
|
|
107
99
|
|
|
108
|
-
page_id:
|
|
109
|
-
space_key:
|
|
110
|
-
generated_by:
|
|
111
|
-
title:
|
|
112
|
-
tags:
|
|
113
|
-
synchronized:
|
|
114
|
-
properties:
|
|
115
|
-
alignment:
|
|
100
|
+
page_id: str | None
|
|
101
|
+
space_key: str | None
|
|
102
|
+
generated_by: str | None
|
|
103
|
+
title: str | None
|
|
104
|
+
tags: list[str] | None
|
|
105
|
+
synchronized: bool | None
|
|
106
|
+
properties: dict[str, JsonType] | None
|
|
107
|
+
alignment: Literal["center", "left", "right"] | None
|
|
116
108
|
text: str
|
|
117
109
|
|
|
118
110
|
|
|
@@ -135,16 +127,16 @@ class Scanner:
|
|
|
135
127
|
# extract 'generated-by' tag text
|
|
136
128
|
generated_by, text = extract_value(r"<!--\s+generated[-_]by:\s*(.*)\s+-->", text)
|
|
137
129
|
|
|
138
|
-
title:
|
|
139
|
-
tags:
|
|
140
|
-
synchronized:
|
|
141
|
-
properties:
|
|
142
|
-
alignment:
|
|
130
|
+
title: str | None = None
|
|
131
|
+
tags: list[str] | None = None
|
|
132
|
+
synchronized: bool | None = None
|
|
133
|
+
properties: dict[str, JsonType] | None = None
|
|
134
|
+
alignment: Literal["center", "left", "right"] | None = None
|
|
143
135
|
|
|
144
136
|
# extract front-matter
|
|
145
137
|
data, text = extract_frontmatter_properties(text)
|
|
146
138
|
if data is not None:
|
|
147
|
-
p =
|
|
139
|
+
p = json_to_object(DocumentProperties, data)
|
|
148
140
|
page_id = page_id or p.confluence_page_id or p.page_id
|
|
149
141
|
space_key = space_key or p.confluence_space_key or p.space_key
|
|
150
142
|
generated_by = generated_by or p.generated_by
|
|
@@ -176,8 +168,8 @@ class MermaidProperties:
|
|
|
176
168
|
:param config: Configuration options for rendering.
|
|
177
169
|
"""
|
|
178
170
|
|
|
179
|
-
title:
|
|
180
|
-
config:
|
|
171
|
+
title: str | None = None
|
|
172
|
+
config: MermaidConfigProperties | None = None
|
|
181
173
|
|
|
182
174
|
|
|
183
175
|
class MermaidScanner:
|
|
@@ -203,7 +195,7 @@ class MermaidScanner:
|
|
|
203
195
|
|
|
204
196
|
properties, _ = extract_frontmatter_properties(content)
|
|
205
197
|
if properties is not None:
|
|
206
|
-
front_matter =
|
|
198
|
+
front_matter = json_to_object(MermaidProperties, properties)
|
|
207
199
|
config = front_matter.config or MermaidConfigProperties()
|
|
208
200
|
|
|
209
201
|
return MermaidProperties(title=front_matter.title, config=config)
|
md2conf/serializer.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish Markdown files to Confluence wiki.
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/md2conf
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import TypeVar
|
|
12
|
+
|
|
13
|
+
from cattrs.preconf.orjson import make_converter
|
|
14
|
+
|
|
15
|
+
JsonType = None | bool | int | float | str | dict[str, "JsonType"] | list["JsonType"]
|
|
16
|
+
JsonComposite = dict[str, "JsonType"] | list["JsonType"]
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_converter = make_converter(forbid_extra_keys=False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if sys.version_info < (3, 11):
|
|
25
|
+
|
|
26
|
+
@_converter.register_structure_hook
|
|
27
|
+
def datetime_structure_hook(value: str, cls: type[datetime]) -> datetime:
|
|
28
|
+
if value.endswith("Z"):
|
|
29
|
+
# fromisoformat() prior to Python version 3.11 does not support military time zones like "Zulu" for UTC
|
|
30
|
+
value = f"{value[:-1]}+00:00"
|
|
31
|
+
return datetime.fromisoformat(value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@_converter.register_structure_hook
|
|
35
|
+
def json_type_structure_hook(value: JsonType, cls: type[JsonType]) -> JsonType:
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@_converter.register_structure_hook
|
|
40
|
+
def json_composite_structure_hook(value: JsonComposite, cls: type[JsonComposite]) -> JsonComposite:
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def json_to_object(typ: type[T], data: JsonType) -> T:
|
|
45
|
+
"""
|
|
46
|
+
Converts a raw JSON object to a structured object, validating input data.
|
|
47
|
+
|
|
48
|
+
:param typ: Target structured type.
|
|
49
|
+
:param data: Source data as a JSON object.
|
|
50
|
+
:returns: A valid object instance of the expected type.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
return _converter.structure(data, typ)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def object_to_json_payload(data: object) -> bytes:
|
|
57
|
+
"""
|
|
58
|
+
Converts a structured object to a JSON string encoded in UTF-8.
|
|
59
|
+
|
|
60
|
+
:param data: Object to convert to a JSON string.
|
|
61
|
+
:returns: JSON string encoded in UTF-8.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
return _converter.dumps(data)
|