markdown-to-confluence 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: markdown-to-confluence
3
- Version: 0.2.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
@@ -208,13 +208,13 @@ You can run the Docker container via `docker run` or via `Dockerfile`. Either ca
208
208
  With `docker run`, you can pass Confluence domain, user, API and space key directly to `docker run`:
209
209
 
210
210
  ```sh
211
- docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf -d instructure.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s DAP ./
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
212
  ```
213
213
 
214
214
  Alternatively, you can use a separate file `.env` to pass these parameters as environment variables:
215
215
 
216
216
  ```sh
217
- docker run --rm --env-file .env --name md2conf -v $(pwd):/data leventehunyadi/md2conf ./
217
+ docker run --rm --env-file .env --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest ./
218
218
  ```
219
219
 
220
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.
@@ -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.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
@@ -95,7 +95,7 @@ class Application:
95
95
  files: List[Path] = []
96
96
  directories: List[Path] = []
97
97
  for entry in os.scandir(local_dir):
98
- if matcher.is_excluded(entry.name):
98
+ if matcher.is_excluded(entry.name, entry.is_dir()):
99
99
  continue
100
100
 
101
101
  if entry.is_file():
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="&#128521;"/>
715
+ # <ac:emoticon ac:name="blue-star" ac:emoji-shortname=":heavy_plus_sign:" ac:emoji-id="2795" ac:emoji-fallback="&#10133;"/>
716
+ # <ac:emoticon ac:name="blue-star" ac:emoji-shortname=":heavy_minus_sign:" ac:emoji-id="2796" ac:emoji-fallback="&#10134;"/>
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 CHANGED
@@ -5,6 +5,14 @@ from pathlib import Path
5
5
  from typing import Iterable, List, Optional
6
6
 
7
7
 
8
+ @dataclass
9
+ class Entry:
10
+ "Represents a file or directory entry."
11
+
12
+ name: str
13
+ is_dir: bool
14
+
15
+
8
16
  @dataclass
9
17
  class MatcherOptions:
10
18
  """
@@ -42,13 +50,21 @@ class Matcher:
42
50
 
43
51
  return self.options.extension is None or name.endswith(self.options.extension)
44
52
 
45
- def is_excluded(self, name: str) -> bool:
46
- "True if the file or directory name matches any of the exclusion patterns."
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
+ """
47
61
 
62
+ # skip hidden files and directories
48
63
  if name.startswith("."):
49
64
  return True
50
65
 
51
- if not self.extension_matches(name):
66
+ # match extension for regular files
67
+ if not is_dir and not self.extension_matches(name):
52
68
  return True
53
69
 
54
70
  for rule in self.rules:
@@ -57,12 +73,18 @@ class Matcher:
57
73
  else:
58
74
  return False
59
75
 
60
- def is_included(self, name: str) -> bool:
61
- "True if the file or directory name matches none of the exclusion patterns."
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
+ """
62
84
 
63
- return not self.is_excluded(name)
85
+ return not self.is_excluded(name, is_dir)
64
86
 
65
- def filter(self, items: Iterable[str]) -> List[str]:
87
+ def filter(self, items: Iterable[Entry]) -> List[Entry]:
66
88
  """
67
89
  Returns only those elements from the input that don't match any of the exclusion rules.
68
90
 
@@ -70,9 +92,9 @@ class Matcher:
70
92
  :returns: A filtered list of names that didn't match any of the exclusion rules.
71
93
  """
72
94
 
73
- return [item for item in items if self.is_included(item)]
95
+ return [item for item in items if self.is_included(item.name, item.is_dir)]
74
96
 
75
- def scandir(self, path: Path) -> List[str]:
97
+ def scandir(self, path: Path) -> List[Entry]:
76
98
  """
77
99
  Returns only those entries in a directory whose name doesn't match any of the exclusion rules.
78
100
 
@@ -80,4 +102,6 @@ class Matcher:
80
102
  :returns: A filtered list of entries whose name didn't match any of the exclusion rules.
81
103
  """
82
104
 
83
- return self.filter(entry.name for entry in os.scandir(path))
105
+ return self.filter(
106
+ Entry(entry.name, entry.is_dir()) for entry in os.scandir(path)
107
+ )
md2conf/processor.py CHANGED
@@ -75,7 +75,7 @@ class Processor:
75
75
  files: List[Path] = []
76
76
  directories: List[Path] = []
77
77
  for entry in os.scandir(local_dir):
78
- if matcher.is_excluded(entry.name):
78
+ if matcher.is_excluded(entry.name, entry.is_dir()):
79
79
  continue
80
80
 
81
81
  if entry.is_file():
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,19 +0,0 @@
1
- md2conf/__init__.py,sha256=1DSbQlz0zNxil7Lbsh7VjmGvJdtKhOjtd67r2elUSjE,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=mQusGnzu-ssFn9-aC_rGsqsWpDtw8qFJDnPW7cRkXC0,7762
5
- md2conf/converter.py,sha256=_zFk-H4NZuY2Y58enVGgFNubOJv9EI2u8tS7RQRiD3A,30391
6
- md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
7
- md2conf/matcher.py,sha256=SAmXQzQNan05jVcmZ8PEONynj-SEcVrkCHyXvBxEi2Q,2690
8
- md2conf/mermaid.py,sha256=a7PVcd7kcFBOMw7Z2mOfvWC1JIVR4Q1EkkanLk1SLx0,1981
9
- md2conf/processor.py,sha256=V_kxpk4da8vzSLx4Zixhf1sEWdVIxKZeJocJvWhOK6Y,4020
10
- md2conf/properties.py,sha256=2l1tW8HmnrEsXN4-Dtby2tYJQTG1MirRpM3H6ykjQ4c,1858
11
- md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
12
- md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- markdown_to_confluence-0.2.2.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
14
- markdown_to_confluence-0.2.2.dist-info/METADATA,sha256=a_CQkC2-De5lcIAudWShsx0m1DIAtA6utrsJKcAi20I,11571
15
- markdown_to_confluence-0.2.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
16
- markdown_to_confluence-0.2.2.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
17
- markdown_to_confluence-0.2.2.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
18
- markdown_to_confluence-0.2.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
19
- markdown_to_confluence-0.2.2.dist-info/RECORD,,