markdown-to-confluence 0.4.5__py3-none-any.whl → 0.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/METADATA +38 -13
- markdown_to_confluence-0.4.7.dist-info/RECORD +34 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +34 -36
- md2conf/api.py +121 -43
- md2conf/converter.py +312 -165
- md2conf/csf.py +10 -8
- md2conf/domain.py +4 -0
- md2conf/drawio.py +5 -3
- md2conf/emoticon.py +22 -0
- md2conf/latex.py +2 -2
- md2conf/matcher.py +1 -3
- md2conf/mermaid.py +18 -3
- md2conf/processor.py +2 -2
- md2conf/{application.py → publisher.py} +15 -12
- md2conf/scanner.py +57 -3
- md2conf/xml.py +8 -5
- markdown_to_confluence-0.4.5.dist-info/RECORD +0 -33
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/zip-safe +0 -0
- /md2conf/{properties.py → environment.py} +0 -0
md2conf/csf.py
CHANGED
|
@@ -14,6 +14,8 @@ from typing import Callable, TypeVar
|
|
|
14
14
|
import lxml.etree as ET
|
|
15
15
|
from lxml.builder import ElementMaker
|
|
16
16
|
|
|
17
|
+
ElementType = ET._Element # pyright: ignore [reportPrivateUsage]
|
|
18
|
+
|
|
17
19
|
# XML namespaces typically associated with Confluence Storage Format documents
|
|
18
20
|
_namespaces = {
|
|
19
21
|
"ac": "http://atlassian.com/content",
|
|
@@ -54,7 +56,7 @@ def with_entities(func: Callable[[Path], R]) -> R:
|
|
|
54
56
|
return func(dtd_path)
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
def _elements_from_strings(dtd_path: Path, items: list[str]) ->
|
|
59
|
+
def _elements_from_strings(dtd_path: Path, items: list[str]) -> ElementType:
|
|
58
60
|
"""
|
|
59
61
|
Creates an XML document tree from XML fragment strings.
|
|
60
62
|
|
|
@@ -90,7 +92,7 @@ def _elements_from_strings(dtd_path: Path, items: list[str]) -> ET._Element:
|
|
|
90
92
|
raise ParseError() from ex
|
|
91
93
|
|
|
92
94
|
|
|
93
|
-
def elements_from_strings(items: list[str]) ->
|
|
95
|
+
def elements_from_strings(items: list[str]) -> ElementType:
|
|
94
96
|
"""
|
|
95
97
|
Creates a Confluence Storage Format XML document tree from XML fragment strings.
|
|
96
98
|
|
|
@@ -103,7 +105,7 @@ def elements_from_strings(items: list[str]) -> ET._Element:
|
|
|
103
105
|
return with_entities(lambda dtd_path: _elements_from_strings(dtd_path, items))
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
def elements_from_string(content: str) ->
|
|
108
|
+
def elements_from_string(content: str) -> ElementType:
|
|
107
109
|
"""
|
|
108
110
|
Creates a Confluence Storage Format XML document tree from an XML string.
|
|
109
111
|
|
|
@@ -135,7 +137,7 @@ def content_to_string(content: str) -> str:
|
|
|
135
137
|
return with_entities(lambda dtd_path: _content_to_string(dtd_path, content))
|
|
136
138
|
|
|
137
139
|
|
|
138
|
-
def elements_to_string(root:
|
|
140
|
+
def elements_to_string(root: ElementType) -> str:
|
|
139
141
|
"""
|
|
140
142
|
Converts a Confluence Storage Format element tree into an XML string to push to Confluence REST API.
|
|
141
143
|
|
|
@@ -151,11 +153,11 @@ def elements_to_string(root: ET._Element) -> str:
|
|
|
151
153
|
raise ValueError("expected: Confluence content")
|
|
152
154
|
|
|
153
155
|
|
|
154
|
-
def is_block_like(elem:
|
|
155
|
-
return elem.tag in ["div", "li", "ol", "p", "pre", "ul"]
|
|
156
|
+
def is_block_like(elem: ElementType) -> bool:
|
|
157
|
+
return elem.tag in ["div", "li", "ol", "p", "pre", "td", "th", "ul"]
|
|
156
158
|
|
|
157
159
|
|
|
158
|
-
def normalize_inline(elem:
|
|
160
|
+
def normalize_inline(elem: ElementType) -> None:
|
|
159
161
|
"""
|
|
160
162
|
Ensures that inline elements are direct children of an eligible block element.
|
|
161
163
|
|
|
@@ -179,7 +181,7 @@ def normalize_inline(elem: ET._Element) -> None:
|
|
|
179
181
|
if not is_block_like(elem):
|
|
180
182
|
raise ValueError(f"expected: block element; got: {elem.tag!s}")
|
|
181
183
|
|
|
182
|
-
contents: list[
|
|
184
|
+
contents: list[ElementType] = []
|
|
183
185
|
|
|
184
186
|
paragraph = HTML.p()
|
|
185
187
|
contents.append(paragraph)
|
md2conf/domain.py
CHANGED
|
@@ -33,6 +33,8 @@ class ConfluenceDocumentOptions:
|
|
|
33
33
|
:param render_latex: Whether to pre-render LaTeX formulas into PNG/SVG images.
|
|
34
34
|
:param diagram_output_format: Target image format for diagrams.
|
|
35
35
|
:param webui_links: When true, convert relative URLs to Confluence Web UI links.
|
|
36
|
+
:param alignment: Alignment for block-level images and formulas.
|
|
37
|
+
:param use_panel: Whether to transform admonitions and alerts into a Confluence custom panel.
|
|
36
38
|
"""
|
|
37
39
|
|
|
38
40
|
ignore_invalid_url: bool = False
|
|
@@ -46,3 +48,5 @@ class ConfluenceDocumentOptions:
|
|
|
46
48
|
render_latex: bool = False
|
|
47
49
|
diagram_output_format: Literal["png", "svg"] = "png"
|
|
48
50
|
webui_links: bool = False
|
|
51
|
+
alignment: Literal["center", "left", "right"] = "center"
|
|
52
|
+
use_panel: bool = False
|
md2conf/drawio.py
CHANGED
|
@@ -20,6 +20,8 @@ from urllib.parse import unquote_to_bytes
|
|
|
20
20
|
|
|
21
21
|
import lxml.etree as ET
|
|
22
22
|
|
|
23
|
+
ElementType = ET._Element # pyright: ignore [reportPrivateUsage]
|
|
24
|
+
|
|
23
25
|
LOGGER = logging.getLogger(__name__)
|
|
24
26
|
|
|
25
27
|
|
|
@@ -49,7 +51,7 @@ def inflate(data: bytes) -> bytes:
|
|
|
49
51
|
return zlib.decompress(data, -zlib.MAX_WBITS)
|
|
50
52
|
|
|
51
53
|
|
|
52
|
-
def decompress_diagram(xml_data: typing.Union[bytes, str]) ->
|
|
54
|
+
def decompress_diagram(xml_data: typing.Union[bytes, str]) -> ElementType:
|
|
53
55
|
"""
|
|
54
56
|
Decompresses the text content of the `<diagram>` element in a draw.io XML document.
|
|
55
57
|
|
|
@@ -129,7 +131,7 @@ def decompress_diagram(xml_data: typing.Union[bytes, str]) -> ET._Element:
|
|
|
129
131
|
return root
|
|
130
132
|
|
|
131
133
|
|
|
132
|
-
def extract_xml_from_png(png_data: bytes) ->
|
|
134
|
+
def extract_xml_from_png(png_data: bytes) -> ElementType:
|
|
133
135
|
"""
|
|
134
136
|
Extracts an editable draw.io diagram from a PNG file.
|
|
135
137
|
|
|
@@ -190,7 +192,7 @@ def extract_xml_from_png(png_data: bytes) -> ET._Element:
|
|
|
190
192
|
raise DrawioError("not a PNG file made with draw.io")
|
|
191
193
|
|
|
192
194
|
|
|
193
|
-
def extract_xml_from_svg(svg_data: bytes) ->
|
|
195
|
+
def extract_xml_from_svg(svg_data: bytes) -> ElementType:
|
|
194
196
|
"""
|
|
195
197
|
Extracts an editable draw.io diagram from an SVG file.
|
|
196
198
|
|
md2conf/emoticon.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
_EMOJI_TO_EMOTICON = {
|
|
10
|
+
"grinning": "laugh",
|
|
11
|
+
"heart": "heart",
|
|
12
|
+
"slight_frown": "sad",
|
|
13
|
+
"slight_smile": "smile",
|
|
14
|
+
"stuck_out_tongue": "cheeky",
|
|
15
|
+
"thumbsdown": "thumbs-down",
|
|
16
|
+
"thumbsup": "thumbs-up",
|
|
17
|
+
"wink": "wink",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def emoji_to_emoticon(shortname: str) -> str:
|
|
22
|
+
return _EMOJI_TO_EMOTICON.get(shortname) or "blue-star"
|
md2conf/latex.py
CHANGED
|
@@ -40,7 +40,7 @@ else:
|
|
|
40
40
|
|
|
41
41
|
matplotlib.rcParams["mathtext.fontset"] = "cm" # change font to "Computer Modern"
|
|
42
42
|
|
|
43
|
-
LATEX_ENABLED = True
|
|
43
|
+
LATEX_ENABLED = True # pyright: ignore[reportConstantRedefinition]
|
|
44
44
|
|
|
45
45
|
def _render_latex(expression: str, f: BinaryIO, *, format: Literal["png", "svg"], dpi: int, font_size: int) -> None:
|
|
46
46
|
# create a figure with no axis
|
|
@@ -219,7 +219,7 @@ def _get_png_dimensions(source_file: BinaryIO) -> tuple[int, int]:
|
|
|
219
219
|
if ihdr.name != b"IHDR":
|
|
220
220
|
raise ValueError(f"expected: IHDR chunk; got: {ihdr.name!r}")
|
|
221
221
|
|
|
222
|
-
(width, height, bit_depth, color_type, compression, filter, interlace) = unpack(">IIBBBBB", ihdr.data)
|
|
222
|
+
(width, height, bit_depth, color_type, compression, filter, interlace) = unpack(">IIBBBBB", ihdr.data) # pyright: ignore[reportUnusedVariable]
|
|
223
223
|
return width, height
|
|
224
224
|
|
|
225
225
|
|
md2conf/matcher.py
CHANGED
|
@@ -105,10 +105,8 @@ class MatcherOptions:
|
|
|
105
105
|
def _entry_name_dir(entry: Union[Entry, os.DirEntry[str]]) -> tuple[str, bool]:
|
|
106
106
|
if isinstance(entry, Entry):
|
|
107
107
|
return entry.name, entry.is_dir
|
|
108
|
-
elif isinstance(entry, os.DirEntry):
|
|
109
|
-
return entry.name, entry.is_dir()
|
|
110
108
|
else:
|
|
111
|
-
|
|
109
|
+
return entry.name, entry.is_dir()
|
|
112
110
|
|
|
113
111
|
|
|
114
112
|
class Matcher:
|
md2conf/mermaid.py
CHANGED
|
@@ -11,11 +11,23 @@ import os
|
|
|
11
11
|
import os.path
|
|
12
12
|
import shutil
|
|
13
13
|
import subprocess
|
|
14
|
-
from
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Literal, Optional
|
|
15
16
|
|
|
16
17
|
LOGGER = logging.getLogger(__name__)
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
@dataclass
|
|
21
|
+
class MermaidConfigProperties:
|
|
22
|
+
"""
|
|
23
|
+
Configuration options for rendering Mermaid diagrams.
|
|
24
|
+
|
|
25
|
+
:param scale: Scaling factor for the rendered diagram.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
scale: Optional[float] = None
|
|
29
|
+
|
|
30
|
+
|
|
19
31
|
def is_docker() -> bool:
|
|
20
32
|
"True if the application is running in a Docker container."
|
|
21
33
|
|
|
@@ -44,9 +56,12 @@ def has_mmdc() -> bool:
|
|
|
44
56
|
return shutil.which(executable) is not None
|
|
45
57
|
|
|
46
58
|
|
|
47
|
-
def render_diagram(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
|
|
59
|
+
def render_diagram(source: str, output_format: Literal["png", "svg"] = "png", config: Optional[MermaidConfigProperties] = None) -> bytes:
|
|
48
60
|
"Generates a PNG or SVG image from a Mermaid diagram source."
|
|
49
61
|
|
|
62
|
+
if config is None:
|
|
63
|
+
config = MermaidConfigProperties()
|
|
64
|
+
|
|
50
65
|
cmd = [
|
|
51
66
|
get_mmdc(),
|
|
52
67
|
"--input",
|
|
@@ -58,7 +73,7 @@ def render_diagram(source: str, output_format: Literal["png", "svg"] = "png") ->
|
|
|
58
73
|
"--backgroundColor",
|
|
59
74
|
"transparent",
|
|
60
75
|
"--scale",
|
|
61
|
-
|
|
76
|
+
str(config.scale or 2),
|
|
62
77
|
]
|
|
63
78
|
root = os.path.dirname(__file__)
|
|
64
79
|
if is_docker():
|
md2conf/processor.py
CHANGED
|
@@ -16,9 +16,9 @@ from typing import Iterable, Optional
|
|
|
16
16
|
from .collection import ConfluencePageCollection
|
|
17
17
|
from .converter import ConfluenceDocument
|
|
18
18
|
from .domain import ConfluenceDocumentOptions, ConfluencePageID
|
|
19
|
+
from .environment import ArgumentError
|
|
19
20
|
from .matcher import DirectoryEntry, FileEntry, Matcher, MatcherOptions
|
|
20
21
|
from .metadata import ConfluenceSiteMetadata
|
|
21
|
-
from .properties import ArgumentError
|
|
22
22
|
from .scanner import Scanner
|
|
23
23
|
|
|
24
24
|
LOGGER = logging.getLogger(__name__)
|
|
@@ -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,
|
|
143
|
+
def _synchronize_tree(self, root: DocumentNode, root_id: Optional[ConfluencePageID]) -> None:
|
|
144
144
|
"""
|
|
145
145
|
Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
|
|
146
146
|
|
|
@@ -14,10 +14,10 @@ from .api import ConfluenceContentProperty, ConfluenceLabel, ConfluenceSession,
|
|
|
14
14
|
from .converter import ConfluenceDocument, attachment_name, get_volatile_attributes, get_volatile_elements
|
|
15
15
|
from .csf import AC_ATTR, elements_from_string
|
|
16
16
|
from .domain import ConfluenceDocumentOptions, ConfluencePageID
|
|
17
|
+
from .environment import PageError
|
|
17
18
|
from .extra import override, path_relative_to
|
|
18
19
|
from .metadata import ConfluencePageMetadata
|
|
19
20
|
from .processor import Converter, DocumentNode, Processor, ProcessorFactory
|
|
20
|
-
from .properties import PageError
|
|
21
21
|
from .xml import is_xml_equal, unwrap_substitute
|
|
22
22
|
|
|
23
23
|
LOGGER = logging.getLogger(__name__)
|
|
@@ -73,20 +73,23 @@ class SynchronizingProcessor(Processor):
|
|
|
73
73
|
# verify if page exists
|
|
74
74
|
page = self.api.get_page_properties(node.page_id)
|
|
75
75
|
update = False
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
else:
|
|
77
|
+
if node.title is not None:
|
|
78
|
+
# use title extracted from source metadata
|
|
79
|
+
title = node.title
|
|
80
|
+
else:
|
|
81
|
+
# assign an auto-generated title
|
|
82
|
+
digest = self._generate_hash(node.absolute_path)
|
|
83
|
+
title = f"{node.absolute_path.stem} [{digest}]"
|
|
84
|
+
|
|
85
|
+
# look up page by (possibly auto-generated) title
|
|
86
|
+
page = self.api.get_or_create_page(title, parent_id.page_id)
|
|
79
87
|
|
|
80
88
|
if page.status is ConfluenceStatus.ARCHIVED:
|
|
89
|
+
# user has archived a page with this (auto-generated) title
|
|
81
90
|
raise PageError(f"unable to update archived page with ID {page.id}")
|
|
82
91
|
|
|
83
92
|
update = True
|
|
84
|
-
else:
|
|
85
|
-
# always create a new page
|
|
86
|
-
digest = self._generate_hash(node.absolute_path)
|
|
87
|
-
title = f"{node.absolute_path.stem} [{digest}]"
|
|
88
|
-
page = self.api.create_page(parent_id.page_id, title, "")
|
|
89
|
-
update = True
|
|
90
93
|
|
|
91
94
|
space_key = self.api.space_id_to_key(page.spaceId)
|
|
92
95
|
if update:
|
|
@@ -138,7 +141,7 @@ class SynchronizingProcessor(Processor):
|
|
|
138
141
|
title = None
|
|
139
142
|
if document.title is not None:
|
|
140
143
|
meta = self.page_metadata.get(path)
|
|
141
|
-
if meta is not None and meta.
|
|
144
|
+
if meta is not None and meta.title != document.title:
|
|
142
145
|
conflicting_page_id = self.api.page_exists(document.title, space_id=self.api.space_key_to_id(meta.space_key))
|
|
143
146
|
if conflicting_page_id is None:
|
|
144
147
|
title = document.title
|
|
@@ -215,7 +218,7 @@ class SynchronizingProcessorFactory(ProcessorFactory):
|
|
|
215
218
|
return SynchronizingProcessor(self.api, self.options, root_dir)
|
|
216
219
|
|
|
217
220
|
|
|
218
|
-
class
|
|
221
|
+
class Publisher(Converter):
|
|
219
222
|
"""
|
|
220
223
|
The entry point for Markdown to Confluence conversion.
|
|
221
224
|
|
md2conf/scanner.py
CHANGED
|
@@ -7,14 +7,17 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import re
|
|
10
|
+
import typing
|
|
10
11
|
from dataclasses import dataclass
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Any, Optional, TypeVar
|
|
13
|
+
from typing import Any, Literal, Optional, TypeVar
|
|
13
14
|
|
|
14
15
|
import yaml
|
|
15
16
|
from strong_typing.core import JsonType
|
|
16
17
|
from strong_typing.serialization import DeserializerOptions, json_to_object
|
|
17
18
|
|
|
19
|
+
from .mermaid import MermaidConfigProperties
|
|
20
|
+
|
|
18
21
|
T = TypeVar("T")
|
|
19
22
|
|
|
20
23
|
|
|
@@ -43,7 +46,7 @@ def extract_frontmatter_block(text: str) -> tuple[Optional[str], str]:
|
|
|
43
46
|
return extract_value(r"(?ms)\A---$(.+?)^---$", text)
|
|
44
47
|
|
|
45
48
|
|
|
46
|
-
def extract_frontmatter_properties(text: str) -> tuple[Optional[dict[str,
|
|
49
|
+
def extract_frontmatter_properties(text: str) -> tuple[Optional[dict[str, JsonType]], str]:
|
|
47
50
|
"Extracts the front-matter from a Markdown document as a dictionary."
|
|
48
51
|
|
|
49
52
|
block, text = extract_frontmatter_block(text)
|
|
@@ -52,7 +55,7 @@ def extract_frontmatter_properties(text: str) -> tuple[Optional[dict[str, Any]],
|
|
|
52
55
|
if block is not None:
|
|
53
56
|
data = yaml.safe_load(block)
|
|
54
57
|
if isinstance(data, dict):
|
|
55
|
-
properties = data
|
|
58
|
+
properties = typing.cast(dict[str, JsonType], data)
|
|
56
59
|
|
|
57
60
|
return properties, text
|
|
58
61
|
|
|
@@ -71,6 +74,7 @@ class DocumentProperties:
|
|
|
71
74
|
:param tags: A list of tags (content labels) extracted from front-matter.
|
|
72
75
|
:param synchronized: True if the document content is parsed and synchronized with Confluence.
|
|
73
76
|
:param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
|
|
77
|
+
:param alignment: Alignment for block-level images and formulas.
|
|
74
78
|
"""
|
|
75
79
|
|
|
76
80
|
page_id: Optional[str]
|
|
@@ -82,6 +86,7 @@ class DocumentProperties:
|
|
|
82
86
|
tags: Optional[list[str]]
|
|
83
87
|
synchronized: Optional[bool]
|
|
84
88
|
properties: Optional[dict[str, JsonType]]
|
|
89
|
+
alignment: Optional[Literal["center", "left", "right"]]
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
@dataclass
|
|
@@ -96,6 +101,7 @@ class ScannedDocument:
|
|
|
96
101
|
:param tags: A list of tags (content labels) extracted from front-matter.
|
|
97
102
|
:param synchronized: True if the document content is parsed and synchronized with Confluence.
|
|
98
103
|
:param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
|
|
104
|
+
:param alignment: Alignment for block-level images and formulas.
|
|
99
105
|
:param text: Text that remains after front-matter and inline properties have been extracted.
|
|
100
106
|
"""
|
|
101
107
|
|
|
@@ -106,6 +112,7 @@ class ScannedDocument:
|
|
|
106
112
|
tags: Optional[list[str]]
|
|
107
113
|
synchronized: Optional[bool]
|
|
108
114
|
properties: Optional[dict[str, JsonType]]
|
|
115
|
+
alignment: Optional[Literal["center", "left", "right"]]
|
|
109
116
|
text: str
|
|
110
117
|
|
|
111
118
|
|
|
@@ -132,6 +139,7 @@ class Scanner:
|
|
|
132
139
|
tags: Optional[list[str]] = None
|
|
133
140
|
synchronized: Optional[bool] = None
|
|
134
141
|
properties: Optional[dict[str, JsonType]] = None
|
|
142
|
+
alignment: Optional[Literal["center", "left", "right"]] = None
|
|
135
143
|
|
|
136
144
|
# extract front-matter
|
|
137
145
|
data, text = extract_frontmatter_properties(text)
|
|
@@ -144,6 +152,7 @@ class Scanner:
|
|
|
144
152
|
tags = p.tags
|
|
145
153
|
synchronized = p.synchronized
|
|
146
154
|
properties = p.properties
|
|
155
|
+
alignment = p.alignment
|
|
147
156
|
|
|
148
157
|
return ScannedDocument(
|
|
149
158
|
page_id=page_id,
|
|
@@ -153,5 +162,50 @@ class Scanner:
|
|
|
153
162
|
tags=tags,
|
|
154
163
|
synchronized=synchronized,
|
|
155
164
|
properties=properties,
|
|
165
|
+
alignment=alignment,
|
|
156
166
|
text=text,
|
|
157
167
|
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class MermaidProperties:
|
|
172
|
+
"""
|
|
173
|
+
An object that holds the front-matter properties structure for Mermaid diagrams.
|
|
174
|
+
|
|
175
|
+
:param title: The title of the diagram.
|
|
176
|
+
:param config: Configuration options for rendering.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
title: Optional[str] = None
|
|
180
|
+
config: Optional[MermaidConfigProperties] = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class MermaidScanner:
|
|
184
|
+
"""
|
|
185
|
+
Extracts properties from the JSON/YAML front-matter of a Mermaid diagram.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def read(self, content: str) -> MermaidProperties:
|
|
189
|
+
"""
|
|
190
|
+
Extracts rendering preferences from a Mermaid front-matter content.
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
---
|
|
194
|
+
title: Tiny flow diagram
|
|
195
|
+
config:
|
|
196
|
+
scale: 1
|
|
197
|
+
---
|
|
198
|
+
flowchart LR
|
|
199
|
+
A[Component A] --> B[Component B]
|
|
200
|
+
B --> C[Component C]
|
|
201
|
+
```
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
properties, _ = extract_frontmatter_properties(content)
|
|
205
|
+
if properties is not None:
|
|
206
|
+
front_matter = _json_to_object(MermaidProperties, properties)
|
|
207
|
+
config = front_matter.config or MermaidConfigProperties()
|
|
208
|
+
|
|
209
|
+
return MermaidProperties(title=front_matter.title, config=config)
|
|
210
|
+
|
|
211
|
+
return MermaidProperties()
|
md2conf/xml.py
CHANGED
|
@@ -10,8 +10,11 @@ from typing import Iterable, Optional
|
|
|
10
10
|
|
|
11
11
|
import lxml.etree as ET
|
|
12
12
|
|
|
13
|
+
ElementType = ET._Element # pyright: ignore [reportPrivateUsage]
|
|
14
|
+
AttribType = ET._Attrib # pyright: ignore[reportPrivateUsage]
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
def _attrs_equal_excluding(attrs1: AttribType, attrs2: AttribType, exclude: set[str]) -> bool:
|
|
15
18
|
"""
|
|
16
19
|
Compares two dictionary objects, excluding keys in the skip set.
|
|
17
20
|
|
|
@@ -47,7 +50,7 @@ class ElementComparator:
|
|
|
47
50
|
self.skip_attributes = set(skip_attributes) if skip_attributes else set()
|
|
48
51
|
self.skip_elements = set(skip_elements) if skip_elements else set()
|
|
49
52
|
|
|
50
|
-
def is_equal(self, e1:
|
|
53
|
+
def is_equal(self, e1: ElementType, e2: ElementType) -> bool:
|
|
51
54
|
"""
|
|
52
55
|
Recursively check if two XML elements are equal.
|
|
53
56
|
"""
|
|
@@ -82,7 +85,7 @@ class ElementComparator:
|
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
def is_xml_equal(
|
|
85
|
-
tree1:
|
|
88
|
+
tree1: ElementType, tree2: ElementType, *, skip_attributes: Optional[Iterable[str]] = None, skip_elements: Optional[Iterable[str]] = None
|
|
86
89
|
) -> bool:
|
|
87
90
|
"""
|
|
88
91
|
Compare two XML documents for equivalence, ignoring leading/trailing whitespace differences and attribute definition order.
|
|
@@ -99,13 +102,13 @@ def is_xml_equal(
|
|
|
99
102
|
return ElementComparator(skip_attributes=skip_attributes, skip_elements=skip_elements).is_equal(tree1, tree2)
|
|
100
103
|
|
|
101
104
|
|
|
102
|
-
def element_to_text(node:
|
|
105
|
+
def element_to_text(node: ElementType) -> str:
|
|
103
106
|
"Returns all text contained in an element as a concatenated string."
|
|
104
107
|
|
|
105
108
|
return "".join(node.itertext()).strip()
|
|
106
109
|
|
|
107
110
|
|
|
108
|
-
def unwrap_substitute(name: str, root:
|
|
111
|
+
def unwrap_substitute(name: str, root: ElementType) -> None:
|
|
109
112
|
"""
|
|
110
113
|
Substitutes all occurrences of an element with its contents.
|
|
111
114
|
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
markdown_to_confluence-0.4.5.dist-info/licenses/LICENSE,sha256=56L-Y0dyZwyVlINRJRz3PNw-ka-oLVaAq-7d8zo6qlc,1077
|
|
2
|
-
md2conf/__init__.py,sha256=uvviya0xS1aCIW7IU3EfeI_QkC_d9T_PiVsLEMXo9S4,402
|
|
3
|
-
md2conf/__main__.py,sha256=gQncJ-mkhRyQyhrZg-uJ1RnN8aGw-sr0c83ydunFNj0,11661
|
|
4
|
-
md2conf/api.py,sha256=VjXD0da4de5YtPCbUCjK0k1oD6vl59IQLItEapj0pyM,37861
|
|
5
|
-
md2conf/application.py,sha256=PZDPUpoKjKBPTHwgVO20pGzTwER3paZuQbI-2_TWBgE,8563
|
|
6
|
-
md2conf/collection.py,sha256=EobgMRJgkYloWlY03NZJ52MRC_SGLpTVCHkltDbQyt0,837
|
|
7
|
-
md2conf/converter.py,sha256=hWqbXYSFymMkvobh-f3uUO6JG28EWHU_7s0QYPI6NKM,61400
|
|
8
|
-
md2conf/csf.py,sha256=WIzGrX-RXAkr4XsgLIUT11WM1qwhjgcXZHI_cALXpyM,6397
|
|
9
|
-
md2conf/domain.py,sha256=NpeGl-I9_rgKYCKKZT1Ygg3nl5U0-jJHYYrzDVpMSGQ,1965
|
|
10
|
-
md2conf/drawio.py,sha256=3RJFFzlp5a7SNVNCnwO_HCDfMy0DqYQeXfHWRPInOVE,8527
|
|
11
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
12
|
-
md2conf/extra.py,sha256=VuMxuOnnC2Qwy6y52ukIxsaYhrZArRqMmRHRE4QZl8g,687
|
|
13
|
-
md2conf/latex.py,sha256=yAClNclguPv-xWBMVWbqvYWLbyUHBVufc2aUzwyKHew,7586
|
|
14
|
-
md2conf/local.py,sha256=mvp2kA_eo6JUQ_rlM7zDdEFgBPVxMr3VKP_X1nsLjHE,3747
|
|
15
|
-
md2conf/markdown.py,sha256=czabU17tUfhSX1JQGiI_TrMrTmtoVThOwFu_To_Oi_w,3176
|
|
16
|
-
md2conf/matcher.py,sha256=m5rZjYZSjhKfdeKS8JdPq7cG861Mc6rVZBkrIOZTHGE,6916
|
|
17
|
-
md2conf/mermaid.py,sha256=7iziRC1Li3D85psR5NlnZ6BOePsOfFELgkNCAxahbZU,2240
|
|
18
|
-
md2conf/metadata.py,sha256=LzZM-oPNnzCULmLhF516tPlV5zZBknccwMHt8Nan-xg,1007
|
|
19
|
-
md2conf/processor.py,sha256=z2d2KMPEYWaxflOtH2UTwrjzpPU8TtLSEUvor85ez1Q,9732
|
|
20
|
-
md2conf/properties.py,sha256=RC1jY_TKVbOv2bJxXn27Fj4fNWzyoNUQt6ltgUyVQAQ,3987
|
|
21
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
22
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
md2conf/scanner.py,sha256=Cyvjab8tBvKgubttQvNagS8nailuTvFBqUGoiX5MNp8,5351
|
|
24
|
-
md2conf/text.py,sha256=fHOrUaPXAjE4iRhHqFq-CiI-knpo4wvyHCWp0crewqA,1736
|
|
25
|
-
md2conf/toc.py,sha256=hpqqDbFgNJg5-ul8qWjOglI3Am0sbwR-TLwGN5G9Qo0,2447
|
|
26
|
-
md2conf/uri.py,sha256=KbLBdRFtZTQTZd8b4j0LtE8Pb68Ly0WkemF4iW-EAB4,1158
|
|
27
|
-
md2conf/xml.py,sha256=Ybf3Ctt6EurVvel0eb1KezF33_e_cDpMwlUqHi4kNLE,5411
|
|
28
|
-
markdown_to_confluence-0.4.5.dist-info/METADATA,sha256=zhCjkqQkp71Z28iRzGH-vLMczWaELkd-2FU_kiNv61k,33724
|
|
29
|
-
markdown_to_confluence-0.4.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
-
markdown_to_confluence-0.4.5.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
31
|
-
markdown_to_confluence-0.4.5.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
32
|
-
markdown_to_confluence-0.4.5.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
33
|
-
markdown_to_confluence-0.4.5.dist-info/RECORD,,
|
|
File without changes
|
{markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|