markdown-to-confluence 0.5.2__py3-none-any.whl → 0.5.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/METADATA +258 -157
  2. markdown_to_confluence-0.5.4.dist-info/RECORD +55 -0
  3. {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/licenses/LICENSE +1 -1
  4. md2conf/__init__.py +2 -2
  5. md2conf/__main__.py +83 -44
  6. md2conf/api.py +30 -10
  7. md2conf/attachment.py +72 -0
  8. md2conf/coalesce.py +43 -0
  9. md2conf/collection.py +1 -1
  10. md2conf/{extra.py → compatibility.py} +1 -1
  11. md2conf/converter.py +240 -657
  12. md2conf/csf.py +13 -11
  13. md2conf/drawio/__init__.py +0 -0
  14. md2conf/drawio/extension.py +116 -0
  15. md2conf/{drawio.py → drawio/render.py} +1 -1
  16. md2conf/emoticon.py +3 -3
  17. md2conf/environment.py +2 -2
  18. md2conf/extension.py +82 -0
  19. md2conf/external.py +66 -0
  20. md2conf/formatting.py +135 -0
  21. md2conf/frontmatter.py +70 -0
  22. md2conf/image.py +128 -0
  23. md2conf/latex.py +4 -183
  24. md2conf/local.py +8 -8
  25. md2conf/markdown.py +1 -1
  26. md2conf/matcher.py +1 -1
  27. md2conf/mermaid/__init__.py +0 -0
  28. md2conf/mermaid/config.py +20 -0
  29. md2conf/mermaid/extension.py +109 -0
  30. md2conf/{mermaid.py → mermaid/render.py} +10 -38
  31. md2conf/mermaid/scanner.py +55 -0
  32. md2conf/metadata.py +1 -1
  33. md2conf/{domain.py → options.py} +75 -16
  34. md2conf/plantuml/__init__.py +0 -0
  35. md2conf/plantuml/config.py +20 -0
  36. md2conf/plantuml/extension.py +158 -0
  37. md2conf/plantuml/render.py +138 -0
  38. md2conf/plantuml/scanner.py +56 -0
  39. md2conf/png.py +206 -0
  40. md2conf/processor.py +55 -13
  41. md2conf/publisher.py +127 -39
  42. md2conf/scanner.py +38 -129
  43. md2conf/serializer.py +2 -2
  44. md2conf/svg.py +144 -103
  45. md2conf/text.py +1 -1
  46. md2conf/toc.py +73 -1
  47. md2conf/uri.py +1 -1
  48. md2conf/xml.py +1 -1
  49. markdown_to_confluence-0.5.2.dist-info/RECORD +0 -36
  50. {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/WHEEL +0 -0
  51. {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/entry_points.txt +0 -0
  52. {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/top_level.txt +0 -0
  53. {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/zip-safe +0 -0
  54. /md2conf/{puppeteer-config.json → mermaid/puppeteer-config.json} +0 -0
md2conf/metadata.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
@@ -1,11 +1,12 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
8
8
 
9
+ import dataclasses
9
10
  from dataclasses import dataclass
10
11
  from typing import Literal
11
12
 
@@ -16,44 +17,102 @@ class ConfluencePageID:
16
17
 
17
18
 
18
19
  @dataclass
19
- class ConfluenceDocumentOptions:
20
+ class ImageLayoutOptions:
20
21
  """
21
- Options that control the generated page content.
22
+ Image layout options on a Confluence page.
23
+
24
+ :param alignment: Alignment for block-level images and formulas.
25
+ :param max_width: Maximum display width for images [px]. Wider images are scaled down for page display. Original size kept for full-size viewing.
26
+ """
27
+
28
+ alignment: Literal["center", "left", "right"] | None = None
29
+ max_width: int | None = None
30
+
31
+
32
+ @dataclass
33
+ class TableLayoutOptions:
34
+ """
35
+ Table layout options on a Confluence page.
36
+
37
+ :param width: Maximum table width in pixels.
38
+ :param display_mode: Whether to use fixed or responsive column widths.
39
+ """
40
+
41
+ width: int | None = None
42
+ display_mode: Literal["fixed", "responsive"] | None = None
43
+
44
+
45
+ @dataclass
46
+ class LayoutOptions:
47
+ """
48
+ Layout options for content on a Confluence page.
49
+
50
+ Layout options can be overridden in Markdown front-matter.
51
+
52
+ :param image: Image layout options.
53
+ :param table: Table layout options.
54
+ :param alignment: Default alignment (unless overridden with more specific setting).
55
+ """
56
+
57
+ image: ImageLayoutOptions = dataclasses.field(default_factory=ImageLayoutOptions)
58
+ table: TableLayoutOptions = dataclasses.field(default_factory=TableLayoutOptions)
59
+ alignment: Literal["center", "left", "right"] | None = None
60
+
61
+ def get_image_alignment(self) -> Literal["center", "left", "right"]:
62
+ return self.image.alignment or self.alignment or "center"
63
+
64
+
65
+ @dataclass
66
+ class ConverterOptions:
67
+ """
68
+ Options for converting an HTML tree into Confluence Storage Format.
22
69
 
23
70
  :param heading_anchors: When true, emit a structured macro *anchor* for each section heading using GitHub
24
71
  conversion rules for the identifier.
25
72
  :param ignore_invalid_url: When true, ignore invalid URLs in input, emit a warning and replace the anchor with
26
73
  plain text; when false, raise an exception.
27
74
  :param skip_title_heading: Whether to remove the first heading from document body when used as page title.
28
- :param title_prefix: String to prepend to Confluence page title for each published page.
29
- :param generated_by: Text to use as the generated-by prompt (or `None` to omit a prompt).
30
- :param root_page_id: Confluence page to assume root page role for publishing a directory of Markdown files.
31
- :param keep_hierarchy: Whether to maintain source directory structure when exporting to Confluence.
32
75
  :param prefer_raster: Whether to choose PNG files over SVG files when available.
33
76
  :param render_drawio: Whether to pre-render (or use the pre-rendered version of) draw.io diagrams.
34
77
  :param render_mermaid: Whether to pre-render Mermaid diagrams into PNG/SVG images.
78
+ :param render_plantuml: Whether to pre-render PlantUML diagrams into PNG/SVG images.
35
79
  :param render_latex: Whether to pre-render LaTeX formulas into PNG/SVG images.
36
80
  :param diagram_output_format: Target image format for diagrams.
37
81
  :param webui_links: When true, convert relative URLs to Confluence Web UI links.
38
- :param alignment: Alignment for block-level images and formulas.
39
- :param max_image_width: Maximum display width for images [px]. Wider images are scaled down for page display.
40
- Original size kept for full-size viewing.
41
82
  :param use_panel: Whether to transform admonitions and alerts into a Confluence custom panel.
83
+ :param layout: Layout options for content on a Confluence page.
42
84
  """
43
85
 
44
86
  heading_anchors: bool = False
45
87
  ignore_invalid_url: bool = False
46
88
  skip_title_heading: bool = False
47
- title_prefix: str | None = None
48
- generated_by: str | None = "This page has been generated with a tool."
49
- root_page_id: ConfluencePageID | None = None
50
- keep_hierarchy: bool = False
51
89
  prefer_raster: bool = True
52
90
  render_drawio: bool = False
53
91
  render_mermaid: bool = False
92
+ render_plantuml: bool = False
54
93
  render_latex: bool = False
55
94
  diagram_output_format: Literal["png", "svg"] = "png"
56
95
  webui_links: bool = False
57
- alignment: Literal["center", "left", "right"] = "center"
58
- max_image_width: int | None = None
59
96
  use_panel: bool = False
97
+ layout: LayoutOptions = dataclasses.field(default_factory=LayoutOptions)
98
+
99
+
100
+ @dataclass
101
+ class DocumentOptions:
102
+ """
103
+ Options that control the generated page content.
104
+
105
+ :param root_page_id: Confluence page to assume root page role for publishing a directory of Markdown files.
106
+ :param keep_hierarchy: Whether to maintain source directory structure when exporting to Confluence.
107
+ :param title_prefix: String to prepend to Confluence page title for each published page.
108
+ :param generated_by: Text to use as the generated-by prompt (or `None` to omit a prompt).
109
+ :param skip_update: Whether to skip saving Confluence page ID in Markdown files.
110
+ :param converter: Options for converting an HTML tree into Confluence Storage Format.
111
+ """
112
+
113
+ root_page_id: ConfluencePageID | None = None
114
+ keep_hierarchy: bool = False
115
+ title_prefix: str | None = None
116
+ generated_by: str | None = "This page has been generated with a tool."
117
+ skip_update: bool = False
118
+ converter: ConverterOptions = dataclasses.field(default_factory=ConverterOptions)
File without changes
@@ -0,0 +1,20 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class PlantUMLConfigProperties:
14
+ """
15
+ Configuration options for rendering PlantUML diagrams.
16
+
17
+ :param scale: Scaling factor for the rendered diagram.
18
+ """
19
+
20
+ scale: float | None = None
@@ -0,0 +1,158 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import hashlib
10
+ import logging
11
+ import uuid
12
+ from pathlib import Path
13
+
14
+ import lxml.etree as ET
15
+ from cattrs import BaseValidationError
16
+
17
+ from md2conf.attachment import EmbeddedFileData, ImageData, attachment_name
18
+ from md2conf.compatibility import override, path_relative_to
19
+ from md2conf.csf import AC_ATTR, AC_ELEM
20
+ from md2conf.extension import MarketplaceExtension
21
+ from md2conf.formatting import ImageAttributes
22
+ from md2conf.svg import get_svg_dimensions
23
+
24
+ from .config import PlantUMLConfigProperties
25
+ from .render import compress_plantuml_data, has_plantuml, render_diagram
26
+ from .scanner import PlantUMLScanner
27
+
28
+ ElementType = ET._Element # pyright: ignore [reportPrivateUsage]
29
+
30
+ LOGGER = logging.getLogger(__name__)
31
+
32
+
33
+ class PlantUMLExtension(MarketplaceExtension):
34
+ @override
35
+ def matches_image(self, absolute_path: Path) -> bool:
36
+ return absolute_path.name.endswith((".puml", ".plantuml"))
37
+
38
+ @override
39
+ def matches_fenced(self, language: str, content: str) -> bool:
40
+ return language == "plantuml"
41
+
42
+ def _extract_plantuml_config(self, content: str) -> PlantUMLConfigProperties | None:
43
+ "Extract config from PlantUML YAML front matter configuration."
44
+
45
+ try:
46
+ properties = PlantUMLScanner().read(content)
47
+ return properties.config
48
+ except BaseValidationError as ex:
49
+ LOGGER.warning("Failed to extract PlantUML properties: %s", ex)
50
+ return None
51
+
52
+ @override
53
+ def transform_image(self, absolute_path: Path, attrs: ImageAttributes) -> ElementType:
54
+ relative_path = path_relative_to(absolute_path, self.base_dir)
55
+
56
+ # read PlantUML source
57
+ with open(absolute_path, "r", encoding="utf-8") as f:
58
+ content = f.read()
59
+
60
+ return self._transform_plantuml(content, attrs, relative_path)
61
+
62
+ @override
63
+ def transform_fenced(self, content: str) -> ElementType:
64
+ return self._transform_plantuml(content, ImageAttributes.EMPTY_BLOCK)
65
+
66
+ def _transform_plantuml(self, content: str, attrs: ImageAttributes, relative_path: Path | None = None) -> ElementType:
67
+ """
68
+ Emits Confluence Storage Format XHTML for a PlantUML diagram read from an external file or defined in a fenced code block.
69
+
70
+ When `render_plantuml` is enabled, renders as an image attachment. Otherwise, uses a structured macro with embedded SVG and compressed source.
71
+ """
72
+
73
+ if self.options.render:
74
+ # render diagram as image file (PNG or SVG based on diagram output format)
75
+ config = self._extract_plantuml_config(content)
76
+ image_data = render_diagram(content, self.generator.options.output_format, config=config)
77
+ return self.generator.transform_attached_data(image_data, attrs, relative_path)
78
+ else:
79
+ if relative_path is not None:
80
+ absolute_path = self.base_dir / relative_path
81
+ self.attachments.add_image(ImageData(absolute_path, attrs.alt))
82
+
83
+ # use `structured-macro` with SVG attachment
84
+ if has_plantuml():
85
+ # render to SVG for structured macro (macro requires SVG)
86
+ config = self._extract_plantuml_config(content)
87
+ image_data = render_diagram(content, "svg", config=config)
88
+
89
+ # extract dimensions from SVG
90
+ dimensions = get_svg_dimensions(image_data)
91
+
92
+ # generate SVG filename and add as attachment
93
+ if relative_path is not None:
94
+ svg_filename = attachment_name(relative_path.with_suffix(".svg"))
95
+ self.attachments.add_embed(svg_filename, EmbeddedFileData(image_data, attrs.alt))
96
+ else:
97
+ plantuml_hash = hashlib.md5(content.encode("utf-8")).hexdigest()
98
+ svg_filename = attachment_name(f"embedded_{plantuml_hash}.svg")
99
+ self.attachments.add_embed(svg_filename, EmbeddedFileData(image_data))
100
+
101
+ return self._create_plantuml_macro(content, svg_filename, dimensions)
102
+ else:
103
+ return self._create_plantuml_macro(content)
104
+
105
+ def _create_plantuml_macro(self, source: str, filename: str | None = None, dimensions: tuple[int, int] | None = None) -> ElementType:
106
+ """
107
+ A PlantUML diagram using a `structured-macro` with embedded data.
108
+
109
+ Generates a macro compatible with "PlantUML Diagrams for Confluence" app.
110
+
111
+ :see: https://stratus-addons.atlassian.net/wiki/spaces/PDFC/pages/1839333377
112
+ """
113
+
114
+ local_id = str(uuid.uuid4())
115
+ macro_id = str(uuid.uuid4())
116
+
117
+ # Compress PlantUML source for embedding
118
+ compressed_data = compress_plantuml_data(source)
119
+
120
+ # Build mandatory parameters
121
+ parameters: list[ElementType] = [
122
+ AC_ELEM("parameter", {AC_ATTR("name"): "data"}, compressed_data),
123
+ AC_ELEM("parameter", {AC_ATTR("name"): "compressed"}, "true"),
124
+ AC_ELEM("parameter", {AC_ATTR("name"): "revision"}, "1"),
125
+ AC_ELEM("parameter", {AC_ATTR("name"): "toolbar"}, "bottom"),
126
+ ]
127
+ if filename is not None:
128
+ parameters.append(AC_ELEM("parameter", {AC_ATTR("name"): "filename"}, filename))
129
+
130
+ # add optional dimension parameters if available
131
+ if dimensions is not None:
132
+ width, height = dimensions
133
+ parameters.append(
134
+ AC_ELEM(
135
+ "parameter",
136
+ {AC_ATTR("name"): "originalWidth"},
137
+ str(width),
138
+ )
139
+ )
140
+ parameters.append(
141
+ AC_ELEM(
142
+ "parameter",
143
+ {AC_ATTR("name"): "originalHeight"},
144
+ str(height),
145
+ )
146
+ )
147
+
148
+ return AC_ELEM(
149
+ "structured-macro",
150
+ {
151
+ AC_ATTR("name"): "plantumlcloud",
152
+ AC_ATTR("schema-version"): "1",
153
+ "data-layout": "default",
154
+ AC_ATTR("local-id"): local_id,
155
+ AC_ATTR("macro-id"): macro_id,
156
+ },
157
+ *parameters,
158
+ )
@@ -0,0 +1,138 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import base64
10
+ import logging
11
+ import os
12
+ import shlex
13
+ import shutil
14
+ import zlib
15
+ from pathlib import Path
16
+ from typing import Literal
17
+ from urllib.parse import quote
18
+
19
+ import md2conf
20
+ from md2conf.external import execute_subprocess
21
+
22
+ from .config import PlantUMLConfigProperties
23
+
24
+ LOGGER = logging.getLogger(__name__)
25
+
26
+
27
+ def _get_plantuml_jar_path() -> Path:
28
+ """
29
+ Returns the expected path to `plantuml.jar`.
30
+
31
+ Priority:
32
+
33
+ 1. value of environment variable `PLANTUML_JAR`
34
+ 2. path to `plantuml.jar` if found in parent directory of module `md2conf`
35
+ 3. path to `plantuml.jar` if found in current directory
36
+ """
37
+
38
+ # check environment variable
39
+ env_jar = os.environ.get("PLANTUML_JAR")
40
+ if env_jar:
41
+ return Path(env_jar)
42
+
43
+ # check parent directory of module `md2conf`
44
+ base_path = Path(md2conf.__file__).parent.parent
45
+ jar_path = base_path / "plantuml.jar"
46
+ if jar_path.exists():
47
+ return jar_path
48
+
49
+ # check current directory
50
+ return Path("plantuml.jar")
51
+
52
+
53
+ def _get_plantuml_command() -> list[str]:
54
+ """
55
+ Returns the command to invoke PlantUML.
56
+
57
+ :raises RuntimeError: Raised when `plantuml.jar` is not found.
58
+ """
59
+
60
+ env_cmd = os.environ.get("PLANTUML_CMD")
61
+ if env_cmd:
62
+ LOGGER.debug(f"Using PlantUML command: {env_cmd}")
63
+ return shlex.split(env_cmd)
64
+
65
+ jar_path = _get_plantuml_jar_path()
66
+ if jar_path.is_file():
67
+ LOGGER.debug(f"Using PlantUML JAR at: {jar_path}")
68
+ return ["java", "-jar", str(jar_path)]
69
+
70
+ # JAR not found - fail with helpful message
71
+ raise RuntimeError(
72
+ "PlantUML JAR not found. Download `plantuml.jar` from https://github.com/plantuml/plantuml/releases and set the PLANTUML_JAR environment variable."
73
+ )
74
+
75
+
76
+ def has_plantuml() -> bool:
77
+ """True if PlantUML JAR is available and Java is installed."""
78
+
79
+ jar_path = _get_plantuml_jar_path()
80
+
81
+ # Check if we have JAR file and Java is available
82
+ return jar_path.is_file() and shutil.which("java") is not None
83
+
84
+
85
+ def render_diagram(
86
+ source: str,
87
+ output_format: Literal["png", "svg"] = "png",
88
+ config: PlantUMLConfigProperties | None = None,
89
+ ) -> bytes:
90
+ "Generates a PNG or SVG image from a PlantUML diagram source."
91
+
92
+ if config is None:
93
+ config = PlantUMLConfigProperties()
94
+
95
+ # command for PlantUML with pipe mode
96
+ cmd = _get_plantuml_command()
97
+ cmd.extend(
98
+ [
99
+ "--charset",
100
+ "utf-8",
101
+ "--format",
102
+ output_format,
103
+ "--no-error-image",
104
+ "--pipe",
105
+ ]
106
+ )
107
+
108
+ # Add scale if specified
109
+ if config.scale is not None:
110
+ cmd.extend(["-scale", str(config.scale)])
111
+
112
+ return execute_subprocess(cmd, source.encode("utf-8"), application="PlantUML")
113
+
114
+
115
+ def compress_plantuml_data(source: str) -> str:
116
+ """
117
+ Compress PlantUML source for embedding in plantumlcloud macro.
118
+
119
+ Implements the encoding used by PlantUML Diagrams for Confluence:
120
+
121
+ 1. URI encode the source
122
+ 2. Deflate with raw deflate (zlib)
123
+ 3. Base64 encode
124
+
125
+ :param source: PlantUML diagram source code.
126
+ :returns: Compressed and encoded data suitable for macro data parameter.
127
+ :see: https://stratus-addons.atlassian.net/wiki/spaces/PDFC/pages/1839333377
128
+ """
129
+
130
+ # Step 1: URI encode
131
+ encoded = quote(source, safe="")
132
+
133
+ # Step 2: Deflate with raw deflate (remove zlib header/trailer)
134
+ # zlib.compress() adds 2-byte header and 4-byte trailer
135
+ deflated = zlib.compress(encoded.encode("utf-8"))[2:-4]
136
+
137
+ # Step 3: Base64 encode
138
+ return base64.b64encode(deflated).decode("ascii")
@@ -0,0 +1,56 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+
11
+ from md2conf.frontmatter import extract_frontmatter_object
12
+
13
+ from .config import PlantUMLConfigProperties
14
+
15
+
16
+ @dataclass
17
+ class PlantUMLProperties:
18
+ """
19
+ An object that holds the front-matter properties structure
20
+ for PlantUML diagrams.
21
+
22
+ :param title: The title of the diagram.
23
+ :param config: Configuration options for rendering.
24
+ """
25
+
26
+ title: str | None = None
27
+ config: PlantUMLConfigProperties | None = None
28
+
29
+
30
+ class PlantUMLScanner:
31
+ """
32
+ Extracts properties from the JSON/YAML front-matter of a PlantUML diagram.
33
+ """
34
+
35
+ def read(self, content: str) -> PlantUMLProperties:
36
+ """
37
+ Extracts rendering preferences from a PlantUML front-matter content.
38
+
39
+ ```
40
+ ---
41
+ title: Class diagram
42
+ config:
43
+ scale: 1
44
+ ---
45
+ @startuml
46
+ class Example
47
+ @enduml
48
+ ```
49
+ """
50
+
51
+ properties, _ = extract_frontmatter_object(PlantUMLProperties, content)
52
+ if properties is not None:
53
+ config = properties.config or PlantUMLConfigProperties()
54
+ return PlantUMLProperties(title=properties.title, config=config)
55
+
56
+ return PlantUMLProperties()