markdown-to-confluence 0.2.7__py3-none-any.whl → 0.3.1__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.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/METADATA +44 -7
- markdown_to_confluence-0.3.1.dist-info/RECORD +20 -0
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +2 -2
- md2conf/__main__.py +12 -4
- md2conf/api.py +203 -146
- md2conf/application.py +27 -19
- md2conf/converter.py +38 -45
- md2conf/emoji.py +1 -1
- md2conf/matcher.py +11 -6
- md2conf/mermaid.py +6 -2
- md2conf/processor.py +7 -7
- md2conf/properties.py +5 -7
- markdown_to_confluence-0.2.7.dist-info/RECORD +0 -21
- md2conf/util.py +0 -27
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/LICENSE +0 -0
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/zip-safe +0 -0
md2conf/application.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Publish Markdown files to Confluence wiki.
|
|
3
3
|
|
|
4
|
-
Copyright 2022-
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
@@ -9,7 +9,7 @@ Copyright 2022-2024, Levente Hunyadi
|
|
|
9
9
|
import logging
|
|
10
10
|
import os.path
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import
|
|
12
|
+
from typing import Optional
|
|
13
13
|
|
|
14
14
|
from .api import ConfluencePage, ConfluenceSession
|
|
15
15
|
from .converter import (
|
|
@@ -77,7 +77,7 @@ class Application:
|
|
|
77
77
|
LOGGER.info("Synchronizing directory: %s", local_dir)
|
|
78
78
|
|
|
79
79
|
# Step 1: build index of all page metadata
|
|
80
|
-
page_metadata:
|
|
80
|
+
page_metadata: dict[Path, ConfluencePageMetadata] = {}
|
|
81
81
|
root_id = (
|
|
82
82
|
ConfluenceQualifiedID(self.options.root_page_id, self.api.space_key)
|
|
83
83
|
if self.options.root_page_id
|
|
@@ -94,24 +94,19 @@ class Application:
|
|
|
94
94
|
self,
|
|
95
95
|
page_path: Path,
|
|
96
96
|
root_dir: Path,
|
|
97
|
-
page_metadata:
|
|
97
|
+
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
98
98
|
) -> None:
|
|
99
99
|
base_path = page_path.parent
|
|
100
100
|
|
|
101
101
|
LOGGER.info("Synchronizing page: %s", page_path)
|
|
102
102
|
document = ConfluenceDocument(page_path, self.options, root_dir, page_metadata)
|
|
103
|
-
|
|
104
|
-
if document.id.space_key:
|
|
105
|
-
with self.api.switch_space(document.id.space_key):
|
|
106
|
-
self._update_document(document, base_path)
|
|
107
|
-
else:
|
|
108
|
-
self._update_document(document, base_path)
|
|
103
|
+
self._update_document(document, base_path)
|
|
109
104
|
|
|
110
105
|
def _index_directory(
|
|
111
106
|
self,
|
|
112
107
|
local_dir: Path,
|
|
113
108
|
root_id: Optional[ConfluenceQualifiedID],
|
|
114
|
-
page_metadata:
|
|
109
|
+
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
115
110
|
) -> None:
|
|
116
111
|
"Indexes Markdown files in a directory recursively."
|
|
117
112
|
|
|
@@ -119,8 +114,8 @@ class Application:
|
|
|
119
114
|
|
|
120
115
|
matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
|
|
121
116
|
|
|
122
|
-
files:
|
|
123
|
-
directories:
|
|
117
|
+
files: list[Path] = []
|
|
118
|
+
directories: list[Path] = []
|
|
124
119
|
for entry in os.scandir(local_dir):
|
|
125
120
|
if matcher.is_excluded(entry.name, entry.is_dir()):
|
|
126
121
|
continue
|
|
@@ -136,6 +131,15 @@ class Application:
|
|
|
136
131
|
parent_doc = Path(local_dir) / "index.md"
|
|
137
132
|
elif (Path(local_dir) / "README.md") in files:
|
|
138
133
|
parent_doc = Path(local_dir) / "README.md"
|
|
134
|
+
elif (Path(local_dir) / f"{local_dir.name}.md") in files:
|
|
135
|
+
parent_doc = Path(local_dir) / f"{local_dir.name}.md"
|
|
136
|
+
|
|
137
|
+
if parent_doc is None and self.options.keep_hierarchy:
|
|
138
|
+
parent_doc = Path(local_dir) / "index.md"
|
|
139
|
+
|
|
140
|
+
# create a blank page in Confluence for the directory entry
|
|
141
|
+
with open(parent_doc, "w"):
|
|
142
|
+
pass
|
|
139
143
|
|
|
140
144
|
if parent_doc is not None:
|
|
141
145
|
files.remove(parent_doc)
|
|
@@ -175,9 +179,7 @@ class Application:
|
|
|
175
179
|
frontmatter_title, _ = extract_frontmatter_title(document)
|
|
176
180
|
|
|
177
181
|
if qualified_id is not None:
|
|
178
|
-
confluence_page = self.api.get_page(
|
|
179
|
-
qualified_id.page_id, space_key=qualified_id.space_key
|
|
180
|
-
)
|
|
182
|
+
confluence_page = self.api.get_page(qualified_id.page_id)
|
|
181
183
|
else:
|
|
182
184
|
if parent_id is None:
|
|
183
185
|
raise ValueError(
|
|
@@ -189,11 +191,17 @@ class Application:
|
|
|
189
191
|
absolute_path, document, title or frontmatter_title, parent_id
|
|
190
192
|
)
|
|
191
193
|
|
|
194
|
+
space_key = (
|
|
195
|
+
self.api.space_id_to_key(confluence_page.space_id)
|
|
196
|
+
if confluence_page.space_id
|
|
197
|
+
else self.api.space_key
|
|
198
|
+
)
|
|
199
|
+
|
|
192
200
|
return ConfluencePageMetadata(
|
|
193
201
|
domain=self.api.domain,
|
|
194
202
|
base_path=self.api.base_path,
|
|
195
203
|
page_id=confluence_page.id,
|
|
196
|
-
space_key=
|
|
204
|
+
space_key=space_key,
|
|
197
205
|
title=confluence_page.title or "",
|
|
198
206
|
)
|
|
199
207
|
|
|
@@ -217,7 +225,7 @@ class Application:
|
|
|
217
225
|
absolute_path,
|
|
218
226
|
document,
|
|
219
227
|
confluence_page.id,
|
|
220
|
-
confluence_page.
|
|
228
|
+
self.api.space_id_to_key(confluence_page.space_id),
|
|
221
229
|
)
|
|
222
230
|
return confluence_page
|
|
223
231
|
|
|
@@ -251,7 +259,7 @@ class Application:
|
|
|
251
259
|
) -> None:
|
|
252
260
|
"Writes the Confluence page ID and space key at the beginning of the Markdown file."
|
|
253
261
|
|
|
254
|
-
content:
|
|
262
|
+
content: list[str] = []
|
|
255
263
|
|
|
256
264
|
# check if the file has frontmatter
|
|
257
265
|
index = 0
|
md2conf/converter.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Publish Markdown files to Confluence wiki.
|
|
3
3
|
|
|
4
|
-
Copyright 2022-
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
@@ -13,12 +13,11 @@ import importlib.resources as resources
|
|
|
13
13
|
import logging
|
|
14
14
|
import os.path
|
|
15
15
|
import re
|
|
16
|
-
import sys
|
|
17
16
|
import uuid
|
|
18
17
|
import xml.etree.ElementTree
|
|
19
18
|
from dataclasses import dataclass
|
|
20
19
|
from pathlib import Path
|
|
21
|
-
from typing import Any,
|
|
20
|
+
from typing import Any, Literal, Optional, Union
|
|
22
21
|
from urllib.parse import ParseResult, urlparse, urlunparse
|
|
23
22
|
|
|
24
23
|
import lxml.etree as ET
|
|
@@ -46,7 +45,7 @@ class ParseError(RuntimeError):
|
|
|
46
45
|
pass
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
def starts_with_any(text: str, prefixes:
|
|
48
|
+
def starts_with_any(text: str, prefixes: list[str]) -> bool:
|
|
50
49
|
"True if text starts with any of the listed prefixes."
|
|
51
50
|
|
|
52
51
|
for prefix in prefixes:
|
|
@@ -73,7 +72,7 @@ def emoji_generator(
|
|
|
73
72
|
alt: str,
|
|
74
73
|
title: Optional[str],
|
|
75
74
|
category: Optional[str],
|
|
76
|
-
options:
|
|
75
|
+
options: dict[str, Any],
|
|
77
76
|
md: markdown.Markdown,
|
|
78
77
|
) -> xml.etree.ElementTree.Element:
|
|
79
78
|
name = (alias or shortname).strip(":")
|
|
@@ -107,7 +106,7 @@ def markdown_to_html(content: str) -> str:
|
|
|
107
106
|
)
|
|
108
107
|
|
|
109
108
|
|
|
110
|
-
def _elements_from_strings(dtd_path: Path, items:
|
|
109
|
+
def _elements_from_strings(dtd_path: Path, items: list[str]) -> ET._Element:
|
|
111
110
|
"""
|
|
112
111
|
Creates a fragment of several XML nodes from their string representation wrapped in a root element.
|
|
113
112
|
|
|
@@ -129,7 +128,7 @@ def _elements_from_strings(dtd_path: Path, items: List[str]) -> ET._Element:
|
|
|
129
128
|
|
|
130
129
|
data = [
|
|
131
130
|
'<?xml version="1.0"?>',
|
|
132
|
-
f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path}">'
|
|
131
|
+
f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path.as_posix()}">'
|
|
133
132
|
f"<root{ns_attr_list}>",
|
|
134
133
|
]
|
|
135
134
|
data.extend(items)
|
|
@@ -141,16 +140,12 @@ def _elements_from_strings(dtd_path: Path, items: List[str]) -> ET._Element:
|
|
|
141
140
|
raise ParseError(e)
|
|
142
141
|
|
|
143
142
|
|
|
144
|
-
def elements_from_strings(items:
|
|
143
|
+
def elements_from_strings(items: list[str]) -> ET._Element:
|
|
145
144
|
"Creates a fragment of several XML nodes from their string representation wrapped in a root element."
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return _elements_from_strings(dtd_path, items)
|
|
151
|
-
else:
|
|
152
|
-
with resources.path(__package__, "entities.dtd") as dtd_path:
|
|
153
|
-
return _elements_from_strings(dtd_path, items)
|
|
146
|
+
resource_path = resources.files(__package__).joinpath("entities.dtd")
|
|
147
|
+
with resources.as_file(resource_path) as dtd_path:
|
|
148
|
+
return _elements_from_strings(dtd_path, items)
|
|
154
149
|
|
|
155
150
|
|
|
156
151
|
def elements_from_string(content: str) -> ET._Element:
|
|
@@ -244,7 +239,7 @@ class ConfluencePageMetadata:
|
|
|
244
239
|
domain: str
|
|
245
240
|
base_path: str
|
|
246
241
|
page_id: str
|
|
247
|
-
space_key: str
|
|
242
|
+
space_key: Optional[str]
|
|
248
243
|
title: str
|
|
249
244
|
|
|
250
245
|
|
|
@@ -287,7 +282,7 @@ class ConfluenceConverterOptions:
|
|
|
287
282
|
conversion rules for the identifier.
|
|
288
283
|
:param render_mermaid: Whether to pre-render Mermaid diagrams into PNG/SVG images.
|
|
289
284
|
:param diagram_output_format: Target image format for diagrams.
|
|
290
|
-
:param
|
|
285
|
+
:param webui_links: When true, convert relative URLs to Confluence Web UI links.
|
|
291
286
|
"""
|
|
292
287
|
|
|
293
288
|
ignore_invalid_url: bool = False
|
|
@@ -304,17 +299,17 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
304
299
|
path: Path
|
|
305
300
|
base_dir: Path
|
|
306
301
|
root_dir: Path
|
|
307
|
-
links:
|
|
308
|
-
images:
|
|
309
|
-
embedded_images:
|
|
310
|
-
page_metadata:
|
|
302
|
+
links: list[str]
|
|
303
|
+
images: list[Path]
|
|
304
|
+
embedded_images: dict[str, bytes]
|
|
305
|
+
page_metadata: dict[Path, ConfluencePageMetadata]
|
|
311
306
|
|
|
312
307
|
def __init__(
|
|
313
308
|
self,
|
|
314
309
|
options: ConfluenceConverterOptions,
|
|
315
310
|
path: Path,
|
|
316
311
|
root_dir: Path,
|
|
317
|
-
page_metadata:
|
|
312
|
+
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
318
313
|
) -> None:
|
|
319
314
|
super().__init__()
|
|
320
315
|
self.options = options
|
|
@@ -438,7 +433,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
438
433
|
if not src:
|
|
439
434
|
raise DocumentError("image lacks `src` attribute")
|
|
440
435
|
|
|
441
|
-
attributes:
|
|
436
|
+
attributes: dict[str, Any] = {
|
|
442
437
|
ET.QName(namespaces["ac"], "align"): "center",
|
|
443
438
|
ET.QName(namespaces["ac"], "layout"): "center",
|
|
444
439
|
}
|
|
@@ -457,11 +452,11 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
457
452
|
return self._transform_attached_image(Path(src), caption, attributes)
|
|
458
453
|
|
|
459
454
|
def _transform_external_image(
|
|
460
|
-
self, url: str, caption: Optional[str], attributes:
|
|
455
|
+
self, url: str, caption: Optional[str], attributes: dict[str, Any]
|
|
461
456
|
) -> ET._Element:
|
|
462
457
|
"Emits Confluence Storage Format XHTML for an external image."
|
|
463
458
|
|
|
464
|
-
elements:
|
|
459
|
+
elements: list[ET._Element] = []
|
|
465
460
|
elements.append(
|
|
466
461
|
RI(
|
|
467
462
|
"url",
|
|
@@ -475,7 +470,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
475
470
|
return AC("image", attributes, *elements)
|
|
476
471
|
|
|
477
472
|
def _transform_attached_image(
|
|
478
|
-
self, path: Path, caption: Optional[str], attributes:
|
|
473
|
+
self, path: Path, caption: Optional[str], attributes: dict[str, Any]
|
|
479
474
|
) -> ET._Element:
|
|
480
475
|
"Emits Confluence Storage Format XHTML for an attached image."
|
|
481
476
|
|
|
@@ -487,7 +482,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
487
482
|
self.images.append(path)
|
|
488
483
|
image_name = attachment_name(path)
|
|
489
484
|
|
|
490
|
-
elements:
|
|
485
|
+
elements: list[ET._Element] = []
|
|
491
486
|
elements.append(
|
|
492
487
|
RI(
|
|
493
488
|
"attachment",
|
|
@@ -525,7 +520,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
525
520
|
AC(
|
|
526
521
|
"parameter",
|
|
527
522
|
{ET.QName(namespaces["ac"], "name"): "theme"},
|
|
528
|
-
"
|
|
523
|
+
"Default",
|
|
529
524
|
),
|
|
530
525
|
AC(
|
|
531
526
|
"parameter",
|
|
@@ -899,8 +894,8 @@ class DocumentError(RuntimeError):
|
|
|
899
894
|
pass
|
|
900
895
|
|
|
901
896
|
|
|
902
|
-
def extract_value(pattern: str, text: str) ->
|
|
903
|
-
values:
|
|
897
|
+
def extract_value(pattern: str, text: str) -> tuple[Optional[str], str]:
|
|
898
|
+
values: list[str] = []
|
|
904
899
|
|
|
905
900
|
def _repl_func(matchobj: re.Match) -> str:
|
|
906
901
|
values.append(matchobj.group(1))
|
|
@@ -921,7 +916,7 @@ class ConfluenceQualifiedID:
|
|
|
921
916
|
self.space_key = space_key
|
|
922
917
|
|
|
923
918
|
|
|
924
|
-
def extract_qualified_id(text: str) ->
|
|
919
|
+
def extract_qualified_id(text: str) -> tuple[Optional[ConfluenceQualifiedID], str]:
|
|
925
920
|
"Extracts the Confluence page ID and space key from a Markdown document."
|
|
926
921
|
|
|
927
922
|
page_id, text = extract_value(r"<!--\s+confluence-page-id:\s*(\d+)\s+-->", text)
|
|
@@ -935,13 +930,13 @@ def extract_qualified_id(text: str) -> Tuple[Optional[ConfluenceQualifiedID], st
|
|
|
935
930
|
return ConfluenceQualifiedID(page_id, space_key), text
|
|
936
931
|
|
|
937
932
|
|
|
938
|
-
def extract_frontmatter(text: str) ->
|
|
933
|
+
def extract_frontmatter(text: str) -> tuple[Optional[str], str]:
|
|
939
934
|
"Extracts the front matter from a Markdown document."
|
|
940
935
|
|
|
941
936
|
return extract_value(r"(?ms)\A---$(.+?)^---$", text)
|
|
942
937
|
|
|
943
938
|
|
|
944
|
-
def extract_frontmatter_title(text: str) ->
|
|
939
|
+
def extract_frontmatter_title(text: str) -> tuple[Optional[str], str]:
|
|
945
940
|
frontmatter, text = extract_frontmatter(text)
|
|
946
941
|
|
|
947
942
|
title: Optional[str] = None
|
|
@@ -974,8 +969,9 @@ class ConfluenceDocumentOptions:
|
|
|
974
969
|
plain text; when false, raise an exception.
|
|
975
970
|
:param heading_anchors: When true, emit a structured macro *anchor* for each section heading using GitHub
|
|
976
971
|
conversion rules for the identifier.
|
|
977
|
-
:param generated_by: Text to use as the generated-by prompt.
|
|
978
|
-
:param
|
|
972
|
+
:param generated_by: Text to use as the generated-by prompt (or `None` to omit a prompt).
|
|
973
|
+
:param root_page_id: Confluence page to assume root page role for publishing a directory of Markdown files.
|
|
974
|
+
:param keep_hierarchy: Whether to maintain source directory structure when exporting to Confluence.
|
|
979
975
|
:param render_mermaid: Whether to pre-render Mermaid diagrams into PNG/SVG images.
|
|
980
976
|
:param diagram_output_format: Target image format for diagrams.
|
|
981
977
|
:param webui_links: When true, convert relative URLs to Confluence Web UI links.
|
|
@@ -985,6 +981,7 @@ class ConfluenceDocumentOptions:
|
|
|
985
981
|
heading_anchors: bool = False
|
|
986
982
|
generated_by: Optional[str] = "This page has been generated with a tool."
|
|
987
983
|
root_page_id: Optional[str] = None
|
|
984
|
+
keep_hierarchy: bool = False
|
|
988
985
|
render_mermaid: bool = False
|
|
989
986
|
diagram_output_format: Literal["png", "svg"] = "png"
|
|
990
987
|
webui_links: bool = False
|
|
@@ -993,8 +990,8 @@ class ConfluenceDocumentOptions:
|
|
|
993
990
|
class ConfluenceDocument:
|
|
994
991
|
id: ConfluenceQualifiedID
|
|
995
992
|
title: Optional[str]
|
|
996
|
-
links:
|
|
997
|
-
images:
|
|
993
|
+
links: list[str]
|
|
994
|
+
images: list[Path]
|
|
998
995
|
|
|
999
996
|
options: ConfluenceDocumentOptions
|
|
1000
997
|
root: ET._Element
|
|
@@ -1004,7 +1001,7 @@ class ConfluenceDocument:
|
|
|
1004
1001
|
path: Path,
|
|
1005
1002
|
options: ConfluenceDocumentOptions,
|
|
1006
1003
|
root_dir: Path,
|
|
1007
|
-
page_metadata:
|
|
1004
|
+
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
1008
1005
|
) -> None:
|
|
1009
1006
|
self.options = options
|
|
1010
1007
|
path = path.resolve(True)
|
|
@@ -1132,10 +1129,6 @@ def _content_to_string(dtd_path: Path, content: str) -> str:
|
|
|
1132
1129
|
def content_to_string(content: str) -> str:
|
|
1133
1130
|
"Converts a Confluence Storage Format document returned by the API into a readable XML document."
|
|
1134
1131
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
return _content_to_string(dtd_path, content)
|
|
1139
|
-
else:
|
|
1140
|
-
with resources.path(__package__, "entities.dtd") as dtd_path:
|
|
1141
|
-
return _content_to_string(dtd_path, content)
|
|
1132
|
+
resource_path = resources.files(__package__).joinpath("entities.dtd")
|
|
1133
|
+
with resources.as_file(resource_path) as dtd_path:
|
|
1134
|
+
return _content_to_string(dtd_path, content)
|
md2conf/emoji.py
CHANGED
md2conf/matcher.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Publish Markdown files to Confluence wiki.
|
|
3
3
|
|
|
4
|
-
Copyright 2022-
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
@@ -10,12 +10,17 @@ import os.path
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from fnmatch import fnmatch
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Iterable,
|
|
13
|
+
from typing import Iterable, Optional
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass
|
|
17
17
|
class Entry:
|
|
18
|
-
"
|
|
18
|
+
"""
|
|
19
|
+
Represents a file or directory entry.
|
|
20
|
+
|
|
21
|
+
:param name: Name of the file-system entry.
|
|
22
|
+
:param is_dir: True if the entry is a directory.
|
|
23
|
+
"""
|
|
19
24
|
|
|
20
25
|
name: str
|
|
21
26
|
is_dir: bool
|
|
@@ -42,7 +47,7 @@ class Matcher:
|
|
|
42
47
|
"Compares file and directory names against a list of exclude/include patterns."
|
|
43
48
|
|
|
44
49
|
options: MatcherOptions
|
|
45
|
-
rules:
|
|
50
|
+
rules: list[str]
|
|
46
51
|
|
|
47
52
|
def __init__(self, options: MatcherOptions, directory: Path) -> None:
|
|
48
53
|
self.options = options
|
|
@@ -92,7 +97,7 @@ class Matcher:
|
|
|
92
97
|
|
|
93
98
|
return not self.is_excluded(name, is_dir)
|
|
94
99
|
|
|
95
|
-
def filter(self, items: Iterable[Entry]) ->
|
|
100
|
+
def filter(self, items: Iterable[Entry]) -> list[Entry]:
|
|
96
101
|
"""
|
|
97
102
|
Returns only those elements from the input that don't match any of the exclusion rules.
|
|
98
103
|
|
|
@@ -102,7 +107,7 @@ class Matcher:
|
|
|
102
107
|
|
|
103
108
|
return [item for item in items if self.is_included(item.name, item.is_dir)]
|
|
104
109
|
|
|
105
|
-
def scandir(self, path: Path) ->
|
|
110
|
+
def scandir(self, path: Path) -> list[Entry]:
|
|
106
111
|
"""
|
|
107
112
|
Returns only those entries in a directory whose name doesn't match any of the exclusion rules.
|
|
108
113
|
|
md2conf/mermaid.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Publish Markdown files to Confluence wiki.
|
|
3
3
|
|
|
4
|
-
Copyright 2022-
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
@@ -29,7 +29,11 @@ def get_mmdc() -> str:
|
|
|
29
29
|
"Path to the Mermaid diagram converter."
|
|
30
30
|
|
|
31
31
|
if is_docker():
|
|
32
|
-
|
|
32
|
+
full_path = "/home/md2conf/node_modules/.bin/mmdc"
|
|
33
|
+
if os.path.exists(full_path):
|
|
34
|
+
return full_path
|
|
35
|
+
else:
|
|
36
|
+
return "mmdc"
|
|
33
37
|
elif os.name == "nt":
|
|
34
38
|
return "mmdc.cmd"
|
|
35
39
|
else:
|
md2conf/processor.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Publish Markdown files to Confluence wiki.
|
|
3
3
|
|
|
4
|
-
Copyright 2022-
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
@@ -10,7 +10,7 @@ import hashlib
|
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import Optional
|
|
14
14
|
|
|
15
15
|
from .converter import (
|
|
16
16
|
ConfluenceDocument,
|
|
@@ -60,7 +60,7 @@ class Processor:
|
|
|
60
60
|
LOGGER.info("Synchronizing directory: %s", local_dir)
|
|
61
61
|
|
|
62
62
|
# Step 1: build index of all page metadata
|
|
63
|
-
page_metadata:
|
|
63
|
+
page_metadata: dict[Path, ConfluencePageMetadata] = {}
|
|
64
64
|
self._index_directory(local_dir, page_metadata)
|
|
65
65
|
LOGGER.info("Indexed %d page(s)", len(page_metadata))
|
|
66
66
|
|
|
@@ -83,7 +83,7 @@ class Processor:
|
|
|
83
83
|
self,
|
|
84
84
|
path: Path,
|
|
85
85
|
root_dir: Path,
|
|
86
|
-
page_metadata:
|
|
86
|
+
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
87
87
|
) -> None:
|
|
88
88
|
"Processes a single Markdown file."
|
|
89
89
|
|
|
@@ -95,7 +95,7 @@ class Processor:
|
|
|
95
95
|
def _index_directory(
|
|
96
96
|
self,
|
|
97
97
|
local_dir: Path,
|
|
98
|
-
page_metadata:
|
|
98
|
+
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
99
99
|
) -> None:
|
|
100
100
|
"Indexes Markdown files in a directory recursively."
|
|
101
101
|
|
|
@@ -103,8 +103,8 @@ class Processor:
|
|
|
103
103
|
|
|
104
104
|
matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
|
|
105
105
|
|
|
106
|
-
files:
|
|
107
|
-
directories:
|
|
106
|
+
files: list[Path] = []
|
|
107
|
+
directories: list[Path] = []
|
|
108
108
|
for entry in os.scandir(local_dir):
|
|
109
109
|
if matcher.is_excluded(entry.name, entry.is_dir()):
|
|
110
110
|
continue
|
md2conf/properties.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Publish Markdown files to Confluence wiki.
|
|
3
3
|
|
|
4
|
-
Copyright 2022-
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import Optional
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class ConfluenceError(RuntimeError):
|
|
@@ -17,10 +17,10 @@ class ConfluenceError(RuntimeError):
|
|
|
17
17
|
class ConfluenceProperties:
|
|
18
18
|
domain: str
|
|
19
19
|
base_path: str
|
|
20
|
-
space_key: str
|
|
20
|
+
space_key: Optional[str]
|
|
21
21
|
user_name: Optional[str]
|
|
22
22
|
api_key: str
|
|
23
|
-
headers: Optional[
|
|
23
|
+
headers: Optional[dict[str, str]]
|
|
24
24
|
|
|
25
25
|
def __init__(
|
|
26
26
|
self,
|
|
@@ -29,7 +29,7 @@ class ConfluenceProperties:
|
|
|
29
29
|
user_name: Optional[str] = None,
|
|
30
30
|
api_key: Optional[str] = None,
|
|
31
31
|
space_key: Optional[str] = None,
|
|
32
|
-
headers: Optional[
|
|
32
|
+
headers: Optional[dict[str, str]] = None,
|
|
33
33
|
) -> None:
|
|
34
34
|
opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
|
|
35
35
|
opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
|
|
@@ -43,8 +43,6 @@ class ConfluenceProperties:
|
|
|
43
43
|
opt_base_path = "/wiki/"
|
|
44
44
|
if not opt_api_key:
|
|
45
45
|
raise ConfluenceError("Confluence API key not specified")
|
|
46
|
-
if not opt_space_key:
|
|
47
|
-
raise ConfluenceError("Confluence space key not specified")
|
|
48
46
|
|
|
49
47
|
if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
|
|
50
48
|
raise ConfluenceError(
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
md2conf/__init__.py,sha256=U8zdop7-AIrfwCYzWiwKfhCEPF_1QEKPt4Zwq-38LlU,402
|
|
2
|
-
md2conf/__main__.py,sha256=6iOI28W_d71tlnCMFpZwvkBmBt5-HazlZsz69gS4Oak,6894
|
|
3
|
-
md2conf/api.py,sha256=NmAbNWTrTSi2ZDGYymy70Fw6HcgrmB-Ua4re4yLJvVc,17715
|
|
4
|
-
md2conf/application.py,sha256=-kFpMRtSpQUU1hsiW5O73gL1X9McQWpvyAAEUxEnpuU,8869
|
|
5
|
-
md2conf/converter.py,sha256=S8Kka35Y99w0J00CYi-DQwsKzlHAvBfaSCf10mb1FZk,36596
|
|
6
|
-
md2conf/emoji.py,sha256=w9oiOIxzObAE7HTo3f6aETT1_D3t3yZwr88ynU4ENm0,1924
|
|
7
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
8
|
-
md2conf/matcher.py,sha256=mYMltZOLypK4O-SJugLgicOwUMem67hiNLg_kPFoJkU,3583
|
|
9
|
-
md2conf/mermaid.py,sha256=gqA6Hg6WcPDdR7JOClezAgNZj2Gq4pXJSgmOUlUt6Dk,2192
|
|
10
|
-
md2conf/processor.py,sha256=E-Na-a8tNp4CaoRPA5etcXdHXNRdgyMrf6bfKa9P7O4,4781
|
|
11
|
-
md2conf/properties.py,sha256=iVIc0h0XtS3Y2LCywX1C9cvmVQ0WljOMt8pl2MDMVCI,1990
|
|
12
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
13
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
md2conf/util.py,sha256=ftf60MiW7S7rW45ipWX6efP_Sv2F2qpyIDHrGA0cBiw,743
|
|
15
|
-
markdown_to_confluence-0.2.7.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
16
|
-
markdown_to_confluence-0.2.7.dist-info/METADATA,sha256=76K_O_5b__MnKT7FuLXgCHX6hR5dZio3mK6RWR4DyCA,13551
|
|
17
|
-
markdown_to_confluence-0.2.7.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
18
|
-
markdown_to_confluence-0.2.7.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
19
|
-
markdown_to_confluence-0.2.7.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
20
|
-
markdown_to_confluence-0.2.7.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
21
|
-
markdown_to_confluence-0.2.7.dist-info/RECORD,,
|
md2conf/util.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Publish Markdown files to Confluence wiki.
|
|
3
|
-
|
|
4
|
-
Copyright 2022-2024, Levente Hunyadi
|
|
5
|
-
|
|
6
|
-
:see: https://github.com/hunyadi/md2conf
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
if sys.version_info >= (3, 9):
|
|
12
|
-
|
|
13
|
-
def removeprefix(string: str, prefix: str) -> str:
|
|
14
|
-
"If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
|
|
15
|
-
|
|
16
|
-
return string.removeprefix(prefix)
|
|
17
|
-
|
|
18
|
-
else:
|
|
19
|
-
|
|
20
|
-
def removeprefix(string: str, prefix: str) -> str:
|
|
21
|
-
"If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
|
|
22
|
-
|
|
23
|
-
if string.startswith(prefix):
|
|
24
|
-
prefix_len = len(prefix)
|
|
25
|
-
return string[prefix_len:]
|
|
26
|
-
else:
|
|
27
|
-
return string
|
|
File without changes
|
{markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|