markdown-to-confluence 0.3.5__py3-none-any.whl → 0.4.1__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.
md2conf/converter.py CHANGED
@@ -23,9 +23,12 @@ from urllib.parse import ParseResult, quote_plus, urlparse, urlunparse
23
23
  import lxml.etree as ET
24
24
  import markdown
25
25
  from lxml.builder import ElementMaker
26
+ from strong_typing.core import JsonType
26
27
 
28
+ from .collection import ConfluencePageCollection
29
+ from .extra import path_relative_to
27
30
  from .mermaid import render_diagram
28
- from .metadata import ConfluencePageMetadata, ConfluenceSiteMetadata
31
+ from .metadata import ConfluenceSiteMetadata
29
32
  from .properties import PageError
30
33
  from .scanner import ScannedDocument, Scanner
31
34
 
@@ -66,6 +69,12 @@ def is_relative_url(url: str) -> bool:
66
69
  return not bool(urlparts.scheme) and not bool(urlparts.netloc)
67
70
 
68
71
 
72
+ def is_directory_within(absolute_path: Path, base_path: Path) -> bool:
73
+ "True if the absolute path is nested within the base path."
74
+
75
+ return absolute_path.as_posix().startswith(base_path.as_posix())
76
+
77
+
69
78
  def encode_title(text: str) -> str:
70
79
  "Converts a title string such that it is safe to embed into a Confluence URL."
71
80
 
@@ -91,8 +100,10 @@ def emoji_generator(
91
100
  md: markdown.Markdown,
92
101
  ) -> xml.etree.ElementTree.Element:
93
102
  name = (alias or shortname).strip(":")
94
- span = xml.etree.ElementTree.Element("span", {"data-emoji": name})
103
+ span = xml.etree.ElementTree.Element("span", {"data-emoji-shortname": name})
95
104
  if uc is not None:
105
+ span.attrib["data-emoji-unicode"] = uc
106
+
96
107
  # convert series of Unicode code point hexadecimal values into characters
97
108
  span.text = "".join(chr(int(item, base=16)) for item in uc.split("-"))
98
109
  else:
@@ -142,14 +153,11 @@ def _elements_from_strings(dtd_path: Path, items: list[str]) -> ET._Element:
142
153
  load_dtd=True,
143
154
  )
144
155
 
145
- ns_attr_list = "".join(
146
- f' xmlns:{key}="{value}"' for key, value in namespaces.items()
147
- )
156
+ ns_attr_list = "".join(f' xmlns:{key}="{value}"' for key, value in namespaces.items())
148
157
 
149
158
  data = [
150
159
  '<?xml version="1.0"?>',
151
- f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path.as_posix()}">'
152
- f"<root{ns_attr_list}>",
160
+ f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path.as_posix()}"><root{ns_attr_list}>',
153
161
  ]
154
162
  data.extend(items)
155
163
  data.append("</root>")
@@ -362,7 +370,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
362
370
  images: list[Path]
363
371
  embedded_images: dict[str, bytes]
364
372
  site_metadata: ConfluenceSiteMetadata
365
- page_metadata: dict[Path, ConfluencePageMetadata]
373
+ page_metadata: ConfluencePageCollection
366
374
 
367
375
  def __init__(
368
376
  self,
@@ -370,9 +378,13 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
370
378
  path: Path,
371
379
  root_dir: Path,
372
380
  site_metadata: ConfluenceSiteMetadata,
373
- page_metadata: dict[Path, ConfluencePageMetadata],
381
+ page_metadata: ConfluencePageCollection,
374
382
  ) -> None:
375
383
  super().__init__()
384
+
385
+ path = path.resolve(True)
386
+ root_dir = root_dir.resolve(True)
387
+
376
388
  self.options = options
377
389
  self.path = path
378
390
  self.base_dir = path.parent
@@ -406,6 +418,14 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
406
418
  anchor.tail = heading.text
407
419
  heading.text = None
408
420
 
421
+ def _warn_or_raise(self, msg: str) -> None:
422
+ "Emit a warning or raise an exception when a path points to a resource that doesn't exist."
423
+
424
+ if self.options.ignore_invalid_url:
425
+ LOGGER.warning(msg)
426
+ else:
427
+ raise DocumentError(msg)
428
+
409
429
  def _transform_link(self, anchor: ET._Element) -> Optional[ET._Element]:
410
430
  url = anchor.attrib.get("href")
411
431
  if url is None or is_absolute_url(url):
@@ -414,13 +434,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
414
434
  LOGGER.debug("Found link %s relative to %s", url, self.path)
415
435
  relative_url: ParseResult = urlparse(url)
416
436
 
417
- if (
418
- not relative_url.scheme
419
- and not relative_url.netloc
420
- and not relative_url.path
421
- and not relative_url.params
422
- and not relative_url.query
423
- ):
437
+ if not relative_url.scheme and not relative_url.netloc and not relative_url.path and not relative_url.params and not relative_url.query:
424
438
  LOGGER.debug("Found local URL: %s", url)
425
439
  if self.options.heading_anchors:
426
440
  # <ac:link ac:anchor="anchor"><ac:link-body>...</ac:link-body></ac:link>
@@ -443,15 +457,11 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
443
457
  # convert the relative URL to absolute URL based on the base path value, then look up
444
458
  # the absolute path in the page metadata dictionary to discover the relative path
445
459
  # within Confluence that should be used
446
- absolute_path = (self.base_dir / relative_url.path).resolve(True)
447
- if not str(absolute_path).startswith(str(self.root_dir)):
448
- msg = f"relative URL {url} points to outside root path: {self.root_dir}"
449
- if self.options.ignore_invalid_url:
450
- LOGGER.warning(msg)
451
- anchor.attrib.pop("href")
452
- return None
453
- else:
454
- raise DocumentError(msg)
460
+ absolute_path = (self.base_dir / relative_url.path).resolve()
461
+ if not is_directory_within(absolute_path, self.root_dir):
462
+ anchor.attrib.pop("href")
463
+ self._warn_or_raise(f"relative URL {url} points to outside root path: {self.root_dir}")
464
+ return None
455
465
 
456
466
  link_metadata = self.page_metadata.get(absolute_path)
457
467
  if link_metadata is None:
@@ -464,9 +474,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
464
474
  raise DocumentError(msg)
465
475
 
466
476
  relative_path = os.path.relpath(absolute_path, self.base_dir)
467
- LOGGER.debug(
468
- "found link to page %s with metadata: %s", relative_path, link_metadata
469
- )
477
+ LOGGER.debug("found link to page %s with metadata: %s", relative_path, link_metadata)
470
478
  self.links.append(url)
471
479
 
472
480
  if self.options.webui_links:
@@ -475,9 +483,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
475
483
  space_key = link_metadata.space_key or self.site_metadata.space_key
476
484
 
477
485
  if space_key is None:
478
- raise DocumentError(
479
- "Confluence space key required for building full web URLs"
480
- )
486
+ raise DocumentError("Confluence space key required for building full web URLs")
481
487
 
482
488
  page_url = f"{self.site_metadata.base_path}spaces/{space_key}/pages/{link_metadata.page_id}/{encode_title(link_metadata.title)}"
483
489
 
@@ -519,9 +525,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
519
525
  else:
520
526
  return self._transform_attached_image(Path(src), caption, attributes)
521
527
 
522
- def _transform_external_image(
523
- self, url: str, caption: Optional[str], attributes: dict[str, Any]
524
- ) -> ET._Element:
528
+ def _transform_external_image(self, url: str, caption: Optional[str], attributes: dict[str, Any]) -> ET._Element:
525
529
  "Emits Confluence Storage Format XHTML for an external image."
526
530
 
527
531
  elements: list[ET._Element] = []
@@ -537,18 +541,28 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
537
541
 
538
542
  return AC("image", attributes, *elements)
539
543
 
540
- def _transform_attached_image(
541
- self, path: Path, caption: Optional[str], attributes: dict[str, Any]
542
- ) -> ET._Element:
544
+ def _transform_attached_image(self, path: Path, caption: Optional[str], attributes: dict[str, Any]) -> ET._Element:
543
545
  "Emits Confluence Storage Format XHTML for an attached image."
544
546
 
545
- # prefer PNG over SVG; Confluence displays SVG in wrong size, and text labels are truncated
546
- png_file = path.with_suffix(".png")
547
- if path.suffix == ".svg" and (self.base_dir / png_file).exists():
548
- path = png_file
547
+ # resolve relative path into absolute path w.r.t. base dir
548
+ absolute_path = (self.base_dir / path).resolve()
549
549
 
550
- self.images.append(path)
551
- image_name = attachment_name(path)
550
+ if absolute_path.exists():
551
+ # prefer PNG over SVG; Confluence displays SVG in wrong size, and text labels are truncated
552
+ if absolute_path.suffix == ".svg":
553
+ png_file = absolute_path.with_suffix(".png")
554
+ if png_file.exists():
555
+ absolute_path = png_file
556
+
557
+ if is_directory_within(absolute_path, self.root_dir):
558
+ self.images.append(absolute_path)
559
+ image_name = attachment_name(path_relative_to(absolute_path, self.base_dir))
560
+ else:
561
+ image_name = ""
562
+ self._warn_or_raise(f"path to image {path} points to outside root path {self.root_dir}")
563
+ else:
564
+ image_name = ""
565
+ self._warn_or_raise(f"path to image {path} does not exist")
552
566
 
553
567
  elements: list[ET._Element] = []
554
568
  elements.append(
@@ -604,9 +618,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
604
618
  if self.options.render_mermaid:
605
619
  image_data = render_diagram(content, self.options.diagram_output_format)
606
620
  image_hash = hashlib.md5(image_data).hexdigest()
607
- image_filename = attachment_name(
608
- f"embedded_{image_hash}.{self.options.diagram_output_format}"
609
- )
621
+ image_filename = attachment_name(f"embedded_{image_hash}.{self.options.diagram_output_format}")
610
622
  self.embedded_images[image_filename] = image_data
611
623
  return AC(
612
624
  "image",
@@ -766,9 +778,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
766
778
 
767
779
  return self._transform_alert(elem, class_name, skip)
768
780
 
769
- def _transform_alert(
770
- self, elem: ET._Element, class_name: Optional[str], skip: int
771
- ) -> ET._Element:
781
+ def _transform_alert(self, elem: ET._Element, class_name: Optional[str], skip: int) -> ET._Element:
772
782
  """
773
783
  Creates an info, tip, note or warning panel from a GitHub or GitLab alert.
774
784
 
@@ -803,14 +813,12 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
803
813
  Creates a collapsed section.
804
814
 
805
815
  Transforms
806
- [GitHub collapsed section](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections) # noqa: E501 # no way to make this link shorter
816
+ [GitHub collapsed section](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections)
807
817
  syntax into the Confluence structured macro *expand*.
808
818
  """
809
819
 
810
820
  if elem[0].tag != "summary":
811
- raise DocumentError(
812
- "expected: `<summary>` as first direct child of `<details>`"
813
- )
821
+ raise DocumentError("expected: `<summary>` as first direct child of `<details>`")
814
822
  if elem[0].tail is not None:
815
823
  raise DocumentError('expected: attribute `markdown="1"` on `<details>`')
816
824
 
@@ -834,7 +842,8 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
834
842
  )
835
843
 
836
844
  def _transform_emoji(self, elem: ET._Element) -> ET._Element:
837
- shortname = elem.attrib.get("data-emoji", "")
845
+ shortname = elem.attrib.get("data-emoji-shortname", "")
846
+ unicode = elem.attrib.get("data-emoji-unicode", None)
838
847
  alt = elem.text or ""
839
848
 
840
849
  # <ac:emoticon ac:name="wink" ac:emoji-shortname=":wink:" ac:emoji-id="1f609" ac:emoji-fallback="&#128521;"/>
@@ -844,8 +853,9 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
844
853
  "emoticon",
845
854
  {
846
855
  # use "blue-star" as a placeholder name to ensure wiki page loads in timely manner
847
- ET.QName(namespaces["ac"], "name"): "blue-star",
856
+ ET.QName(namespaces["ac"], "name"): shortname,
848
857
  ET.QName(namespaces["ac"], "emoji-shortname"): f":{shortname}:",
858
+ ET.QName(namespaces["ac"], "emoji-id"): unicode,
849
859
  ET.QName(namespaces["ac"], "emoji-fallback"): alt,
850
860
  },
851
861
  )
@@ -900,13 +910,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
900
910
  # <blockquote>
901
911
  # <p>[!TIP] ...</p>
902
912
  # </blockquote>
903
- elif (
904
- child.tag == "blockquote"
905
- and len(child) > 0
906
- and child[0].tag == "p"
907
- and child[0].text is not None
908
- and child[0].text.startswith("[!")
909
- ):
913
+ elif child.tag == "blockquote" and len(child) > 0 and child[0].tag == "p" and child[0].text is not None and child[0].text.startswith("[!"):
910
914
  return self._transform_github_alert(child)
911
915
 
912
916
  # Alerts in GitLab
@@ -918,9 +922,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
918
922
  and len(child) > 0
919
923
  and child[0].tag == "p"
920
924
  and child[0].text is not None
921
- and starts_with_any(
922
- child[0].text, ["FLAG:", "NOTE:", "WARNING:", "DISCLAIMER:"]
923
- )
925
+ and starts_with_any(child[0].text, ["FLAG:", "NOTE:", "WARNING:", "DISCLAIMER:"])
924
926
  ):
925
927
  return self._transform_gitlab_alert(child)
926
928
 
@@ -943,7 +945,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
943
945
  elif child.tag == "pre" and len(child) == 1 and child[0].tag == "code":
944
946
  return self._transform_block(child[0])
945
947
 
946
- elif child.tag == "span" and child.attrib.has_key("data-emoji"):
948
+ elif child.tag == "span" and child.attrib.has_key("data-emoji-shortname"):
947
949
  return self._transform_emoji(child)
948
950
 
949
951
  return None
@@ -1006,6 +1008,8 @@ class ConversionError(RuntimeError):
1006
1008
 
1007
1009
  class ConfluenceDocument:
1008
1010
  title: Optional[str]
1011
+ labels: Optional[list[str]]
1012
+ properties: Optional[dict[str, JsonType]]
1009
1013
  links: list[str]
1010
1014
  images: list[Path]
1011
1015
 
@@ -1019,7 +1023,7 @@ class ConfluenceDocument:
1019
1023
  options: ConfluenceDocumentOptions,
1020
1024
  root_dir: Path,
1021
1025
  site_metadata: ConfluenceSiteMetadata,
1022
- page_metadata: dict[Path, ConfluencePageMetadata],
1026
+ page_metadata: ConfluencePageCollection,
1023
1027
  ) -> tuple[ConfluencePageID, "ConfluenceDocument"]:
1024
1028
  path = path.resolve(True)
1025
1029
 
@@ -1035,9 +1039,7 @@ class ConfluenceDocument:
1035
1039
  else:
1036
1040
  raise PageError("missing Confluence page ID")
1037
1041
 
1038
- return page_id, ConfluenceDocument(
1039
- path, document, options, root_dir, site_metadata, page_metadata
1040
- )
1042
+ return page_id, ConfluenceDocument(path, document, options, root_dir, site_metadata, page_metadata)
1041
1043
 
1042
1044
  def __init__(
1043
1045
  self,
@@ -1046,7 +1048,7 @@ class ConfluenceDocument:
1046
1048
  options: ConfluenceDocumentOptions,
1047
1049
  root_dir: Path,
1048
1050
  site_metadata: ConfluenceSiteMetadata,
1049
- page_metadata: dict[Path, ConfluencePageMetadata],
1051
+ page_metadata: ConfluencePageCollection,
1050
1052
  ) -> None:
1051
1053
  self.options = options
1052
1054
 
@@ -1095,21 +1097,43 @@ class ConfluenceDocument:
1095
1097
  self.embedded_images = converter.embedded_images
1096
1098
 
1097
1099
  self.title = document.title or converter.toc.get_title()
1100
+ self.labels = document.tags
1101
+ self.properties = document.properties
1098
1102
 
1099
1103
  def xhtml(self) -> str:
1100
1104
  return elements_to_string(self.root)
1101
1105
 
1102
1106
 
1103
- def attachment_name(name: Union[Path, str]) -> str:
1107
+ def attachment_name(ref: Union[Path, str]) -> str:
1104
1108
  """
1105
1109
  Safe name for use with attachment uploads.
1106
1110
 
1111
+ Mutates a relative path such that it meets Confluence's attachment naming requirements.
1112
+
1107
1113
  Allowed characters:
1114
+
1108
1115
  * Alphanumeric characters: 0-9, a-z, A-Z
1109
1116
  * Special characters: hyphen (-), underscore (_), period (.)
1110
1117
  """
1111
1118
 
1112
- return re.sub(r"[^\-0-9A-Za-z_.]", "_", str(name))
1119
+ if isinstance(ref, Path):
1120
+ path = ref
1121
+ else:
1122
+ path = Path(ref)
1123
+
1124
+ if path.drive or path.root:
1125
+ raise ValueError(f"required: relative path; got: {ref}")
1126
+
1127
+ regexp = re.compile(r"[^\-0-9A-Za-z_.]", re.UNICODE)
1128
+
1129
+ def replace_part(part: str) -> str:
1130
+ if part == "..":
1131
+ return "PAR"
1132
+ else:
1133
+ return regexp.sub("_", part)
1134
+
1135
+ parts = [replace_part(p) for p in path.parts]
1136
+ return Path(*parts).as_posix().replace("/", "_")
1113
1137
 
1114
1138
 
1115
1139
  def sanitize_confluence(html: str) -> str:
@@ -1140,14 +1164,11 @@ def _content_to_string(dtd_path: Path, content: str) -> str:
1140
1164
  load_dtd=True,
1141
1165
  )
1142
1166
 
1143
- ns_attr_list = "".join(
1144
- f' xmlns:{key}="{value}"' for key, value in namespaces.items()
1145
- )
1167
+ ns_attr_list = "".join(f' xmlns:{key}="{value}"' for key, value in namespaces.items())
1146
1168
 
1147
1169
  data = [
1148
1170
  '<?xml version="1.0"?>',
1149
- f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path}">'
1150
- f"<root{ns_attr_list}>",
1171
+ f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path.as_posix()}"><root{ns_attr_list}>',
1151
1172
  ]
1152
1173
  data.append(content)
1153
1174
  data.append("</root>")
md2conf/emoji.py CHANGED
@@ -10,7 +10,24 @@ import pathlib
10
10
 
11
11
  import pymdownx.emoji1_db as emoji_db
12
12
 
13
- EMOJI_PAGE_ID = "86918529216"
13
+ EMOJI_PAGE_ID = "13500452"
14
+
15
+
16
+ def to_html(cp: int) -> str:
17
+ """
18
+ Returns the safe HTML representation for a Unicode code point.
19
+
20
+ Converts non-ASCII and non-printable characters into HTML entities with decimal notation.
21
+
22
+ :param cp: Unicode code point.
23
+ :returns: An HTML representation of the Unicode character.
24
+ """
25
+
26
+ ch = chr(cp)
27
+ if ch.isascii() and ch.isalnum():
28
+ return ch
29
+ else:
30
+ return f"&#{cp};"
14
31
 
15
32
 
16
33
  def generate_source(path: pathlib.Path) -> None:
@@ -47,11 +64,19 @@ def generate_target(path: pathlib.Path) -> None:
47
64
  print("<thead><tr><th>Icon</th><th>Emoji code</th></tr></thead>", file=f)
48
65
  print("<tbody>", file=f)
49
66
  for key, data in emojis.items():
67
+ unicode = data["unicode"]
50
68
  key = key.strip(":")
51
- unicode = "".join(f"&#x{item};" for item in data["unicode"].split("-"))
69
+ html = "".join(to_html(int(item, base=16)) for item in unicode.split("-"))
52
70
 
53
71
  print(
54
- f'<tr><td><ac:emoticon ac:name="blue-star" ac:emoji-shortname=":{key}:" ac:emoji-fallback="{unicode}"/></td><td><code>:{key}:</code></td></tr>',
72
+ f"<tr>\n"
73
+ f" <td>\n"
74
+ f' <ac:emoticon ac:name="{key}" ac:emoji-shortname=":{key}:" ac:emoji-id="{unicode}" ac:emoji-fallback="{html}"/>\n'
75
+ f" </td>\n"
76
+ f" <td>\n"
77
+ f" <code>:{key}:</code>\n"
78
+ f" </td>\n"
79
+ f"</tr>",
55
80
  file=f,
56
81
  )
57
82
  print("</tbody>", file=f)
md2conf/extra.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2025, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import sys
10
+
11
+ if sys.version_info >= (3, 12):
12
+ from typing import override as override # noqa: F401
13
+ else:
14
+ from typing_extensions import override as override # noqa: F401
15
+
16
+ if sys.version_info >= (3, 12):
17
+ from pathlib import Path
18
+
19
+ def path_relative_to(destination: Path, origin: Path) -> Path:
20
+ return destination.relative_to(origin, walk_up=True)
21
+
22
+ else:
23
+ import os.path
24
+ from pathlib import Path
25
+
26
+ def path_relative_to(destination: Path, origin: Path) -> Path:
27
+ return Path(os.path.relpath(destination, start=origin))
md2conf/local.py CHANGED
@@ -6,17 +6,15 @@ Copyright 2022-2025, Levente Hunyadi
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
8
8
 
9
- import hashlib
10
9
  import logging
11
10
  import os
12
11
  from pathlib import Path
13
12
  from typing import Optional
14
13
 
15
14
  from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID
15
+ from .extra import override
16
16
  from .metadata import ConfluencePageMetadata, ConfluenceSiteMetadata
17
- from .processor import Converter, Processor, ProcessorFactory
18
- from .properties import PageError
19
- from .scanner import Scanner
17
+ from .processor import Converter, DocumentNode, Processor, ProcessorFactory
20
18
 
21
19
  LOGGER = logging.getLogger(__name__)
22
20
 
@@ -46,44 +44,35 @@ class LocalProcessor(Processor):
46
44
  super().__init__(options, site, root_dir)
47
45
  self.out_dir = out_dir or root_dir
48
46
 
49
- def _get_or_create_page(
50
- self, absolute_path: Path, parent_id: Optional[ConfluencePageID]
51
- ) -> ConfluencePageMetadata:
52
- """
53
- Extracts metadata from a Markdown file.
47
+ @override
48
+ def _synchronize_tree(self, root: DocumentNode, root_id: Optional[ConfluencePageID]) -> None:
54
49
  """
50
+ Creates the cross-reference index.
55
51
 
56
- # parse file
57
- document = Scanner().read(absolute_path)
58
- if document.page_id is not None:
59
- page_id = document.page_id
60
- space_key = document.space_key or self.site.space_key or "HOME"
61
- else:
62
- if parent_id is None:
63
- raise PageError(
64
- f"expected: parent page ID for Markdown file with no linked Confluence page: {absolute_path}"
65
- )
66
-
67
- hash = hashlib.md5(document.text.encode("utf-8"))
68
- digest = "".join(f"{c:x}" for c in hash.digest())
69
- LOGGER.info("Identifier %s assigned to page: %s", digest, absolute_path)
70
- page_id = digest
71
- space_key = self.site.space_key or "HOME"
72
-
73
- return ConfluencePageMetadata(
74
- page_id=page_id,
75
- space_key=space_key,
76
- title="",
77
- overwrite=True,
78
- )
79
-
80
- def _save_document(
81
- self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path
82
- ) -> None:
52
+ Does not change Markdown files.
83
53
  """
84
- Saves a new version of a Confluence document.
85
54
 
86
- A derived class may invoke Confluence REST API to persist the new version.
55
+ for node in root.all():
56
+ if node.page_id is not None:
57
+ page_id = node.page_id
58
+ else:
59
+ digest = self._generate_hash(node.absolute_path)
60
+ LOGGER.info("Identifier %s assigned to page: %s", digest, node.absolute_path)
61
+ page_id = digest
62
+
63
+ self.page_metadata.add(
64
+ node.absolute_path,
65
+ ConfluencePageMetadata(
66
+ page_id=page_id,
67
+ space_key=node.space_key or self.site.space_key or "HOME",
68
+ title=node.title or "",
69
+ ),
70
+ )
71
+
72
+ @override
73
+ def _update_page(self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path) -> None:
74
+ """
75
+ Saves the document as Confluence Storage Format XHTML to the local disk.
87
76
  """
88
77
 
89
78
  content = document.xhtml()
@@ -106,9 +95,7 @@ class LocalProcessorFactory(ProcessorFactory):
106
95
  self.out_dir = out_dir
107
96
 
108
97
  def create(self, root_dir: Path) -> Processor:
109
- return LocalProcessor(
110
- self.options, self.site, out_dir=self.out_dir, root_dir=root_dir
111
- )
98
+ return LocalProcessor(self.options, self.site, out_dir=self.out_dir, root_dir=root_dir)
112
99
 
113
100
 
114
101
  class LocalConverter(Converter):
md2conf/matcher.py CHANGED
@@ -156,6 +156,4 @@ class Matcher:
156
156
  :returns: A filtered list of entries whose name didn't match any of the exclusion rules.
157
157
  """
158
158
 
159
- return self.filter(
160
- Entry(entry.name, entry.is_dir()) for entry in os.scandir(path)
161
- )
159
+ return self.filter(Entry(entry.name, entry.is_dir()) for entry in os.scandir(path))
md2conf/mermaid.py CHANGED
@@ -19,10 +19,7 @@ LOGGER = logging.getLogger(__name__)
19
19
  def is_docker() -> bool:
20
20
  "True if the application is running in a Docker container."
21
21
 
22
- return (
23
- os.environ.get("CHROME_BIN") == "/usr/bin/chromium-browser"
24
- and os.environ.get("PUPPETEER_SKIP_DOWNLOAD") == "true"
25
- )
22
+ return os.environ.get("CHROME_BIN") == "/usr/bin/chromium-browser" and os.environ.get("PUPPETEER_SKIP_DOWNLOAD") == "true"
26
23
 
27
24
 
28
25
  def get_mmdc() -> str:
@@ -79,9 +76,7 @@ def render_diagram(source: str, output_format: Literal["png", "svg"] = "png") ->
79
76
  )
80
77
  stdout, stderr = proc.communicate(input=source.encode("utf-8"))
81
78
  if proc.returncode:
82
- messages = [
83
- f"failed to convert Mermaid diagram; exit code: {proc.returncode}"
84
- ]
79
+ messages = [f"failed to convert Mermaid diagram; exit code: {proc.returncode}"]
85
80
  console_output = stdout.decode("utf-8")
86
81
  if console_output:
87
82
  messages.append(f"output:\n{console_output}")
md2conf/metadata.py CHANGED
@@ -33,10 +33,8 @@ class ConfluencePageMetadata:
33
33
  :param page_id: Confluence page ID.
34
34
  :param space_key: Confluence space key.
35
35
  :param title: Document title.
36
- :param overwrite: True if operations are allowed to update document properties (e.g. title).
37
36
  """
38
37
 
39
38
  page_id: str
40
39
  space_key: str
41
40
  title: str
42
- overwrite: bool