markdown-to-confluence 0.5.3__py3-none-any.whl → 0.5.5__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.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/METADATA +275 -208
- markdown_to_confluence-0.5.5.dist-info/RECORD +57 -0
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +61 -189
- md2conf/api.py +35 -69
- md2conf/attachment.py +4 -3
- md2conf/clio.py +226 -0
- md2conf/compatibility.py +5 -0
- md2conf/converter.py +239 -147
- md2conf/csf.py +89 -9
- md2conf/drawio/extension.py +3 -3
- md2conf/drawio/render.py +2 -0
- md2conf/extension.py +4 -0
- md2conf/external.py +25 -8
- md2conf/frontmatter.py +18 -6
- md2conf/image.py +17 -14
- md2conf/latex.py +8 -1
- md2conf/markdown.py +68 -1
- md2conf/mermaid/render.py +1 -1
- md2conf/options.py +95 -24
- md2conf/plantuml/extension.py +7 -7
- md2conf/plantuml/render.py +6 -7
- md2conf/png.py +10 -6
- md2conf/processor.py +24 -3
- md2conf/publisher.py +193 -36
- md2conf/reflection.py +74 -0
- md2conf/scanner.py +16 -6
- md2conf/serializer.py +12 -1
- md2conf/svg.py +131 -109
- md2conf/toc.py +72 -0
- md2conf/xml.py +45 -0
- markdown_to_confluence-0.5.3.dist-info/RECORD +0 -55
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/zip-safe +0 -0
- /md2conf/{puppeteer-config.json → mermaid/puppeteer-config.json} +0 -0
md2conf/svg.py
CHANGED
|
@@ -9,6 +9,7 @@ Copyright 2022-2026, Levente Hunyadi
|
|
|
9
9
|
import logging
|
|
10
10
|
import re
|
|
11
11
|
from pathlib import Path
|
|
12
|
+
from typing import overload
|
|
12
13
|
|
|
13
14
|
import lxml.etree as ET
|
|
14
15
|
|
|
@@ -19,6 +20,10 @@ LOGGER = logging.getLogger(__name__)
|
|
|
19
20
|
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
class SVGParseError(RuntimeError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
22
27
|
def _check_svg(root: ElementType) -> bool:
|
|
23
28
|
"Tests if the element is a plain or scoped SVG element."
|
|
24
29
|
|
|
@@ -31,7 +36,7 @@ def _check_svg(root: ElementType) -> bool:
|
|
|
31
36
|
return qname.localname == "svg" and (not qname.namespace or qname.namespace == SVG_NAMESPACE)
|
|
32
37
|
|
|
33
38
|
|
|
34
|
-
def _extract_dimensions_from_root(root: ElementType) -> tuple[int
|
|
39
|
+
def _extract_dimensions_from_root(root: ElementType) -> tuple[int, int] | None:
|
|
35
40
|
"""
|
|
36
41
|
Extracts width and height from an SVG root element.
|
|
37
42
|
|
|
@@ -40,11 +45,11 @@ def _extract_dimensions_from_root(root: ElementType) -> tuple[int | None, int |
|
|
|
40
45
|
2. The viewBox attribute if width/height are not specified
|
|
41
46
|
|
|
42
47
|
:param root: The root element of the SVG document.
|
|
43
|
-
:returns: A tuple of (width, height) in pixels, or
|
|
48
|
+
:returns: A tuple of (width, height) in pixels, or `None` if dimensions cannot be determined.
|
|
44
49
|
"""
|
|
45
50
|
|
|
46
51
|
if not _check_svg(root):
|
|
47
|
-
|
|
52
|
+
raise SVGParseError("SVG file does not have an <svg> root element")
|
|
48
53
|
|
|
49
54
|
width_attr = root.get("width")
|
|
50
55
|
height_attr = root.get("height")
|
|
@@ -52,69 +57,86 @@ def _extract_dimensions_from_root(root: ElementType) -> tuple[int | None, int |
|
|
|
52
57
|
width = _parse_svg_length(width_attr) if width_attr else None
|
|
53
58
|
height = _parse_svg_length(height_attr) if height_attr else None
|
|
54
59
|
|
|
55
|
-
#
|
|
60
|
+
# if width/height not specified, try to derive from view-box
|
|
61
|
+
if width is None or height is None:
|
|
62
|
+
viewbox_attr = root.get("viewBox")
|
|
63
|
+
if viewbox_attr:
|
|
64
|
+
viewbox = _parse_viewbox(viewbox_attr)
|
|
65
|
+
if viewbox is not None:
|
|
66
|
+
vb_width, vb_height = viewbox
|
|
67
|
+
if width is not None:
|
|
68
|
+
height = width * vb_height // vb_width
|
|
69
|
+
elif height is not None:
|
|
70
|
+
width = height * vb_width // vb_height
|
|
71
|
+
else:
|
|
72
|
+
width = vb_width
|
|
73
|
+
height = vb_height
|
|
74
|
+
|
|
56
75
|
if width is None or height is None:
|
|
57
|
-
|
|
58
|
-
if viewbox:
|
|
59
|
-
vb_width, vb_height = _parse_viewbox(viewbox)
|
|
60
|
-
if width is None:
|
|
61
|
-
width = vb_width
|
|
62
|
-
if height is None:
|
|
63
|
-
height = vb_height
|
|
76
|
+
return None
|
|
64
77
|
|
|
65
78
|
return width, height
|
|
66
79
|
|
|
67
80
|
|
|
68
|
-
|
|
81
|
+
@overload
|
|
82
|
+
def get_svg_dimensions(svg: Path) -> tuple[int, int] | None:
|
|
69
83
|
"""
|
|
70
84
|
Extracts width and height from an SVG file.
|
|
71
85
|
|
|
72
86
|
Attempts to read dimensions from:
|
|
73
|
-
1. Explicit width/height attributes on the root <svg> element
|
|
74
|
-
2. The viewBox attribute if width/height are not specified
|
|
75
|
-
|
|
76
|
-
:param path: Path to the SVG file.
|
|
77
|
-
:returns: A tuple of (width, height) in pixels, or (None, None) if dimensions cannot be determined.
|
|
78
|
-
"""
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
root = tree.getroot()
|
|
83
|
-
width, height = _extract_dimensions_from_root(root)
|
|
84
|
-
if width is None and height is None:
|
|
85
|
-
LOGGER.warning("SVG file %s does not have an <svg> root element", path)
|
|
86
|
-
return width, height
|
|
88
|
+
1. Explicit `width` and `height` attributes on the root `<svg>` element
|
|
89
|
+
2. The `viewBox` attribute if `width` or `height` is not specified
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
LOGGER.warning("Unexpected error reading SVG dimensions from %s: %s", path, ex)
|
|
93
|
-
return None, None
|
|
91
|
+
:param svg: Path to the SVG file.
|
|
92
|
+
:returns: A tuple of (width, height) in pixels, or `None` if dimensions cannot be determined.
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
@overload
|
|
98
|
+
def get_svg_dimensions(svg: bytes | str) -> tuple[int, int] | None:
|
|
97
99
|
"""
|
|
98
100
|
Extracts width and height from SVG data in memory.
|
|
99
101
|
|
|
100
102
|
Attempts to read dimensions from:
|
|
101
|
-
1. Explicit width/height attributes on the root <svg> element
|
|
102
|
-
2. The viewBox attribute if width/height are not specified
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"""
|
|
104
|
+
1. Explicit `width` and `height` attributes on the root `<svg>` element
|
|
105
|
+
2. The `viewBox` attribute if `width` or `height` is not specified
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
:param svg: The SVG content as bytes.
|
|
108
|
+
:returns: A tuple of (width, height) in pixels, or `None` if dimensions cannot be determined.
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_svg_dimensions(svg: Path | bytes | str) -> tuple[int, int] | None:
|
|
114
|
+
if isinstance(svg, Path):
|
|
115
|
+
path = svg
|
|
116
|
+
try:
|
|
117
|
+
tree = ET.parse(path)
|
|
118
|
+
root = tree.getroot()
|
|
119
|
+
return _extract_dimensions_from_root(root)
|
|
120
|
+
except OSError as ex:
|
|
121
|
+
LOGGER.warning("Failed to open SVG file: %s", path, exc_info=ex)
|
|
122
|
+
return None
|
|
123
|
+
except ET.XMLSyntaxError as ex:
|
|
124
|
+
LOGGER.warning("Failed to parse SVG file: %s", path, exc_info=ex)
|
|
125
|
+
return None
|
|
126
|
+
except SVGParseError as ex:
|
|
127
|
+
LOGGER.warning("Failed to extract dimensions from SVG file: %s", path, exc_info=ex)
|
|
128
|
+
return None
|
|
129
|
+
else:
|
|
130
|
+
data = svg
|
|
131
|
+
try:
|
|
132
|
+
root = ET.fromstring(data)
|
|
133
|
+
return _extract_dimensions_from_root(root)
|
|
134
|
+
except ET.XMLSyntaxError as ex:
|
|
135
|
+
LOGGER.warning("Failed to parse SVG data", exc_info=ex)
|
|
136
|
+
return None
|
|
137
|
+
except SVGParseError as ex:
|
|
138
|
+
LOGGER.warning("Failed to extract dimensions from SVG data", exc_info=ex)
|
|
139
|
+
return None
|
|
118
140
|
|
|
119
141
|
|
|
120
142
|
def _serialize_svg_opening_tag(root: ElementType) -> str:
|
|
@@ -128,7 +150,7 @@ def _serialize_svg_opening_tag(root: ElementType) -> str:
|
|
|
128
150
|
# Build the opening tag from element name and attributes
|
|
129
151
|
root_tag = root.tag
|
|
130
152
|
if not isinstance(root_tag, str):
|
|
131
|
-
raise
|
|
153
|
+
raise SVGParseError("expected: tag names as `str`")
|
|
132
154
|
tag_name = ET.QName(root_tag).localname
|
|
133
155
|
parts = [f"<{tag_name}"]
|
|
134
156
|
|
|
@@ -180,64 +202,59 @@ def fix_svg_dimensions(data: bytes) -> bytes:
|
|
|
180
202
|
:returns: The modified SVG content with explicit dimensions, or original data if modification fails.
|
|
181
203
|
"""
|
|
182
204
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if "<foreignObject" in text:
|
|
189
|
-
LOGGER.debug("Skipping dimension fix for SVG with foreignObject elements")
|
|
190
|
-
return data
|
|
191
|
-
|
|
192
|
-
# Parse the SVG to extract root element attributes
|
|
193
|
-
root = ET.fromstring(data)
|
|
205
|
+
# Skip SVGs with foreignObject - Confluence has issues rendering
|
|
206
|
+
# foreignObject content when explicit width/height are set on the SVG
|
|
207
|
+
if b"<foreignObject" in data:
|
|
208
|
+
LOGGER.debug("Skipping dimension fix for SVG with foreignObject elements")
|
|
209
|
+
return data
|
|
194
210
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return data
|
|
211
|
+
# Parse the SVG to extract root element attributes
|
|
212
|
+
root = ET.fromstring(data)
|
|
198
213
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
# Check if it already has a valid numeric width
|
|
203
|
-
if width_attr is not None and _parse_svg_length(width_attr) is not None:
|
|
204
|
-
return data # Already has numeric width
|
|
214
|
+
# Verify it's an SVG element
|
|
215
|
+
if not _check_svg(root):
|
|
216
|
+
return data
|
|
205
217
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
218
|
+
# Check if we need to fix (has width="100%" or similar percentage)
|
|
219
|
+
width_attr = root.get("width")
|
|
220
|
+
if width_attr != "100%":
|
|
221
|
+
# Check if it already has a valid numeric width
|
|
222
|
+
if width_attr is not None and _parse_svg_length(width_attr) is not None:
|
|
223
|
+
return data # Already has numeric width
|
|
224
|
+
|
|
225
|
+
# Get viewBox dimensions
|
|
226
|
+
viewbox_attr = root.get("viewBox")
|
|
227
|
+
if not viewbox_attr:
|
|
228
|
+
return data
|
|
210
229
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
viewbox = _parse_viewbox(viewbox_attr)
|
|
231
|
+
if viewbox is None:
|
|
232
|
+
return data
|
|
233
|
+
vb_width, vb_height = viewbox
|
|
214
234
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
235
|
+
# Extract the original opening tag from the text
|
|
236
|
+
svg_tag_match = re.search(rb"<svg\b[^>]*>", data)
|
|
237
|
+
if not svg_tag_match:
|
|
238
|
+
return data
|
|
219
239
|
|
|
220
|
-
|
|
240
|
+
original_tag = svg_tag_match.group(0)
|
|
221
241
|
|
|
222
|
-
|
|
223
|
-
|
|
242
|
+
# Modify the root element's attributes
|
|
243
|
+
root.set("width", str(vb_width))
|
|
224
244
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
245
|
+
# Set height if missing or if it's a percentage
|
|
246
|
+
height_attr = root.get("height")
|
|
247
|
+
if height_attr is None or height_attr == "100%":
|
|
248
|
+
root.set("height", str(vb_height))
|
|
229
249
|
|
|
230
|
-
|
|
231
|
-
|
|
250
|
+
# Serialize just the opening tag with modified attributes
|
|
251
|
+
new_tag = _serialize_svg_opening_tag(root).encode("utf-8")
|
|
232
252
|
|
|
233
|
-
|
|
234
|
-
|
|
253
|
+
# Replace the original opening tag with the new one
|
|
254
|
+
return data.replace(original_tag, new_tag, 1)
|
|
235
255
|
|
|
236
|
-
return text.encode("utf-8")
|
|
237
256
|
|
|
238
|
-
|
|
239
|
-
LOGGER.warning("Unexpected error fixing SVG dimensions: %s", ex)
|
|
240
|
-
return data
|
|
257
|
+
_MEASURE_REGEXP = re.compile(r"^([+-]?(?:\d+\.?\d*|\.\d+))(%|px|pt|em|ex|in|cm|mm|pc)?$", re.IGNORECASE)
|
|
241
258
|
|
|
242
259
|
|
|
243
260
|
def _parse_svg_length(value: str) -> int | None:
|
|
@@ -257,7 +274,7 @@ def _parse_svg_length(value: str) -> int | None:
|
|
|
257
274
|
value = value.strip()
|
|
258
275
|
|
|
259
276
|
# Match number with optional unit
|
|
260
|
-
match =
|
|
277
|
+
match = _MEASURE_REGEXP.match(value)
|
|
261
278
|
if not match:
|
|
262
279
|
return None
|
|
263
280
|
|
|
@@ -294,32 +311,32 @@ def _parse_svg_length(value: str) -> int | None:
|
|
|
294
311
|
return int(round(pixels))
|
|
295
312
|
|
|
296
313
|
|
|
297
|
-
def _parse_viewbox(viewbox: str) -> tuple[int
|
|
314
|
+
def _parse_viewbox(viewbox: str) -> tuple[int, int] | None:
|
|
298
315
|
"""
|
|
299
316
|
Parses an SVG viewBox attribute and extracts width and height.
|
|
300
317
|
|
|
301
318
|
:param viewbox: The viewBox string (e.g., "0 0 100 200").
|
|
302
|
-
:returns: A tuple of (width, height) in pixels, or
|
|
319
|
+
:returns: A tuple of (width, height) in pixels, or `None` if parsing fails.
|
|
303
320
|
"""
|
|
304
321
|
|
|
305
322
|
if not viewbox:
|
|
306
|
-
return None
|
|
323
|
+
return None
|
|
307
324
|
|
|
308
325
|
# viewBox format: "min-x min-y width height"
|
|
309
326
|
# Values can be separated by whitespace and/or commas
|
|
310
|
-
parts = re.split(r"
|
|
327
|
+
parts = re.split(r"\s*,\s*|\s+", viewbox.strip())
|
|
311
328
|
if len(parts) != 4:
|
|
312
|
-
return None
|
|
329
|
+
return None
|
|
313
330
|
|
|
314
331
|
try:
|
|
315
332
|
width = int(round(float(parts[2])))
|
|
316
333
|
height = int(round(float(parts[3])))
|
|
317
334
|
return width, height
|
|
318
335
|
except ValueError:
|
|
319
|
-
return None
|
|
336
|
+
return None
|
|
320
337
|
|
|
321
338
|
|
|
322
|
-
def fix_svg_get_dimensions(
|
|
339
|
+
def fix_svg_get_dimensions(data: bytes) -> tuple[bytes, tuple[int, int] | None]:
|
|
323
340
|
"""
|
|
324
341
|
Post-processes SVG diagram data by fixing dimensions and extracting metadata.
|
|
325
342
|
|
|
@@ -328,14 +345,19 @@ def fix_svg_get_dimensions(image_data: bytes) -> tuple[bytes, int | None, int |
|
|
|
328
345
|
1. fixes SVG dimensions (converts percentage-based to explicit pixels), and
|
|
329
346
|
2. extracts width/height from the SVG.
|
|
330
347
|
|
|
331
|
-
:param
|
|
348
|
+
:param data: Raw SVG data as bytes.
|
|
332
349
|
:returns: Tuple of update raw data, image width, image height.
|
|
333
350
|
"""
|
|
334
351
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
# extract dimensions from the fixed SVG
|
|
339
|
-
width, height = get_svg_dimensions_from_bytes(image_data)
|
|
352
|
+
try:
|
|
353
|
+
# fix SVG to have explicit width/height instead of percentages
|
|
354
|
+
data = fix_svg_dimensions(data)
|
|
340
355
|
|
|
341
|
-
|
|
356
|
+
# extract dimensions from the fixed SVG
|
|
357
|
+
return data, get_svg_dimensions(data)
|
|
358
|
+
except ET.XMLSyntaxError as ex:
|
|
359
|
+
LOGGER.warning("Failed to parse SVG data", exc_info=ex)
|
|
360
|
+
return data, None
|
|
361
|
+
except SVGParseError as ex:
|
|
362
|
+
LOGGER.warning("Failed to extract dimensions from SVG data", exc_info=ex)
|
|
363
|
+
return data, None
|
md2conf/toc.py
CHANGED
|
@@ -6,7 +6,9 @@ Copyright 2022-2026, Levente Hunyadi
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import re
|
|
9
10
|
from dataclasses import dataclass
|
|
11
|
+
from typing import Iterable, Iterator
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
@dataclass(eq=True)
|
|
@@ -86,3 +88,73 @@ class TableOfContentsBuilder:
|
|
|
86
88
|
return self.tree[0].text
|
|
87
89
|
else:
|
|
88
90
|
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_FENCED_CODE_REGEXP = re.compile(r"^\s*(?:`{3,}|~{3,})", re.MULTILINE)
|
|
94
|
+
_ATX_HEADING_REGEXP = re.compile(r"^(#{1,6})\s+(.*?)$", re.MULTILINE)
|
|
95
|
+
_SETEXT_HEADING_REGEXP = re.compile(r"^(=+|-+)\s*?$", re.MULTILINE)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def headings(lines: Iterable[str]) -> Iterator[tuple[int, str]]:
|
|
99
|
+
fence_marker: str | None = None
|
|
100
|
+
heading_text: str | None = None
|
|
101
|
+
|
|
102
|
+
for line in lines:
|
|
103
|
+
# fenced code blocks
|
|
104
|
+
fence_match = _FENCED_CODE_REGEXP.match(line)
|
|
105
|
+
if fence_match:
|
|
106
|
+
marker = fence_match.group()
|
|
107
|
+
if fence_marker is None:
|
|
108
|
+
fence_marker = marker
|
|
109
|
+
elif marker == fence_marker:
|
|
110
|
+
fence_marker = None
|
|
111
|
+
heading_text = None
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if fence_marker is not None:
|
|
115
|
+
heading_text = None
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# ATX headings
|
|
119
|
+
atx = _ATX_HEADING_REGEXP.match(line)
|
|
120
|
+
if atx is not None:
|
|
121
|
+
level = len(atx.group(1))
|
|
122
|
+
title = atx.group(2).rstrip().rstrip("#").rstrip() # remove decorative text: `## Section 1.2 ##`
|
|
123
|
+
yield level, title
|
|
124
|
+
|
|
125
|
+
heading_text = None
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Setext headings
|
|
129
|
+
setext = _SETEXT_HEADING_REGEXP.match(line)
|
|
130
|
+
if setext is not None and heading_text is not None:
|
|
131
|
+
match setext.group(1)[0:1]:
|
|
132
|
+
case "=":
|
|
133
|
+
level = 1
|
|
134
|
+
case "-":
|
|
135
|
+
level = 2
|
|
136
|
+
case _:
|
|
137
|
+
level = 0
|
|
138
|
+
yield level, heading_text.rstrip()
|
|
139
|
+
|
|
140
|
+
heading_text = None
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# candidate for Setext title
|
|
144
|
+
heading_text = line
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def unique_title(content: str) -> str | None:
|
|
148
|
+
"""
|
|
149
|
+
Returns a proposed document title.
|
|
150
|
+
|
|
151
|
+
The proposed title is text of the top-level heading if and only if that heading is unique.
|
|
152
|
+
|
|
153
|
+
:returns: Title text, or `None` if no title can be inferred.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
builder = TableOfContentsBuilder()
|
|
157
|
+
for heading in headings(content.splitlines(keepends=True)): # spellchecker:disable-line
|
|
158
|
+
level, text = heading
|
|
159
|
+
builder.add(level, text)
|
|
160
|
+
return builder.get_title()
|
md2conf/xml.py
CHANGED
|
@@ -106,6 +106,51 @@ def element_to_text(node: ElementType) -> str:
|
|
|
106
106
|
return "".join(node.itertext()).strip()
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def remove_element(child: ElementType) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Removes a child element, taking care of its tail text.
|
|
112
|
+
|
|
113
|
+
This function may be unsafe when called in the body of a loop of a live collection iterator, i.e. use
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
for child in list(node): ...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
instead of
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
for child in node: ...
|
|
123
|
+
```
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
parent = child.getparent()
|
|
127
|
+
if parent is None:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# preserve any text that comes after the heading (tail text)
|
|
131
|
+
tail = child.tail
|
|
132
|
+
|
|
133
|
+
# if there was tail text, attach it to the previous sibling's tail or to the parent's text if this was the first child
|
|
134
|
+
if tail:
|
|
135
|
+
index = parent.index(child)
|
|
136
|
+
if index > 0:
|
|
137
|
+
# append to previous sibling's tail
|
|
138
|
+
prev_sibling = parent[index - 1]
|
|
139
|
+
if prev_sibling.tail:
|
|
140
|
+
prev_sibling.tail += tail
|
|
141
|
+
else:
|
|
142
|
+
prev_sibling.tail = tail
|
|
143
|
+
else:
|
|
144
|
+
# no previous sibling, append to parent's text
|
|
145
|
+
if parent.text:
|
|
146
|
+
parent.text += tail
|
|
147
|
+
else:
|
|
148
|
+
parent.text = tail
|
|
149
|
+
|
|
150
|
+
# remove the element
|
|
151
|
+
parent.remove(child)
|
|
152
|
+
|
|
153
|
+
|
|
109
154
|
def unwrap_substitute(name: str, root: ElementType) -> None:
|
|
110
155
|
"""
|
|
111
156
|
Substitutes all occurrences of an element with its contents.
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
markdown_to_confluence-0.5.3.dist-info/licenses/LICENSE,sha256=SEEBf2BMI1LUHnDvyHnV6L12A6zTAOQcsyMvaawAXWo,1077
|
|
2
|
-
md2conf/__init__.py,sha256=CqQHkYFE1AAs4GhMm1nConbt3V7wksgDXzqm5mg6F6I,402
|
|
3
|
-
md2conf/__main__.py,sha256=Wxz6Rx1Sx_J0FbaCY2HpByHqTMbxDEs1v5VzfdQh32U,13891
|
|
4
|
-
md2conf/api.py,sha256=GVPElSP9qILA5IaEvtKaxjsoRQSsGMMoRhCxjjzLadM,42703
|
|
5
|
-
md2conf/attachment.py,sha256=Nc3qGDENWBnsI6OVwMLXnk0EyEITpvov9MluDFD90ZI,1689
|
|
6
|
-
md2conf/coalesce.py,sha256=YHnqFwow5wCj6OQ3oosig01D2lxWusAScMF4HAUO2-g,1305
|
|
7
|
-
md2conf/collection.py,sha256=ukN74VCa4HaGSh6tLXpLd0j_UNPywcnKI0X7usgdSCo,824
|
|
8
|
-
md2conf/compatibility.py,sha256=4ZNN6VLqxSbI1kowdsPproGZqwxBISys4Z22vBfe6Z8,687
|
|
9
|
-
md2conf/converter.py,sha256=C6cROI8bUswyNZ1jSC0lf_9J-S6ojrXLbrkSDALuKR8,63065
|
|
10
|
-
md2conf/csf.py,sha256=6H9G-5cZyyWMJr0tFskPNiWdQ2Ehq-V8EhlvvxhukWY,6582
|
|
11
|
-
md2conf/emoticon.py,sha256=0g4rkx3d58xU4nnLak5ms7i0FSDnq0WJrLVFRgGyLC8,542
|
|
12
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
13
|
-
md2conf/environment.py,sha256=TfNEz3Pyw9qe7f8i7e_kph16c09fhZ4cLNZZzIjmI18,3892
|
|
14
|
-
md2conf/extension.py,sha256=5d2CtfULwiqlqwpOqbvlVu2KQ-kVBkDDYAhnHXbq79I,2227
|
|
15
|
-
md2conf/external.py,sha256=ZRL0mCY02Gp4XfqRdbap2YdOihSpY2LA6tdGJBfC-FQ,1693
|
|
16
|
-
md2conf/formatting.py,sha256=ygL59VgpioX069axEX-7XjKs0sUjTfIZiBE5fWmITxc,4557
|
|
17
|
-
md2conf/frontmatter.py,sha256=iWtn_oXoLQxvCsdI3OXs1ylWGmB-gc7mMLpSGg113i4,1888
|
|
18
|
-
md2conf/image.py,sha256=udlnUY_xOmI-PJAqWqXBF7hAO3pj_VW7u5hXhBw4HcA,5260
|
|
19
|
-
md2conf/latex.py,sha256=haZKkUxSEcPj3fVmiIVZAwgszqNqGLk1GQ7i8KGHpo0,2226
|
|
20
|
-
md2conf/local.py,sha256=eY3WpY-lNzLZeAfxX1ACVEhuzz0HDYX_sNQogJfkqcM,3673
|
|
21
|
-
md2conf/markdown.py,sha256=4Km-AbQH04nDgPF0ijo-Ld7o8jTPXzENIMn7P1qIk0o,3148
|
|
22
|
-
md2conf/matcher.py,sha256=Xg4YSb87iPkCzhKuKytBut6NOkEab3IM-AjzXbwy64U,6774
|
|
23
|
-
md2conf/metadata.py,sha256=NOjbCIrwLgTIIeNgmo7w5JXuT-pxOXBGSg-irfdpokk,976
|
|
24
|
-
md2conf/options.py,sha256=MJka7h0ly6D5QQNXEi9ios1uQbqHQpyn07XciHV-mF8,4385
|
|
25
|
-
md2conf/png.py,sha256=wbo8tgm8Vxi56q5PLoTutZBU6qF73KJ6TJbs7G7LPG4,6166
|
|
26
|
-
md2conf/processor.py,sha256=yqRb1Njn8XWxx22byq5nrobeH9AOp55mHTVoBUJlN2A,10203
|
|
27
|
-
md2conf/publisher.py,sha256=6WfyZxWB59PrOro6wgkgXuFcnf7AqarlUfHo-Twzu9k,8509
|
|
28
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
29
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
-
md2conf/scanner.py,sha256=wvh5IlD2VnAwulqY6PUe33k1D8Uag0kqTs9pQ-YFq8g,3736
|
|
31
|
-
md2conf/serializer.py,sha256=W4_yLJfT3vLw0PUg88lpUEnvn64CjaX3ZaKgIrwcxfw,1786
|
|
32
|
-
md2conf/svg.py,sha256=b98hxpVL-yFojln8NunxQRJSabMdfvazo7FkFySjALY,11054
|
|
33
|
-
md2conf/text.py,sha256=cnYV_JQp_v91LbQHo3qvxcEuhIdaPjCjkmLOKINcNv4,1736
|
|
34
|
-
md2conf/toc.py,sha256=HCtnREEucmGfqzwq-Dk6Q4ZgxW5Z8nZk6lg8UfhcoI0,2413
|
|
35
|
-
md2conf/uri.py,sha256=my0deyR5SlppJrYCbXF1Zz94QA1JT-HTWe9pKw7AJ_A,1158
|
|
36
|
-
md2conf/xml.py,sha256=uaaUDs0hfluNX74dfkY_Dxu1KmeNDGogpGRGpUVEfE4,5526
|
|
37
|
-
md2conf/drawio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
-
md2conf/drawio/extension.py,sha256=-k0pAhIZXDjiKeHlTJl2MGWMYpG-PmXK91-_vBMQZnE,4389
|
|
39
|
-
md2conf/drawio/render.py,sha256=veSu5gjm5ggLnmaH7uvH9qNeOygBJpqhSKK_LJs0QTk,8581
|
|
40
|
-
md2conf/mermaid/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
-
md2conf/mermaid/config.py,sha256=5Dec2QcdB_GtnuXIW6nhJK8J5caduNZU1oz1mcmmb44,376
|
|
42
|
-
md2conf/mermaid/extension.py,sha256=1drXVM_KbS00dcjSCRru0wwbil4zq3aR81dHMhfe7zA,4021
|
|
43
|
-
md2conf/mermaid/render.py,sha256=cGq3hOemgsbh6btt7uJaRxzH4EmGis0K6_qNW5YrhIk,1834
|
|
44
|
-
md2conf/mermaid/scanner.py,sha256=oIpaNxiZBNcmggnjlyYGcIVOXcYQWjf1lEVdyIwE4xE,1379
|
|
45
|
-
md2conf/plantuml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
-
md2conf/plantuml/config.py,sha256=j0ONhkzmAPagh00ltamTKlVEvXa6R284We9pDxRy-5U,378
|
|
47
|
-
md2conf/plantuml/extension.py,sha256=-WEL6Ssa3Kz_7D_AQSnB1da0bylyYZivmRQwlOBfYcM,6281
|
|
48
|
-
md2conf/plantuml/render.py,sha256=fwwKJIv_q1Fm9Vd8af7OcEG-5MkojZ-fxrQgA33grE8,3818
|
|
49
|
-
md2conf/plantuml/scanner.py,sha256=Oso6VbHVuMaPMKMazQc_bf4hhOT5WeJN5WiVPM8peyM,1347
|
|
50
|
-
markdown_to_confluence-0.5.3.dist-info/METADATA,sha256=7r3TveKAaJPBk14SGcJHuCXuDYD0rt7KUK6a_A8p1gE,43775
|
|
51
|
-
markdown_to_confluence-0.5.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
52
|
-
markdown_to_confluence-0.5.3.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
53
|
-
markdown_to_confluence-0.5.3.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
54
|
-
markdown_to_confluence-0.5.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
55
|
-
markdown_to_confluence-0.5.3.dist-info/RECORD,,
|
{markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|