markdown-to-confluence 0.2.1__py3-none-any.whl → 0.2.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.
- {markdown_to_confluence-0.2.1.dist-info → markdown_to_confluence-0.2.3.dist-info}/METADATA +25 -7
- markdown_to_confluence-0.2.3.dist-info/RECORD +21 -0
- md2conf/__init__.py +1 -1
- md2conf/api.py +1 -20
- md2conf/application.py +9 -6
- md2conf/converter.py +86 -1
- md2conf/emoji.py +48 -0
- md2conf/matcher.py +107 -0
- md2conf/mermaid.py +41 -19
- md2conf/processor.py +8 -5
- md2conf/puppeteer-config.json +8 -0
- md2conf/util.py +19 -0
- markdown_to_confluence-0.2.1.dist-info/RECORD +0 -17
- {markdown_to_confluence-0.2.1.dist-info → markdown_to_confluence-0.2.3.dist-info}/LICENSE +0 -0
- {markdown_to_confluence-0.2.1.dist-info → markdown_to_confluence-0.2.3.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.2.1.dist-info → markdown_to_confluence-0.2.3.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.2.1.dist-info → markdown_to_confluence-0.2.3.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.2.1.dist-info → markdown_to_confluence-0.2.3.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.3
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -144,6 +144,12 @@ Provide generated-by prompt text in the Markdown file with a tag:
|
|
|
144
144
|
|
|
145
145
|
Alternatively, use the `--generated-by GENERATED_BY` option. The tag takes precedence.
|
|
146
146
|
|
|
147
|
+
### Ignoring files
|
|
148
|
+
|
|
149
|
+
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.
|
|
150
|
+
|
|
151
|
+
Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
|
|
152
|
+
|
|
147
153
|
### Running the tool
|
|
148
154
|
|
|
149
155
|
You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
|
|
@@ -195,18 +201,30 @@ options:
|
|
|
195
201
|
--webui-links Enable Confluence Web UI links.
|
|
196
202
|
```
|
|
197
203
|
|
|
198
|
-
### Using the
|
|
204
|
+
### Using the Docker container
|
|
205
|
+
|
|
206
|
+
You can run the Docker container via `docker run` or via `Dockerfile`. Either can accept the environment variables or arguments similar to the Python options. The final argument `./` corresponds to `mdpath` in the command-line utility.
|
|
207
|
+
|
|
208
|
+
With `docker run`, you can pass Confluence domain, user, API and space key directly to `docker run`:
|
|
199
209
|
|
|
200
|
-
|
|
210
|
+
```sh
|
|
211
|
+
docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d instructure.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s DAP ./
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Alternatively, you can use a separate file `.env` to pass these parameters as environment variables:
|
|
201
215
|
|
|
202
216
|
```sh
|
|
203
|
-
docker run --rm --
|
|
217
|
+
docker run --rm --env-file .env --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest ./
|
|
204
218
|
```
|
|
205
219
|
|
|
206
|
-
|
|
220
|
+
In each case, `-v $(pwd):/data` maps the current directory to Docker container's `WORKDIR` such *md2conf* can scan files and directories in the local file system.
|
|
221
|
+
|
|
222
|
+
Note that the entry point for the Docker container's base image is `ENTRYPOINT ["python3", "-m", "md2conf"]`.
|
|
223
|
+
|
|
224
|
+
With the `Dockerfile` approach, you can extend the base image:
|
|
207
225
|
|
|
208
226
|
```Dockerfile
|
|
209
|
-
FROM
|
|
227
|
+
FROM leventehunyadi/md2conf:latest
|
|
210
228
|
|
|
211
229
|
ENV CONFLUENCE_DOMAIN='instructure.atlassian.net'
|
|
212
230
|
ENV CONFLUENCE_PATH='/wiki/'
|
|
@@ -220,7 +238,7 @@ CMD ["./"]
|
|
|
220
238
|
Alternatively,
|
|
221
239
|
|
|
222
240
|
```Dockerfile
|
|
223
|
-
FROM
|
|
241
|
+
FROM leventehunyadi/md2conf:latest
|
|
224
242
|
|
|
225
243
|
CMD ["-d", "instructure.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "DAP", "./"]
|
|
226
244
|
```
|
|
@@ -0,0 +1,21 @@
|
|
|
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,,
|
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.3"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2024, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
md2conf/api.py
CHANGED
|
@@ -2,7 +2,6 @@ import io
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import mimetypes
|
|
5
|
-
import sys
|
|
6
5
|
import typing
|
|
7
6
|
from contextlib import contextmanager
|
|
8
7
|
from dataclasses import dataclass
|
|
@@ -15,6 +14,7 @@ import requests
|
|
|
15
14
|
|
|
16
15
|
from .converter import ParseError, sanitize_confluence
|
|
17
16
|
from .properties import ConfluenceError, ConfluenceProperties
|
|
17
|
+
from .util import removeprefix
|
|
18
18
|
|
|
19
19
|
# a JSON type with possible `null` values
|
|
20
20
|
JsonType = Union[
|
|
@@ -44,25 +44,6 @@ def build_url(base_url: str, query: Optional[Dict[str, str]] = None) -> str:
|
|
|
44
44
|
return urlunparse(url_parts)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
if sys.version_info >= (3, 9):
|
|
48
|
-
|
|
49
|
-
def removeprefix(string: str, prefix: str) -> str:
|
|
50
|
-
"If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
|
|
51
|
-
|
|
52
|
-
return string.removeprefix(prefix)
|
|
53
|
-
|
|
54
|
-
else:
|
|
55
|
-
|
|
56
|
-
def removeprefix(string: str, prefix: str) -> str:
|
|
57
|
-
"If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
|
|
58
|
-
|
|
59
|
-
if string.startswith(prefix):
|
|
60
|
-
prefix_len = len(prefix)
|
|
61
|
-
return string[prefix_len:]
|
|
62
|
-
else:
|
|
63
|
-
return string
|
|
64
|
-
|
|
65
|
-
|
|
66
47
|
LOGGER = logging.getLogger(__name__)
|
|
67
48
|
|
|
68
49
|
|
md2conf/application.py
CHANGED
|
@@ -13,6 +13,7 @@ from .converter import (
|
|
|
13
13
|
extract_qualified_id,
|
|
14
14
|
read_qualified_id,
|
|
15
15
|
)
|
|
16
|
+
from .matcher import Matcher, MatcherOptions
|
|
16
17
|
|
|
17
18
|
LOGGER = logging.getLogger(__name__)
|
|
18
19
|
|
|
@@ -89,16 +90,18 @@ class Application:
|
|
|
89
90
|
|
|
90
91
|
LOGGER.info(f"Indexing directory: {local_dir}")
|
|
91
92
|
|
|
93
|
+
matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
|
|
94
|
+
|
|
92
95
|
files: List[Path] = []
|
|
93
96
|
directories: List[Path] = []
|
|
94
97
|
for entry in os.scandir(local_dir):
|
|
98
|
+
if matcher.is_excluded(entry.name, entry.is_dir()):
|
|
99
|
+
continue
|
|
100
|
+
|
|
95
101
|
if entry.is_file():
|
|
96
|
-
|
|
97
|
-
# skip non-markdown files
|
|
98
|
-
files.append((Path(local_dir) / entry.name).absolute())
|
|
102
|
+
files.append((Path(local_dir) / entry.name).absolute())
|
|
99
103
|
elif entry.is_dir():
|
|
100
|
-
|
|
101
|
-
directories.append((Path(local_dir) / entry.name).absolute())
|
|
104
|
+
directories.append((Path(local_dir) / entry.name).absolute())
|
|
102
105
|
|
|
103
106
|
# make page act as parent node in Confluence
|
|
104
107
|
parent_id: Optional[ConfluenceQualifiedID] = None
|
|
@@ -141,7 +144,7 @@ class Application:
|
|
|
141
144
|
else:
|
|
142
145
|
if parent_id is None:
|
|
143
146
|
raise ValueError(
|
|
144
|
-
"expected:
|
|
147
|
+
f"expected: parent page ID for Markdown file with no linked Confluence page: {absolute_path}"
|
|
145
148
|
)
|
|
146
149
|
|
|
147
150
|
confluence_page = self._create_page(
|
md2conf/converter.py
CHANGED
|
@@ -7,9 +7,10 @@ import os.path
|
|
|
7
7
|
import re
|
|
8
8
|
import sys
|
|
9
9
|
import uuid
|
|
10
|
+
import xml.etree.ElementTree
|
|
10
11
|
from dataclasses import dataclass
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Dict, List, Literal, Optional, Tuple
|
|
13
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
13
14
|
from urllib.parse import ParseResult, urlparse, urlunparse
|
|
14
15
|
|
|
15
16
|
import lxml.etree as ET
|
|
@@ -55,6 +56,27 @@ def is_relative_url(url: str) -> bool:
|
|
|
55
56
|
return not bool(urlparts.scheme) and not bool(urlparts.netloc)
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
def emoji_generator(
|
|
60
|
+
index: str,
|
|
61
|
+
shortname: str,
|
|
62
|
+
alias: Optional[str],
|
|
63
|
+
uc: Optional[str],
|
|
64
|
+
alt: str,
|
|
65
|
+
title: Optional[str],
|
|
66
|
+
category: Optional[str],
|
|
67
|
+
options: Dict[str, Any],
|
|
68
|
+
md: markdown.Markdown,
|
|
69
|
+
) -> xml.etree.ElementTree.Element:
|
|
70
|
+
name = (alias or shortname).strip(":")
|
|
71
|
+
span = xml.etree.ElementTree.Element("span", {"data-emoji": name})
|
|
72
|
+
if uc is not None:
|
|
73
|
+
# convert series of Unicode code point hexadecimal values into characters
|
|
74
|
+
span.text = "".join(chr(int(item, base=16)) for item in uc.split("-"))
|
|
75
|
+
else:
|
|
76
|
+
span.text = alt
|
|
77
|
+
return span
|
|
78
|
+
|
|
79
|
+
|
|
58
80
|
def markdown_to_html(content: str) -> str:
|
|
59
81
|
return markdown.markdown(
|
|
60
82
|
content,
|
|
@@ -62,11 +84,17 @@ def markdown_to_html(content: str) -> str:
|
|
|
62
84
|
"admonition",
|
|
63
85
|
"markdown.extensions.tables",
|
|
64
86
|
"markdown.extensions.fenced_code",
|
|
87
|
+
"pymdownx.emoji",
|
|
65
88
|
"pymdownx.magiclink",
|
|
66
89
|
"pymdownx.tilde",
|
|
67
90
|
"sane_lists",
|
|
68
91
|
"md_in_html",
|
|
69
92
|
],
|
|
93
|
+
extension_configs={
|
|
94
|
+
"pymdownx.emoji": {
|
|
95
|
+
"emoji_generator": emoji_generator,
|
|
96
|
+
}
|
|
97
|
+
},
|
|
70
98
|
)
|
|
71
99
|
|
|
72
100
|
|
|
@@ -81,6 +109,7 @@ def _elements_from_strings(dtd_path: Path, items: List[str]) -> ET._Element:
|
|
|
81
109
|
|
|
82
110
|
parser = ET.XMLParser(
|
|
83
111
|
remove_blank_text=True,
|
|
112
|
+
remove_comments=True,
|
|
84
113
|
strip_cdata=False,
|
|
85
114
|
load_dtd=True,
|
|
86
115
|
)
|
|
@@ -678,6 +707,23 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
678
707
|
AC("rich-text-body", {}, *list(elem)),
|
|
679
708
|
)
|
|
680
709
|
|
|
710
|
+
def _transform_emoji(self, elem: ET._Element) -> ET._Element:
|
|
711
|
+
shortname = elem.attrib.get("data-emoji", "")
|
|
712
|
+
alt = elem.text or ""
|
|
713
|
+
|
|
714
|
+
# <ac:emoticon ac:name="wink" ac:emoji-shortname=":wink:" ac:emoji-id="1f609" ac:emoji-fallback="😉"/>
|
|
715
|
+
# <ac:emoticon ac:name="blue-star" ac:emoji-shortname=":heavy_plus_sign:" ac:emoji-id="2795" ac:emoji-fallback="➕"/>
|
|
716
|
+
# <ac:emoticon ac:name="blue-star" ac:emoji-shortname=":heavy_minus_sign:" ac:emoji-id="2796" ac:emoji-fallback="➖"/>
|
|
717
|
+
return AC(
|
|
718
|
+
"emoticon",
|
|
719
|
+
{
|
|
720
|
+
# use "blue-star" as a placeholder name to ensure wiki page loads in timely manner
|
|
721
|
+
ET.QName(namespaces["ac"], "name"): "blue-star",
|
|
722
|
+
ET.QName(namespaces["ac"], "emoji-shortname"): f":{shortname}:",
|
|
723
|
+
ET.QName(namespaces["ac"], "emoji-fallback"): alt,
|
|
724
|
+
},
|
|
725
|
+
)
|
|
726
|
+
|
|
681
727
|
def transform(self, child: ET._Element) -> Optional[ET._Element]:
|
|
682
728
|
# normalize line breaks to regular space in element text
|
|
683
729
|
if child.text:
|
|
@@ -764,6 +810,9 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
764
810
|
elif child.tag == "pre" and len(child) == 1 and child[0].tag == "code":
|
|
765
811
|
return self._transform_block(child[0])
|
|
766
812
|
|
|
813
|
+
elif child.tag == "span" and child.attrib.has_key("data-emoji"):
|
|
814
|
+
return self._transform_emoji(child)
|
|
815
|
+
|
|
767
816
|
return None
|
|
768
817
|
|
|
769
818
|
|
|
@@ -963,3 +1012,39 @@ def elements_to_string(root: ET._Element) -> str:
|
|
|
963
1012
|
return m.group(1)
|
|
964
1013
|
else:
|
|
965
1014
|
raise ValueError("expected: Confluence content")
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _content_to_string(dtd_path: Path, content: str) -> str:
|
|
1018
|
+
parser = ET.XMLParser(
|
|
1019
|
+
remove_blank_text=True,
|
|
1020
|
+
remove_comments=True,
|
|
1021
|
+
strip_cdata=False,
|
|
1022
|
+
load_dtd=True,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
ns_attr_list = "".join(
|
|
1026
|
+
f' xmlns:{key}="{value}"' for key, value in namespaces.items()
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
data = [
|
|
1030
|
+
'<?xml version="1.0"?>',
|
|
1031
|
+
f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path}">'
|
|
1032
|
+
f"<root{ns_attr_list}>",
|
|
1033
|
+
]
|
|
1034
|
+
data.append(content)
|
|
1035
|
+
data.append("</root>")
|
|
1036
|
+
|
|
1037
|
+
tree = ET.fromstringlist(data, parser=parser)
|
|
1038
|
+
return ET.tostring(tree, pretty_print=True).decode("utf-8")
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def content_to_string(content: str) -> str:
|
|
1042
|
+
"Converts a Confluence Storage Format document returned by the API into a readable XML document."
|
|
1043
|
+
|
|
1044
|
+
if sys.version_info >= (3, 9):
|
|
1045
|
+
resource_path = resources.files(__package__).joinpath("entities.dtd")
|
|
1046
|
+
with resources.as_file(resource_path) as dtd_path:
|
|
1047
|
+
return _content_to_string(dtd_path, content)
|
|
1048
|
+
else:
|
|
1049
|
+
with resources.path(__package__, "entities.dtd") as dtd_path:
|
|
1050
|
+
return _content_to_string(dtd_path, content)
|
md2conf/emoji.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import pymdownx.emoji1_db as emoji_db
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_source(path: pathlib.Path) -> None:
|
|
7
|
+
"Generates a source Markdown document for testing emojis."
|
|
8
|
+
|
|
9
|
+
emojis = emoji_db.emoji
|
|
10
|
+
|
|
11
|
+
with open(path, "w") as f:
|
|
12
|
+
print("<!-- confluence-page-id: 86918529216 -->", file=f)
|
|
13
|
+
print("<!-- This file has been generated by a script. -->", file=f)
|
|
14
|
+
print(file=f)
|
|
15
|
+
print("## Emoji", file=f)
|
|
16
|
+
print(file=f)
|
|
17
|
+
print("| Icon | Emoji code |", file=f)
|
|
18
|
+
print("| ---- | ---------- |", file=f)
|
|
19
|
+
for key in emojis.keys():
|
|
20
|
+
key = key.strip(":")
|
|
21
|
+
print(f"| :{key}: | `:{key}:` |", file=f)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_target(path: pathlib.Path) -> None:
|
|
25
|
+
"Generates a target Confluence Storage Format (XML) document for testing emojis."
|
|
26
|
+
|
|
27
|
+
emojis = emoji_db.emoji
|
|
28
|
+
|
|
29
|
+
with open(path, "w") as f:
|
|
30
|
+
print('<ac:structured-macro ac:name="info" ac:schema-version="1">', file=f)
|
|
31
|
+
print("<ac:rich-text-body>", file=f)
|
|
32
|
+
print("<p>This page has been generated with a tool.</p>", file=f)
|
|
33
|
+
print("</ac:rich-text-body>", file=f)
|
|
34
|
+
print("</ac:structured-macro>", file=f)
|
|
35
|
+
print("<h2>Emoji</h2>", file=f)
|
|
36
|
+
print("<table>", file=f)
|
|
37
|
+
print("<thead><tr><th>Icon</th><th>Emoji code</th></tr></thead>", file=f)
|
|
38
|
+
print("<tbody>", file=f)
|
|
39
|
+
for key, data in emojis.items():
|
|
40
|
+
key = key.strip(":")
|
|
41
|
+
unicode = "".join(f"&#x{item};" for item in data["unicode"].split("-"))
|
|
42
|
+
|
|
43
|
+
print(
|
|
44
|
+
f'<tr><td><ac:emoticon ac:name="blue-star" ac:emoji-shortname=":{key}:" ac:emoji-fallback="{unicode}"/></td><td><code>:{key}:</code></td></tr>',
|
|
45
|
+
file=f,
|
|
46
|
+
)
|
|
47
|
+
print("</tbody>", file=f)
|
|
48
|
+
print("</table>", file=f)
|
md2conf/matcher.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from fnmatch import fnmatch
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Entry:
|
|
10
|
+
"Represents a file or directory entry."
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
is_dir: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MatcherOptions:
|
|
18
|
+
"""
|
|
19
|
+
Options for checking against a list of exclude/include patterns.
|
|
20
|
+
|
|
21
|
+
:param source: File name to read exclusion rules from.
|
|
22
|
+
:param extension: Extension to narrow down search to.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
source: str
|
|
26
|
+
extension: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
if self.extension is not None and not self.extension.startswith("."):
|
|
30
|
+
self.extension = f".{self.extension}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Matcher:
|
|
34
|
+
"Compares file and directory names against a list of exclude/include patterns."
|
|
35
|
+
|
|
36
|
+
options: MatcherOptions
|
|
37
|
+
rules: List[str]
|
|
38
|
+
|
|
39
|
+
def __init__(self, options: MatcherOptions, directory: Path) -> None:
|
|
40
|
+
self.options = options
|
|
41
|
+
if os.path.exists(directory / options.source):
|
|
42
|
+
with open(directory / options.source, "r") as f:
|
|
43
|
+
rules = f.read().splitlines()
|
|
44
|
+
self.rules = [rule for rule in rules if rule and not rule.startswith("#")]
|
|
45
|
+
else:
|
|
46
|
+
self.rules = []
|
|
47
|
+
|
|
48
|
+
def extension_matches(self, name: str) -> bool:
|
|
49
|
+
"True if the file name has the expected extension."
|
|
50
|
+
|
|
51
|
+
return self.options.extension is None or name.endswith(self.options.extension)
|
|
52
|
+
|
|
53
|
+
def is_excluded(self, name: str, is_dir: bool) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
True if the file or directory name matches any of the exclusion patterns.
|
|
56
|
+
|
|
57
|
+
:param name: Name to match against the rule-set.
|
|
58
|
+
:param is_dir: Whether the name identifies a directory.
|
|
59
|
+
:returns: True if the name matches at least one of the exclusion patterns.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# skip hidden files and directories
|
|
63
|
+
if name.startswith("."):
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
# match extension for regular files
|
|
67
|
+
if not is_dir and not self.extension_matches(name):
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
for rule in self.rules:
|
|
71
|
+
if fnmatch(name, rule):
|
|
72
|
+
return True
|
|
73
|
+
else:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def is_included(self, name: str, is_dir: bool) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
True if the file or directory name matches none of the exclusion patterns.
|
|
79
|
+
|
|
80
|
+
:param name: Name to match against the rule-set.
|
|
81
|
+
:param is_dir: Whether the name identifies a directory.
|
|
82
|
+
:returns: True if the name doesn't match any of the exclusion patterns.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
return not self.is_excluded(name, is_dir)
|
|
86
|
+
|
|
87
|
+
def filter(self, items: Iterable[Entry]) -> List[Entry]:
|
|
88
|
+
"""
|
|
89
|
+
Returns only those elements from the input that don't match any of the exclusion rules.
|
|
90
|
+
|
|
91
|
+
:param items: A list of names to filter.
|
|
92
|
+
:returns: A filtered list of names that didn't match any of the exclusion rules.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
return [item for item in items if self.is_included(item.name, item.is_dir)]
|
|
96
|
+
|
|
97
|
+
def scandir(self, path: Path) -> List[Entry]:
|
|
98
|
+
"""
|
|
99
|
+
Returns only those entries in a directory whose name doesn't match any of the exclusion rules.
|
|
100
|
+
|
|
101
|
+
:param path: Directory to scan.
|
|
102
|
+
:returns: A filtered list of entries whose name didn't match any of the exclusion rules.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
return self.filter(
|
|
106
|
+
Entry(entry.name, entry.is_dir()) for entry in os.scandir(path)
|
|
107
|
+
)
|
md2conf/mermaid.py
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
3
|
import os.path
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess
|
|
5
6
|
from typing import Literal
|
|
6
7
|
|
|
8
|
+
LOGGER = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_docker() -> bool:
|
|
12
|
+
"True if the application is running in a Docker container."
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
os.environ.get("CHROME_BIN") == "/usr/bin/chromium-browser"
|
|
16
|
+
and os.environ.get("PUPPETEER_SKIP_DOWNLOAD") == "true"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_mmdc() -> str:
|
|
21
|
+
"Path to the Mermaid diagram converter."
|
|
22
|
+
|
|
23
|
+
if is_docker():
|
|
24
|
+
return "/home/md2conf/node_modules/.bin/mmdc"
|
|
25
|
+
elif os.name == "nt":
|
|
26
|
+
return "mmdc.cmd"
|
|
27
|
+
else:
|
|
28
|
+
return "mmdc"
|
|
29
|
+
|
|
7
30
|
|
|
8
31
|
def has_mmdc() -> bool:
|
|
9
32
|
"True if Mermaid diagram converter is available on the OS."
|
|
10
33
|
|
|
11
|
-
|
|
12
|
-
executable = "mmdc.cmd"
|
|
13
|
-
else:
|
|
14
|
-
executable = "mmdc"
|
|
34
|
+
executable = get_mmdc()
|
|
15
35
|
return shutil.which(executable) is not None
|
|
16
36
|
|
|
17
37
|
|
|
@@ -20,20 +40,21 @@ def render(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
|
|
|
20
40
|
|
|
21
41
|
filename = f"tmp_mermaid.{output_format}"
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
cmd = [
|
|
44
|
+
get_mmdc(),
|
|
45
|
+
"--input",
|
|
46
|
+
"-",
|
|
47
|
+
"--output",
|
|
48
|
+
filename,
|
|
49
|
+
"--outputFormat",
|
|
50
|
+
output_format,
|
|
51
|
+
]
|
|
52
|
+
if is_docker():
|
|
53
|
+
cmd.extend(
|
|
54
|
+
["-p", os.path.join(os.path.dirname(__file__), "puppeteer-config.json")]
|
|
55
|
+
)
|
|
56
|
+
LOGGER.debug(f"Executing: {' '.join(cmd)}")
|
|
27
57
|
try:
|
|
28
|
-
cmd = [
|
|
29
|
-
executable,
|
|
30
|
-
"--input",
|
|
31
|
-
"-",
|
|
32
|
-
"--output",
|
|
33
|
-
filename,
|
|
34
|
-
"--outputFormat",
|
|
35
|
-
output_format,
|
|
36
|
-
]
|
|
37
58
|
proc = subprocess.Popen(
|
|
38
59
|
cmd,
|
|
39
60
|
stdout=subprocess.PIPE,
|
|
@@ -41,10 +62,11 @@ def render(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
|
|
|
41
62
|
stderr=subprocess.PIPE,
|
|
42
63
|
text=False,
|
|
43
64
|
)
|
|
44
|
-
proc.communicate(input=source.encode("utf-8"))
|
|
65
|
+
stdout, stderr = proc.communicate(input=source.encode("utf-8"))
|
|
45
66
|
if proc.returncode:
|
|
46
67
|
raise RuntimeError(
|
|
47
|
-
f"failed to convert Mermaid diagram; exit code: {proc.returncode}"
|
|
68
|
+
f"failed to convert Mermaid diagram; exit code: {proc.returncode}, "
|
|
69
|
+
f"output:\n{stdout.decode('utf-8')}\n{stderr.decode('utf-8')}"
|
|
48
70
|
)
|
|
49
71
|
with open(filename, "rb") as image:
|
|
50
72
|
return image.read()
|
md2conf/processor.py
CHANGED
|
@@ -11,6 +11,7 @@ from .converter import (
|
|
|
11
11
|
ConfluenceQualifiedID,
|
|
12
12
|
extract_qualified_id,
|
|
13
13
|
)
|
|
14
|
+
from .matcher import Matcher, MatcherOptions
|
|
14
15
|
from .properties import ConfluenceProperties
|
|
15
16
|
|
|
16
17
|
LOGGER = logging.getLogger(__name__)
|
|
@@ -69,16 +70,18 @@ class Processor:
|
|
|
69
70
|
|
|
70
71
|
LOGGER.info(f"Indexing directory: {local_dir}")
|
|
71
72
|
|
|
73
|
+
matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
|
|
74
|
+
|
|
72
75
|
files: List[Path] = []
|
|
73
76
|
directories: List[Path] = []
|
|
74
77
|
for entry in os.scandir(local_dir):
|
|
78
|
+
if matcher.is_excluded(entry.name, entry.is_dir()):
|
|
79
|
+
continue
|
|
80
|
+
|
|
75
81
|
if entry.is_file():
|
|
76
|
-
|
|
77
|
-
# skip non-markdown files
|
|
78
|
-
files.append((Path(local_dir) / entry.name).absolute())
|
|
82
|
+
files.append((Path(local_dir) / entry.name).absolute())
|
|
79
83
|
elif entry.is_dir():
|
|
80
|
-
|
|
81
|
-
directories.append((Path(local_dir) / entry.name).absolute())
|
|
84
|
+
directories.append((Path(local_dir) / entry.name).absolute())
|
|
82
85
|
|
|
83
86
|
for doc in files:
|
|
84
87
|
metadata = self._get_page(doc)
|
md2conf/util.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info >= (3, 9):
|
|
4
|
+
|
|
5
|
+
def removeprefix(string: str, prefix: str) -> str:
|
|
6
|
+
"If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
|
|
7
|
+
|
|
8
|
+
return string.removeprefix(prefix)
|
|
9
|
+
|
|
10
|
+
else:
|
|
11
|
+
|
|
12
|
+
def removeprefix(string: str, prefix: str) -> str:
|
|
13
|
+
"If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
|
|
14
|
+
|
|
15
|
+
if string.startswith(prefix):
|
|
16
|
+
prefix_len = len(prefix)
|
|
17
|
+
return string[prefix_len:]
|
|
18
|
+
else:
|
|
19
|
+
return string
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
md2conf/__init__.py,sha256=xyEemQnRFIqHO1wvcc3eovTSr1CDUve26Sq0msXUIZw,402
|
|
2
|
-
md2conf/__main__.py,sha256=_qUspNQmQdhpH4Myh9vXDcauPyUx_FyEzNtaW_c8ytY,6601
|
|
3
|
-
md2conf/api.py,sha256=UZ7mkeE1d_f_bACj8LC-t6d4EqXFQCufbeVVdi4FsTs,16947
|
|
4
|
-
md2conf/application.py,sha256=W5OV86XbHWP1qqmJAcRvHPH7NpKQE6yF6nlGpN6RmoU,7709
|
|
5
|
-
md2conf/converter.py,sha256=_zFk-H4NZuY2Y58enVGgFNubOJv9EI2u8tS7RQRiD3A,30391
|
|
6
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
7
|
-
md2conf/mermaid.py,sha256=3zawPXHXkCDhEK-WNtCH-gTqsLBDRzLrmlSo8ZW-Ii8,1371
|
|
8
|
-
md2conf/processor.py,sha256=Tx8t7S8Wl1a4rgMvn2-qw8ob9Q5w2L81a0mfqFYmRJg,3963
|
|
9
|
-
md2conf/properties.py,sha256=2l1tW8HmnrEsXN4-Dtby2tYJQTG1MirRpM3H6ykjQ4c,1858
|
|
10
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
markdown_to_confluence-0.2.1.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
12
|
-
markdown_to_confluence-0.2.1.dist-info/METADATA,sha256=AiazDLT-VIO7txFz-lJvvCA0ckVfPNjXNXHV6csWOl4,10422
|
|
13
|
-
markdown_to_confluence-0.2.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
14
|
-
markdown_to_confluence-0.2.1.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
15
|
-
markdown_to_confluence-0.2.1.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
16
|
-
markdown_to_confluence-0.2.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
17
|
-
markdown_to_confluence-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{markdown_to_confluence-0.2.1.dist-info → markdown_to_confluence-0.2.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.2.1.dist-info → markdown_to_confluence-0.2.3.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|