markdown-to-confluence 0.4.6__py3-none-any.whl → 0.4.8__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.8.dist-info}/METADATA +28 -11
- markdown_to_confluence-0.4.8.dist-info/RECORD +34 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +18 -1
- md2conf/api.py +37 -23
- md2conf/converter.py +255 -135
- 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.8.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.8.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.8.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.8.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.6.dist-info → markdown_to_confluence-0.4.8.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", "var(--ds-background-accent-gray-subtlest)"), # rST admonition
|
|
409
|
+
"caution": ConfluencePanel("❌", "x", "var(--ds-background-accent-orange-subtlest)"),
|
|
410
|
+
"danger": ConfluencePanel("☠️", "skull_crossbones", "var(--ds-background-accent-red-subtlest)"), # rST admonition
|
|
411
|
+
"disclaimer": ConfluencePanel("❗", "exclamation", "var(--ds-background-accent-gray-subtlest)"), # GitLab
|
|
412
|
+
"error": ConfluencePanel("❌", "x", "var(--ds-background-accent-red-subtlest)"), # rST admonition
|
|
413
|
+
"flag": ConfluencePanel("🚩", "triangular_flag_on_post", "var(--ds-background-accent-orange-subtlest"), # GitLab
|
|
414
|
+
"hint": ConfluencePanel("💡", "bulb", "var(--ds-background-accent-green-subtlest)"), # rST admonition
|
|
415
|
+
"info": ConfluencePanel("ℹ️", "information_source", "var(--ds-background-accent-blue-subtlest)"),
|
|
416
|
+
"note": ConfluencePanel("📝", "pencil", "var(--ds-background-accent-teal-subtlest)"),
|
|
417
|
+
"tip": ConfluencePanel("💡", "bulb", "var(--ds-background-accent-green-subtlest)"),
|
|
418
|
+
"important": ConfluencePanel("❗", "exclamation", "var(--ds-background-accent-purple-subtlest)"),
|
|
419
|
+
"warning": ConfluencePanel("⚠️", "warning", "var(--ds-background-accent-yellow-subtlest)"),
|
|
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"},
|
|
@@ -749,6 +817,14 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
749
817
|
str(attrs.height),
|
|
750
818
|
),
|
|
751
819
|
)
|
|
820
|
+
if attrs.alignment is ImageAlignment.CENTER:
|
|
821
|
+
parameters.append(
|
|
822
|
+
AC_ELEM(
|
|
823
|
+
"parameter",
|
|
824
|
+
{AC_ATTR("name"): "pCenter"},
|
|
825
|
+
str(1),
|
|
826
|
+
),
|
|
827
|
+
)
|
|
752
828
|
|
|
753
829
|
local_id = str(uuid.uuid4())
|
|
754
830
|
macro_id = str(uuid.uuid4())
|
|
@@ -764,7 +840,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
764
840
|
*parameters,
|
|
765
841
|
)
|
|
766
842
|
|
|
767
|
-
def _create_missing(self, path: Path, attrs: ImageAttributes) ->
|
|
843
|
+
def _create_missing(self, path: Path, attrs: ImageAttributes) -> ElementType:
|
|
768
844
|
"A warning panel for a missing image."
|
|
769
845
|
|
|
770
846
|
if attrs.context is FormattingContext.BLOCK:
|
|
@@ -792,7 +868,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
792
868
|
else:
|
|
793
869
|
return HTML.span({"style": "color: rgb(255,86,48);"}, "❌ ", HTML.code(path.as_posix()))
|
|
794
870
|
|
|
795
|
-
def _transform_code_block(self, code:
|
|
871
|
+
def _transform_code_block(self, code: ElementType) -> ElementType:
|
|
796
872
|
"Transforms a code block."
|
|
797
873
|
|
|
798
874
|
if language_class := code.get("class"):
|
|
@@ -838,7 +914,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
838
914
|
LOGGER.warning("Failed to extract Mermaid properties: %s", ex)
|
|
839
915
|
return None
|
|
840
916
|
|
|
841
|
-
def _transform_external_mermaid(self, absolute_path: Path, attrs: ImageAttributes) ->
|
|
917
|
+
def _transform_external_mermaid(self, absolute_path: Path, attrs: ImageAttributes) -> ElementType:
|
|
842
918
|
"Emits Confluence Storage Format XHTML for a Mermaid diagram read from an external file."
|
|
843
919
|
|
|
844
920
|
if not absolute_path.name.endswith(".mmd") and not absolute_path.name.endswith(".mermaid"):
|
|
@@ -858,7 +934,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
858
934
|
mermaid_filename = attachment_name(relative_path)
|
|
859
935
|
return self._create_mermaid_embed(mermaid_filename)
|
|
860
936
|
|
|
861
|
-
def _transform_fenced_mermaid(self, content: str) ->
|
|
937
|
+
def _transform_fenced_mermaid(self, content: str) -> ElementType:
|
|
862
938
|
"Emits Confluence Storage Format XHTML for a Mermaid diagram defined in a fenced code block."
|
|
863
939
|
|
|
864
940
|
if self.options.render_mermaid:
|
|
@@ -875,7 +951,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
875
951
|
self.embedded_files[mermaid_filename] = EmbeddedFileData(mermaid_data)
|
|
876
952
|
return self._create_mermaid_embed(mermaid_filename)
|
|
877
953
|
|
|
878
|
-
def _create_mermaid_embed(self, filename: str) ->
|
|
954
|
+
def _create_mermaid_embed(self, filename: str) -> ElementType:
|
|
879
955
|
"A Mermaid diagram, linking to an attachment that captures the Mermaid source."
|
|
880
956
|
|
|
881
957
|
local_id = str(uuid.uuid4())
|
|
@@ -907,7 +983,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
907
983
|
AC_ELEM("parameter", {AC_ATTR("name"): "revision"}, "1"),
|
|
908
984
|
)
|
|
909
985
|
|
|
910
|
-
def _transform_toc(self, code:
|
|
986
|
+
def _transform_toc(self, code: ElementType) -> ElementType:
|
|
911
987
|
"Creates a table of contents, constructed from headings in the document."
|
|
912
988
|
|
|
913
989
|
return AC_ELEM(
|
|
@@ -921,7 +997,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
921
997
|
AC_ELEM("parameter", {AC_ATTR("name"): "style"}, "default"),
|
|
922
998
|
)
|
|
923
999
|
|
|
924
|
-
def _transform_listing(self, code:
|
|
1000
|
+
def _transform_listing(self, code: ElementType) -> ElementType:
|
|
925
1001
|
"Creates a list of child pages."
|
|
926
1002
|
|
|
927
1003
|
return AC_ELEM(
|
|
@@ -934,7 +1010,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
934
1010
|
AC_ELEM("parameter", {AC_ATTR("name"): "allChildren"}, "true"),
|
|
935
1011
|
)
|
|
936
1012
|
|
|
937
|
-
def _transform_admonition(self, elem:
|
|
1013
|
+
def _transform_admonition(self, elem: ElementType) -> ElementType:
|
|
938
1014
|
"""
|
|
939
1015
|
Creates an info, tip, note or warning panel from a Markdown admonition.
|
|
940
1016
|
|
|
@@ -947,45 +1023,51 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
947
1023
|
|
|
948
1024
|
# <div class="admonition note">
|
|
949
1025
|
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}")
|
|
1026
|
+
class_list.remove("admonition")
|
|
1027
|
+
if len(class_list) > 1:
|
|
1028
|
+
raise DocumentError(f"too many admonition types: {class_list}")
|
|
1029
|
+
elif len(class_list) < 1:
|
|
1030
|
+
raise DocumentError("missing specific admonition type")
|
|
1031
|
+
admonition = class_list[0]
|
|
962
1032
|
|
|
963
1033
|
for e in elem:
|
|
964
1034
|
self.visit(e)
|
|
965
1035
|
|
|
966
1036
|
# <p class="admonition-title">Note</p>
|
|
967
1037
|
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
|
-
]
|
|
1038
|
+
content = [HTML.p(HTML.strong(elem[0].text or "")), *list(elem[1:])]
|
|
976
1039
|
else:
|
|
977
|
-
content =
|
|
1040
|
+
content = list(elem)
|
|
978
1041
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1042
|
+
if self.options.use_panel:
|
|
1043
|
+
return self._transform_panel(content, admonition)
|
|
1044
|
+
else:
|
|
1045
|
+
admonition_to_csf = {
|
|
1046
|
+
"attention": "note",
|
|
1047
|
+
"caution": "warning",
|
|
1048
|
+
"danger": "warning",
|
|
1049
|
+
"error": "warning",
|
|
1050
|
+
"hint": "tip",
|
|
1051
|
+
"important": "note",
|
|
1052
|
+
"info": "info",
|
|
1053
|
+
"note": "info",
|
|
1054
|
+
"tip": "tip",
|
|
1055
|
+
"warning": "note",
|
|
1056
|
+
}
|
|
1057
|
+
class_name = admonition_to_csf.get(admonition)
|
|
1058
|
+
if class_name is None:
|
|
1059
|
+
raise DocumentError(f"unsupported admonition type: {admonition}")
|
|
1060
|
+
|
|
1061
|
+
return AC_ELEM(
|
|
1062
|
+
"structured-macro",
|
|
1063
|
+
{
|
|
1064
|
+
AC_ATTR("name"): class_name,
|
|
1065
|
+
AC_ATTR("schema-version"): "1",
|
|
1066
|
+
},
|
|
1067
|
+
AC_ELEM("rich-text-body", {}, *content),
|
|
1068
|
+
)
|
|
987
1069
|
|
|
988
|
-
def _transform_github_alert(self, blockquote:
|
|
1070
|
+
def _transform_github_alert(self, blockquote: ElementType) -> ElementType:
|
|
989
1071
|
"""
|
|
990
1072
|
Creates a GitHub-style panel, normally triggered with a block-quote starting with a capitalized string such as `[!TIP]`.
|
|
991
1073
|
"""
|
|
@@ -997,30 +1079,29 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
997
1079
|
if content.text is None:
|
|
998
1080
|
raise DocumentError("empty content")
|
|
999
1081
|
|
|
1000
|
-
class_name: Optional[str] = None
|
|
1001
|
-
skip = 0
|
|
1002
|
-
|
|
1003
1082
|
pattern = re.compile(r"^\[!([A-Z]+)\]\s*")
|
|
1004
1083
|
match = pattern.match(content.text)
|
|
1005
|
-
if match:
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1084
|
+
if not match:
|
|
1085
|
+
raise DocumentError("not a GitHub alert")
|
|
1086
|
+
|
|
1087
|
+
# remove alert indicator prefix
|
|
1088
|
+
content.text = content.text[len(match.group(0)) :]
|
|
1089
|
+
|
|
1090
|
+
for e in blockquote:
|
|
1091
|
+
self.visit(e)
|
|
1092
|
+
|
|
1093
|
+
alert = match.group(1)
|
|
1094
|
+
if self.options.use_panel:
|
|
1095
|
+
return self._transform_panel(list(blockquote), alert.lower())
|
|
1096
|
+
else:
|
|
1097
|
+
alert_to_csf = {"NOTE": "info", "TIP": "tip", "IMPORTANT": "note", "WARNING": "note", "CAUTION": "warning"}
|
|
1098
|
+
class_name = alert_to_csf.get(alert)
|
|
1099
|
+
if class_name is None:
|
|
1019
1100
|
raise DocumentError(f"unsupported GitHub alert: {alert}")
|
|
1020
1101
|
|
|
1021
|
-
|
|
1102
|
+
return self._transform_alert(blockquote, class_name)
|
|
1022
1103
|
|
|
1023
|
-
def _transform_gitlab_alert(self, blockquote:
|
|
1104
|
+
def _transform_gitlab_alert(self, blockquote: ElementType) -> ElementType:
|
|
1024
1105
|
"""
|
|
1025
1106
|
Creates a classic GitLab-style panel.
|
|
1026
1107
|
|
|
@@ -1035,58 +1116,91 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1035
1116
|
if content.text is None:
|
|
1036
1117
|
raise DocumentError("empty content")
|
|
1037
1118
|
|
|
1038
|
-
class_name: Optional[str] = None
|
|
1039
|
-
skip = 0
|
|
1040
|
-
|
|
1041
1119
|
pattern = re.compile(r"^(FLAG|NOTE|WARNING|DISCLAIMER):\s*")
|
|
1042
1120
|
match = pattern.match(content.text)
|
|
1043
|
-
if match:
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1121
|
+
if not match:
|
|
1122
|
+
raise DocumentError("not a GitLab alert")
|
|
1123
|
+
|
|
1124
|
+
# remove alert indicator prefix
|
|
1125
|
+
content.text = content.text[len(match.group(0)) :]
|
|
1126
|
+
|
|
1127
|
+
for e in blockquote:
|
|
1128
|
+
self.visit(e)
|
|
1129
|
+
|
|
1130
|
+
alert = match.group(1)
|
|
1131
|
+
if self.options.use_panel:
|
|
1132
|
+
return self._transform_panel(list(blockquote), alert.lower())
|
|
1133
|
+
else:
|
|
1134
|
+
alert_to_csf = {"FLAG": "note", "NOTE": "info", "WARNING": "note", "DISCLAIMER": "info"}
|
|
1135
|
+
class_name = alert_to_csf.get(alert)
|
|
1136
|
+
if class_name is None:
|
|
1055
1137
|
raise DocumentError(f"unsupported GitLab alert: {alert}")
|
|
1056
1138
|
|
|
1057
|
-
|
|
1139
|
+
return self._transform_alert(blockquote, class_name)
|
|
1058
1140
|
|
|
1059
|
-
def _transform_alert(self, blockquote:
|
|
1141
|
+
def _transform_alert(self, blockquote: ElementType, class_name: str) -> ElementType:
|
|
1060
1142
|
"""
|
|
1061
|
-
Creates an info
|
|
1143
|
+
Creates an `info`, `tip`, `note` or `warning` panel from a GitHub or GitLab alert.
|
|
1144
|
+
|
|
1145
|
+
Transforms GitHub alert or GitLab alert syntax into one of the Confluence structured macros `info`, `tip`, `note`, or `warning`.
|
|
1146
|
+
|
|
1147
|
+
Confusingly, these structured macros have completely different alternate names on the UI, namely: *Info*, *Success*, *Warning* and *Error* (in this
|
|
1148
|
+
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`.
|
|
1149
|
+
|
|
1150
|
+
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
|
|
1151
|
+
an element `ac:adf-extension`:
|
|
1152
|
+
|
|
1153
|
+
```
|
|
1154
|
+
<ac:adf-node type="panel">
|
|
1155
|
+
<ac:adf-attribute key="panel-type">note</ac:adf-attribute>
|
|
1156
|
+
<ac:adf-content>
|
|
1157
|
+
<p><strong>A note</strong></p>
|
|
1158
|
+
<p>This is a panel showing a note.</p>
|
|
1159
|
+
</ac:adf-content>
|
|
1160
|
+
</ac:adf-node>
|
|
1161
|
+
```
|
|
1062
1162
|
|
|
1063
|
-
|
|
1163
|
+
As of today, *md2conf* does not generate `ac:adf-extension` output, including *Note* and *Custom panel* (which shows an emoji selected by the user).
|
|
1164
|
+
|
|
1165
|
+
:param blockquote: HTML element tree to transform to Confluence Storage Format (CSF).
|
|
1166
|
+
:param class_name: Corresponds to `name` attribute for CSF `structured-macro`.
|
|
1064
1167
|
|
|
1065
1168
|
: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
1169
|
:see: https://docs.gitlab.com/ee/development/documentation/styleguide/#alert-boxes
|
|
1067
1170
|
"""
|
|
1068
1171
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1172
|
+
return AC_ELEM(
|
|
1173
|
+
"structured-macro",
|
|
1174
|
+
{
|
|
1175
|
+
AC_ATTR("name"): class_name,
|
|
1176
|
+
AC_ATTR("schema-version"): "1",
|
|
1177
|
+
},
|
|
1178
|
+
AC_ELEM("rich-text-body", {}, *list(blockquote)),
|
|
1179
|
+
)
|
|
1072
1180
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1181
|
+
def _transform_panel(self, content: list[ElementType], class_name: str) -> ElementType:
|
|
1182
|
+
"Transforms a blockquote into a themed panel."
|
|
1075
1183
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1184
|
+
panel = ConfluencePanel.from_class.get(class_name)
|
|
1185
|
+
if panel is None:
|
|
1186
|
+
raise DocumentError(f"unsupported panel class: {class_name}")
|
|
1078
1187
|
|
|
1079
|
-
|
|
1188
|
+
macro_id = str(uuid.uuid4())
|
|
1080
1189
|
return AC_ELEM(
|
|
1081
1190
|
"structured-macro",
|
|
1082
1191
|
{
|
|
1083
|
-
AC_ATTR("name"):
|
|
1192
|
+
AC_ATTR("name"): "panel",
|
|
1084
1193
|
AC_ATTR("schema-version"): "1",
|
|
1194
|
+
AC_ATTR("macro-id"): macro_id,
|
|
1085
1195
|
},
|
|
1086
|
-
AC_ELEM("
|
|
1196
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "panelIcon"}, f":{panel.emoji_shortname}:"),
|
|
1197
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "panelIconId"}, panel.emoji_unicode),
|
|
1198
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "panelIconText"}, panel.emoji),
|
|
1199
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "bgColor"}, panel.background_color),
|
|
1200
|
+
AC_ELEM("rich-text-body", {}, *content),
|
|
1087
1201
|
)
|
|
1088
1202
|
|
|
1089
|
-
def _transform_collapsed(self, details:
|
|
1203
|
+
def _transform_collapsed(self, details: ElementType) -> ElementType:
|
|
1090
1204
|
"""
|
|
1091
1205
|
Creates a collapsed section.
|
|
1092
1206
|
|
|
@@ -1135,7 +1249,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1135
1249
|
AC_ELEM("rich-text-body", {}, *list(details)),
|
|
1136
1250
|
)
|
|
1137
1251
|
|
|
1138
|
-
def _transform_emoji(self, elem:
|
|
1252
|
+
def _transform_emoji(self, elem: ElementType) -> ElementType:
|
|
1139
1253
|
"""
|
|
1140
1254
|
Inserts an inline emoji character.
|
|
1141
1255
|
"""
|
|
@@ -1159,7 +1273,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1159
1273
|
},
|
|
1160
1274
|
)
|
|
1161
1275
|
|
|
1162
|
-
def _transform_mark(self, mark:
|
|
1276
|
+
def _transform_mark(self, mark: ElementType) -> ElementType:
|
|
1163
1277
|
"""
|
|
1164
1278
|
Adds inline highlighting to text.
|
|
1165
1279
|
"""
|
|
@@ -1174,7 +1288,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1174
1288
|
span.text = mark.text
|
|
1175
1289
|
return span
|
|
1176
1290
|
|
|
1177
|
-
def _transform_latex(self, elem:
|
|
1291
|
+
def _transform_latex(self, elem: ElementType, context: FormattingContext) -> ElementType:
|
|
1178
1292
|
"""
|
|
1179
1293
|
Creates an image rendering of a LaTeX formula with Matplotlib.
|
|
1180
1294
|
"""
|
|
@@ -1187,7 +1301,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1187
1301
|
if self.options.diagram_output_format == "png":
|
|
1188
1302
|
width, height = get_png_dimensions(data=image_data)
|
|
1189
1303
|
image_data = remove_png_chunks(["pHYs"], source_data=image_data)
|
|
1190
|
-
attrs = ImageAttributes(context, width, height, content, None, "")
|
|
1304
|
+
attrs = ImageAttributes(context, width=width, height=height, alt=content, title=None, caption="", alignment=ImageAlignment(self.options.alignment))
|
|
1191
1305
|
else:
|
|
1192
1306
|
attrs = ImageAttributes.empty(context)
|
|
1193
1307
|
|
|
@@ -1197,7 +1311,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1197
1311
|
image = self._create_attached_image(image_filename, attrs)
|
|
1198
1312
|
return image
|
|
1199
1313
|
|
|
1200
|
-
def _transform_inline_math(self, elem:
|
|
1314
|
+
def _transform_inline_math(self, elem: ElementType) -> ElementType:
|
|
1201
1315
|
"""
|
|
1202
1316
|
Creates an inline LaTeX formula using the Confluence extension "LaTeX Math for Confluence - Math Formula & Equations".
|
|
1203
1317
|
|
|
@@ -1228,11 +1342,11 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1228
1342
|
{AC_ATTR("name"): "body"},
|
|
1229
1343
|
content,
|
|
1230
1344
|
),
|
|
1231
|
-
AC_ELEM("parameter", {AC_ATTR("name"): "align"},
|
|
1345
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "align"}, self.options.alignment),
|
|
1232
1346
|
)
|
|
1233
1347
|
return macro
|
|
1234
1348
|
|
|
1235
|
-
def _transform_block_math(self, elem:
|
|
1349
|
+
def _transform_block_math(self, elem: ElementType) -> ElementType:
|
|
1236
1350
|
"""
|
|
1237
1351
|
Creates a block-level LaTeX formula using the Confluence extension "LaTeX Math for Confluence - Math Formula & Equations".
|
|
1238
1352
|
|
|
@@ -1265,10 +1379,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1265
1379
|
{AC_ATTR("name"): "body"},
|
|
1266
1380
|
content,
|
|
1267
1381
|
),
|
|
1268
|
-
AC_ELEM("parameter", {AC_ATTR("name"): "align"},
|
|
1382
|
+
AC_ELEM("parameter", {AC_ATTR("name"): "align"}, self.options.alignment),
|
|
1269
1383
|
)
|
|
1270
1384
|
|
|
1271
|
-
def _transform_footnote_ref(self, elem:
|
|
1385
|
+
def _transform_footnote_ref(self, elem: ElementType) -> None:
|
|
1272
1386
|
"""
|
|
1273
1387
|
Transforms a footnote reference.
|
|
1274
1388
|
|
|
@@ -1325,7 +1439,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1325
1439
|
elem.append(ref_anchor)
|
|
1326
1440
|
elem.append(def_link)
|
|
1327
1441
|
|
|
1328
|
-
def _transform_footnote_def(self, elem:
|
|
1442
|
+
def _transform_footnote_def(self, elem: ElementType) -> None:
|
|
1329
1443
|
"""
|
|
1330
1444
|
Transforms the footnote definition block.
|
|
1331
1445
|
|
|
@@ -1399,7 +1513,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1399
1513
|
paragraph.text = None
|
|
1400
1514
|
paragraph.append(ref_link)
|
|
1401
1515
|
|
|
1402
|
-
def _transform_tasklist(self, elem:
|
|
1516
|
+
def _transform_tasklist(self, elem: ElementType) -> ElementType:
|
|
1403
1517
|
"""
|
|
1404
1518
|
Transforms a list of tasks into an action widget.
|
|
1405
1519
|
|
|
@@ -1415,7 +1529,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1415
1529
|
if not element_text_starts_with_any(item, ["[ ]", "[x]", "[X]"]):
|
|
1416
1530
|
raise DocumentError("expected: each `<li>` in a task list starting with [ ] or [x]")
|
|
1417
1531
|
|
|
1418
|
-
tasks: list[
|
|
1532
|
+
tasks: list[ElementType] = []
|
|
1419
1533
|
for index, item in enumerate(elem, start=1):
|
|
1420
1534
|
if item.text is None:
|
|
1421
1535
|
raise NotImplementedError("pre-condition check not exhaustive")
|
|
@@ -1444,7 +1558,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1444
1558
|
return AC_ELEM("task-list", {}, *tasks)
|
|
1445
1559
|
|
|
1446
1560
|
@override
|
|
1447
|
-
def transform(self, child:
|
|
1561
|
+
def transform(self, child: ElementType) -> Optional[ElementType]:
|
|
1448
1562
|
"""
|
|
1449
1563
|
Transforms an HTML element tree obtained from a Markdown document into a Confluence Storage Format element tree.
|
|
1450
1564
|
"""
|
|
@@ -1544,7 +1658,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1544
1658
|
# <li>[x] ...</li>
|
|
1545
1659
|
# </ul>
|
|
1546
1660
|
elif child.tag == "ul":
|
|
1547
|
-
if len(child) > 0 and element_text_starts_with_any(
|
|
1661
|
+
if len(child) > 0 and all(element_text_starts_with_any(item, ["[ ]", "[x]", "[X]"]) for item in child):
|
|
1548
1662
|
return self._transform_tasklist(child)
|
|
1549
1663
|
|
|
1550
1664
|
return None
|
|
@@ -1637,7 +1751,7 @@ class ConfluenceDocument:
|
|
|
1637
1751
|
embedded_files: dict[str, EmbeddedFileData]
|
|
1638
1752
|
|
|
1639
1753
|
options: ConfluenceDocumentOptions
|
|
1640
|
-
root:
|
|
1754
|
+
root: ElementType
|
|
1641
1755
|
|
|
1642
1756
|
@classmethod
|
|
1643
1757
|
def create(
|
|
@@ -1683,10 +1797,10 @@ class ConfluenceDocument:
|
|
|
1683
1797
|
lines.append(f"[STATUS-{color.upper()}]: {data_uri}")
|
|
1684
1798
|
lines.append(document.text)
|
|
1685
1799
|
|
|
1686
|
-
# convert to HTML
|
|
1800
|
+
# parse Markdown document and convert to HTML
|
|
1687
1801
|
html = markdown_to_html("\n".join(lines))
|
|
1688
1802
|
|
|
1689
|
-
#
|
|
1803
|
+
# modify HTML as necessary
|
|
1690
1804
|
if self.options.generated_by is not None:
|
|
1691
1805
|
generated_by = document.generated_by or self.options.generated_by
|
|
1692
1806
|
else:
|
|
@@ -1704,26 +1818,32 @@ class ConfluenceDocument:
|
|
|
1704
1818
|
else:
|
|
1705
1819
|
content = [html]
|
|
1706
1820
|
|
|
1821
|
+
# parse HTML into element tree
|
|
1707
1822
|
try:
|
|
1708
1823
|
self.root = elements_from_strings(content)
|
|
1709
1824
|
except ParseError as ex:
|
|
1710
1825
|
raise ConversionError(path) from ex
|
|
1711
1826
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
root_dir,
|
|
1716
|
-
site_metadata,
|
|
1717
|
-
page_metadata,
|
|
1827
|
+
# configure HTML-to-Confluence converter
|
|
1828
|
+
converter_options = ConfluenceConverterOptions(
|
|
1829
|
+
**{field.name: getattr(self.options, field.name) for field in dataclasses.fields(ConfluenceConverterOptions)}
|
|
1718
1830
|
)
|
|
1831
|
+
if document.alignment is not None:
|
|
1832
|
+
converter_options.alignment = document.alignment
|
|
1833
|
+
converter = ConfluenceStorageFormatConverter(converter_options, path, root_dir, site_metadata, page_metadata)
|
|
1834
|
+
|
|
1835
|
+
# execute HTML-to-Confluence converter
|
|
1719
1836
|
try:
|
|
1720
1837
|
converter.visit(self.root)
|
|
1721
1838
|
except DocumentError as ex:
|
|
1722
1839
|
raise ConversionError(path) from ex
|
|
1840
|
+
|
|
1841
|
+
# extract information discovered by converter
|
|
1723
1842
|
self.links = converter.links
|
|
1724
1843
|
self.images = converter.images
|
|
1725
1844
|
self.embedded_files = converter.embedded_files
|
|
1726
1845
|
|
|
1846
|
+
# assign global properties for document
|
|
1727
1847
|
self.title = document.title or converter.toc.get_title()
|
|
1728
1848
|
self.labels = document.tags
|
|
1729
1849
|
self.properties = document.properties
|