markdown-to-confluence 0.2.2__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.
@@ -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.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.
@@ -208,13 +216,13 @@ You can run the Docker container via `docker run` or via `Dockerfile`. Either ca
208
216
  With `docker run`, you can pass Confluence domain, user, API and space key directly to `docker run`:
209
217
 
210
218
  ```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 ./
219
+ 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
220
  ```
213
221
 
214
222
  Alternatively, you can use a separate file `.env` to pass these parameters as environment variables:
215
223
 
216
224
  ```sh
217
- docker run --rm --env-file .env --name md2conf -v $(pwd):/data leventehunyadi/md2conf ./
225
+ docker run --rm --env-file .env --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest ./
218
226
  ```
219
227
 
220
228
  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=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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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.4"
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
@@ -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
  )
@@ -95,19 +98,19 @@ class Application:
95
98
  files: List[Path] = []
96
99
  directories: List[Path] = []
97
100
  for entry in os.scandir(local_dir):
98
- if matcher.is_excluded(entry.name):
101
+ if matcher.is_excluded(entry.name, entry.is_dir()):
99
102
  continue
100
103
 
101
104
  if entry.is_file():
102
- files.append((Path(local_dir) / entry.name).absolute())
105
+ files.append(Path(local_dir) / entry.name)
103
106
  elif entry.is_dir():
104
- directories.append((Path(local_dir) / entry.name).absolute())
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
@@ -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
  )
@@ -309,10 +338,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
309
338
  anchor.tail = heading.text
310
339
  heading.text = None
311
340
 
312
- def _transform_link(self, anchor: ET._Element) -> None:
341
+ def _transform_link(self, anchor: ET._Element) -> Optional[ET._Element]:
313
342
  url = anchor.attrib["href"]
314
343
  if is_absolute_url(url):
315
- return
344
+ return None
316
345
 
317
346
  LOGGER.debug(f"found link {url} relative to {self.path}")
318
347
  relative_url: ParseResult = urlparse(url)
@@ -325,8 +354,23 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
325
354
  and not relative_url.query
326
355
  ):
327
356
  LOGGER.debug(f"found local URL: {url}")
328
- anchor.attrib["href"] = url
329
- return
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
330
374
 
331
375
  # convert the relative URL to absolute URL based on the base path value, then look up
332
376
  # the absolute path in the page metadata dictionary to discover the relative path
@@ -337,7 +381,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
337
381
  if self.options.ignore_invalid_url:
338
382
  LOGGER.warning(msg)
339
383
  anchor.attrib.pop("href")
340
- return
384
+ return None
341
385
  else:
342
386
  raise DocumentError(msg)
343
387
 
@@ -349,7 +393,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
349
393
  if self.options.ignore_invalid_url:
350
394
  LOGGER.warning(msg)
351
395
  anchor.attrib.pop("href")
352
- return
396
+ return None
353
397
  else:
354
398
  raise DocumentError(msg)
355
399
 
@@ -375,6 +419,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
375
419
 
376
420
  LOGGER.debug(f"transformed relative URL: {url} to URL: {transformed_url}")
377
421
  anchor.attrib["href"] = transformed_url
422
+ return None
378
423
 
379
424
  def _transform_image(self, image: ET._Element) -> ET._Element:
380
425
  path: str = image.attrib["src"]
@@ -678,6 +723,23 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
678
723
  AC("rich-text-body", {}, *list(elem)),
679
724
  )
680
725
 
726
+ def _transform_emoji(self, elem: ET._Element) -> ET._Element:
727
+ shortname = elem.attrib.get("data-emoji", "")
728
+ alt = elem.text or ""
729
+
730
+ # <ac:emoticon ac:name="wink" ac:emoji-shortname=":wink:" ac:emoji-id="1f609" ac:emoji-fallback="&#128521;"/>
731
+ # <ac:emoticon ac:name="blue-star" ac:emoji-shortname=":heavy_plus_sign:" ac:emoji-id="2795" ac:emoji-fallback="&#10133;"/>
732
+ # <ac:emoticon ac:name="blue-star" ac:emoji-shortname=":heavy_minus_sign:" ac:emoji-id="2796" ac:emoji-fallback="&#10134;"/>
733
+ return AC(
734
+ "emoticon",
735
+ {
736
+ # use "blue-star" as a placeholder name to ensure wiki page loads in timely manner
737
+ ET.QName(namespaces["ac"], "name"): "blue-star",
738
+ ET.QName(namespaces["ac"], "emoji-shortname"): f":{shortname}:",
739
+ ET.QName(namespaces["ac"], "emoji-fallback"): alt,
740
+ },
741
+ )
742
+
681
743
  def transform(self, child: ET._Element) -> Optional[ET._Element]:
682
744
  # normalize line breaks to regular space in element text
683
745
  if child.text:
@@ -757,13 +819,15 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
757
819
 
758
820
  # <a href="..."> ... </a>
759
821
  elif child.tag == "a":
760
- self._transform_link(child)
761
- return None
822
+ return self._transform_link(child)
762
823
 
763
824
  # <pre><code class="language-java"> ... </code></pre>
764
825
  elif child.tag == "pre" and len(child) == 1 and child[0].tag == "code":
765
826
  return self._transform_block(child[0])
766
827
 
828
+ elif child.tag == "span" and child.attrib.has_key("data-emoji"):
829
+ return self._transform_emoji(child)
830
+
767
831
  return None
768
832
 
769
833
 
@@ -780,16 +844,16 @@ class DocumentError(RuntimeError):
780
844
  pass
781
845
 
782
846
 
783
- def extract_value(pattern: str, string: str) -> Tuple[Optional[str], str]:
847
+ def extract_value(pattern: str, text: str) -> Tuple[Optional[str], str]:
784
848
  values: List[str] = []
785
849
 
786
850
  def _repl_func(matchobj: re.Match) -> str:
787
851
  values.append(matchobj.group(1))
788
852
  return ""
789
853
 
790
- string = re.sub(pattern, _repl_func, string, 1, re.ASCII)
854
+ text = re.sub(pattern, _repl_func, text, 1, re.ASCII)
791
855
  value = values[0] if values else None
792
- return value, string
856
+ return value, text
793
857
 
794
858
 
795
859
  @dataclass
@@ -802,20 +866,24 @@ class ConfluenceQualifiedID:
802
866
  self.space_key = space_key
803
867
 
804
868
 
805
- def extract_qualified_id(string: str) -> Tuple[Optional[ConfluenceQualifiedID], str]:
869
+ def extract_qualified_id(text: str) -> Tuple[Optional[ConfluenceQualifiedID], str]:
806
870
  "Extracts the Confluence page ID and space key from a Markdown document."
807
871
 
808
- page_id, string = extract_value(r"<!--\s+confluence-page-id:\s*(\d+)\s+-->", string)
872
+ page_id, text = extract_value(r"<!--\s+confluence-page-id:\s*(\d+)\s+-->", text)
809
873
 
810
874
  if page_id is None:
811
- return None, string
875
+ return None, text
812
876
 
813
877
  # extract Confluence space key
814
- space_key, string = extract_value(
815
- r"<!--\s+confluence-space-key:\s*(\S+)\s+-->", string
816
- )
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
+
817
882
 
818
- return ConfluenceQualifiedID(page_id, space_key), string
883
+ def extract_frontmatter(text: str) -> Tuple[Optional[str], str]:
884
+ "Extracts the front matter from a Markdown document."
885
+
886
+ return extract_value(r"(?ms)\A---$(.+?)^---$", text)
819
887
 
820
888
 
821
889
  def read_qualified_id(absolute_path: Path) -> Optional[ConfluenceQualifiedID]:
@@ -892,7 +960,7 @@ class ConfluenceDocument:
892
960
  )
893
961
 
894
962
  # extract frontmatter
895
- frontmatter, text = extract_value(r"(?ms)\A---$(.+?)^---$", text)
963
+ frontmatter, text = extract_frontmatter(text)
896
964
 
897
965
  # convert to HTML
898
966
  html = markdown_to_html(text)
@@ -963,3 +1031,39 @@ def elements_to_string(root: ET._Element) -> str:
963
1031
  return m.group(1)
964
1032
  else:
965
1033
  raise ValueError("expected: Confluence content")
1034
+
1035
+
1036
+ def _content_to_string(dtd_path: Path, content: str) -> str:
1037
+ parser = ET.XMLParser(
1038
+ remove_blank_text=True,
1039
+ remove_comments=True,
1040
+ strip_cdata=False,
1041
+ load_dtd=True,
1042
+ )
1043
+
1044
+ ns_attr_list = "".join(
1045
+ f' xmlns:{key}="{value}"' for key, value in namespaces.items()
1046
+ )
1047
+
1048
+ data = [
1049
+ '<?xml version="1.0"?>',
1050
+ f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path}">'
1051
+ f"<root{ns_attr_list}>",
1052
+ ]
1053
+ data.append(content)
1054
+ data.append("</root>")
1055
+
1056
+ tree = ET.fromstringlist(data, parser=parser)
1057
+ return ET.tostring(tree, pretty_print=True).decode("utf-8")
1058
+
1059
+
1060
+ def content_to_string(content: str) -> str:
1061
+ "Converts a Confluence Storage Format document returned by the API into a readable XML document."
1062
+
1063
+ if sys.version_info >= (3, 9):
1064
+ resource_path = resources.files(__package__).joinpath("entities.dtd")
1065
+ with resources.as_file(resource_path) as dtd_path:
1066
+ return _content_to_string(dtd_path, content)
1067
+ else:
1068
+ with resources.path(__package__, "entities.dtd") as dtd_path:
1069
+ 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/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
@@ -75,13 +75,13 @@ 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():
82
- files.append((Path(local_dir) / entry.name).absolute())
82
+ files.append(Path(local_dir) / entry.name)
83
83
  elif entry.is_dir():
84
- directories.append((Path(local_dir) / entry.name).absolute())
84
+ directories.append(Path(local_dir) / entry.name)
85
85
 
86
86
  for doc in files:
87
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,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,,