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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: markdown-to-confluence
3
- Version: 0.2.3
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,,
@@ -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.3"
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((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
@@ -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) -> None:
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
- anchor.attrib["href"] = url
358
- 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
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, string: str) -> Tuple[Optional[str], 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
- string = re.sub(pattern, _repl_func, string, 1, re.ASCII)
854
+ text = re.sub(pattern, _repl_func, text, 1, re.ASCII)
840
855
  value = values[0] if values else None
841
- return value, string
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(string: str) -> Tuple[Optional[ConfluenceQualifiedID], str]:
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, 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)
858
873
 
859
874
  if page_id is None:
860
- return None, string
875
+ return None, text
861
876
 
862
877
  # extract Confluence space key
863
- space_key, string = extract_value(
864
- r"<!--\s+confluence-space-key:\s*(\S+)\s+-->", string
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 ConfluenceQualifiedID(page_id, space_key), string
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 = extract_value(r"(?ms)\A---$(.+?)^---$", 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((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)
@@ -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,,