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.
- {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/METADATA +160 -11
- markdown_to_confluence-0.5.3.dist-info/RECORD +55 -0
- {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/licenses/LICENSE +1 -1
- md2conf/__init__.py +2 -2
- md2conf/__main__.py +94 -29
- md2conf/api.py +55 -10
- md2conf/attachment.py +72 -0
- md2conf/coalesce.py +43 -0
- md2conf/collection.py +1 -1
- md2conf/{extra.py → compatibility.py} +1 -1
- md2conf/converter.py +417 -590
- md2conf/csf.py +13 -11
- md2conf/drawio/__init__.py +0 -0
- md2conf/drawio/extension.py +116 -0
- md2conf/{drawio.py → drawio/render.py} +1 -1
- md2conf/emoticon.py +3 -3
- md2conf/environment.py +2 -2
- md2conf/extension.py +78 -0
- md2conf/external.py +49 -0
- md2conf/formatting.py +135 -0
- md2conf/frontmatter.py +70 -0
- md2conf/image.py +127 -0
- md2conf/latex.py +7 -186
- md2conf/local.py +8 -8
- md2conf/markdown.py +1 -1
- md2conf/matcher.py +1 -1
- md2conf/mermaid/__init__.py +0 -0
- md2conf/mermaid/config.py +20 -0
- md2conf/mermaid/extension.py +109 -0
- md2conf/{mermaid.py → mermaid/render.py} +10 -38
- md2conf/mermaid/scanner.py +55 -0
- md2conf/metadata.py +1 -1
- md2conf/options.py +116 -0
- md2conf/plantuml/__init__.py +0 -0
- md2conf/plantuml/config.py +20 -0
- md2conf/plantuml/extension.py +158 -0
- md2conf/plantuml/render.py +139 -0
- md2conf/plantuml/scanner.py +56 -0
- md2conf/png.py +202 -0
- md2conf/processor.py +32 -11
- md2conf/publisher.py +17 -18
- md2conf/scanner.py +31 -128
- md2conf/serializer.py +2 -2
- md2conf/svg.py +341 -0
- md2conf/text.py +1 -1
- md2conf/toc.py +1 -1
- md2conf/uri.py +1 -1
- md2conf/xml.py +1 -1
- markdown_to_confluence-0.5.1.dist-info/RECORD +0 -35
- md2conf/domain.py +0 -52
- {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/top_level.txt +0 -0
- {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-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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:
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
113
|
+
options: DocumentOptions,
|
|
114
114
|
site: ConfluenceSiteMetadata,
|
|
115
115
|
out_dir: Path | None = None,
|
|
116
116
|
) -> None:
|
md2conf/markdown.py
CHANGED
md2conf/matcher.py
CHANGED
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
"""
|
|
17
|
+
from .config import MermaidConfigProperties
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
LOGGER = logging.getLogger(__name__)
|
|
29
20
|
|
|
30
21
|
|
|
31
|
-
def
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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()
|