markdown-to-confluence 0.5.1__py3-none-any.whl → 0.5.3__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.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/METADATA +160 -11
  2. markdown_to_confluence-0.5.3.dist-info/RECORD +55 -0
  3. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/licenses/LICENSE +1 -1
  4. md2conf/__init__.py +2 -2
  5. md2conf/__main__.py +94 -29
  6. md2conf/api.py +55 -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 +417 -590
  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 +78 -0
  19. md2conf/external.py +49 -0
  20. md2conf/formatting.py +135 -0
  21. md2conf/frontmatter.py +70 -0
  22. md2conf/image.py +127 -0
  23. md2conf/latex.py +7 -186
  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/options.py +116 -0
  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 +139 -0
  38. md2conf/plantuml/scanner.py +56 -0
  39. md2conf/png.py +202 -0
  40. md2conf/processor.py +32 -11
  41. md2conf/publisher.py +17 -18
  42. md2conf/scanner.py +31 -128
  43. md2conf/serializer.py +2 -2
  44. md2conf/svg.py +341 -0
  45. md2conf/text.py +1 -1
  46. md2conf/toc.py +1 -1
  47. md2conf/uri.py +1 -1
  48. md2conf/xml.py +1 -1
  49. markdown_to_confluence-0.5.1.dist-info/RECORD +0 -35
  50. md2conf/domain.py +0 -52
  51. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/WHEEL +0 -0
  52. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/entry_points.txt +0 -0
  53. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/top_level.txt +0 -0
  54. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/zip-safe +0 -0
md2conf/processor.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
  """
@@ -15,16 +15,18 @@ from typing import Iterable
15
15
 
16
16
  from .collection import ConfluencePageCollection
17
17
  from .converter import ConfluenceDocument
18
- from .domain import ConfluenceDocumentOptions, ConfluencePageID
19
18
  from .environment import ArgumentError
20
19
  from .matcher import DirectoryEntry, FileEntry, Matcher, MatcherOptions
21
20
  from .metadata import ConfluenceSiteMetadata
21
+ from .options import ConfluencePageID, DocumentOptions
22
22
  from .scanner import Scanner
23
23
 
24
24
  LOGGER = logging.getLogger(__name__)
25
25
 
26
26
 
27
27
  class DocumentNode:
28
+ "Represents a Markdown document in a hierarchy."
29
+
28
30
  absolute_path: Path
29
31
  page_id: str | None
30
32
  space_key: str | None
@@ -49,24 +51,42 @@ class DocumentNode:
49
51
  self._children = []
50
52
 
51
53
  def count(self) -> int:
54
+ "Number of descendants in the sub-tree spanned by this node (excluding the top-level node)."
55
+
52
56
  c = len(self._children)
53
57
  for child in self._children:
54
58
  c += child.count()
55
59
  return c
56
60
 
57
61
  def add_child(self, child: "DocumentNode") -> None:
62
+ "Adds a new node to the list of direct children."
63
+
58
64
  self._children.append(child)
59
65
 
60
66
  def children(self) -> Iterable["DocumentNode"]:
67
+ "Direct children of this node."
68
+
61
69
  for child in self._children:
62
70
  yield child
63
71
 
64
72
  def descendants(self) -> Iterable["DocumentNode"]:
73
+ """
74
+ Descendants of this node, part of its sub-tree.
75
+
76
+ Traversal follows depth-first search.
77
+ """
78
+
65
79
  for child in self._children:
66
80
  yield child
67
81
  yield from child.descendants()
68
82
 
69
83
  def all(self) -> Iterable["DocumentNode"]:
84
+ """
85
+ Descendants of this node, part of the sub-tree including the top-level node.
86
+
87
+ Traversal follows depth-first search.
88
+ """
89
+
70
90
  yield self
71
91
  for child in self._children:
72
92
  yield from child.all()
@@ -77,7 +97,7 @@ class Processor:
77
97
  Processes a single Markdown page or a directory of Markdown pages.
78
98
  """
79
99
 
80
- options: ConfluenceDocumentOptions
100
+ options: DocumentOptions
81
101
  site: ConfluenceSiteMetadata
82
102
  root_dir: Path
83
103
 
@@ -85,7 +105,7 @@ class Processor:
85
105
 
86
106
  def __init__(
87
107
  self,
88
- options: ConfluenceDocumentOptions,
108
+ options: DocumentOptions,
89
109
  site: ConfluenceSiteMetadata,
90
110
  root_dir: Path,
91
111
  ) -> None:
@@ -140,7 +160,7 @@ class Processor:
140
160
  self._update_page(page_id, document, path)
141
161
 
142
162
  @abstractmethod
143
- def _synchronize_tree(self, root: DocumentNode, root_id: ConfluencePageID | None) -> None:
163
+ def _synchronize_tree(self, tree: DocumentNode, root_id: ConfluencePageID | None) -> None:
144
164
  """
145
165
  Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
146
166
 
@@ -228,12 +248,13 @@ class Processor:
228
248
  # extract information from a Markdown document found in a local directory.
229
249
  document = Scanner().read(path)
230
250
 
251
+ props = document.properties
231
252
  return DocumentNode(
232
253
  absolute_path=path,
233
- page_id=document.page_id,
234
- space_key=document.space_key,
235
- title=document.title,
236
- synchronized=document.synchronized if document.synchronized is not None else True,
254
+ page_id=props.page_id,
255
+ space_key=props.space_key,
256
+ title=props.title,
257
+ synchronized=props.synchronized if props.synchronized is not None else True,
237
258
  )
238
259
 
239
260
  def _generate_hash(self, absolute_path: Path) -> str:
@@ -247,10 +268,10 @@ class Processor:
247
268
 
248
269
 
249
270
  class ProcessorFactory:
250
- options: ConfluenceDocumentOptions
271
+ options: DocumentOptions
251
272
  site: ConfluenceSiteMetadata
252
273
 
253
- def __init__(self, options: ConfluenceDocumentOptions, site: ConfluenceSiteMetadata) -> None:
274
+ def __init__(self, options: DocumentOptions, site: ConfluenceSiteMetadata) -> None:
254
275
  self.options = options
255
276
  self.site = site
256
277
 
md2conf/publisher.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
  """
@@ -10,12 +10,13 @@ import logging
10
10
  from pathlib import Path
11
11
 
12
12
  from .api import ConfluenceContentProperty, ConfluenceLabel, ConfluenceSession, ConfluenceStatus
13
- from .converter import ConfluenceDocument, attachment_name, get_volatile_attributes, get_volatile_elements
13
+ from .attachment import attachment_name
14
+ from .compatibility import override, path_relative_to
15
+ from .converter import ConfluenceDocument, get_volatile_attributes, get_volatile_elements
14
16
  from .csf import AC_ATTR, elements_from_string
15
- from .domain import ConfluenceDocumentOptions, ConfluencePageID
16
17
  from .environment import PageError
17
- from .extra import override, path_relative_to
18
18
  from .metadata import ConfluencePageMetadata
19
+ from .options import ConfluencePageID, DocumentOptions
19
20
  from .processor import Converter, DocumentNode, Processor, ProcessorFactory
20
21
  from .xml import is_xml_equal, unwrap_substitute
21
22
 
@@ -29,7 +30,7 @@ class SynchronizingProcessor(Processor):
29
30
 
30
31
  api: ConfluenceSession
31
32
 
32
- def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path) -> None:
33
+ def __init__(self, api: ConfluenceSession, options: DocumentOptions, root_dir: Path) -> None:
33
34
  """
34
35
  Initializes a new processor instance.
35
36
 
@@ -42,7 +43,7 @@ class SynchronizingProcessor(Processor):
42
43
  self.api = api
43
44
 
44
45
  @override
45
- def _synchronize_tree(self, root: DocumentNode, root_id: ConfluencePageID | None) -> None:
46
+ def _synchronize_tree(self, tree: DocumentNode, root_id: ConfluencePageID | None) -> None:
46
47
  """
47
48
  Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
48
49
 
@@ -51,21 +52,16 @@ class SynchronizingProcessor(Processor):
51
52
  Updates the original Markdown document to add tags to associate the document with its corresponding Confluence page.
52
53
  """
53
54
 
54
- if root.page_id is None and root_id is None:
55
- raise PageError(f"expected: root page ID in options, or explicit page ID in {root.absolute_path}")
56
- elif root.page_id is not None and root_id is not None:
57
- if root.page_id != root_id.page_id:
58
- raise PageError(f"mismatched inferred page ID of {root_id.page_id} and explicit page ID in {root.absolute_path}")
59
-
60
- real_id = root_id
55
+ if tree.page_id is None and root_id is None:
56
+ raise PageError(f"expected: root page ID in options, or explicit page ID in {tree.absolute_path}")
57
+ elif tree.page_id is not None:
58
+ real_id = ConfluencePageID(tree.page_id) # explicit page ID takes precedence
61
59
  elif root_id is not None:
62
60
  real_id = root_id
63
- elif root.page_id is not None:
64
- real_id = ConfluencePageID(root.page_id)
65
61
  else:
66
62
  raise NotImplementedError("condition not exhaustive")
67
63
 
68
- self._synchronize_subtree(root, real_id)
64
+ self._synchronize_subtree(tree, real_id)
69
65
 
70
66
  def _synchronize_subtree(self, node: DocumentNode, parent_id: ConfluencePageID) -> None:
71
67
  if node.page_id is not None:
@@ -81,6 +77,9 @@ class SynchronizingProcessor(Processor):
81
77
  digest = self._generate_hash(node.absolute_path)
82
78
  title = f"{node.absolute_path.stem} [{digest}]"
83
79
 
80
+ if self.options.title_prefix is not None:
81
+ title = f"{self.options.title_prefix} {title}"
82
+
84
83
  # look up page by (possibly auto-generated) title
85
84
  page = self.api.get_or_create_page(title, parent_id.page_id)
86
85
 
@@ -209,7 +208,7 @@ class SynchronizingProcessor(Processor):
209
208
  class SynchronizingProcessorFactory(ProcessorFactory):
210
209
  api: ConfluenceSession
211
210
 
212
- def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
211
+ def __init__(self, api: ConfluenceSession, options: DocumentOptions) -> None:
213
212
  super().__init__(options, api.site)
214
213
  self.api = api
215
214
 
@@ -224,5 +223,5 @@ class Publisher(Converter):
224
223
  This is the class instantiated by the command-line application.
225
224
  """
226
225
 
227
- def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
226
+ def __init__(self, api: ConfluenceSession, options: DocumentOptions) -> None:
228
227
  super().__init__(SynchronizingProcessorFactory(api, options))
md2conf/scanner.py CHANGED
@@ -1,55 +1,34 @@
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 re
10
- import typing
11
9
  from dataclasses import dataclass
12
10
  from pathlib import Path
13
- from typing import Any, Literal, TypeVar
11
+ from typing import TypeVar
14
12
 
15
- import yaml
16
-
17
- from .mermaid import MermaidConfigProperties
13
+ from .coalesce import coalesce
14
+ from .frontmatter import extract_frontmatter_json, extract_value
15
+ from .options import LayoutOptions
18
16
  from .serializer import JsonType, json_to_object
19
17
 
20
18
  T = TypeVar("T")
21
19
 
22
20
 
23
- def extract_value(pattern: str, text: str) -> tuple[str | None, str]:
24
- values: list[str] = []
25
-
26
- def _repl_func(matchobj: re.Match[str]) -> str:
27
- values.append(matchobj.group(1))
28
- return ""
29
-
30
- text = re.sub(pattern, _repl_func, text, count=1, flags=re.ASCII)
31
- value = values[0] if values else None
32
- return value, text
33
-
34
-
35
- def extract_frontmatter_block(text: str) -> tuple[str | None, str]:
36
- "Extracts the front-matter from a Markdown document as a blob of unparsed text."
37
-
38
- return extract_value(r"(?ms)\A---$(.+?)^---$", text)
39
-
40
-
41
- def extract_frontmatter_properties(text: str) -> tuple[dict[str, JsonType] | None, str]:
42
- "Extracts the front-matter from a Markdown document as a dictionary."
43
-
44
- block, text = extract_frontmatter_block(text)
21
+ @dataclass
22
+ class AliasProperties:
23
+ """
24
+ An object that holds properties extracted from the front-matter of a Markdown document.
45
25
 
46
- properties: dict[str, Any] | None = None
47
- if block is not None:
48
- data = yaml.safe_load(block)
49
- if isinstance(data, dict):
50
- properties = typing.cast(dict[str, JsonType], data)
26
+ :param confluence_page_id: Confluence page ID. (Alternative name for JSON de-serialization.)
27
+ :param confluence_space_key: Confluence space key. (Alternative name for JSON de-serialization.)
28
+ """
51
29
 
52
- return properties, text
30
+ confluence_page_id: str | None = None
31
+ confluence_space_key: str | None = None
53
32
 
54
33
 
55
34
  @dataclass
@@ -59,26 +38,22 @@ class DocumentProperties:
59
38
 
60
39
  :param page_id: Confluence page ID.
61
40
  :param space_key: Confluence space key.
62
- :param confluence_page_id: Confluence page ID. (Alternative name for JSON de-serialization.)
63
- :param confluence_space_key: Confluence space key. (Alternative name for JSON de-serialization.)
64
41
  :param generated_by: Text identifying the tool that generated the document.
65
42
  :param title: The title extracted from front-matter.
66
43
  :param tags: A list of tags (content labels) extracted from front-matter.
67
44
  :param synchronized: True if the document content is parsed and synchronized with Confluence.
68
45
  :param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
69
- :param alignment: Alignment for block-level images and formulas.
46
+ :param layout: Layout options for content on a Confluence page.
70
47
  """
71
48
 
72
49
  page_id: str | None = None
73
50
  space_key: str | None = None
74
- confluence_page_id: str | None = None
75
- confluence_space_key: str | None = None
76
51
  generated_by: str | None = None
77
52
  title: str | None = None
78
53
  tags: list[str] | None = None
79
54
  synchronized: bool | None = None
80
55
  properties: dict[str, JsonType] | None = None
81
- alignment: Literal["center", "left", "right"] | None = None
56
+ layout: LayoutOptions | None = None
82
57
 
83
58
 
84
59
  @dataclass
@@ -86,25 +61,11 @@ class ScannedDocument:
86
61
  """
87
62
  An object that holds properties extracted from a Markdown document, including remaining source text.
88
63
 
89
- :param page_id: Confluence page ID.
90
- :param space_key: Confluence space key.
91
- :param generated_by: Text identifying the tool that generated the document.
92
- :param title: The title extracted from front-matter.
93
- :param tags: A list of tags (content labels) extracted from front-matter.
94
- :param synchronized: True if the document content is parsed and synchronized with Confluence.
95
- :param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
96
- :param alignment: Alignment for block-level images and formulas.
64
+ :param properties: Properties extracted from the front-matter of a Markdown document.
97
65
  :param text: Text that remains after front-matter and inline properties have been extracted.
98
66
  """
99
67
 
100
- page_id: str | None
101
- space_key: str | None
102
- generated_by: str | None
103
- title: str | None
104
- tags: list[str] | None
105
- synchronized: bool | None
106
- properties: dict[str, JsonType] | None
107
- alignment: Literal["center", "left", "right"] | None
68
+ properties: DocumentProperties
108
69
  text: str
109
70
 
110
71
 
@@ -127,77 +88,19 @@ class Scanner:
127
88
  # extract 'generated-by' tag text
128
89
  generated_by, text = extract_value(r"<!--\s+generated[-_]by:\s*(.*)\s+-->", text)
129
90
 
130
- title: str | None = None
131
- tags: list[str] | None = None
132
- synchronized: bool | None = None
133
- properties: dict[str, JsonType] | None = None
134
- alignment: Literal["center", "left", "right"] | None = None
91
+ body_props = DocumentProperties(page_id=page_id, space_key=space_key, generated_by=generated_by)
135
92
 
136
93
  # extract front-matter
137
- data, text = extract_frontmatter_properties(text)
94
+ data, text = extract_frontmatter_json(text)
138
95
  if data is not None:
139
- p = json_to_object(DocumentProperties, data)
140
- page_id = page_id or p.confluence_page_id or p.page_id
141
- space_key = space_key or p.confluence_space_key or p.space_key
142
- generated_by = generated_by or p.generated_by
143
- title = p.title
144
- tags = p.tags
145
- synchronized = p.synchronized
146
- properties = p.properties
147
- alignment = p.alignment
148
-
149
- return ScannedDocument(
150
- page_id=page_id,
151
- space_key=space_key,
152
- generated_by=generated_by,
153
- title=title,
154
- tags=tags,
155
- synchronized=synchronized,
156
- properties=properties,
157
- alignment=alignment,
158
- text=text,
159
- )
160
-
161
-
162
- @dataclass
163
- class MermaidProperties:
164
- """
165
- An object that holds the front-matter properties structure for Mermaid diagrams.
166
-
167
- :param title: The title of the diagram.
168
- :param config: Configuration options for rendering.
169
- """
170
-
171
- title: str | None = None
172
- config: MermaidConfigProperties | None = None
173
-
174
-
175
- class MermaidScanner:
176
- """
177
- Extracts properties from the JSON/YAML front-matter of a Mermaid diagram.
178
- """
179
-
180
- def read(self, content: str) -> MermaidProperties:
181
- """
182
- Extracts rendering preferences from a Mermaid front-matter content.
183
-
184
- ```
185
- ---
186
- title: Tiny flow diagram
187
- config:
188
- scale: 1
189
- ---
190
- flowchart LR
191
- A[Component A] --> B[Component B]
192
- B --> C[Component C]
193
- ```
194
- """
195
-
196
- properties, _ = extract_frontmatter_properties(content)
197
- if properties is not None:
198
- front_matter = json_to_object(MermaidProperties, properties)
199
- config = front_matter.config or MermaidConfigProperties()
200
-
201
- return MermaidProperties(title=front_matter.title, config=config)
202
-
203
- return MermaidProperties()
96
+ frontmatter_props = json_to_object(DocumentProperties, data)
97
+ alias_props = json_to_object(AliasProperties, data)
98
+ if alias_props.confluence_page_id is not None:
99
+ frontmatter_props.page_id = alias_props.confluence_page_id
100
+ if alias_props.confluence_space_key is not None:
101
+ frontmatter_props.space_key = alias_props.confluence_space_key
102
+ props = coalesce(body_props, frontmatter_props)
103
+ else:
104
+ props = body_props
105
+
106
+ return ScannedDocument(properties=props, text=text)
md2conf/serializer.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
  """
@@ -10,7 +10,7 @@ import sys
10
10
  from datetime import datetime
11
11
  from typing import TypeVar
12
12
 
13
- from cattrs.preconf.orjson import make_converter
13
+ from cattrs.preconf.orjson import make_converter # spellchecker:disable-line
14
14
 
15
15
  JsonType = None | bool | int | float | str | dict[str, "JsonType"] | list["JsonType"]
16
16
  JsonComposite = dict[str, "JsonType"] | list["JsonType"]