markdown-to-confluence 0.4.7__py3-none-any.whl → 0.5.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.
md2conf/converter.py CHANGED
@@ -16,12 +16,11 @@ import uuid
16
16
  from abc import ABC, abstractmethod
17
17
  from dataclasses import dataclass
18
18
  from pathlib import Path
19
- from typing import ClassVar, Literal, Optional, Union
19
+ from typing import ClassVar, Literal
20
20
  from urllib.parse import ParseResult, quote_plus, urlparse
21
21
 
22
22
  import lxml.etree as ET
23
- from strong_typing.core import JsonType
24
- from strong_typing.exception import JsonTypeError
23
+ from cattrs import BaseValidationError
25
24
 
26
25
  from . import drawio, mermaid
27
26
  from .collection import ConfluencePageCollection
@@ -35,6 +34,7 @@ from .markdown import markdown_to_html
35
34
  from .mermaid import MermaidConfigProperties
36
35
  from .metadata import ConfluenceSiteMetadata
37
36
  from .scanner import MermaidScanner, ScannedDocument, Scanner
37
+ from .serializer import JsonType
38
38
  from .toc import TableOfContentsBuilder
39
39
  from .uri import is_absolute_url, to_uuid_urn
40
40
  from .xml import element_to_text
@@ -202,7 +202,7 @@ class NodeVisitor(ABC):
202
202
  self.visit(source)
203
203
 
204
204
  @abstractmethod
205
- def transform(self, child: ElementType) -> Optional[ElementType]: ...
205
+ def transform(self, child: ElementType) -> ElementType | None: ...
206
206
 
207
207
 
208
208
  def title_to_identifier(title: str) -> str:
@@ -273,11 +273,11 @@ class ImageAttributes:
273
273
  """
274
274
 
275
275
  context: FormattingContext
276
- width: Optional[int]
277
- height: Optional[int]
278
- alt: Optional[str]
279
- title: Optional[str]
280
- caption: Optional[str]
276
+ width: int | None
277
+ height: int | None
278
+ alt: str | None
279
+ title: str | None
280
+ caption: str | None
281
281
  alignment: ImageAlignment = ImageAlignment.CENTER
282
282
 
283
283
  def __post_init__(self) -> None:
@@ -374,13 +374,13 @@ class ConfluenceConverterOptions:
374
374
  @dataclass
375
375
  class ImageData:
376
376
  path: Path
377
- description: Optional[str] = None
377
+ description: str | None = None
378
378
 
379
379
 
380
380
  @dataclass
381
381
  class EmbeddedFileData:
382
382
  data: bytes
383
- description: Optional[str] = None
383
+ description: str | None = None
384
384
 
385
385
 
386
386
  @dataclass
@@ -405,18 +405,18 @@ class ConfluencePanel:
405
405
 
406
406
 
407
407
  ConfluencePanel.from_class = {
408
- "attention": ConfluencePanel("❗", "exclamation", "#F9F9F9"), # rST admonition
409
- "caution": ConfluencePanel("❌", "x", "#FFEBE9"),
410
- "danger": ConfluencePanel("☠️", "skull_crossbones", "#FFE5E5"), # rST admonition
411
- "disclaimer": ConfluencePanel("❗", "exclamation", "#F9F9F9"), # GitLab
412
- "error": ConfluencePanel("❌", "x", "#FFEBE9"), # rST admonition
413
- "flag": ConfluencePanel("🚩", "triangular_flag_on_post", "#FDECEA"), # GitLab
414
- "hint": ConfluencePanel("💡", "bulb", "#DAFBE1"), # rST admonition
415
- "info": ConfluencePanel("ℹ️", "information_source", "#DDF4FF"),
416
- "note": ConfluencePanel("📝", "pencil", "#DDF4FF"),
417
- "tip": ConfluencePanel("💡", "bulb", "#DAFBE1"),
418
- "important": ConfluencePanel("❗", "exclamation", "#FBEFFF"),
419
- "warning": ConfluencePanel("⚠️", "warning", "#FFF8C5"),
408
+ "attention": ConfluencePanel("❗", "exclamation", "var(--ds-background-accent-gray-subtlest)"), # rST admonition
409
+ "caution": ConfluencePanel("❌", "x", "var(--ds-background-accent-orange-subtlest)"),
410
+ "danger": ConfluencePanel("☠️", "skull_crossbones", "var(--ds-background-accent-red-subtlest)"), # rST admonition
411
+ "disclaimer": ConfluencePanel("❗", "exclamation", "var(--ds-background-accent-gray-subtlest)"), # GitLab
412
+ "error": ConfluencePanel("❌", "x", "var(--ds-background-accent-red-subtlest)"), # rST admonition
413
+ "flag": ConfluencePanel("🚩", "triangular_flag_on_post", "var(--ds-background-accent-orange-subtlest"), # GitLab
414
+ "hint": ConfluencePanel("💡", "bulb", "var(--ds-background-accent-green-subtlest)"), # rST admonition
415
+ "info": ConfluencePanel("ℹ️", "information_source", "var(--ds-background-accent-blue-subtlest)"),
416
+ "note": ConfluencePanel("📝", "pencil", "var(--ds-background-accent-teal-subtlest)"),
417
+ "tip": ConfluencePanel("💡", "bulb", "var(--ds-background-accent-green-subtlest)"),
418
+ "important": ConfluencePanel("❗", "exclamation", "var(--ds-background-accent-purple-subtlest)"),
419
+ "warning": ConfluencePanel("⚠️", "warning", "var(--ds-background-accent-yellow-subtlest)"),
420
420
  }
421
421
 
422
422
 
@@ -506,7 +506,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
506
506
  else:
507
507
  raise DocumentError(msg)
508
508
 
509
- def _transform_link(self, anchor: ElementType) -> Optional[ElementType]:
509
+ def _transform_link(self, anchor: ElementType) -> ElementType | None:
510
510
  """
511
511
  Transforms links (HTML anchor `<a>`).
512
512
 
@@ -559,7 +559,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
559
559
  else:
560
560
  return self._transform_attachment_link(anchor, absolute_path)
561
561
 
562
- def _transform_page_link(self, anchor: ElementType, relative_url: ParseResult, absolute_path: Path) -> Optional[ElementType]:
562
+ def _transform_page_link(self, anchor: ElementType, relative_url: ParseResult, absolute_path: Path) -> ElementType | None:
563
563
  """
564
564
  Transforms links to other Markdown documents (Confluence pages).
565
565
  """
@@ -596,7 +596,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
596
596
  anchor.set("href", transformed_url.geturl())
597
597
  return None
598
598
 
599
- def _transform_attachment_link(self, anchor: ElementType, absolute_path: Path) -> Optional[ElementType]:
599
+ def _transform_attachment_link(self, anchor: ElementType, absolute_path: Path) -> ElementType | None:
600
600
  """
601
601
  Transforms links to document binaries such as PDF, DOCX or XLSX.
602
602
  """
@@ -713,7 +713,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
713
713
  else:
714
714
  raise DocumentError(msg)
715
715
 
716
- def _verify_image_path(self, path: Path) -> Optional[Path]:
716
+ def _verify_image_path(self, path: Path) -> Path | None:
717
717
  "Checks whether an image path is safe to use."
718
718
 
719
719
  # resolve relative path into absolute path w.r.t. base dir
@@ -817,6 +817,14 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
817
817
  str(attrs.height),
818
818
  ),
819
819
  )
820
+ if attrs.alignment is ImageAlignment.CENTER:
821
+ parameters.append(
822
+ AC_ELEM(
823
+ "parameter",
824
+ {AC_ATTR("name"): "pCenter"},
825
+ str(1),
826
+ ),
827
+ )
820
828
 
821
829
  local_id = str(uuid.uuid4())
822
830
  macro_id = str(uuid.uuid4())
@@ -897,12 +905,12 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
897
905
  AC_ELEM("plain-text-body", ET.CDATA(content)),
898
906
  )
899
907
 
900
- def _extract_mermaid_config(self, content: str) -> Optional[MermaidConfigProperties]:
908
+ def _extract_mermaid_config(self, content: str) -> MermaidConfigProperties | None:
901
909
  """Extract scale from Mermaid YAML front matter configuration."""
902
910
  try:
903
911
  properties = MermaidScanner().read(content)
904
912
  return properties.config
905
- except JsonTypeError as ex:
913
+ except BaseValidationError as ex:
906
914
  LOGGER.warning("Failed to extract Mermaid properties: %s", ex)
907
915
  return None
908
916
 
@@ -1550,7 +1558,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
1550
1558
  return AC_ELEM("task-list", {}, *tasks)
1551
1559
 
1552
1560
  @override
1553
- def transform(self, child: ElementType) -> Optional[ElementType]:
1561
+ def transform(self, child: ElementType) -> ElementType | None:
1554
1562
  """
1555
1563
  Transforms an HTML element tree obtained from a Markdown document into a Confluence Storage Format element tree.
1556
1564
  """
@@ -1650,7 +1658,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
1650
1658
  # <li>[x] ...</li>
1651
1659
  # </ul>
1652
1660
  elif child.tag == "ul":
1653
- if len(child) > 0 and element_text_starts_with_any(child[0], ["[ ]", "[x]", "[X]"]):
1661
+ if len(child) > 0 and all(element_text_starts_with_any(item, ["[ ]", "[x]", "[X]"]) for item in child):
1654
1662
  return self._transform_tasklist(child)
1655
1663
 
1656
1664
  return None
@@ -1734,9 +1742,9 @@ class ConversionError(RuntimeError):
1734
1742
  class ConfluenceDocument:
1735
1743
  "Encapsulates an element tree for a Confluence document created by parsing a Markdown document."
1736
1744
 
1737
- title: Optional[str]
1738
- labels: Optional[list[str]]
1739
- properties: Optional[dict[str, JsonType]]
1745
+ title: str | None
1746
+ labels: list[str] | None
1747
+ properties: dict[str, JsonType] | None
1740
1748
 
1741
1749
  links: list[str]
1742
1750
  images: list[ImageData]
@@ -1844,7 +1852,7 @@ class ConfluenceDocument:
1844
1852
  return elements_to_string(self.root)
1845
1853
 
1846
1854
 
1847
- def attachment_name(ref: Union[Path, str]) -> str:
1855
+ def attachment_name(ref: Path | str) -> str:
1848
1856
  """
1849
1857
  Safe name for use with attachment uploads.
1850
1858
 
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