markdown-to-confluence 0.2.6__py3-none-any.whl → 0.2.7__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.6
3
+ Version: 0.2.7
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Home-page: https://github.com/hunyadi/md2conf
6
6
  Author: Levente Hunyadi
@@ -50,7 +50,7 @@ This Python package
50
50
  * Link to [sections on the same page](#getting-started) or [external locations](http://example.com/)
51
51
  * Ordered and unordered lists
52
52
  * Code blocks (e.g. Python, JSON, XML)
53
- * Image references (uploaded as Confluence page attachments)
53
+ * Images (uploaded as Confluence page attachments or hosted externally)
54
54
  * Tables
55
55
  * [Table of contents](https://docs.gitlab.com/ee/user/markdown.html#table-of-contents)
56
56
  * [Admonitions](https://python-markdown.github.io/extensions/admonition/) and alert boxes in [GitHub](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) and [GitLab](https://docs.gitlab.com/ee/development/documentation/styleguide/#alert-boxes)
@@ -1,8 +1,8 @@
1
- md2conf/__init__.py,sha256=aD2z2fkqyEVbUDQvLSJxfFUOpwMYt5lAZIUAQocULuM,402
1
+ md2conf/__init__.py,sha256=U8zdop7-AIrfwCYzWiwKfhCEPF_1QEKPt4Zwq-38LlU,402
2
2
  md2conf/__main__.py,sha256=6iOI28W_d71tlnCMFpZwvkBmBt5-HazlZsz69gS4Oak,6894
3
- md2conf/api.py,sha256=T-g_VS_cVahcYOs2jBVW38J7MSS94JxzMXlqohd_Sfw,17326
4
- md2conf/application.py,sha256=DSnqBx5hOWWVopnjo1iK_tbQg_7H8MhNPx_SAC3ovXQ,9157
5
- md2conf/converter.py,sha256=5cxxHnI9ux0pi-VW-CArBCGcpMClb8qEJZQd--NyrdY,35042
3
+ md2conf/api.py,sha256=NmAbNWTrTSi2ZDGYymy70Fw6HcgrmB-Ua4re4yLJvVc,17715
4
+ md2conf/application.py,sha256=-kFpMRtSpQUU1hsiW5O73gL1X9McQWpvyAAEUxEnpuU,8869
5
+ md2conf/converter.py,sha256=S8Kka35Y99w0J00CYi-DQwsKzlHAvBfaSCf10mb1FZk,36596
6
6
  md2conf/emoji.py,sha256=w9oiOIxzObAE7HTo3f6aETT1_D3t3yZwr88ynU4ENm0,1924
7
7
  md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
8
8
  md2conf/matcher.py,sha256=mYMltZOLypK4O-SJugLgicOwUMem67hiNLg_kPFoJkU,3583
@@ -12,10 +12,10 @@ md2conf/properties.py,sha256=iVIc0h0XtS3Y2LCywX1C9cvmVQ0WljOMt8pl2MDMVCI,1990
12
12
  md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
13
13
  md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  md2conf/util.py,sha256=ftf60MiW7S7rW45ipWX6efP_Sv2F2qpyIDHrGA0cBiw,743
15
- markdown_to_confluence-0.2.6.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
16
- markdown_to_confluence-0.2.6.dist-info/METADATA,sha256=kRQoSGz7LUCqHS7YhX9xsGae9OTql11hMWEnNP0ZRAw,13540
17
- markdown_to_confluence-0.2.6.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
18
- markdown_to_confluence-0.2.6.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
19
- markdown_to_confluence-0.2.6.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
20
- markdown_to_confluence-0.2.6.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
21
- markdown_to_confluence-0.2.6.dist-info/RECORD,,
15
+ markdown_to_confluence-0.2.7.dist-info/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
16
+ markdown_to_confluence-0.2.7.dist-info/METADATA,sha256=76K_O_5b__MnKT7FuLXgCHX6hR5dZio3mK6RWR4DyCA,13551
17
+ markdown_to_confluence-0.2.7.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
18
+ markdown_to_confluence-0.2.7.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
19
+ markdown_to_confluence-0.2.7.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
20
+ markdown_to_confluence-0.2.7.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
21
+ markdown_to_confluence-0.2.7.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.6"
8
+ __version__ = "0.2.7"
9
9
  __author__ = "Levente Hunyadi"
10
10
  __copyright__ = "Copyright 2022-2024, Levente Hunyadi"
11
11
  __license__ = "MIT"
md2conf/api.py CHANGED
@@ -420,12 +420,23 @@ class ConfluenceSession:
420
420
  new_content: str,
421
421
  *,
422
422
  space_key: Optional[str] = None,
423
+ title: Optional[str] = None,
423
424
  ) -> None:
425
+ """
426
+ Update a page via the Confluence API.
427
+
428
+ :param page_id: The Confluence page ID.
429
+ :param new_content: Confluence Storage Format XHTML.
430
+ :param space_key: The Confluence space key (unless the default space is to be used).
431
+ :param title: New title to assign to the page. Needs to be unique within a space.
432
+ """
433
+
424
434
  page = self.get_page(page_id, space_key=space_key)
435
+ new_title = title or page.title
425
436
 
426
437
  try:
427
438
  old_content = sanitize_confluence(page.content)
428
- if old_content == new_content:
439
+ if page.title == new_title and old_content == new_content:
429
440
  LOGGER.info("Up-to-date page: %s", page_id)
430
441
  return
431
442
  except ParseError as exc:
@@ -435,7 +446,7 @@ class ConfluenceSession:
435
446
  data = {
436
447
  "id": page_id,
437
448
  "type": "page",
438
- "title": page.title, # title needs to be unique within a space so the original title is maintained
449
+ "title": new_title,
439
450
  "space": {"key": space_key or self.space_key},
440
451
  "body": {"storage": {"value": new_content, "representation": "storage"}},
441
452
  "version": {"minorEdit": True, "number": page.version + 1},
md2conf/application.py CHANGED
@@ -11,8 +11,6 @@ import os.path
11
11
  from pathlib import Path
12
12
  from typing import Dict, List, Optional
13
13
 
14
- import yaml
15
-
16
14
  from .api import ConfluencePage, ConfluenceSession
17
15
  from .converter import (
18
16
  ConfluenceDocument,
@@ -20,7 +18,7 @@ from .converter import (
20
18
  ConfluencePageMetadata,
21
19
  ConfluenceQualifiedID,
22
20
  attachment_name,
23
- extract_frontmatter,
21
+ extract_frontmatter_title,
24
22
  extract_qualified_id,
25
23
  read_qualified_id,
26
24
  )
@@ -174,7 +172,7 @@ class Application:
174
172
  document = f.read()
175
173
 
176
174
  qualified_id, document = extract_qualified_id(document)
177
- frontmatter, document = extract_frontmatter(document)
175
+ frontmatter_title, _ = extract_frontmatter_title(document)
178
176
 
179
177
  if qualified_id is not None:
180
178
  confluence_page = self.api.get_page(
@@ -187,15 +185,8 @@ class Application:
187
185
  )
188
186
 
189
187
  # assign title from frontmatter if present
190
- if title is None and frontmatter is not None:
191
- properties = yaml.safe_load(frontmatter)
192
- if isinstance(properties, dict):
193
- property_title = properties.get("title")
194
- if isinstance(property_title, str):
195
- title = property_title
196
-
197
188
  confluence_page = self._create_page(
198
- absolute_path, document, title, parent_id
189
+ absolute_path, document, title or frontmatter_title, parent_id
199
190
  )
200
191
 
201
192
  return ConfluencePageMetadata(
@@ -249,7 +240,7 @@ class Application:
249
240
 
250
241
  content = document.xhtml()
251
242
  LOGGER.debug("Generated Confluence Storage Format document:\n%s", content)
252
- self.api.update_page(document.id.page_id, content)
243
+ self.api.update_page(document.id.page_id, content, title=document.title)
253
244
 
254
245
  def _update_markdown(
255
246
  self,
md2conf/converter.py CHANGED
@@ -23,6 +23,7 @@ from urllib.parse import ParseResult, urlparse, urlunparse
23
23
 
24
24
  import lxml.etree as ET
25
25
  import markdown
26
+ import yaml
26
27
  from lxml.builder import ElementMaker
27
28
 
28
29
  from . import mermaid
@@ -350,8 +351,8 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
350
351
  heading.text = None
351
352
 
352
353
  def _transform_link(self, anchor: ET._Element) -> Optional[ET._Element]:
353
- url = anchor.attrib["href"]
354
- if is_absolute_url(url):
354
+ url = anchor.attrib.get("href")
355
+ if url is None or is_absolute_url(url):
355
356
  return None
356
357
 
357
358
  LOGGER.debug("Found link %s relative to %s", url, self.path)
@@ -432,39 +433,72 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
432
433
  return None
433
434
 
434
435
  def _transform_image(self, image: ET._Element) -> ET._Element:
435
- path: str = image.attrib["src"]
436
+ src = image.attrib.get("src")
436
437
 
437
- if not path:
438
+ if not src:
438
439
  raise DocumentError("image lacks `src` attribute")
439
440
 
440
- if is_absolute_url(path):
441
- # images whose `src` attribute is an absolute URL cannot be converted into an `ac:image`;
442
- # Confluence images are expected to refer to an uploaded attachment
443
- raise DocumentError("image has a `src` attribute that is an absolute URL")
441
+ attributes: Dict[str, Any] = {
442
+ ET.QName(namespaces["ac"], "align"): "center",
443
+ ET.QName(namespaces["ac"], "layout"): "center",
444
+ }
445
+ width = image.attrib.get("width")
446
+ if width is not None:
447
+ attributes.update({ET.QName(namespaces["ac"], "width"): width})
448
+ height = image.attrib.get("height")
449
+ if height is not None:
450
+ attributes.update({ET.QName(namespaces["ac"], "height"): height})
451
+
452
+ caption = image.attrib.get("alt")
453
+
454
+ if is_absolute_url(src):
455
+ return self._transform_external_image(src, caption, attributes)
456
+ else:
457
+ return self._transform_attached_image(Path(src), caption, attributes)
458
+
459
+ def _transform_external_image(
460
+ self, url: str, caption: Optional[str], attributes: Dict[str, Any]
461
+ ) -> ET._Element:
462
+ "Emits Confluence Storage Format XHTML for an external image."
463
+
464
+ elements: List[ET._Element] = []
465
+ elements.append(
466
+ RI(
467
+ "url",
468
+ # refers to an external image
469
+ {ET.QName(namespaces["ri"], "value"): url},
470
+ )
471
+ )
472
+ if caption is not None:
473
+ elements.append(AC("caption", HTML.p(caption)))
444
474
 
445
- relative_path = Path(path)
475
+ return AC("image", attributes, *elements)
476
+
477
+ def _transform_attached_image(
478
+ self, path: Path, caption: Optional[str], attributes: Dict[str, Any]
479
+ ) -> ET._Element:
480
+ "Emits Confluence Storage Format XHTML for an attached image."
446
481
 
447
482
  # prefer PNG over SVG; Confluence displays SVG in wrong size, and text labels are truncated
448
- png_file = relative_path.with_suffix(".png")
449
- if relative_path.suffix == ".svg" and (self.base_dir / png_file).exists():
450
- relative_path = png_file
483
+ png_file = path.with_suffix(".png")
484
+ if path.suffix == ".svg" and (self.base_dir / png_file).exists():
485
+ path = png_file
451
486
 
452
- self.images.append(relative_path)
453
- caption = image.attrib["alt"]
454
- image_name = attachment_name(relative_path)
455
- return AC(
456
- "image",
457
- {
458
- ET.QName(namespaces["ac"], "align"): "center",
459
- ET.QName(namespaces["ac"], "layout"): "center",
460
- },
487
+ self.images.append(path)
488
+ image_name = attachment_name(path)
489
+
490
+ elements: List[ET._Element] = []
491
+ elements.append(
461
492
  RI(
462
493
  "attachment",
463
494
  # refers to an attachment uploaded alongside the page
464
495
  {ET.QName(namespaces["ri"], "filename"): image_name},
465
- ),
466
- AC("caption", HTML.p(caption)),
496
+ )
467
497
  )
498
+ if caption is not None:
499
+ elements.append(AC("caption", HTML.p(caption)))
500
+
501
+ return AC("image", attributes, *elements)
468
502
 
469
503
  def _transform_block(self, code: ET._Element) -> ET._Element:
470
504
  language = code.attrib.get("class")
@@ -907,6 +941,20 @@ def extract_frontmatter(text: str) -> Tuple[Optional[str], str]:
907
941
  return extract_value(r"(?ms)\A---$(.+?)^---$", text)
908
942
 
909
943
 
944
+ def extract_frontmatter_title(text: str) -> Tuple[Optional[str], str]:
945
+ frontmatter, text = extract_frontmatter(text)
946
+
947
+ title: Optional[str] = None
948
+ if frontmatter is not None:
949
+ properties = yaml.safe_load(frontmatter)
950
+ if isinstance(properties, dict):
951
+ property_title = properties.get("title")
952
+ if isinstance(property_title, str):
953
+ title = property_title
954
+
955
+ return title, text
956
+
957
+
910
958
  def read_qualified_id(absolute_path: Path) -> Optional[ConfluenceQualifiedID]:
911
959
  "Reads the Confluence page ID and space key from a Markdown document."
912
960
 
@@ -944,6 +992,7 @@ class ConfluenceDocumentOptions:
944
992
 
945
993
  class ConfluenceDocument:
946
994
  id: ConfluenceQualifiedID
995
+ title: Optional[str]
947
996
  links: List[str]
948
997
  images: List[Path]
949
998
 
@@ -982,7 +1031,7 @@ class ConfluenceDocument:
982
1031
  )
983
1032
 
984
1033
  # extract frontmatter
985
- frontmatter, text = extract_frontmatter(text)
1034
+ self.title, text = extract_frontmatter_title(text)
986
1035
 
987
1036
  # convert to HTML
988
1037
  html = markdown_to_html(text)