markdown-to-confluence 0.5.1__py3-none-any.whl → 0.5.3__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.
Files changed (54) hide show
  1. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/METADATA +160 -11
  2. markdown_to_confluence-0.5.3.dist-info/RECORD +55 -0
  3. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/licenses/LICENSE +1 -1
  4. md2conf/__init__.py +2 -2
  5. md2conf/__main__.py +94 -29
  6. md2conf/api.py +55 -10
  7. md2conf/attachment.py +72 -0
  8. md2conf/coalesce.py +43 -0
  9. md2conf/collection.py +1 -1
  10. md2conf/{extra.py → compatibility.py} +1 -1
  11. md2conf/converter.py +417 -590
  12. md2conf/csf.py +13 -11
  13. md2conf/drawio/__init__.py +0 -0
  14. md2conf/drawio/extension.py +116 -0
  15. md2conf/{drawio.py → drawio/render.py} +1 -1
  16. md2conf/emoticon.py +3 -3
  17. md2conf/environment.py +2 -2
  18. md2conf/extension.py +78 -0
  19. md2conf/external.py +49 -0
  20. md2conf/formatting.py +135 -0
  21. md2conf/frontmatter.py +70 -0
  22. md2conf/image.py +127 -0
  23. md2conf/latex.py +7 -186
  24. md2conf/local.py +8 -8
  25. md2conf/markdown.py +1 -1
  26. md2conf/matcher.py +1 -1
  27. md2conf/mermaid/__init__.py +0 -0
  28. md2conf/mermaid/config.py +20 -0
  29. md2conf/mermaid/extension.py +109 -0
  30. md2conf/{mermaid.py → mermaid/render.py} +10 -38
  31. md2conf/mermaid/scanner.py +55 -0
  32. md2conf/metadata.py +1 -1
  33. md2conf/options.py +116 -0
  34. md2conf/plantuml/__init__.py +0 -0
  35. md2conf/plantuml/config.py +20 -0
  36. md2conf/plantuml/extension.py +158 -0
  37. md2conf/plantuml/render.py +139 -0
  38. md2conf/plantuml/scanner.py +56 -0
  39. md2conf/png.py +202 -0
  40. md2conf/processor.py +32 -11
  41. md2conf/publisher.py +17 -18
  42. md2conf/scanner.py +31 -128
  43. md2conf/serializer.py +2 -2
  44. md2conf/svg.py +341 -0
  45. md2conf/text.py +1 -1
  46. md2conf/toc.py +1 -1
  47. md2conf/uri.py +1 -1
  48. md2conf/xml.py +1 -1
  49. markdown_to_confluence-0.5.1.dist-info/RECORD +0 -35
  50. md2conf/domain.py +0 -52
  51. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/WHEEL +0 -0
  52. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/entry_points.txt +0 -0
  53. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/top_level.txt +0 -0
  54. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/zip-safe +0 -0
md2conf/latex.py CHANGED
@@ -1,16 +1,14 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
8
8
 
9
9
  import importlib.util
10
10
  from io import BytesIO
11
- from pathlib import Path
12
- from struct import unpack
13
- from typing import BinaryIO, Iterable, Literal, overload
11
+ from typing import BinaryIO, Literal
14
12
 
15
13
 
16
14
  def render_latex(expression: str, *, format: Literal["png", "svg"] = "png", dpi: int = 100, font_size: int = 12) -> bytes:
@@ -38,22 +36,24 @@ else:
38
36
  import matplotlib
39
37
  import matplotlib.pyplot as plt
40
38
 
39
+ # spellchecker:disable-next-line
41
40
  matplotlib.rcParams["mathtext.fontset"] = "cm" # change font to "Computer Modern"
42
41
 
43
42
  LATEX_ENABLED = True # pyright: ignore[reportConstantRedefinition]
44
43
 
45
44
  def _render_latex(expression: str, f: BinaryIO, *, format: Literal["png", "svg"], dpi: int, font_size: int) -> None:
46
45
  # create a figure with no axis
47
- fig = plt.figure(dpi=dpi)
46
+ fig = plt.figure(dpi=dpi) # pyright: ignore[reportUnknownMemberType]
48
47
 
49
48
  # transparent background
50
49
  fig.patch.set_alpha(0)
51
50
 
52
51
  # add LaTeX text
53
- fig.text(x=0, y=0, s=f"${expression}$", fontsize=font_size)
52
+ # spellchecker:disable-next-line
53
+ fig.text(x=0, y=0, s=f"${expression}$", fontsize=font_size) # pyright: ignore[reportUnknownMemberType]
54
54
 
55
55
  # save the image
56
- fig.savefig(
56
+ fig.savefig( # pyright: ignore[reportUnknownMemberType]
57
57
  f,
58
58
  transparent=True,
59
59
  format=format,
@@ -64,182 +64,3 @@ else:
64
64
 
65
65
  # close the figure to free memory
66
66
  plt.close(fig)
67
-
68
-
69
- @overload
70
- def get_png_dimensions(*, data: bytes) -> tuple[int, int]: ...
71
-
72
-
73
- @overload
74
- def get_png_dimensions(*, path: str | Path) -> tuple[int, int]: ...
75
-
76
-
77
- def get_png_dimensions(*, data: bytes | None = None, path: str | Path | None = None) -> tuple[int, int]:
78
- """
79
- Returns the width and height of a PNG image inspecting its header.
80
-
81
- :param data: PNG image data.
82
- :param path: Path to the PNG image file.
83
- :returns: A tuple of the image's width and height in pixels.
84
- """
85
-
86
- if data is not None and path is not None:
87
- raise TypeError("expected: either `data` or `path`; got: both")
88
- elif data is not None:
89
- with BytesIO(data) as f:
90
- return _get_png_dimensions(f)
91
- elif path is not None:
92
- with open(path, "rb") as f:
93
- return _get_png_dimensions(f)
94
- else:
95
- raise TypeError("expected: either `data` or `path`; got: neither")
96
-
97
-
98
- @overload
99
- def remove_png_chunks(names: Iterable[str], *, source_data: bytes) -> bytes: ...
100
-
101
-
102
- @overload
103
- def remove_png_chunks(names: Iterable[str], *, source_path: str | Path) -> bytes: ...
104
-
105
-
106
- @overload
107
- def remove_png_chunks(names: Iterable[str], *, source_data: bytes, target_path: str | Path) -> None: ...
108
-
109
-
110
- @overload
111
- def remove_png_chunks(names: Iterable[str], *, source_path: str | Path, target_path: str | Path) -> None: ...
112
-
113
-
114
- def remove_png_chunks(
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
- """
118
- Rewrites a PNG file by removing chunks with the specified names.
119
-
120
- :param source_data: PNG image data.
121
- :param source_path: Path to the file to read from.
122
- :param target_path: Path to the file to write to.
123
- """
124
-
125
- if source_data is not None and source_path is not None:
126
- raise TypeError("expected: either `source_data` or `source_path`; got: both")
127
- elif source_data is not None:
128
-
129
- def source_reader() -> BinaryIO:
130
- return BytesIO(source_data)
131
- elif source_path is not None:
132
-
133
- def source_reader() -> BinaryIO:
134
- return open(source_path, "rb")
135
- else:
136
- raise TypeError("expected: either `source_data` or `source_path`; got: neither")
137
-
138
- if target_path is None:
139
- with source_reader() as source_file, BytesIO() as memory_file:
140
- _remove_png_chunks(names, source_file, memory_file)
141
- return memory_file.getvalue()
142
- else:
143
- with source_reader() as source_file, open(target_path, "wb") as target_file:
144
- _remove_png_chunks(names, source_file, target_file)
145
- return None
146
-
147
-
148
- class _Chunk:
149
- __slots__ = ("length", "name", "data", "crc")
150
-
151
- length: int
152
- name: bytes
153
- data: bytes
154
- crc: bytes
155
-
156
- def __init__(self, length: int, name: bytes, data: bytes, crc: bytes):
157
- self.length = length
158
- self.name = name
159
- self.data = data
160
- self.crc = crc
161
-
162
-
163
- def _read_signature(f: BinaryIO) -> None:
164
- "Reads and checks PNG signature (first 8 bytes)."
165
-
166
- signature = f.read(8)
167
- if signature != b"\x89PNG\r\n\x1a\n":
168
- raise ValueError("not a valid PNG file")
169
-
170
-
171
- def _read_chunk(f: BinaryIO) -> _Chunk | None:
172
- "Reads and parses a PNG chunk such as `IHDR` or `tEXt`."
173
-
174
- length_bytes = f.read(4)
175
- if not length_bytes:
176
- return None
177
-
178
- if len(length_bytes) != 4:
179
- raise ValueError("insufficient bytes to read chunk length")
180
-
181
- length = int.from_bytes(length_bytes, "big")
182
-
183
- data_length = 4 + length + 4
184
- data_bytes = f.read(data_length)
185
- if len(data_bytes) != data_length:
186
- raise ValueError(f"insufficient bytes to read chunk data of length {length}")
187
-
188
- chunk_type = data_bytes[0:4]
189
- chunk_data = data_bytes[4:-4]
190
- crc = data_bytes[-4:]
191
-
192
- return _Chunk(length, chunk_type, chunk_data, crc)
193
-
194
-
195
- def _write_chunk(f: BinaryIO, chunk: _Chunk) -> None:
196
- f.write(chunk.length.to_bytes(4, "big"))
197
- f.write(chunk.name)
198
- f.write(chunk.data)
199
- f.write(chunk.crc)
200
-
201
-
202
- def _get_png_dimensions(source_file: BinaryIO) -> tuple[int, int]:
203
- """
204
- Returns the width and height of a PNG image inspecting its header.
205
-
206
- :param source_file: A binary file opened for reading that contains PNG image data.
207
- :returns: A tuple of the image's width and height in pixels.
208
- """
209
-
210
- _read_signature(source_file)
211
-
212
- # validate IHDR chunk
213
- ihdr = _read_chunk(source_file)
214
- if ihdr is None:
215
- raise ValueError("missing IHDR chunk")
216
-
217
- if ihdr.length != 13:
218
- raise ValueError("invalid chunk length")
219
- if ihdr.name != b"IHDR":
220
- raise ValueError(f"expected: IHDR chunk; got: {ihdr.name!r}")
221
-
222
- (width, height, bit_depth, color_type, compression, filter, interlace) = unpack(">IIBBBBB", ihdr.data) # pyright: ignore[reportUnusedVariable]
223
- return width, height
224
-
225
-
226
- def _remove_png_chunks(names: Iterable[str], source_file: BinaryIO, target_file: BinaryIO) -> None:
227
- """
228
- Rewrites a PNG file by removing chunks with the specified names.
229
-
230
- :param source_file: A binary file opened for reading that contains PNG image data.
231
- :param target_file: A binary file opened for writing to receive PNG image data.
232
- """
233
-
234
- exclude_set = set(name.encode("ascii") for name in names)
235
-
236
- _read_signature(source_file)
237
- target_file.write(b"\x89PNG\r\n\x1a\n")
238
-
239
- while True:
240
- chunk = _read_chunk(source_file)
241
- if chunk is None:
242
- break
243
-
244
- if chunk.name not in exclude_set:
245
- _write_chunk(target_file, chunk)
md2conf/local.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
@@ -10,10 +10,10 @@ import logging
10
10
  import os
11
11
  from pathlib import Path
12
12
 
13
+ from .compatibility import override
13
14
  from .converter import ConfluenceDocument
14
- from .domain import ConfluenceDocumentOptions, ConfluencePageID
15
- from .extra import override
16
15
  from .metadata import ConfluencePageMetadata, ConfluenceSiteMetadata
16
+ from .options import ConfluencePageID, DocumentOptions
17
17
  from .processor import Converter, DocumentNode, Processor, ProcessorFactory
18
18
 
19
19
  LOGGER = logging.getLogger(__name__)
@@ -26,7 +26,7 @@ class LocalProcessor(Processor):
26
26
 
27
27
  def __init__(
28
28
  self,
29
- options: ConfluenceDocumentOptions,
29
+ options: DocumentOptions,
30
30
  site: ConfluenceSiteMetadata,
31
31
  *,
32
32
  out_dir: Path | None,
@@ -45,14 +45,14 @@ class LocalProcessor(Processor):
45
45
  self.out_dir = out_dir or root_dir
46
46
 
47
47
  @override
48
- def _synchronize_tree(self, root: DocumentNode, root_id: ConfluencePageID | None) -> None:
48
+ def _synchronize_tree(self, tree: DocumentNode, root_id: ConfluencePageID | None) -> None:
49
49
  """
50
50
  Creates the cross-reference index.
51
51
 
52
52
  Does not change Markdown files.
53
53
  """
54
54
 
55
- for node in root.all():
55
+ for node in tree.all():
56
56
  if node.page_id is not None:
57
57
  page_id = node.page_id
58
58
  else:
@@ -92,7 +92,7 @@ class LocalProcessorFactory(ProcessorFactory):
92
92
 
93
93
  def __init__(
94
94
  self,
95
- options: ConfluenceDocumentOptions,
95
+ options: DocumentOptions,
96
96
  site: ConfluenceSiteMetadata,
97
97
  out_dir: Path | None = None,
98
98
  ) -> None:
@@ -110,7 +110,7 @@ class LocalConverter(Converter):
110
110
 
111
111
  def __init__(
112
112
  self,
113
- options: ConfluenceDocumentOptions,
113
+ options: DocumentOptions,
114
114
  site: ConfluenceSiteMetadata,
115
115
  out_dir: Path | None = None,
116
116
  ) -> None:
md2conf/markdown.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
md2conf/matcher.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
File without changes
@@ -0,0 +1,20 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class MermaidConfigProperties:
14
+ """
15
+ Configuration options for rendering Mermaid diagrams.
16
+
17
+ :param scale: Scaling factor for the rendered diagram.
18
+ """
19
+
20
+ scale: float | None = None
@@ -0,0 +1,109 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import hashlib
10
+ import logging
11
+ import uuid
12
+ from pathlib import Path
13
+
14
+ import lxml.etree as ET
15
+ from cattrs import BaseValidationError
16
+
17
+ from md2conf.attachment import EmbeddedFileData, ImageData, attachment_name
18
+ from md2conf.compatibility import override, path_relative_to
19
+ from md2conf.csf import AC_ATTR, AC_ELEM
20
+ from md2conf.extension import MarketplaceExtension
21
+ from md2conf.formatting import ImageAttributes
22
+
23
+ from .config import MermaidConfigProperties
24
+ from .render import render_diagram
25
+ from .scanner import MermaidScanner
26
+
27
+ ElementType = ET._Element # pyright: ignore [reportPrivateUsage]
28
+
29
+ LOGGER = logging.getLogger(__name__)
30
+
31
+
32
+ class MermaidExtension(MarketplaceExtension):
33
+ @override
34
+ def matches_image(self, absolute_path: Path) -> bool:
35
+ return absolute_path.name.endswith((".mmd", ".mermaid"))
36
+
37
+ @override
38
+ def matches_fenced(self, language: str, content: str) -> bool:
39
+ return language == "mermaid"
40
+
41
+ def _extract_mermaid_config(self, content: str) -> MermaidConfigProperties | None:
42
+ """Extract scale from Mermaid YAML front matter configuration."""
43
+
44
+ try:
45
+ properties = MermaidScanner().read(content)
46
+ return properties.config
47
+ except BaseValidationError as ex:
48
+ LOGGER.warning("Failed to extract Mermaid properties: %s", ex)
49
+ return None
50
+
51
+ @override
52
+ def transform_image(self, absolute_path: Path, attrs: ImageAttributes) -> ElementType:
53
+ relative_path = path_relative_to(absolute_path, self.base_dir)
54
+ if self.options.render:
55
+ with open(absolute_path, "r", encoding="utf-8") as f:
56
+ content = f.read()
57
+
58
+ config = self._extract_mermaid_config(content)
59
+ image_data = render_diagram(content, self.generator.options.output_format, config=config)
60
+ return self.generator.transform_attached_data(image_data, attrs, relative_path)
61
+ else:
62
+ self.attachments.add_image(ImageData(absolute_path, attrs.alt))
63
+ mermaid_filename = attachment_name(relative_path)
64
+ return self._create_mermaid_embed(mermaid_filename)
65
+
66
+ @override
67
+ def transform_fenced(self, content: str) -> ElementType:
68
+ if self.options.render:
69
+ config = self._extract_mermaid_config(content)
70
+ image_data = render_diagram(content, self.generator.options.output_format, config=config)
71
+ return self.generator.transform_attached_data(image_data, ImageAttributes.EMPTY_BLOCK)
72
+ else:
73
+ mermaid_data = content.encode("utf-8")
74
+ mermaid_hash = hashlib.md5(mermaid_data).hexdigest()
75
+ mermaid_filename = attachment_name(f"embedded_{mermaid_hash}.mmd")
76
+ self.attachments.add_embed(mermaid_filename, EmbeddedFileData(mermaid_data))
77
+ return self._create_mermaid_embed(mermaid_filename)
78
+
79
+ def _create_mermaid_embed(self, filename: str) -> ElementType:
80
+ "A Mermaid diagram, linking to an attachment that captures the Mermaid source."
81
+
82
+ local_id = str(uuid.uuid4())
83
+ macro_id = str(uuid.uuid4())
84
+ return AC_ELEM(
85
+ "structured-macro",
86
+ {
87
+ AC_ATTR("name"): "mermaid-cloud",
88
+ AC_ATTR("schema-version"): "1",
89
+ "data-layout": "default",
90
+ AC_ATTR("local-id"): local_id,
91
+ AC_ATTR("macro-id"): macro_id,
92
+ },
93
+ AC_ELEM(
94
+ "parameter",
95
+ {AC_ATTR("name"): "filename"},
96
+ filename,
97
+ ),
98
+ AC_ELEM(
99
+ "parameter",
100
+ {AC_ATTR("name"): "toolbar"},
101
+ "bottom",
102
+ ),
103
+ AC_ELEM(
104
+ "parameter",
105
+ {AC_ATTR("name"): "zoom"},
106
+ "fit",
107
+ ),
108
+ AC_ELEM("parameter", {AC_ATTR("name"): "revision"}, "1"),
109
+ )
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
@@ -10,25 +10,16 @@ import logging
10
10
  import os
11
11
  import os.path
12
12
  import shutil
13
- import subprocess
14
- from dataclasses import dataclass
15
13
  from typing import Literal
16
14
 
17
- LOGGER = logging.getLogger(__name__)
18
-
19
-
20
- @dataclass
21
- class MermaidConfigProperties:
22
- """
23
- Configuration options for rendering Mermaid diagrams.
15
+ from md2conf.external import execute_subprocess
24
16
 
25
- :param scale: Scaling factor for the rendered diagram.
26
- """
17
+ from .config import MermaidConfigProperties
27
18
 
28
- scale: float | None = None
19
+ LOGGER = logging.getLogger(__name__)
29
20
 
30
21
 
31
- def is_docker() -> bool:
22
+ def _is_docker() -> bool:
32
23
  "True if the application is running in a Docker container."
33
24
 
34
25
  return os.environ.get("CHROME_BIN") == "/usr/bin/chromium-browser" and os.environ.get("PUPPETEER_SKIP_DOWNLOAD") == "true"
@@ -37,7 +28,7 @@ def is_docker() -> bool:
37
28
  def get_mmdc() -> str:
38
29
  "Path to the Mermaid diagram converter."
39
30
 
40
- if is_docker():
31
+ if _is_docker():
41
32
  full_path = "/home/md2conf/node_modules/.bin/mmdc"
42
33
  if os.path.exists(full_path):
43
34
  return full_path
@@ -75,27 +66,8 @@ def render_diagram(source: str, output_format: Literal["png", "svg"] = "png", co
75
66
  "--scale",
76
67
  str(config.scale or 2),
77
68
  ]
78
- root = os.path.dirname(__file__)
79
- if is_docker():
69
+ if _is_docker():
70
+ root = os.path.dirname(os.path.dirname(__file__))
80
71
  cmd.extend(["-p", os.path.join(root, "puppeteer-config.json")])
81
- LOGGER.debug("Executing: %s", " ".join(cmd))
82
-
83
- proc = subprocess.Popen(
84
- cmd,
85
- stdout=subprocess.PIPE,
86
- stdin=subprocess.PIPE,
87
- stderr=subprocess.PIPE,
88
- text=False,
89
- )
90
- stdout, stderr = proc.communicate(input=source.encode("utf-8"))
91
- if proc.returncode:
92
- messages = [f"failed to convert Mermaid diagram; exit code: {proc.returncode}"]
93
- console_output = stdout.decode("utf-8")
94
- if console_output:
95
- messages.append(f"output:\n{console_output}")
96
- console_error = stderr.decode("utf-8")
97
- if console_error:
98
- messages.append(f"error:\n{console_error}")
99
- raise RuntimeError("\n".join(messages))
100
-
101
- return stdout
72
+
73
+ return execute_subprocess(cmd, source.encode("utf-8"), application="Mermaid")
@@ -0,0 +1,55 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+
11
+ from md2conf.frontmatter import extract_frontmatter_object
12
+
13
+ from .config import MermaidConfigProperties
14
+
15
+
16
+ @dataclass
17
+ class MermaidProperties:
18
+ """
19
+ An object that holds the front-matter properties structure for Mermaid diagrams.
20
+
21
+ :param title: The title of the diagram.
22
+ :param config: Configuration options for rendering.
23
+ """
24
+
25
+ title: str | None = None
26
+ config: MermaidConfigProperties | None = None
27
+
28
+
29
+ class MermaidScanner:
30
+ """
31
+ Extracts properties from the JSON/YAML front-matter of a Mermaid diagram.
32
+ """
33
+
34
+ def read(self, content: str) -> MermaidProperties:
35
+ """
36
+ Extracts rendering preferences from a Mermaid front-matter content.
37
+
38
+ ```
39
+ ---
40
+ title: Tiny flow diagram
41
+ config:
42
+ scale: 1
43
+ ---
44
+ flowchart LR
45
+ A[Component A] --> B[Component B]
46
+ B --> C[Component C]
47
+ ```
48
+ """
49
+
50
+ properties, _ = extract_frontmatter_object(MermaidProperties, content)
51
+ if properties is not None:
52
+ config = properties.config or MermaidConfigProperties()
53
+ return MermaidProperties(title=properties.title, config=config)
54
+
55
+ return MermaidProperties()
md2conf/metadata.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """