markdown-to-confluence 0.4.2__py3-none-any.whl → 0.4.4__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/csf.py ADDED
@@ -0,0 +1,151 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import importlib.resources as resources
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Callable, TypeVar
13
+
14
+ import lxml.etree as ET
15
+ from lxml.builder import ElementMaker
16
+
17
+ # XML namespaces typically associated with Confluence Storage Format documents
18
+ _namespaces = {
19
+ "ac": "http://atlassian.com/content",
20
+ "ri": "http://atlassian.com/resource/identifier",
21
+ }
22
+ for key, value in _namespaces.items():
23
+ ET.register_namespace(key, value)
24
+
25
+ HTML = ElementMaker()
26
+ AC_ELEM = ElementMaker(namespace=_namespaces["ac"])
27
+ RI_ELEM = ElementMaker(namespace=_namespaces["ri"])
28
+
29
+
30
+ class ParseError(RuntimeError):
31
+ pass
32
+
33
+
34
+ def _qname(namespace_uri: str, name: str) -> str:
35
+ return ET.QName(namespace_uri, name).text
36
+
37
+
38
+ def AC_ATTR(name: str) -> str:
39
+ return _qname(_namespaces["ac"], name)
40
+
41
+
42
+ def RI_ATTR(name: str) -> str:
43
+ return _qname(_namespaces["ri"], name)
44
+
45
+
46
+ R = TypeVar("R")
47
+
48
+
49
+ def with_entities(func: Callable[[Path], R]) -> R:
50
+ "Invokes a callable in the context of an entity definition file."
51
+
52
+ resource_path = resources.files(__package__).joinpath("entities.dtd")
53
+ with resources.as_file(resource_path) as dtd_path:
54
+ return func(dtd_path)
55
+
56
+
57
+ def _elements_from_strings(dtd_path: Path, items: list[str]) -> ET._Element:
58
+ """
59
+ Creates an XML document tree from XML fragment strings.
60
+
61
+ This function
62
+ * adds an XML declaration,
63
+ * wraps the content in a root element,
64
+ * adds namespace declarations associated with Confluence documents.
65
+
66
+ :param dtd_path: Path to a DTD document that defines entities like `¢` or `©`.
67
+ :param items: Strings to parse into XML fragments.
68
+ :returns: An XML document as an element tree.
69
+ """
70
+
71
+ parser = ET.XMLParser(
72
+ remove_blank_text=True,
73
+ remove_comments=True,
74
+ strip_cdata=False,
75
+ load_dtd=True,
76
+ )
77
+
78
+ ns_attr_list = "".join(f' xmlns:{key}="{value}"' for key, value in _namespaces.items())
79
+
80
+ data = [
81
+ '<?xml version="1.0"?>',
82
+ f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path.as_posix()}"><root{ns_attr_list}>',
83
+ ]
84
+ data.extend(items)
85
+ data.append("</root>")
86
+
87
+ try:
88
+ return ET.fromstringlist(data, parser=parser)
89
+ except ET.XMLSyntaxError as ex:
90
+ raise ParseError() from ex
91
+
92
+
93
+ def elements_from_strings(items: list[str]) -> ET._Element:
94
+ """
95
+ Creates a Confluence Storage Format XML document tree from XML fragment strings.
96
+
97
+ A root element is created to hold several XML fragments.
98
+
99
+ :param items: Strings to parse into XML fragments.
100
+ :returns: An XML document as an element tree.
101
+ """
102
+
103
+ return with_entities(lambda dtd_path: _elements_from_strings(dtd_path, items))
104
+
105
+
106
+ def elements_from_string(content: str) -> ET._Element:
107
+ """
108
+ Creates a Confluence Storage Format XML document tree from an XML string.
109
+
110
+ :param content: String to parse into XML.
111
+ :returns: An XML document as an element tree.
112
+ """
113
+
114
+ return elements_from_strings([content])
115
+
116
+
117
+ def _content_to_string(dtd_path: Path, content: str) -> str:
118
+ tree = _elements_from_strings(dtd_path, [content])
119
+ return ET.tostring(tree, pretty_print=True).decode("utf-8")
120
+
121
+
122
+ def content_to_string(content: str) -> str:
123
+ """
124
+ Converts a Confluence Storage Format document returned by the Confluence REST API into a readable XML document.
125
+
126
+ This function
127
+ * adds an XML declaration,
128
+ * wraps the content in a root element,
129
+ * adds namespace declarations associated with Confluence documents.
130
+
131
+ :param content: Confluence Storage Format content as a string.
132
+ :returns: XML as a string.
133
+ """
134
+
135
+ return with_entities(lambda dtd_path: _content_to_string(dtd_path, content))
136
+
137
+
138
+ def elements_to_string(root: ET._Element) -> str:
139
+ """
140
+ Converts a Confluence Storage Format element tree into an XML string to push to Confluence REST API.
141
+
142
+ :param root: Synthesized XML element tree of a Confluence Storage Format document.
143
+ :returns: XML as a string.
144
+ """
145
+
146
+ xml = ET.tostring(root, encoding="utf8", method="xml").decode("utf8")
147
+ m = re.match(r"^<root\s+[^>]*>(.*)</root>\s*$", xml, re.DOTALL)
148
+ if m:
149
+ return m.group(1)
150
+ else:
151
+ raise ValueError("expected: Confluence content")
md2conf/domain.py ADDED
@@ -0,0 +1,46 @@
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
+ from dataclasses import dataclass
10
+ from typing import Literal, Optional
11
+
12
+
13
+ @dataclass
14
+ class ConfluencePageID:
15
+ page_id: str
16
+
17
+
18
+ @dataclass
19
+ class ConfluenceDocumentOptions:
20
+ """
21
+ Options that control the generated page content.
22
+
23
+ :param ignore_invalid_url: When true, ignore invalid URLs in input, emit a warning and replace the anchor with
24
+ plain text; when false, raise an exception.
25
+ :param heading_anchors: When true, emit a structured macro *anchor* for each section heading using GitHub
26
+ conversion rules for the identifier.
27
+ :param generated_by: Text to use as the generated-by prompt (or `None` to omit a prompt).
28
+ :param root_page_id: Confluence page to assume root page role for publishing a directory of Markdown files.
29
+ :param keep_hierarchy: Whether to maintain source directory structure when exporting to Confluence.
30
+ :param prefer_raster: Whether to choose PNG files over SVG files when available.
31
+ :param render_drawio: Whether to pre-render (or use the pre-rendered version of) draw.io diagrams.
32
+ :param render_mermaid: Whether to pre-render Mermaid diagrams into PNG/SVG images.
33
+ :param diagram_output_format: Target image format for diagrams.
34
+ :param webui_links: When true, convert relative URLs to Confluence Web UI links.
35
+ """
36
+
37
+ ignore_invalid_url: bool = False
38
+ heading_anchors: bool = False
39
+ generated_by: Optional[str] = "This page has been generated with a tool."
40
+ root_page_id: Optional[ConfluencePageID] = None
41
+ keep_hierarchy: bool = False
42
+ prefer_raster: bool = True
43
+ render_drawio: bool = False
44
+ render_mermaid: bool = False
45
+ diagram_output_format: Literal["png", "svg"] = "png"
46
+ webui_links: bool = False
md2conf/drawio.py CHANGED
@@ -7,6 +7,11 @@ Copyright 2022-2025, Levente Hunyadi
7
7
  """
8
8
 
9
9
  import base64
10
+ import logging
11
+ import os
12
+ import os.path
13
+ import shutil
14
+ import subprocess
10
15
  import typing
11
16
  import zlib
12
17
  from pathlib import Path
@@ -15,6 +20,8 @@ from urllib.parse import unquote_to_bytes
15
20
 
16
21
  import lxml.etree as ET
17
22
 
23
+ LOGGER = logging.getLogger(__name__)
24
+
18
25
 
19
26
  class DrawioError(ValueError):
20
27
  """
@@ -220,3 +227,45 @@ def extract_diagram(path: Path) -> bytes:
220
227
  raise DrawioError(f"unrecognized file type for {path.name}")
221
228
 
222
229
  return ET.tostring(root, encoding="utf8", method="xml")
230
+
231
+
232
+ def render_diagram(source: Path, output_format: typing.Literal["png", "svg"] = "png") -> bytes:
233
+ "Generates a PNG or SVG image from a draw.io diagram source."
234
+
235
+ executable = shutil.which("draw.io")
236
+ if executable is None:
237
+ raise DrawioError("draw.io executable not found")
238
+
239
+ target = f"tmp_drawio.{output_format}"
240
+
241
+ cmd = [executable, "--export", "--format", output_format, "--output", target]
242
+ if output_format == "png":
243
+ cmd.extend(["--scale", "2", "--transparent"])
244
+ elif output_format == "svg":
245
+ cmd.append("--embed-svg-images")
246
+ cmd.append(str(source))
247
+
248
+ LOGGER.debug("Executing: %s", " ".join(cmd))
249
+ try:
250
+ proc = subprocess.Popen(
251
+ cmd,
252
+ stdout=subprocess.PIPE,
253
+ stderr=subprocess.PIPE,
254
+ text=False,
255
+ )
256
+ stdout, stderr = proc.communicate()
257
+ if proc.returncode:
258
+ messages = [f"failed to convert draw.io diagram; exit code: {proc.returncode}"]
259
+ console_output = stdout.decode("utf-8")
260
+ if console_output:
261
+ messages.append(f"output:\n{console_output}")
262
+ console_error = stderr.decode("utf-8")
263
+ if console_error:
264
+ messages.append(f"error:\n{console_error}")
265
+ raise DrawioError("\n".join(messages))
266
+ with open(target, "rb") as f:
267
+ return f.read()
268
+
269
+ finally:
270
+ if os.path.exists(target):
271
+ os.remove(target)
md2conf/local.py CHANGED
@@ -11,7 +11,8 @@ import os
11
11
  from pathlib import Path
12
12
  from typing import Optional
13
13
 
14
- from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID
14
+ from .converter import ConfluenceDocument
15
+ from .domain import ConfluenceDocumentOptions, ConfluencePageID
15
16
  from .extra import override
16
17
  from .metadata import ConfluencePageMetadata, ConfluenceSiteMetadata
17
18
  from .processor import Converter, DocumentNode, Processor, ProcessorFactory
@@ -77,10 +78,14 @@ class LocalProcessor(Processor):
77
78
  """
78
79
 
79
80
  content = document.xhtml()
80
- out_path = self.out_dir / path.relative_to(self.root_dir).with_suffix(".csf")
81
- os.makedirs(out_path.parent, exist_ok=True)
82
- with open(out_path, "w", encoding="utf-8") as f:
81
+ csf_path = self.out_dir / path.relative_to(self.root_dir).with_suffix(".csf")
82
+ csf_dir = csf_path.parent
83
+ os.makedirs(csf_dir, exist_ok=True)
84
+ with open(csf_path, "w", encoding="utf-8") as f:
83
85
  f.write(content)
86
+ for name, data in document.embedded_files.items():
87
+ with open(csf_dir / name, "wb") as f:
88
+ f.write(data)
84
89
 
85
90
 
86
91
  class LocalProcessorFactory(ProcessorFactory):
md2conf/markdown.py ADDED
@@ -0,0 +1,114 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import xml.etree.ElementTree
10
+ from typing import Any, Optional
11
+
12
+ import markdown
13
+
14
+
15
+ def _emoji_generator(
16
+ index: str,
17
+ shortname: str,
18
+ alias: Optional[str],
19
+ uc: Optional[str],
20
+ alt: str,
21
+ title: Optional[str],
22
+ category: Optional[str],
23
+ options: dict[str, Any],
24
+ md: markdown.Markdown,
25
+ ) -> xml.etree.ElementTree.Element:
26
+ """
27
+ Custom generator for `pymdownx.emoji`.
28
+ """
29
+
30
+ name = (alias or shortname).strip(":")
31
+ emoji = xml.etree.ElementTree.Element("x-emoji", {"data-shortname": name})
32
+ if uc is not None:
33
+ emoji.attrib["data-unicode"] = uc
34
+
35
+ # convert series of Unicode code point hexadecimal values into characters
36
+ emoji.text = "".join(chr(int(item, base=16)) for item in uc.split("-"))
37
+ else:
38
+ emoji.text = alt
39
+
40
+ return emoji
41
+
42
+
43
+ def _verbatim_formatter(
44
+ source: str,
45
+ language: str,
46
+ css_class: str,
47
+ options: dict[str, Any],
48
+ md: markdown.Markdown,
49
+ classes: Optional[list[str]] = None,
50
+ id_value: str = "",
51
+ attrs: Optional[dict[str, str]] = None,
52
+ **kwargs: Any,
53
+ ) -> str:
54
+ """
55
+ Custom formatter for `pymdownx.superfences`.
56
+
57
+ Used by language `math` (a.k.a. `pymdownx.arithmatex`) and pseudo-language `csf` (Confluence Storage Format pass-through).
58
+ """
59
+
60
+ if classes is None:
61
+ classes = [css_class]
62
+ else:
63
+ classes.insert(0, css_class)
64
+
65
+ html_id = f' id="{id_value}"' if id_value else ""
66
+ html_class = ' class="{}"'.format(" ".join(classes))
67
+ html_attrs = " " + " ".join(f'{k}="{v}"' for k, v in attrs.items()) if attrs else ""
68
+
69
+ return f"<div{html_id}{html_class}{html_attrs}>{source}</div>"
70
+
71
+
72
+ _CONVERTER = markdown.Markdown(
73
+ extensions=[
74
+ "admonition",
75
+ "footnotes",
76
+ "markdown.extensions.tables",
77
+ "md_in_html",
78
+ "pymdownx.arithmatex",
79
+ "pymdownx.emoji",
80
+ "pymdownx.highlight", # required by `pymdownx.superfences`
81
+ "pymdownx.magiclink",
82
+ "pymdownx.superfences",
83
+ "pymdownx.tilde",
84
+ "sane_lists",
85
+ ],
86
+ extension_configs={
87
+ "footnotes": {"BACKLINK_TITLE": ""},
88
+ "pymdownx.arithmatex": {"generic": True, "preview": False, "tex_inline_wrap": ["", ""], "tex_block_wrap": ["", ""]},
89
+ "pymdownx.emoji": {"emoji_generator": _emoji_generator, "strict": True},
90
+ "pymdownx.highlight": {
91
+ "use_pygments": False,
92
+ },
93
+ "pymdownx.superfences": {
94
+ "custom_fences": [
95
+ {"name": "math", "class": "arithmatex", "format": _verbatim_formatter},
96
+ {"name": "csf", "class": "csf", "format": _verbatim_formatter},
97
+ ]
98
+ },
99
+ },
100
+ )
101
+
102
+
103
+ def markdown_to_html(content: str) -> str:
104
+ """
105
+ Converts a Markdown document into XHTML with Python-Markdown.
106
+
107
+ :param content: Markdown input as a string.
108
+ :returns: XHTML output as a string.
109
+ :see: https://python-markdown.github.io/
110
+ """
111
+
112
+ _CONVERTER.reset()
113
+ html = _CONVERTER.convert(content)
114
+ return html
md2conf/processor.py CHANGED
@@ -14,7 +14,8 @@ from pathlib import Path
14
14
  from typing import Iterable, Optional
15
15
 
16
16
  from .collection import ConfluencePageCollection
17
- from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID
17
+ from .converter import ConfluenceDocument
18
+ from .domain import ConfluenceDocumentOptions, ConfluencePageID
18
19
  from .matcher import DirectoryEntry, FileEntry, Matcher, MatcherOptions
19
20
  from .metadata import ConfluenceSiteMetadata
20
21
  from .properties import ArgumentError
md2conf/toc.py ADDED
@@ -0,0 +1,89 @@
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
+ from dataclasses import dataclass
10
+ from typing import Optional
11
+
12
+
13
+ @dataclass(eq=True)
14
+ class TableOfContentsEntry:
15
+ """
16
+ Represents a table of contents entry.
17
+
18
+ :param level: The heading level assigned to the entry. Each entry can only contain children whose level is strictly greater than of its parent.
19
+ :param text: The heading text.
20
+ :param children: Direct descendants whose parent is this entry.
21
+ """
22
+
23
+ level: int
24
+ text: str
25
+ children: list["TableOfContentsEntry"]
26
+
27
+ def __init__(self, level: int, text: str, children: Optional[list["TableOfContentsEntry"]] = None) -> None:
28
+ self.level = level
29
+ self.text = text
30
+ self.children = children or []
31
+
32
+
33
+ class TableOfContentsBuilder:
34
+ """
35
+ Builds a table of contents from Markdown headings.
36
+ """
37
+
38
+ _root: TableOfContentsEntry
39
+ _stack: list[TableOfContentsEntry]
40
+
41
+ def __init__(self) -> None:
42
+ self._root = TableOfContentsEntry(0, "<root>")
43
+ self._stack = [self._root]
44
+
45
+ @property
46
+ def tree(self) -> list[TableOfContentsEntry]:
47
+ """
48
+ Table of contents as a hierarchy of headings.
49
+ """
50
+
51
+ return self._root.children
52
+
53
+ def add(self, level: int, text: str) -> None:
54
+ """
55
+ Adds a heading to the table of contents.
56
+
57
+ :param level: Markdown heading level (e.g. `1` for first-level heading).
58
+ :param text: Markdown heading text.
59
+ """
60
+
61
+ if level < 1:
62
+ raise ValueError("expected: Markdown heading level >= 1")
63
+
64
+ # remove any stack items deeper than the current level
65
+ top = self._stack[-1]
66
+ while top.level >= level:
67
+ self._stack.pop()
68
+ top = self._stack[-1]
69
+
70
+ # add the new section under the current top level
71
+ item = TableOfContentsEntry(level, text)
72
+ top.children.append(item)
73
+
74
+ # push new level onto the stack
75
+ self._stack.append(item)
76
+
77
+ def get_title(self) -> Optional[str]:
78
+ """
79
+ Returns a proposed document title.
80
+
81
+ The proposed title is text of the top-level heading if and only if that heading is unique.
82
+
83
+ :returns: Title text, or `None` if no title can be inferred.
84
+ """
85
+
86
+ if len(self.tree) == 1:
87
+ return self.tree[0].text
88
+ else:
89
+ return None
md2conf/uri.py ADDED
@@ -0,0 +1,46 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import hashlib
10
+ import urllib.parse
11
+ import uuid
12
+ from urllib.parse import urlparse
13
+
14
+
15
+ def to_data_uri(mime: str, data: str) -> str:
16
+ "Generates a data URI with the specified MIME type."
17
+
18
+ # URL-encode data
19
+ encoded = urllib.parse.quote(data, safe=";/?:@&=+$,-_.!~*'()#") # minimal encoding
20
+ return f"data:{mime},{encoded}"
21
+
22
+
23
+ def to_uuid(data: str) -> uuid.UUID:
24
+ "Generates a UUID that represents the data."
25
+
26
+ # create SHA-1 hash of the SVG content
27
+ sha1_hash = hashlib.sha1(data.encode("utf-8")).digest()
28
+
29
+ # generate UUID using the first 16 bytes of the hash
30
+ return uuid.UUID(bytes=sha1_hash[:16])
31
+
32
+
33
+ def to_uuid_urn(data: str) -> str:
34
+ "Generates a UUID URN that represents the data."
35
+
36
+ return f"urn:uuid:{str(to_uuid(data))}"
37
+
38
+
39
+ def is_absolute_url(url: str) -> bool:
40
+ urlparts = urlparse(url)
41
+ return bool(urlparts.scheme) or bool(urlparts.netloc)
42
+
43
+
44
+ def is_relative_url(url: str) -> bool:
45
+ urlparts = urlparse(url)
46
+ return not bool(urlparts.scheme) and not bool(urlparts.netloc)
md2conf/xml.py CHANGED
@@ -1,11 +1,21 @@
1
- from typing import Iterable, Optional, Union
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
+ from typing import Iterable, Optional
2
10
 
3
11
  import lxml.etree as ET
4
12
 
5
13
 
6
- def _attrs_equal_excluding(attrs1: ET._Attrib, attrs2: ET._Attrib, exclude: set[Union[str, ET.QName]]) -> bool:
14
+ def _attrs_equal_excluding(attrs1: ET._Attrib, attrs2: ET._Attrib, exclude: set[str]) -> bool:
7
15
  """
8
16
  Compares two dictionary objects, excluding keys in the skip set.
17
+
18
+ :param exclude: Attributes to exclude, in `{namespace}name` notation.
9
19
  """
10
20
 
11
21
  # create key sets to compare, excluding keys to be skipped
@@ -23,10 +33,19 @@ def _attrs_equal_excluding(attrs1: ET._Attrib, attrs2: ET._Attrib, exclude: set[
23
33
 
24
34
 
25
35
  class ElementComparator:
26
- skip_attributes: set[Union[str, ET.QName]]
36
+ skip_attributes: set[str]
37
+ skip_elements: set[str]
38
+
39
+ def __init__(self, *, skip_attributes: Optional[Iterable[str]] = None, skip_elements: Optional[Iterable[str]] = None):
40
+ """
41
+ Initializes a new element tree comparator.
42
+
43
+ :param skip_attributes: Attributes to exclude, in `{namespace}name` notation.
44
+ :param skip_elements: Elements to exclude, in `{namespace}name` notation.
45
+ """
27
46
 
28
- def __init__(self, *, skip_attributes: Optional[Iterable[Union[str, ET.QName]]] = None):
29
47
  self.skip_attributes = set(skip_attributes) if skip_attributes else set()
48
+ self.skip_elements = set(skip_elements) if skip_elements else set()
30
49
 
31
50
  def is_equal(self, e1: ET._Element, e2: ET._Element) -> bool:
32
51
  """
@@ -36,35 +55,49 @@ class ElementComparator:
36
55
  if e1.tag != e2.tag:
37
56
  return False
38
57
 
39
- e1_text = e1.text.strip() if e1.text else ""
40
- e2_text = e2.text.strip() if e2.text else ""
41
- if e1_text != e2_text:
42
- return False
43
-
58
+ # compare tail first, which is outside of element
44
59
  e1_tail = e1.tail.strip() if e1.tail else ""
45
60
  e2_tail = e2.tail.strip() if e2.tail else ""
46
61
  if e1_tail != e2_tail:
47
62
  return False
48
63
 
64
+ # skip element (and content) if on ignore list
65
+ if e1.tag in self.skip_elements:
66
+ return True
67
+
68
+ # compare text second, which is encapsulated by element
69
+ e1_text = e1.text.strip() if e1.text else ""
70
+ e2_text = e2.text.strip() if e2.text else ""
71
+ if e1_text != e2_text:
72
+ return False
73
+
74
+ # compare attributes, disregarding definition order
49
75
  if not _attrs_equal_excluding(e1.attrib, e2.attrib, self.skip_attributes):
50
76
  return False
77
+
78
+ # compare children recursively
51
79
  if len(e1) != len(e2):
52
80
  return False
53
81
  return all(self.is_equal(c1, c2) for c1, c2 in zip(e1, e2))
54
82
 
55
83
 
56
84
  def is_xml_equal(
57
- tree1: ET._Element,
58
- tree2: ET._Element,
59
- *,
60
- skip_attributes: Optional[Iterable[Union[str, ET.QName]]] = None,
85
+ tree1: ET._Element, tree2: ET._Element, *, skip_attributes: Optional[Iterable[str]] = None, skip_elements: Optional[Iterable[str]] = None
61
86
  ) -> bool:
62
87
  """
63
88
  Compare two XML documents for equivalence, ignoring leading/trailing whitespace differences and attribute definition order.
64
89
 
65
90
  :param tree1: XML document as an element tree.
66
91
  :param tree2: XML document as an element tree.
92
+ :param skip_attributes: Attributes to exclude, in `{namespace}name` notation.
93
+ :param skip_elements: Elements to exclude, in `{namespace}name` notation.
67
94
  :returns: True if equivalent, False otherwise.
68
95
  """
69
96
 
70
- return ElementComparator(skip_attributes=skip_attributes).is_equal(tree1, tree2)
97
+ return ElementComparator(skip_attributes=skip_attributes, skip_elements=skip_elements).is_equal(tree1, tree2)
98
+
99
+
100
+ def element_to_text(node: ET._Element) -> str:
101
+ "Returns all text contained in an element as a concatenated string."
102
+
103
+ return "".join(node.itertext()).strip()
@@ -1,27 +0,0 @@
1
- markdown_to_confluence-0.4.2.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
2
- md2conf/__init__.py,sha256=XAJAgUDrYS3PdOzoo2BQ-rM3PbQWOrOW2kPt5iJ8xY0,402
3
- md2conf/__main__.py,sha256=qyboDihyVTm0EZa_c3AFWF1AojhL8-bww_kISAa5nHQ,10130
4
- md2conf/api.py,sha256=DbG1udDb9ti4OjqgSW3DSuHwxKNFPVDTkhjnaB1GNMI,37193
5
- md2conf/application.py,sha256=X_V4KdFACHwl5Nt4BIHQyhtecOqNOzknrPyPTW0d4Z0,8185
6
- md2conf/collection.py,sha256=EobgMRJgkYloWlY03NZJ52MRC_SGLpTVCHkltDbQyt0,837
7
- md2conf/converter.py,sha256=Eg6emS77GQAkhbXutaRjHxcBFgmXp_4z_zOAmBfqxUY,54360
8
- md2conf/drawio.py,sha256=G_pD2nafl7dXuFK_4MBiEUl0ZZGNuagnHw6GFOrev94,6717
9
- md2conf/emoji.py,sha256=UzDrxqFo59wHmbbJmMNdn0rYFDXbZE4qirOM-_egzXc,2603
10
- md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
11
- md2conf/extra.py,sha256=VuMxuOnnC2Qwy6y52ukIxsaYhrZArRqMmRHRE4QZl8g,687
12
- md2conf/local.py,sha256=Ph-cGc_JQ1SkvuZ_Jxn37dlpaKZYKLVPBnsk5CGSVnk,3548
13
- md2conf/matcher.py,sha256=m5rZjYZSjhKfdeKS8JdPq7cG861Mc6rVZBkrIOZTHGE,6916
14
- md2conf/mermaid.py,sha256=f0x7ISj-41ZMh4zTAFPhIWwr94SDcsVZUc1NWqmH_G4,2508
15
- md2conf/metadata.py,sha256=LzZM-oPNnzCULmLhF516tPlV5zZBknccwMHt8Nan-xg,1007
16
- md2conf/processor.py,sha256=59XDWKgTvJwEZ1y52VfRkM67K2-Ivh7kGD6Eg2tfG9c,9713
17
- md2conf/properties.py,sha256=RC1jY_TKVbOv2bJxXn27Fj4fNWzyoNUQt6ltgUyVQAQ,3987
18
- md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
19
- md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- md2conf/scanner.py,sha256=Cyvjab8tBvKgubttQvNagS8nailuTvFBqUGoiX5MNp8,5351
21
- md2conf/xml.py,sha256=HoKJfF1yRZ3Gk8jTS-kRpOqVs0nQJZyr56l0Fo3y9fs,2193
22
- markdown_to_confluence-0.4.2.dist-info/METADATA,sha256=ggIrg30RRTvk3ax2LKC_wKjMhOEhrXOYVrSwvb2dDbw,29495
23
- markdown_to_confluence-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- markdown_to_confluence-0.4.2.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
25
- markdown_to_confluence-0.4.2.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
26
- markdown_to_confluence-0.4.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
27
- markdown_to_confluence-0.4.2.dist-info/RECORD,,