markdown-to-confluence 0.2.3__py3-none-any.whl → 0.2.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.
- {markdown_to_confluence-0.2.3.dist-info → markdown_to_confluence-0.2.4.dist-info}/METADATA +9 -1
- markdown_to_confluence-0.2.4.dist-info/RECORD +21 -0
- {markdown_to_confluence-0.2.3.dist-info → markdown_to_confluence-0.2.4.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/application.py +17 -4
- md2conf/converter.py +38 -19
- md2conf/mermaid.py +2 -3
- md2conf/processor.py +2 -2
- markdown_to_confluence-0.2.3.dist-info/RECORD +0 -21
- {markdown_to_confluence-0.2.3.dist-info → markdown_to_confluence-0.2.4.dist-info}/LICENSE +0 -0
- {markdown_to_confluence-0.2.3.dist-info → markdown_to_confluence-0.2.4.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.2.3.dist-info → markdown_to_confluence-0.2.4.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.2.3.dist-info → markdown_to_confluence-0.2.4.dist-info}/zip-safe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -26,6 +26,8 @@ Requires-Dist: types-lxml>=2024.8.7
|
|
|
26
26
|
Requires-Dist: markdown>=3.6
|
|
27
27
|
Requires-Dist: types-markdown>=3.6
|
|
28
28
|
Requires-Dist: pymdown-extensions>=10.9
|
|
29
|
+
Requires-Dist: pyyaml>=6.0
|
|
30
|
+
Requires-Dist: types-PyYAML>=6.0
|
|
29
31
|
Requires-Dist: requests>=2.32
|
|
30
32
|
Requires-Dist: types-requests>=2.32
|
|
31
33
|
|
|
@@ -144,6 +146,12 @@ Provide generated-by prompt text in the Markdown file with a tag:
|
|
|
144
146
|
|
|
145
147
|
Alternatively, use the `--generated-by GENERATED_BY` option. The tag takes precedence.
|
|
146
148
|
|
|
149
|
+
### Publishing a directory
|
|
150
|
+
|
|
151
|
+
*md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file if you pass a directory as `mdpath`. This will traverse the specified directory recursively, and synchronize each Markdown file.
|
|
152
|
+
|
|
153
|
+
If a Markdown file doesn't yet pair up with a Confluence page, *md2conf* creates a new page and assigns a parent. Parent-child relationships are reflected in the navigation panel in Confluence. You can set a root page ID with the command-line option `-r`, which constitutes the topmost parent. (This could correspond to the landing page of your Confluence space. The Confluence page ID is always revealed when you edit a page.) Whenever a directory contains the file `index.md` or `README.md`, this page becomes the future parent page, and all Markdown files in this directory (and possibly nested directories) become its child pages (unless they already have a page ID). However, if an `index.md` or `README.md` file is subsequently found in one of the nested directories, it becomes the parent page of that directory, and any of its subdirectories.
|
|
154
|
+
|
|
147
155
|
### Ignoring files
|
|
148
156
|
|
|
149
157
|
Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
md2conf/__init__.py,sha256=zCKYQvETObXjgxGYFlwiftPJL64cqwfEW3PGriejyh4,402
|
|
2
|
+
md2conf/__main__.py,sha256=_qUspNQmQdhpH4Myh9vXDcauPyUx_FyEzNtaW_c8ytY,6601
|
|
3
|
+
md2conf/api.py,sha256=bP3Kp4PsGQrPyQMOs-MwE2Znl1ewuKNslMCv7AtXIT0,16366
|
|
4
|
+
md2conf/application.py,sha256=f5O-EUTXh-SO4P57rgqfwBbbX-A8S_n7PM4HW9AsMLc,8277
|
|
5
|
+
md2conf/converter.py,sha256=H9OdaLb_JXAYIa5eEwVmJN75ESWarplq2LRo30gWur4,34271
|
|
6
|
+
md2conf/emoji.py,sha256=2vMZlLD4m2X6MB-Fjv_GDzEUelb_sg4UBtF463d_p90,1792
|
|
7
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
8
|
+
md2conf/matcher.py,sha256=bZMX_GTXuEeKqIPDES8KqAqTBiesKfSH9rwbNFkD25A,3451
|
|
9
|
+
md2conf/mermaid.py,sha256=u2pSKaLrvB3yeDciVO9mIfUT2dbVVfTALYLBaIgaJ-Y,1975
|
|
10
|
+
md2conf/processor.py,sha256=bdwSEnxuvWsZd34_KcvLqigM8GHnll9fc-hf1VQ_5aI,4010
|
|
11
|
+
md2conf/properties.py,sha256=2l1tW8HmnrEsXN4-Dtby2tYJQTG1MirRpM3H6ykjQ4c,1858
|
|
12
|
+
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
13
|
+
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
md2conf/util.py,sha256=mghtBv5c0vOBHi5CxjBh4LZbjQ0Cu0h_vB30RN4N8Bk,611
|
|
15
|
+
markdown_to_confluence-0.2.4.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
16
|
+
markdown_to_confluence-0.2.4.dist-info/METADATA,sha256=vjvF6LP_5tQAKY9ACKkmAllQDlqjyJuSg_W7bBKct5Y,12764
|
|
17
|
+
markdown_to_confluence-0.2.4.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
18
|
+
markdown_to_confluence-0.2.4.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
19
|
+
markdown_to_confluence-0.2.4.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
20
|
+
markdown_to_confluence-0.2.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
21
|
+
markdown_to_confluence-0.2.4.dist-info/RECORD,,
|
md2conf/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ Parses Markdown files, converts Markdown content into the Confluence Storage For
|
|
|
5
5
|
Confluence API endpoints to upload images and content.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.2.
|
|
8
|
+
__version__ = "0.2.4"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2024, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
md2conf/application.py
CHANGED
|
@@ -3,6 +3,8 @@ import os.path
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Dict, List, Optional
|
|
5
5
|
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
6
8
|
from .api import ConfluencePage, ConfluenceSession
|
|
7
9
|
from .converter import (
|
|
8
10
|
ConfluenceDocument,
|
|
@@ -10,6 +12,7 @@ from .converter import (
|
|
|
10
12
|
ConfluencePageMetadata,
|
|
11
13
|
ConfluenceQualifiedID,
|
|
12
14
|
attachment_name,
|
|
15
|
+
extract_frontmatter,
|
|
13
16
|
extract_qualified_id,
|
|
14
17
|
read_qualified_id,
|
|
15
18
|
)
|
|
@@ -99,15 +102,15 @@ class Application:
|
|
|
99
102
|
continue
|
|
100
103
|
|
|
101
104
|
if entry.is_file():
|
|
102
|
-
files.append(
|
|
105
|
+
files.append(Path(local_dir) / entry.name)
|
|
103
106
|
elif entry.is_dir():
|
|
104
|
-
directories.append(
|
|
107
|
+
directories.append(Path(local_dir) / entry.name)
|
|
105
108
|
|
|
106
109
|
# make page act as parent node in Confluence
|
|
107
110
|
parent_id: Optional[ConfluenceQualifiedID] = None
|
|
108
|
-
if "index.md" in files:
|
|
111
|
+
if (Path(local_dir) / "index.md") in files:
|
|
109
112
|
parent_id = read_qualified_id(Path(local_dir) / "index.md")
|
|
110
|
-
elif "README.md" in files:
|
|
113
|
+
elif (Path(local_dir) / "README.md") in files:
|
|
111
114
|
parent_id = read_qualified_id(Path(local_dir) / "README.md")
|
|
112
115
|
|
|
113
116
|
if parent_id is None:
|
|
@@ -137,6 +140,8 @@ class Application:
|
|
|
137
140
|
document = f.read()
|
|
138
141
|
|
|
139
142
|
qualified_id, document = extract_qualified_id(document)
|
|
143
|
+
frontmatter, document = extract_frontmatter(document)
|
|
144
|
+
|
|
140
145
|
if qualified_id is not None:
|
|
141
146
|
confluence_page = self.api.get_page(
|
|
142
147
|
qualified_id.page_id, space_key=qualified_id.space_key
|
|
@@ -147,6 +152,14 @@ class Application:
|
|
|
147
152
|
f"expected: parent page ID for Markdown file with no linked Confluence page: {absolute_path}"
|
|
148
153
|
)
|
|
149
154
|
|
|
155
|
+
# assign title from frontmatter if present
|
|
156
|
+
if title is None and frontmatter is not None:
|
|
157
|
+
properties = yaml.safe_load(frontmatter)
|
|
158
|
+
if isinstance(properties, dict):
|
|
159
|
+
property_title = properties.get("title")
|
|
160
|
+
if isinstance(property_title, str):
|
|
161
|
+
title = property_title
|
|
162
|
+
|
|
150
163
|
confluence_page = self._create_page(
|
|
151
164
|
absolute_path, document, title, parent_id
|
|
152
165
|
)
|
md2conf/converter.py
CHANGED
|
@@ -338,10 +338,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
338
338
|
anchor.tail = heading.text
|
|
339
339
|
heading.text = None
|
|
340
340
|
|
|
341
|
-
def _transform_link(self, anchor: ET._Element) ->
|
|
341
|
+
def _transform_link(self, anchor: ET._Element) -> Optional[ET._Element]:
|
|
342
342
|
url = anchor.attrib["href"]
|
|
343
343
|
if is_absolute_url(url):
|
|
344
|
-
return
|
|
344
|
+
return None
|
|
345
345
|
|
|
346
346
|
LOGGER.debug(f"found link {url} relative to {self.path}")
|
|
347
347
|
relative_url: ParseResult = urlparse(url)
|
|
@@ -354,8 +354,23 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
354
354
|
and not relative_url.query
|
|
355
355
|
):
|
|
356
356
|
LOGGER.debug(f"found local URL: {url}")
|
|
357
|
-
|
|
358
|
-
|
|
357
|
+
if self.options.heading_anchors:
|
|
358
|
+
# <ac:link ac:anchor="anchor"><ac:link-body>...</ac:link-body></ac:link>
|
|
359
|
+
target = relative_url.fragment.lstrip("#")
|
|
360
|
+
link_body = AC("link-body", {}, *list(anchor))
|
|
361
|
+
link_body.text = anchor.text
|
|
362
|
+
link_wrapper = AC(
|
|
363
|
+
"link",
|
|
364
|
+
{
|
|
365
|
+
ET.QName(namespaces["ac"], "anchor"): target,
|
|
366
|
+
},
|
|
367
|
+
link_body,
|
|
368
|
+
)
|
|
369
|
+
link_wrapper.tail = anchor.tail
|
|
370
|
+
return link_wrapper
|
|
371
|
+
else:
|
|
372
|
+
anchor.attrib["href"] = url
|
|
373
|
+
return None
|
|
359
374
|
|
|
360
375
|
# convert the relative URL to absolute URL based on the base path value, then look up
|
|
361
376
|
# the absolute path in the page metadata dictionary to discover the relative path
|
|
@@ -366,7 +381,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
366
381
|
if self.options.ignore_invalid_url:
|
|
367
382
|
LOGGER.warning(msg)
|
|
368
383
|
anchor.attrib.pop("href")
|
|
369
|
-
return
|
|
384
|
+
return None
|
|
370
385
|
else:
|
|
371
386
|
raise DocumentError(msg)
|
|
372
387
|
|
|
@@ -378,7 +393,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
378
393
|
if self.options.ignore_invalid_url:
|
|
379
394
|
LOGGER.warning(msg)
|
|
380
395
|
anchor.attrib.pop("href")
|
|
381
|
-
return
|
|
396
|
+
return None
|
|
382
397
|
else:
|
|
383
398
|
raise DocumentError(msg)
|
|
384
399
|
|
|
@@ -404,6 +419,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
404
419
|
|
|
405
420
|
LOGGER.debug(f"transformed relative URL: {url} to URL: {transformed_url}")
|
|
406
421
|
anchor.attrib["href"] = transformed_url
|
|
422
|
+
return None
|
|
407
423
|
|
|
408
424
|
def _transform_image(self, image: ET._Element) -> ET._Element:
|
|
409
425
|
path: str = image.attrib["src"]
|
|
@@ -803,8 +819,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
803
819
|
|
|
804
820
|
# <a href="..."> ... </a>
|
|
805
821
|
elif child.tag == "a":
|
|
806
|
-
self._transform_link(child)
|
|
807
|
-
return None
|
|
822
|
+
return self._transform_link(child)
|
|
808
823
|
|
|
809
824
|
# <pre><code class="language-java"> ... </code></pre>
|
|
810
825
|
elif child.tag == "pre" and len(child) == 1 and child[0].tag == "code":
|
|
@@ -829,16 +844,16 @@ class DocumentError(RuntimeError):
|
|
|
829
844
|
pass
|
|
830
845
|
|
|
831
846
|
|
|
832
|
-
def extract_value(pattern: str,
|
|
847
|
+
def extract_value(pattern: str, text: str) -> Tuple[Optional[str], str]:
|
|
833
848
|
values: List[str] = []
|
|
834
849
|
|
|
835
850
|
def _repl_func(matchobj: re.Match) -> str:
|
|
836
851
|
values.append(matchobj.group(1))
|
|
837
852
|
return ""
|
|
838
853
|
|
|
839
|
-
|
|
854
|
+
text = re.sub(pattern, _repl_func, text, 1, re.ASCII)
|
|
840
855
|
value = values[0] if values else None
|
|
841
|
-
return value,
|
|
856
|
+
return value, text
|
|
842
857
|
|
|
843
858
|
|
|
844
859
|
@dataclass
|
|
@@ -851,20 +866,24 @@ class ConfluenceQualifiedID:
|
|
|
851
866
|
self.space_key = space_key
|
|
852
867
|
|
|
853
868
|
|
|
854
|
-
def extract_qualified_id(
|
|
869
|
+
def extract_qualified_id(text: str) -> Tuple[Optional[ConfluenceQualifiedID], str]:
|
|
855
870
|
"Extracts the Confluence page ID and space key from a Markdown document."
|
|
856
871
|
|
|
857
|
-
page_id,
|
|
872
|
+
page_id, text = extract_value(r"<!--\s+confluence-page-id:\s*(\d+)\s+-->", text)
|
|
858
873
|
|
|
859
874
|
if page_id is None:
|
|
860
|
-
return None,
|
|
875
|
+
return None, text
|
|
861
876
|
|
|
862
877
|
# extract Confluence space key
|
|
863
|
-
space_key,
|
|
864
|
-
|
|
865
|
-
)
|
|
878
|
+
space_key, text = extract_value(r"<!--\s+confluence-space-key:\s*(\S+)\s+-->", text)
|
|
879
|
+
|
|
880
|
+
return ConfluenceQualifiedID(page_id, space_key), text
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def extract_frontmatter(text: str) -> Tuple[Optional[str], str]:
|
|
884
|
+
"Extracts the front matter from a Markdown document."
|
|
866
885
|
|
|
867
|
-
return
|
|
886
|
+
return extract_value(r"(?ms)\A---$(.+?)^---$", text)
|
|
868
887
|
|
|
869
888
|
|
|
870
889
|
def read_qualified_id(absolute_path: Path) -> Optional[ConfluenceQualifiedID]:
|
|
@@ -941,7 +960,7 @@ class ConfluenceDocument:
|
|
|
941
960
|
)
|
|
942
961
|
|
|
943
962
|
# extract frontmatter
|
|
944
|
-
frontmatter, text =
|
|
963
|
+
frontmatter, text = extract_frontmatter(text)
|
|
945
964
|
|
|
946
965
|
# convert to HTML
|
|
947
966
|
html = markdown_to_html(text)
|
md2conf/mermaid.py
CHANGED
|
@@ -49,10 +49,9 @@ def render(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
|
|
|
49
49
|
"--outputFormat",
|
|
50
50
|
output_format,
|
|
51
51
|
]
|
|
52
|
+
root = os.path.dirname(__file__)
|
|
52
53
|
if is_docker():
|
|
53
|
-
cmd.extend(
|
|
54
|
-
["-p", os.path.join(os.path.dirname(__file__), "puppeteer-config.json")]
|
|
55
|
-
)
|
|
54
|
+
cmd.extend(["-p", os.path.join(root, "puppeteer-config.json")])
|
|
56
55
|
LOGGER.debug(f"Executing: {' '.join(cmd)}")
|
|
57
56
|
try:
|
|
58
57
|
proc = subprocess.Popen(
|
md2conf/processor.py
CHANGED
|
@@ -79,9 +79,9 @@ class Processor:
|
|
|
79
79
|
continue
|
|
80
80
|
|
|
81
81
|
if entry.is_file():
|
|
82
|
-
files.append(
|
|
82
|
+
files.append(Path(local_dir) / entry.name)
|
|
83
83
|
elif entry.is_dir():
|
|
84
|
-
directories.append(
|
|
84
|
+
directories.append(Path(local_dir) / entry.name)
|
|
85
85
|
|
|
86
86
|
for doc in files:
|
|
87
87
|
metadata = self._get_page(doc)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
md2conf/__init__.py,sha256=ypRfZF5ef0nZONGa1E9S2htodyslp3uPDgRUhUD8St4,402
|
|
2
|
-
md2conf/__main__.py,sha256=_qUspNQmQdhpH4Myh9vXDcauPyUx_FyEzNtaW_c8ytY,6601
|
|
3
|
-
md2conf/api.py,sha256=bP3Kp4PsGQrPyQMOs-MwE2Znl1ewuKNslMCv7AtXIT0,16366
|
|
4
|
-
md2conf/application.py,sha256=GUMPZUe_jZTBszKDyh4y-jeOp83VKCR3b_EHmzcL5Qs,7778
|
|
5
|
-
md2conf/converter.py,sha256=F75UtnCR3vxAE1W8JxZ5wmfzgtJLTeQvDN2jH49fNXU,33466
|
|
6
|
-
md2conf/emoji.py,sha256=2vMZlLD4m2X6MB-Fjv_GDzEUelb_sg4UBtF463d_p90,1792
|
|
7
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
8
|
-
md2conf/matcher.py,sha256=bZMX_GTXuEeKqIPDES8KqAqTBiesKfSH9rwbNFkD25A,3451
|
|
9
|
-
md2conf/mermaid.py,sha256=a7PVcd7kcFBOMw7Z2mOfvWC1JIVR4Q1EkkanLk1SLx0,1981
|
|
10
|
-
md2conf/processor.py,sha256=qnoO7kTPF2y5uUATnqGSkgVP2DJJiR8DwkUqWavE6r4,4036
|
|
11
|
-
md2conf/properties.py,sha256=2l1tW8HmnrEsXN4-Dtby2tYJQTG1MirRpM3H6ykjQ4c,1858
|
|
12
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
13
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
md2conf/util.py,sha256=mghtBv5c0vOBHi5CxjBh4LZbjQ0Cu0h_vB30RN4N8Bk,611
|
|
15
|
-
markdown_to_confluence-0.2.3.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
16
|
-
markdown_to_confluence-0.2.3.dist-info/METADATA,sha256=Z7ts-W_aUJiau-mnFZIY6RPF5OdX_xCN081FCW4BNa8,11585
|
|
17
|
-
markdown_to_confluence-0.2.3.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
18
|
-
markdown_to_confluence-0.2.3.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
19
|
-
markdown_to_confluence-0.2.3.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
20
|
-
markdown_to_confluence-0.2.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
21
|
-
markdown_to_confluence-0.2.3.dist-info/RECORD,,
|
|
File without changes
|
{markdown_to_confluence-0.2.3.dist-info → markdown_to_confluence-0.2.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.2.3.dist-info → markdown_to_confluence-0.2.4.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|