markdown-to-confluence 0.4.6__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.6.dist-info → markdown_to_confluence-0.4.7.dist-info}/METADATA +4 -1
- markdown_to_confluence-0.4.7.dist-info/RECORD +34 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +18 -1
- md2conf/api.py +37 -23
- md2conf/converter.py +246 -134
- md2conf/csf.py +9 -7
- md2conf/domain.py +4 -0
- md2conf/drawio.py +5 -3
- md2conf/latex.py +2 -2
- md2conf/matcher.py +1 -3
- md2conf/processor.py +1 -1
- md2conf/publisher.py +1 -1
- md2conf/scanner.py +12 -4
- md2conf/xml.py +8 -5
- markdown_to_confluence-0.4.6.dist-info/RECORD +0 -34
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.7.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.7.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.7.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.7.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.7.dist-info}/zip-safe +0 -0
md2conf/converter.py
CHANGED
|
@@ -39,6 +39,8 @@ from .toc import TableOfContentsBuilder
|
|
|
39
39
|
from .uri import is_absolute_url, to_uuid_urn
|
|
40
40
|
from .xml import element_to_text
|
|
41
41
|
|
|
42
|
+
ElementType = ET._Element # pyright: ignore [reportPrivateUsage]
|
|
43
|
+
|
|
42
44
|
|
|
43
45
|
def get_volatile_attributes() -> list[str]:
|
|
44
46
|
"Returns a list of volatile attributes that frequently change as a Confluence storage format XHTML document is updated."
|
|
@@ -180,7 +182,7 @@ _LANGUAGES = {
|
|
|
180
182
|
|
|
181
183
|
|
|
182
184
|
class NodeVisitor(ABC):
|
|
183
|
-
def visit(self, node:
|
|
185
|
+
def visit(self, node: ElementType) -> None:
|
|
184
186
|
"Recursively visits all descendants of this node."
|
|
185
187
|
|
|
186
188
|
if len(node) < 1:
|
|
@@ -200,7 +202,7 @@ class NodeVisitor(ABC):
|
|
|
200
202
|
self.visit(source)
|
|
201
203
|
|
|
202
204
|
@abstractmethod
|
|
203
|
-
def transform(self, child:
|
|
205
|
+
def transform(self, child: ElementType) -> Optional[ElementType]: ...
|
|
204
206
|
|
|
205
207
|
|
|
206
208
|
def title_to_identifier(title: str) -> str:
|
|
@@ -212,7 +214,7 @@ def title_to_identifier(title: str) -> str:
|
|
|
212
214
|
return s
|
|
213
215
|
|
|
214
216
|
|
|
215
|
-
def element_text_starts_with_any(node:
|
|
217
|
+
def element_text_starts_with_any(node: ElementType, prefixes: list[str]) -> bool:
|
|
216
218
|
"True if the text contained in an element starts with any of the specified prefix strings."
|
|
217
219
|
|
|
218
220
|
if node.text is None:
|
|
@@ -220,7 +222,7 @@ def element_text_starts_with_any(node: ET._Element, prefixes: list[str]) -> bool
|
|
|
220
222
|
return starts_with_any(node.text, prefixes)
|
|
221
223
|
|
|
222
224
|
|
|
223
|
-
def is_placeholder_for(node:
|
|
225
|
+
def is_placeholder_for(node: ElementType, name: str) -> bool:
|
|
224
226
|
"""
|
|
225
227
|
Identifies a Confluence widget placeholder, e.g. `[[_TOC_]]` or `[[_LISTING_]]`.
|
|
226
228
|
|
|
@@ -247,6 +249,15 @@ class FormattingContext(enum.Enum):
|
|
|
247
249
|
INLINE = "inline"
|
|
248
250
|
|
|
249
251
|
|
|
252
|
+
@enum.unique
|
|
253
|
+
class ImageAlignment(enum.Enum):
|
|
254
|
+
"Determines whether to align block-level images to center, left or right."
|
|
255
|
+
|
|
256
|
+
CENTER = "center"
|
|
257
|
+
LEFT = "left"
|
|
258
|
+
RIGHT = "right"
|
|
259
|
+
|
|
260
|
+
|
|
250
261
|
@dataclass
|
|
251
262
|
class ImageAttributes:
|
|
252
263
|
"""
|
|
@@ -258,6 +269,7 @@ class ImageAttributes:
|
|
|
258
269
|
:param alt: Alternate text.
|
|
259
270
|
:param title: Title text (a.k.a. image tooltip).
|
|
260
271
|
:param caption: Caption text (shown below figure).
|
|
272
|
+
:param alignment: Alignment for block-level images.
|
|
261
273
|
"""
|
|
262
274
|
|
|
263
275
|
context: FormattingContext
|
|
@@ -266,6 +278,7 @@ class ImageAttributes:
|
|
|
266
278
|
alt: Optional[str]
|
|
267
279
|
title: Optional[str]
|
|
268
280
|
caption: Optional[str]
|
|
281
|
+
alignment: ImageAlignment = ImageAlignment.CENTER
|
|
269
282
|
|
|
270
283
|
def __post_init__(self) -> None:
|
|
271
284
|
if self.caption is None and self.context is FormattingContext.BLOCK:
|
|
@@ -274,8 +287,16 @@ class ImageAttributes:
|
|
|
274
287
|
def as_dict(self) -> dict[str, str]:
|
|
275
288
|
attributes: dict[str, str] = {}
|
|
276
289
|
if self.context is FormattingContext.BLOCK:
|
|
277
|
-
|
|
278
|
-
|
|
290
|
+
if self.alignment is ImageAlignment.LEFT:
|
|
291
|
+
attributes[AC_ATTR("align")] = "left"
|
|
292
|
+
attributes[AC_ATTR("layout")] = "align-start"
|
|
293
|
+
elif self.alignment is ImageAlignment.RIGHT:
|
|
294
|
+
attributes[AC_ATTR("align")] = "right"
|
|
295
|
+
attributes[AC_ATTR("layout")] = "align-end"
|
|
296
|
+
else:
|
|
297
|
+
attributes[AC_ATTR("align")] = "center"
|
|
298
|
+
attributes[AC_ATTR("layout")] = "center"
|
|
299
|
+
|
|
279
300
|
if self.width is not None:
|
|
280
301
|
attributes[AC_ATTR("original-width")] = str(self.width)
|
|
281
302
|
if self.height is not None:
|
|
@@ -311,8 +332,12 @@ class ImageAttributes:
|
|
|
311
332
|
raise NotImplementedError("match not exhaustive for enumeration")
|
|
312
333
|
|
|
313
334
|
|
|
314
|
-
ImageAttributes.EMPTY_BLOCK = ImageAttributes(
|
|
315
|
-
|
|
335
|
+
ImageAttributes.EMPTY_BLOCK = ImageAttributes(
|
|
336
|
+
FormattingContext.BLOCK, width=None, height=None, alt=None, title=None, caption=None, alignment=ImageAlignment.CENTER
|
|
337
|
+
)
|
|
338
|
+
ImageAttributes.EMPTY_INLINE = ImageAttributes(
|
|
339
|
+
FormattingContext.INLINE, width=None, height=None, alt=None, title=None, caption=None, alignment=ImageAlignment.CENTER
|
|
340
|
+
)
|
|
316
341
|
|
|
317
342
|
|
|
318
343
|
@dataclass
|
|
@@ -330,6 +355,8 @@ class ConfluenceConverterOptions:
|
|
|
330
355
|
:param render_latex: Whether to pre-render LaTeX formulas into PNG/SVG images.
|
|
331
356
|
:param diagram_output_format: Target image format for diagrams.
|
|
332
357
|
:param webui_links: When true, convert relative URLs to Confluence Web UI links.
|
|
358
|
+
:param alignment: Alignment for block-level images and formulas.
|
|
359
|
+
:param use_panel: Whether to transform admonitions and alerts into a Confluence custom panel.
|
|
333
360
|
"""
|
|
334
361
|
|
|
335
362
|
ignore_invalid_url: bool = False
|
|
@@ -340,6 +367,8 @@ class ConfluenceConverterOptions:
|
|
|
340
367
|
render_latex: bool = False
|
|
341
368
|
diagram_output_format: Literal["png", "svg"] = "png"
|
|
342
369
|
webui_links: bool = False
|
|
370
|
+
alignment: Literal["center", "left", "right"] = "center"
|
|
371
|
+
use_panel: bool = False
|
|
343
372
|
|
|
344
373
|
|
|
345
374
|
@dataclass
|
|
@@ -354,6 +383,43 @@ class EmbeddedFileData:
|
|
|
354
383
|
description: Optional[str] = None
|
|
355
384
|
|
|
356
385
|
|
|
386
|
+
@dataclass
|
|
387
|
+
class ConfluencePanel:
|
|
388
|
+
emoji: str
|
|
389
|
+
emoji_shortname: str
|
|
390
|
+
background_color: str
|
|
391
|
+
from_class: ClassVar[dict[str, "ConfluencePanel"]]
|
|
392
|
+
|
|
393
|
+
def __init__(self, emoji: str, emoji_shortname: str, background_color: str) -> None:
|
|
394
|
+
self.emoji = emoji
|
|
395
|
+
self.emoji_shortname = emoji_shortname
|
|
396
|
+
self.background_color = background_color
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def emoji_unicode(self) -> str:
|
|
400
|
+
return "-".join(f"{ord(ch):x}" for ch in self.emoji)
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def emoji_html(self) -> str:
|
|
404
|
+
return "".join(f"&#{ord(ch)};" for ch in self.emoji)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
ConfluencePanel.from_class = {
|
|
408
|
+
"attention": ConfluencePanel("❗", "exclamation", "#F9F9F9"), # rST admonition
|
|
409
|
+
"caution": ConfluencePanel("❌", "x", "#FFEBE9"),
|
|
410
|
+
"danger": ConfluencePanel("☠️", "skull_crossbones", "#FFE5E5"), # rST admonition
|
|
411
|
+
"disclaimer": ConfluencePanel("❗", "exclamation", "#F9F9F9"), # GitLab
|
|
412
|
+
"error": ConfluencePanel("❌", "x", "#FFEBE9"), # rST admonition
|
|
413
|
+
"flag": ConfluencePanel("🚩", "triangular_flag_on_post", "#FDECEA"), # GitLab
|
|
414
|
+
"hint": ConfluencePanel("💡", "bulb", "#DAFBE1"), # rST admonition
|
|
415
|
+
"info": ConfluencePanel("ℹ️", "information_source", "#DDF4FF"),
|
|
416
|
+
"note": ConfluencePanel("📝", "pencil", "#DDF4FF"),
|
|
417
|
+
"tip": ConfluencePanel("💡", "bulb", "#DAFBE1"),
|
|
418
|
+
"important": ConfluencePanel("❗", "exclamation", "#FBEFFF"),
|
|
419
|
+
"warning": ConfluencePanel("⚠️", "warning", "#FFF8C5"),
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
357
423
|
class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
358
424
|
"Transforms a plain HTML tree into Confluence Storage Format."
|
|
359
425
|
|
|
@@ -392,7 +458,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
392
458
|
self.site_metadata = site_metadata
|
|
393
459
|
self.page_metadata = page_metadata
|
|
394
460
|
|
|
395
|
-
def _transform_heading(self, heading:
|
|
461
|
+
def _transform_heading(self, heading: ElementType) -> None:
|
|
396
462
|
"""
|
|
397
463
|
Adds anchors to headings in the same document (if *heading anchors* is enabled).
|
|
398
464
|
|
|
@@ -428,7 +494,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
428
494
|
anchor.tail = heading.text
|
|
429
495
|
heading.text = None
|
|
430
496
|
|
|
431
|
-
def _anchor_warn_or_raise(self, anchor:
|
|
497
|
+
def _anchor_warn_or_raise(self, anchor: ElementType, msg: str) -> None:
|
|
432
498
|
"Emit a warning or raise an exception when a path points to a resource that doesn't exist or is outside of the permitted hierarchy."
|
|
433
499
|
|
|
434
500
|
if self.options.ignore_invalid_url:
|
|
@@ -440,7 +506,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
440
506
|
else:
|
|
441
507
|
raise DocumentError(msg)
|
|
442
508
|
|
|
443
|
-
def _transform_link(self, anchor:
|
|
509
|
+
def _transform_link(self, anchor: ElementType) -> Optional[ElementType]:
|
|
444
510
|
"""
|
|
445
511
|
Transforms links (HTML anchor `<a>`).
|
|
446
512
|
|
|
@@ -493,7 +559,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
493
559
|
else:
|
|
494
560
|
return self._transform_attachment_link(anchor, absolute_path)
|
|
495
561
|
|
|
496
|
-
def _transform_page_link(self, anchor:
|
|
562
|
+
def _transform_page_link(self, anchor: ElementType, relative_url: ParseResult, absolute_path: Path) -> Optional[ElementType]:
|
|
497
563
|
"""
|
|
498
564
|
Transforms links to other Markdown documents (Confluence pages).
|
|
499
565
|
"""
|
|
@@ -530,7 +596,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
530
596
|
anchor.set("href", transformed_url.geturl())
|
|
531
597
|
return None
|
|
532
598
|
|
|
533
|
-
def _transform_attachment_link(self, anchor:
|
|
599
|
+
def _transform_attachment_link(self, anchor: ElementType, absolute_path: Path) -> Optional[ElementType]:
|
|
534
600
|
"""
|
|
535
601
|
Transforms links to document binaries such as PDF, DOCX or XLSX.
|
|
536
602
|
"""
|
|
@@ -552,7 +618,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
552
618
|
)
|
|
553
619
|
return link_wrapper
|
|
554
620
|
|
|
555
|
-
def _transform_status(self, color: str, caption: str) ->
|
|
621
|
+
def _transform_status(self, color: str, caption: str) -> ElementType:
|
|
556
622
|
macro_id = str(uuid.uuid4())
|
|
557
623
|
attributes = {
|
|
558
624
|
AC_ATTR("name"): "status",
|
|
@@ -585,7 +651,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
585
651
|
),
|
|
586
652
|
)
|
|
587
653
|
|
|
588
|
-
def _transform_image(self, context: FormattingContext, image:
|
|
654
|
+
def _transform_image(self, context: FormattingContext, image: ElementType) -> ElementType:
|
|
589
655
|
"Inserts an attached or external image."
|
|
590
656
|
|
|
591
657
|
src = image.get("src")
|
|
@@ -601,7 +667,9 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
601
667
|
height = image.get("height")
|
|
602
668
|
pixel_width = int(width) if width is not None and width.isdecimal() else None
|
|
603
669
|
pixel_height = int(height) if height is not None and height.isdecimal() else None
|
|
604
|
-
attrs = ImageAttributes(
|
|
670
|
+
attrs = ImageAttributes(
|
|
671
|
+
context, width=pixel_width, height=pixel_height, alt=alt, title=title, caption=None, alignment=ImageAlignment(self.options.alignment)
|
|
672
|
+
)
|
|
605
673
|
|
|
606
674
|
if is_absolute_url(src):
|
|
607
675
|
return self._transform_external_image(src, attrs)
|
|
@@ -621,10 +689,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
621
689
|
else:
|
|
622
690
|
return self._transform_attached_image(absolute_path, attrs)
|
|
623
691
|
|
|
624
|
-
def _transform_external_image(self, url: str, attrs: ImageAttributes) ->
|
|
692
|
+
def _transform_external_image(self, url: str, attrs: ImageAttributes) -> ElementType:
|
|
625
693
|
"Emits Confluence Storage Format XHTML for an external image."
|
|
626
694
|
|
|
627
|
-
elements: list[
|
|
695
|
+
elements: list[ElementType] = []
|
|
628
696
|
elements.append(
|
|
629
697
|
RI_ELEM(
|
|
630
698
|
"url",
|
|
@@ -661,7 +729,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
661
729
|
|
|
662
730
|
return absolute_path
|
|
663
731
|
|
|
664
|
-
def _transform_attached_image(self, absolute_path: Path, attrs: ImageAttributes) ->
|
|
732
|
+
def _transform_attached_image(self, absolute_path: Path, attrs: ImageAttributes) -> ElementType:
|
|
665
733
|
"Emits Confluence Storage Format XHTML for an attached raster or vector image."
|
|
666
734
|
|
|
667
735
|
if self.options.prefer_raster and absolute_path.suffix == ".svg":
|
|
@@ -674,7 +742,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
674
742
|
image_name = attachment_name(path_relative_to(absolute_path, self.base_dir))
|
|
675
743
|
return self._create_attached_image(image_name, attrs)
|
|
676
744
|
|
|
677
|
-
def _transform_drawio(self, absolute_path: Path, attrs: ImageAttributes) ->
|
|
745
|
+
def _transform_drawio(self, absolute_path: Path, attrs: ImageAttributes) -> ElementType:
|
|
678
746
|
"Emits Confluence Storage Format XHTML for a draw.io diagram."
|
|
679
747
|
|
|
680
748
|
if not absolute_path.name.endswith(".drawio.xml") and not absolute_path.name.endswith(".drawio"):
|
|
@@ -691,7 +759,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
691
759
|
image_filename = attachment_name(relative_path)
|
|
692
760
|
return self._create_drawio(image_filename, attrs)
|
|
693
761
|
|
|
694
|
-
def _transform_drawio_image(self, absolute_path: Path, attrs: ImageAttributes) ->
|
|
762
|
+
def _transform_drawio_image(self, absolute_path: Path, attrs: ImageAttributes) -> ElementType:
|
|
695
763
|
"Emits Confluence Storage Format XHTML for a draw.io diagram embedded in a PNG or SVG image."
|
|
696
764
|
|
|
697
765
|
if not absolute_path.name.endswith(".drawio.png") and not absolute_path.name.endswith(".drawio.svg"):
|
|
@@ -707,10 +775,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
707
775
|
|
|
708
776
|
return self._create_drawio(image_filename, attrs)
|
|
709
777
|
|
|
710
|
-
def _create_attached_image(self, image_name: str, attrs: ImageAttributes) ->
|
|
778
|
+
def _create_attached_image(self, image_name: str, attrs: ImageAttributes) -> ElementType:
|
|
711
779
|
"An image embedded into the page, linking to an attachment."
|
|
712
780
|
|
|
713
|
-
elements: list[
|
|
781
|
+
elements: list[ElementType] = []
|
|
714
782
|
elements.append(
|
|
715
783
|
RI_ELEM(
|
|
716
784
|
"attachment",
|
|
@@ -723,10 +791,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
723
791
|
|
|
724
792
|
return AC_ELEM("image", attrs.as_dict(), *elements)
|
|
725
793
|
|
|
726
|
-
def _create_drawio(self, filename: str, attrs: ImageAttributes) ->
|
|
794
|
+
def _create_drawio(self, filename: str, attrs: ImageAttributes) -> ElementType:
|
|
727
795
|
"A draw.io diagram embedded into the page, linking to an attachment."
|
|
728
796
|
|
|
729
|
-
parameters: list[
|
|
797
|
+
parameters: list[ElementType] = [
|
|
730
798
|
AC_ELEM(
|
|
731
799
|
"parameter",
|
|
732
800
|
{AC_ATTR("name"): "diagramName"},
|
|
@@ -764,7 +832,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
764
832
|
*parameters,
|
|
765
833
|
)
|
|
766
834
|
|
|
767
|
-
def _create_missing(self, path: Path, attrs: ImageAttributes) ->
|
|
835
|
+
def _create_missing(self, path: Path, attrs: ImageAttributes) -> ElementType:
|
|
768
836
|
"A warning panel for a missing image."
|
|
769
837
|
|
|
770
838
|
if attrs.context is FormattingContext.BLOCK:
|
|
@@ -792,7 +860,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
792
860
|
else:
|
|
793
861
|
return HTML.span({"style": "color: rgb(255,86,48);"}, "❌ ", HTML.code(path.as_posix()))
|
|
794
862
|
|
|
795
|
-
def _transform_code_block(self, code:
|
|
863
|
+
def _transform_code_block(self, code: ElementType) -> ElementType:
|
|
796
864
|
"Transforms a code block."
|
|
797
865
|
|
|
798
866
|
if language_class := code.get("class"):
|
|
@@ -838,7 +906,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
838
906
|
LOGGER.warning("Failed to extract Mermaid properties: %s", ex)
|
|
839
907
|
return None
|
|
840
908
|
|
|
841
|
-
def _transform_external_mermaid(self, absolute_path: Path, attrs: ImageAttributes) ->
|
|
909
|
+
def _transform_external_mermaid(self, absolute_path: Path, attrs: ImageAttributes) -> ElementType:
|
|
842
910
|
"Emits Confluence Storage Format XHTML for a Mermaid diagram read from an external file."
|
|
843
911
|
|
|
844
912
|
if not absolute_path.name.endswith(".mmd") and not absolute_path.name.endswith(".mermaid"):
|
|
@@ -858,7 +926,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
858
926
|
mermaid_filename = attachment_name(relative_path)
|
|
859
927
|
return self._create_mermaid_embed(mermaid_filename)
|
|
860
928
|
|
|
861
|
-
def _transform_fenced_mermaid(self, content: str) ->
|
|
929
|
+
def _transform_fenced_mermaid(self, content: str) -> ElementType:
|
|
862
930
|
"Emits Confluence Storage Format XHTML for a Mermaid diagram defined in a fenced code block."
|
|
863
931
|
|
|
864
932
|
if self.options.render_mermaid:
|
|
@@ -875,7 +943,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
875
943
|
self.embedded_files[mermaid_filename] = EmbeddedFileData(mermaid_data)
|
|
876
944
|
return self._create_mermaid_embed(mermaid_filename)
|
|
877
945
|
|
|
878
|
-
def _create_mermaid_embed(self, filename: str) ->
|
|
946
|
+
def _create_mermaid_embed(self, filename: str) -> ElementType:
|
|
879
947
|
"A Mermaid diagram, linking to an attachment that captures the Mermaid source."
|
|
880
948
|
|
|
881
949
|
local_id = str(uuid.uuid4())
|
|
@@ -907,7 +975,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
907
975
|
AC_ELEM("parameter", {AC_ATTR("name"): "revision"}, "1"),
|
|
908
976
|
)
|
|
909
977
|
|
|
910
|
-
def _transform_toc(self, code:
|
|
978
|
+
def _transform_toc(self, code: ElementType) -> ElementType:
|
|
911
979
|
"Creates a table of contents, constructed from headings in the document."
|
|
912
980
|
|
|
913
981
|
return AC_ELEM(
|
|
@@ -921,7 +989,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
921
989
|
AC_ELEM("parameter", {AC_ATTR("name"): "style"}, "default"),
|
|
922
990
|
)
|
|
923
991
|
|
|
924
|
-
def _transform_listing(self, code:
|
|
992
|
+
def _transform_listing(self, code: ElementType) -> ElementType:
|
|
925
993
|
"Creates a list of child pages."
|
|
926
994
|
|
|
927
995
|
return AC_ELEM(
|
|
@@ -934,7 +1002,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
934
1002
|
AC_ELEM("parameter", {AC_ATTR("name"): "allChildren"}, "true"),
|
|
935
1003
|
)
|
|
936
1004
|
|
|
937
|
-
def _transform_admonition(self, elem:
|
|
1005
|
+
def _transform_admonition(self, elem: ElementType) -> ElementType:
|
|
938
1006
|
"""
|
|
939
1007
|
Creates an info, tip, note or warning panel from a Markdown admonition.
|
|
940
1008
|
|
|
@@ -947,45 +1015,51 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
947
1015
|
|
|
948
1016
|
# <div class="admonition note">
|
|
949
1017
|
class_list = elem.get("class", "").split(" ")
|
|
950
|
-
|
|
951
|
-
if
|
|
952
|
-
|
|
953
|
-
elif
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
class_name = "note"
|
|
957
|
-
elif "warning" in class_list:
|
|
958
|
-
class_name = "warning"
|
|
959
|
-
|
|
960
|
-
if class_name is None:
|
|
961
|
-
raise DocumentError(f"unsupported admonition label: {class_list}")
|
|
1018
|
+
class_list.remove("admonition")
|
|
1019
|
+
if len(class_list) > 1:
|
|
1020
|
+
raise DocumentError(f"too many admonition types: {class_list}")
|
|
1021
|
+
elif len(class_list) < 1:
|
|
1022
|
+
raise DocumentError("missing specific admonition type")
|
|
1023
|
+
admonition = class_list[0]
|
|
962
1024
|
|
|
963
1025
|
for e in elem:
|
|
964
1026
|
self.visit(e)
|
|
965
1027
|
|
|
966
1028
|
# <p class="admonition-title">Note</p>
|
|
967
1029
|
if "admonition-title" in elem[0].get("class", "").split(" "):
|
|
968
|
-
content = [
|
|
969
|
-
AC_ELEM(
|
|
970
|
-
"parameter",
|
|
971
|
-
{AC_ATTR("name"): "title"},
|
|
972
|
-
elem[0].text or "",
|
|
973
|
-
),
|
|
974
|
-
AC_ELEM("rich-text-body", {}, *list(elem[1:])),
|
|
975
|
-
]
|
|
1030
|
+
content = [HTML.p(HTML.strong(elem[0].text or "")), *list(elem[1:])]
|
|
976
1031
|
else:
|
|
977
|
-
content =
|
|
1032
|
+
content = list(elem)
|
|
978
1033
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1034
|
+
if self.options.use_panel:
|
|
1035
|
+
return self._transform_panel(content, admonition)
|
|
1036
|
+
else:
|
|
1037
|
+
admonition_to_csf = {
|
|
1038
|
+
"attention": "note",
|
|
1039
|
+
"caution": "warning",
|
|
1040
|
+
"danger": "warning",
|
|
1041
|
+
"error": "warning",
|
|
1042
|
+
"hint": "tip",
|
|
1043
|
+
"important": "note",
|
|
1044
|
+
"info": "info",
|
|
1045
|
+
"note": "info",
|
|
1046
|
+
"tip": "tip",
|
|
1047
|
+
"warning": "note",
|
|
1048
|
+
}
|
|
1049
|
+
class_name = admonition_to_csf.get(admonition)
|
|
1050
|
+
if class_name is None:
|
|
1051
|
+
raise DocumentError(f"unsupported admonition type: {admonition}")
|
|
987
1052
|
|
|
988
|
-
|
|
1053
|
+
return AC_ELEM(
|
|
1054
|
+
"structured-macro",
|
|
1055
|
+
{
|
|
1056
|
+
AC_ATTR("name"): class_name,
|
|
1057
|
+
AC_ATTR("schema-version"): "1",
|
|
1058
|
+
},
|
|
1059
|
+
AC_ELEM("rich-text-body", {}, *content),
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
def _transform_github_alert(self, blockquote: ElementType) -> ElementType:
|
|
989
1063
|
"""
|
|
990
1064
|
Creates a GitHub-style panel, normally triggered with a block-quote starting with a capitalized string such as `[!TIP]`.
|
|
991
1065
|
"""
|
|
@@ -997,30 +1071,29 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
997
1071
|
if content.text is None:
|
|
998
1072
|
raise DocumentError("empty content")
|
|
999
1073
|
|
|
1000
|
-
class_name: Optional[str] = None
|
|
1001
|
-
skip = 0
|
|
1002
|
-
|
|
1003
1074
|
pattern = re.compile(r"^\[!([A-Z]+)\]\s*")
|
|
1004
1075
|
match = pattern.match(content.text)
|
|
1005
|
-
if match:
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1076
|
+
if not match:
|
|
1077
|
+
raise DocumentError("not a GitHub alert")
|
|
1078
|
+
|
|
1079
|
+
# remove alert indicator prefix
|
|
1080
|
+
content.text = content.text[len(match.group(0)) :]
|
|
1081
|
+
|
|
1082
|
+
for e in blockquote:
|
|
1083
|
+
self.visit(e)
|
|
1084
|
+
|
|
1085
|
+
alert = match.group(1)
|
|
1086
|
+
if self.options.use_panel:
|
|
1087
|
+
return self._transform_panel(list(blockquote), alert.lower())
|
|
1088
|
+
else:
|
|
1089
|
+
alert_to_csf = {"NOTE": "info", "TIP": "tip", "IMPORTANT": "note", "WARNING": "note", "CAUTION": "warning"}
|
|
1090
|
+
class_name = alert_to_csf.get(alert)
|
|
1091
|
+
if class_name is None:
|
|
1019
1092
|
raise DocumentError(f"unsupported GitHub alert: {alert}")
|
|
1020
1093
|
|
|
1021
|
-
|
|
1094
|
+
return self._transform_alert(blockquote, class_name)
|
|
1022
1095
|
|
|
1023
|
-
def _transform_gitlab_alert(self, blockquote:
|
|
1096
|
+
def _transform_gitlab_alert(self, blockquote: ElementType) -> ElementType:
|
|
1024
1097
|
"""
|
|
1025
1098
|
Creates a classic GitLab-style panel.
|
|
1026
1099
|
|
|
@@ -1035,58 +1108,91 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1035
1108
|
if content.text is None:
|
|
1036
1109
|
raise DocumentError("empty content")
|
|
1037
1110
|
|
|
1038
|
-
class_name: Optional[str] = None
|
|
1039
|
-
skip = 0
|
|
1040
|
-
|
|
1041
1111
|
pattern = re.compile(r"^(FLAG|NOTE|WARNING|DISCLAIMER):\s*")
|
|
1042
1112
|
match = pattern.match(content.text)
|
|
1043
|
-
if match:
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1113
|
+
if not match:
|
|
1114
|
+
raise DocumentError("not a GitLab alert")
|
|
1115
|
+
|
|
1116
|
+
# remove alert indicator prefix
|
|
1117
|
+
content.text = content.text[len(match.group(0)) :]
|
|
1118
|
+
|
|
1119
|
+
for e in blockquote:
|
|
1120
|
+
self.visit(e)
|
|
1121
|
+
|
|
1122
|
+
alert = match.group(1)
|
|
1123
|
+
if self.options.use_panel:
|
|
1124
|
+
return self._transform_panel(list(blockquote), alert.lower())
|
|
1125
|
+
else:
|
|
1126
|
+
alert_to_csf = {"FLAG": "note", "NOTE": "info", "WARNING": "note", "DISCLAIMER": "info"}
|
|
1127
|
+
class_name = alert_to_csf.get(alert)
|
|
1128
|
+
if class_name is None:
|
|
1055
1129
|
raise DocumentError(f"unsupported GitLab alert: {alert}")
|
|
1056
1130
|
|
|
1057
|
-
|
|
1131
|
+
return self._transform_alert(blockquote, class_name)
|
|
1058
1132
|
|
|
1059
|
-
def _transform_alert(self, blockquote:
|
|
1133
|
+
def _transform_alert(self, blockquote: ElementType, class_name: str) -> ElementType:
|
|
1060
1134
|
"""
|
|
1061
|
-
Creates an info
|
|
1135
|
+
Creates an `info`, `tip`, `note` or `warning` panel from a GitHub or GitLab alert.
|
|
1136
|
+
|
|
1137
|
+
Transforms GitHub alert or GitLab alert syntax into one of the Confluence structured macros `info`, `tip`, `note`, or `warning`.
|
|
1138
|
+
|
|
1139
|
+
Confusingly, these structured macros have completely different alternate names on the UI, namely: *Info*, *Success*, *Warning* and *Error* (in this
|
|
1140
|
+
order). In other words, to get what is shown as *Error* on the UI, you have to pass `warning` in CSF, and to get *Success*, you have to pass `tip`.
|
|
1062
1141
|
|
|
1063
|
-
|
|
1142
|
+
Confluence UI also has an additional panel type called *Note*. *Note* is not a structured macro but corresponds to a different element tree, wrapped in
|
|
1143
|
+
an element `ac:adf-extension`:
|
|
1144
|
+
|
|
1145
|
+
```
|
|
1146
|
+
<ac:adf-node type="panel">
|
|
1147
|
+
<ac:adf-attribute key="panel-type">note</ac:adf-attribute>
|
|
1148
|
+
<ac:adf-content>
|
|
1149
|
+
<p><strong>A note</strong></p>
|
|
1150
|
+
<p>This is a panel showing a note.</p>
|
|
1151
|
+
</ac:adf-content>
|
|
1152
|
+
</ac:adf-node>
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
As of today, *md2conf* does not generate `ac:adf-extension` output, including *Note* and *Custom panel* (which shows an emoji selected by the user).
|
|
1156
|
+
|
|
1157
|
+
:param blockquote: HTML element tree to transform to Confluence Storage Format (CSF).
|
|
1158
|
+
:param class_name: Corresponds to `name` attribute for CSF `structured-macro`.
|
|
1064
1159
|
|
|
1065
1160
|
:see: https://docs.github.com/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
|
|
1066
1161
|
:see: https://docs.gitlab.com/ee/development/documentation/styleguide/#alert-boxes
|
|
1067
1162
|
"""
|
|
1068
1163
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1164
|
+
return AC_ELEM(
|
|
1165
|
+
"structured-macro",
|
|
1166
|
+
{
|
|
1167
|
+
AC_ATTR("name"): class_name,
|
|
1168
|
+
AC_ATTR("schema-version"): "1",
|
|
1169
|
+
},
|
|
1170
|
+
AC_ELEM("rich-text-body", {}, *list(blockquote)),
|
|
1171
|
+
)
|
|
1072
1172
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1173
|
+
def _transform_panel(self, content: list[ElementType], class_name: str) -> ElementType:
|
|
1174
|
+
"Transforms a blockquote into a themed panel."
|
|
1075
1175
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1176
|
+
panel = ConfluencePanel.from_class.get(class_name)
|
|
1177
|
+
if panel is None:
|
|
1178
|
+
raise DocumentError(f"unsupported panel class: {class_name}")
|
|
1078
1179
|
|
|
1079
|
-
|
|
1180
|
+
macro_id = str(uuid.uuid4())
|
|
1080
1181
|
return AC_ELEM(
|
|
1081
1182
|
"structured-macro",
|
|
1082
1183
|
{
|
|
1083
|
-
AC_ATTR("name"):
|
|
1184
|
+
AC_ATTR("name"): "panel",
|
|
1084
1185
|
AC_ATTR("schema-version"): "1",
|
|
1186
|
+
AC_ATTR("macro-id"): macro_id,
|
|
1085
1187
|
},
|
|
1086
|
-
AC_ELEM("
|
|
1188
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "panelIcon"}, f":{panel.emoji_shortname}:"),
|
|
1189
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "panelIconId"}, panel.emoji_unicode),
|
|
1190
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "panelIconText"}, panel.emoji),
|
|
1191
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "bgColor"}, panel.background_color),
|
|
1192
|
+
AC_ELEM("rich-text-body", {}, *content),
|
|
1087
1193
|
)
|
|
1088
1194
|
|
|
1089
|
-
def _transform_collapsed(self, details:
|
|
1195
|
+
def _transform_collapsed(self, details: ElementType) -> ElementType:
|
|
1090
1196
|
"""
|
|
1091
1197
|
Creates a collapsed section.
|
|
1092
1198
|
|
|
@@ -1135,7 +1241,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1135
1241
|
AC_ELEM("rich-text-body", {}, *list(details)),
|
|
1136
1242
|
)
|
|
1137
1243
|
|
|
1138
|
-
def _transform_emoji(self, elem:
|
|
1244
|
+
def _transform_emoji(self, elem: ElementType) -> ElementType:
|
|
1139
1245
|
"""
|
|
1140
1246
|
Inserts an inline emoji character.
|
|
1141
1247
|
"""
|
|
@@ -1159,7 +1265,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1159
1265
|
},
|
|
1160
1266
|
)
|
|
1161
1267
|
|
|
1162
|
-
def _transform_mark(self, mark:
|
|
1268
|
+
def _transform_mark(self, mark: ElementType) -> ElementType:
|
|
1163
1269
|
"""
|
|
1164
1270
|
Adds inline highlighting to text.
|
|
1165
1271
|
"""
|
|
@@ -1174,7 +1280,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1174
1280
|
span.text = mark.text
|
|
1175
1281
|
return span
|
|
1176
1282
|
|
|
1177
|
-
def _transform_latex(self, elem:
|
|
1283
|
+
def _transform_latex(self, elem: ElementType, context: FormattingContext) -> ElementType:
|
|
1178
1284
|
"""
|
|
1179
1285
|
Creates an image rendering of a LaTeX formula with Matplotlib.
|
|
1180
1286
|
"""
|
|
@@ -1187,7 +1293,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1187
1293
|
if self.options.diagram_output_format == "png":
|
|
1188
1294
|
width, height = get_png_dimensions(data=image_data)
|
|
1189
1295
|
image_data = remove_png_chunks(["pHYs"], source_data=image_data)
|
|
1190
|
-
attrs = ImageAttributes(context, width, height, content, None, "")
|
|
1296
|
+
attrs = ImageAttributes(context, width=width, height=height, alt=content, title=None, caption="", alignment=ImageAlignment(self.options.alignment))
|
|
1191
1297
|
else:
|
|
1192
1298
|
attrs = ImageAttributes.empty(context)
|
|
1193
1299
|
|
|
@@ -1197,7 +1303,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1197
1303
|
image = self._create_attached_image(image_filename, attrs)
|
|
1198
1304
|
return image
|
|
1199
1305
|
|
|
1200
|
-
def _transform_inline_math(self, elem:
|
|
1306
|
+
def _transform_inline_math(self, elem: ElementType) -> ElementType:
|
|
1201
1307
|
"""
|
|
1202
1308
|
Creates an inline LaTeX formula using the Confluence extension "LaTeX Math for Confluence - Math Formula & Equations".
|
|
1203
1309
|
|
|
@@ -1228,11 +1334,11 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1228
1334
|
{AC_ATTR("name"): "body"},
|
|
1229
1335
|
content,
|
|
1230
1336
|
),
|
|
1231
|
-
AC_ELEM("parameter", {AC_ATTR("name"): "align"},
|
|
1337
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "align"}, self.options.alignment),
|
|
1232
1338
|
)
|
|
1233
1339
|
return macro
|
|
1234
1340
|
|
|
1235
|
-
def _transform_block_math(self, elem:
|
|
1341
|
+
def _transform_block_math(self, elem: ElementType) -> ElementType:
|
|
1236
1342
|
"""
|
|
1237
1343
|
Creates a block-level LaTeX formula using the Confluence extension "LaTeX Math for Confluence - Math Formula & Equations".
|
|
1238
1344
|
|
|
@@ -1265,10 +1371,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1265
1371
|
{AC_ATTR("name"): "body"},
|
|
1266
1372
|
content,
|
|
1267
1373
|
),
|
|
1268
|
-
AC_ELEM("parameter", {AC_ATTR("name"): "align"},
|
|
1374
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "align"}, self.options.alignment),
|
|
1269
1375
|
)
|
|
1270
1376
|
|
|
1271
|
-
def _transform_footnote_ref(self, elem:
|
|
1377
|
+
def _transform_footnote_ref(self, elem: ElementType) -> None:
|
|
1272
1378
|
"""
|
|
1273
1379
|
Transforms a footnote reference.
|
|
1274
1380
|
|
|
@@ -1325,7 +1431,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1325
1431
|
elem.append(ref_anchor)
|
|
1326
1432
|
elem.append(def_link)
|
|
1327
1433
|
|
|
1328
|
-
def _transform_footnote_def(self, elem:
|
|
1434
|
+
def _transform_footnote_def(self, elem: ElementType) -> None:
|
|
1329
1435
|
"""
|
|
1330
1436
|
Transforms the footnote definition block.
|
|
1331
1437
|
|
|
@@ -1399,7 +1505,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1399
1505
|
paragraph.text = None
|
|
1400
1506
|
paragraph.append(ref_link)
|
|
1401
1507
|
|
|
1402
|
-
def _transform_tasklist(self, elem:
|
|
1508
|
+
def _transform_tasklist(self, elem: ElementType) -> ElementType:
|
|
1403
1509
|
"""
|
|
1404
1510
|
Transforms a list of tasks into an action widget.
|
|
1405
1511
|
|
|
@@ -1415,7 +1521,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1415
1521
|
if not element_text_starts_with_any(item, ["[ ]", "[x]", "[X]"]):
|
|
1416
1522
|
raise DocumentError("expected: each `<li>` in a task list starting with [ ] or [x]")
|
|
1417
1523
|
|
|
1418
|
-
tasks: list[
|
|
1524
|
+
tasks: list[ElementType] = []
|
|
1419
1525
|
for index, item in enumerate(elem, start=1):
|
|
1420
1526
|
if item.text is None:
|
|
1421
1527
|
raise NotImplementedError("pre-condition check not exhaustive")
|
|
@@ -1444,7 +1550,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1444
1550
|
return AC_ELEM("task-list", {}, *tasks)
|
|
1445
1551
|
|
|
1446
1552
|
@override
|
|
1447
|
-
def transform(self, child:
|
|
1553
|
+
def transform(self, child: ElementType) -> Optional[ElementType]:
|
|
1448
1554
|
"""
|
|
1449
1555
|
Transforms an HTML element tree obtained from a Markdown document into a Confluence Storage Format element tree.
|
|
1450
1556
|
"""
|
|
@@ -1637,7 +1743,7 @@ class ConfluenceDocument:
|
|
|
1637
1743
|
embedded_files: dict[str, EmbeddedFileData]
|
|
1638
1744
|
|
|
1639
1745
|
options: ConfluenceDocumentOptions
|
|
1640
|
-
root:
|
|
1746
|
+
root: ElementType
|
|
1641
1747
|
|
|
1642
1748
|
@classmethod
|
|
1643
1749
|
def create(
|
|
@@ -1683,10 +1789,10 @@ class ConfluenceDocument:
|
|
|
1683
1789
|
lines.append(f"[STATUS-{color.upper()}]: {data_uri}")
|
|
1684
1790
|
lines.append(document.text)
|
|
1685
1791
|
|
|
1686
|
-
# convert to HTML
|
|
1792
|
+
# parse Markdown document and convert to HTML
|
|
1687
1793
|
html = markdown_to_html("\n".join(lines))
|
|
1688
1794
|
|
|
1689
|
-
#
|
|
1795
|
+
# modify HTML as necessary
|
|
1690
1796
|
if self.options.generated_by is not None:
|
|
1691
1797
|
generated_by = document.generated_by or self.options.generated_by
|
|
1692
1798
|
else:
|
|
@@ -1704,26 +1810,32 @@ class ConfluenceDocument:
|
|
|
1704
1810
|
else:
|
|
1705
1811
|
content = [html]
|
|
1706
1812
|
|
|
1813
|
+
# parse HTML into element tree
|
|
1707
1814
|
try:
|
|
1708
1815
|
self.root = elements_from_strings(content)
|
|
1709
1816
|
except ParseError as ex:
|
|
1710
1817
|
raise ConversionError(path) from ex
|
|
1711
1818
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
root_dir,
|
|
1716
|
-
site_metadata,
|
|
1717
|
-
page_metadata,
|
|
1819
|
+
# configure HTML-to-Confluence converter
|
|
1820
|
+
converter_options = ConfluenceConverterOptions(
|
|
1821
|
+
**{field.name: getattr(self.options, field.name) for field in dataclasses.fields(ConfluenceConverterOptions)}
|
|
1718
1822
|
)
|
|
1823
|
+
if document.alignment is not None:
|
|
1824
|
+
converter_options.alignment = document.alignment
|
|
1825
|
+
converter = ConfluenceStorageFormatConverter(converter_options, path, root_dir, site_metadata, page_metadata)
|
|
1826
|
+
|
|
1827
|
+
# execute HTML-to-Confluence converter
|
|
1719
1828
|
try:
|
|
1720
1829
|
converter.visit(self.root)
|
|
1721
1830
|
except DocumentError as ex:
|
|
1722
1831
|
raise ConversionError(path) from ex
|
|
1832
|
+
|
|
1833
|
+
# extract information discovered by converter
|
|
1723
1834
|
self.links = converter.links
|
|
1724
1835
|
self.images = converter.images
|
|
1725
1836
|
self.embedded_files = converter.embedded_files
|
|
1726
1837
|
|
|
1838
|
+
# assign global properties for document
|
|
1727
1839
|
self.title = document.title or converter.toc.get_title()
|
|
1728
1840
|
self.labels = document.tags
|
|
1729
1841
|
self.properties = document.properties
|