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.
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, Optional
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: Optional[str] = "This page has been generated with a tool."
43
- root_page_id: Optional[ConfluencePageID] = None
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: typing.Union[bytes, str]) -> ElementType:
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 Optional, overload
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: Optional[str]) -> Optional[str]: ...
30
+ def _validate_domain(domain: str | None) -> str | None: ...
31
31
 
32
32
 
33
- def _validate_domain(domain: Optional[str]) -> Optional[str]:
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: Optional[str]) -> Optional[str]: ...
48
+ def _validate_base_path(base_path: str | None) -> str | None: ...
49
49
 
50
50
 
51
- def _validate_base_path(base_path: Optional[str]) -> Optional[str]:
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: Optional[str]
64
+ space_key: str | None
65
65
 
66
66
  def __init__(
67
67
  self,
68
- domain: Optional[str] = None,
69
- base_path: Optional[str] = None,
70
- space_key: Optional[str] = None,
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: Optional[str]
97
- base_path: Optional[str]
98
- space_key: Optional[str]
99
- api_url: Optional[str]
100
- user_name: Optional[str]
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: Optional[dict[str, str]]
102
+ headers: dict[str, str] | None
103
103
 
104
104
  def __init__(
105
105
  self,
106
106
  *,
107
- api_url: Optional[str] = None,
108
- domain: Optional[str] = None,
109
- base_path: Optional[str] = None,
110
- user_name: Optional[str] = None,
111
- api_key: Optional[str] = None,
112
- space_key: Optional[str] = None,
113
- headers: Optional[dict[str, str]] = None,
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, Optional, Union, overload
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: Union[str, Path]) -> tuple[int, int]: ...
74
+ def get_png_dimensions(*, path: str | Path) -> tuple[int, int]: ...
75
75
 
76
76
 
77
- def get_png_dimensions(*, data: Optional[bytes] = None, path: Union[str, Path, None] = None) -> tuple[int, int]:
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: Union[str, Path]) -> bytes: ...
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: Union[str, Path]) -> None: ...
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: Union[str, Path], target_path: Union[str, Path]) -> None: ...
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: Optional[bytes] = None, source_path: Union[str, Path, None] = None, target_path: Union[str, Path, None] = None
116
- ) -> Optional[bytes]:
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) -> Optional[_Chunk]:
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: Optional[Path],
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: Optional[ConfluencePageID]) -> None:
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: Optional[Path]
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: Optional[Path] = None,
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: Optional[Path] = None,
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, Optional
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: Optional[str],
19
- uc: Optional[str],
18
+ alias: str | None,
19
+ uc: str | None,
20
20
  alt: str,
21
- title: Optional[str],
22
- category: Optional[str],
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: Optional[list[str]] = None,
49
+ classes: list[str] | None = None,
50
50
  id_value: str = "",
51
- attrs: Optional[dict[str, str]] = None,
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, Optional, Union, final, overload
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: Optional[str] = None
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: Union[Entry, os.DirEntry[str]]) -> tuple[str, bool]:
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: Union[Entry, os.DirEntry[str]]) -> bool:
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: Union[Entry, os.DirEntry[str]]) -> bool:
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, Optional
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: Optional[float] = None
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: Optional[MermaidConfigProperties] = None) -> bytes:
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: Optional[str]
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, Optional
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: Optional[str]
30
- space_key: Optional[str]
31
- title: Optional[str]
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: Optional[str],
40
- space_key: Optional[str],
41
- title: Optional[str],
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: Optional[ConfluencePageID]) -> None:
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: Optional[DocumentNode]) -> DocumentNode:
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: Optional[Path] = None
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: Optional[Path] = None) -> None:
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: Optional[Path] = None) -> None:
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: Optional[ConfluencePageID]) -> None:
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, Optional, TypeVar
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 _json_to_object(
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[Optional[str], str]:
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[Optional[dict[str, JsonType]], str]:
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: Optional[dict[str, Any]] = None
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: Optional[str]
81
- space_key: Optional[str]
82
- confluence_page_id: Optional[str]
83
- confluence_space_key: Optional[str]
84
- generated_by: Optional[str]
85
- title: Optional[str]
86
- tags: Optional[list[str]]
87
- synchronized: Optional[bool]
88
- properties: Optional[dict[str, JsonType]]
89
- alignment: Optional[Literal["center", "left", "right"]]
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: Optional[str]
109
- space_key: Optional[str]
110
- generated_by: Optional[str]
111
- title: Optional[str]
112
- tags: Optional[list[str]]
113
- synchronized: Optional[bool]
114
- properties: Optional[dict[str, JsonType]]
115
- alignment: Optional[Literal["center", "left", "right"]]
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: Optional[str] = None
139
- tags: Optional[list[str]] = None
140
- synchronized: Optional[bool] = None
141
- properties: Optional[dict[str, JsonType]] = None
142
- alignment: Optional[Literal["center", "left", "right"]] = None
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 = _json_to_object(DocumentProperties, data)
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: Optional[str] = None
180
- config: Optional[MermaidConfigProperties] = None
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 = _json_to_object(MermaidProperties, properties)
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)