diff --git a/md2cf/api.py b/md2cf/api.py index 54ba44a..4c9dc6f 100644 --- a/md2cf/api.py +++ b/md2cf/api.py @@ -66,6 +66,7 @@ def __init__( def _request(self, method, path, **kwargs): r = self.api.request(method, urljoin(self.host, path), **kwargs) r.raise_for_status() + return bunchify(r.json()) def _get(self, path, **kwargs): @@ -186,13 +187,14 @@ def update_page( update_message=None, labels=None, minor_edit=False, + title=None, ): update_structure = { "version": { "number": page.version.number + 1, "minorEdit": minor_edit, }, - "title": page.title, + "title": title if title is not None else page.title, "type": content_type, "body": {"storage": {"value": body, "representation": "storage"}}, } diff --git a/md2cf/confluence_renderer.py b/md2cf/confluence_renderer.py index 74b3496..6a81c8d 100644 --- a/md2cf/confluence_renderer.py +++ b/md2cf/confluence_renderer.py @@ -1,6 +1,7 @@ import uuid +import re from pathlib import Path -from typing import List, NamedTuple +from typing import List, NamedTuple, Optional, Any from urllib.parse import unquote, urlparse import mistune @@ -60,7 +61,7 @@ def append(self, child): self.children.append(child) -class ConfluenceRenderer(mistune.Renderer): +class ConfluenceRenderer(mistune.HTMLRenderer): def __init__( self, strip_header=False, @@ -90,8 +91,8 @@ def header(self, text, level, raw=None): return super(ConfluenceRenderer, self).header(text, level, raw=raw) - def structured_macro(self, name): - return ConfluenceTag("structured-macro", attrib={"name": name}) + def structured_macro(self, name, text=""): + return ConfluenceTag("structured-macro", attrib={"name": name}, text=text) def parameter(self, name, value): parameter_tag = ConfluenceTag("parameter", attrib={"name": name}) @@ -103,8 +104,13 @@ def plain_text_body(self, text): body_tag.text = text return body_tag - def link(self, link, title, text): - parsed_link = urlparse(link) + def rich_text_body(self, text): + body_tag = ConfluenceTag("rich-text-body", cdata=False) + body_tag.text = text + return body_tag + + def link(self, text, url, title=None): + parsed_link = urlparse(url) if ( self.enable_relative_links and (not parsed_link.scheme and not parsed_link.netloc) @@ -119,12 +125,12 @@ def link(self, link, title, text): path=unquote(parsed_link.path), replacement=replacement_link, fragment=parsed_link.fragment, - original=link, - escaped_original=mistune.escape_link(link), + original=url, + escaped_original=mistune.escape_link(url), ) ) - link = replacement_link - return super(ConfluenceRenderer, self).link(link, title, text) + url = replacement_link + return super(ConfluenceRenderer, self).link(text, url, title) def text(self, text): if self.remove_text_newlines: @@ -132,31 +138,100 @@ def text(self, text): return super().text(text) - def block_code(self, code, lang=None): + def block_code(self, code, info=None): root_element = self.structured_macro("code") - if lang is not None: - lang_parameter = self.parameter(name="language", value=lang) + if info is not None: + lang_parameter = self.parameter(name="language", value=info) root_element.append(lang_parameter) root_element.append(self.parameter(name="linenumbers", value="true")) root_element.append(self.plain_text_body(code)) return root_element.render() - def image(self, src, title, text): - attributes = {"alt": text} - if title: - attributes["title"] = title + def image(self, alt, url, title=None, width=None, height=None): + attributes = {"alt": alt, + "title": title if title is not None else alt, + } + if width: + attributes["width"] = width + if height: + attributes["height"] = height root_element = ConfluenceTag(name="image", attrib=attributes) - parsed_source = urlparse(src) + parsed_source = urlparse(url) if not parsed_source.netloc: # Local file, requires upload - basename = Path(src).name + basename = Path(url).name url_tag = ConfluenceTag( "attachment", attrib={"filename": basename}, namespace="ri" ) - self.attachments.append(src) + self.attachments.append(url) else: - url_tag = ConfluenceTag("url", attrib={"value": src}, namespace="ri") + url_tag = ConfluenceTag("url", attrib={"value": url}, namespace="ri") root_element.append(url_tag) return root_element.render() + + def strikethrough(self, text): + return f"""{text}""" + + def task_list_item(self, text, checked=False, **attrs): + return f""" + + + {"in" if not checked else ""}complete + {text} + + + """ + + def block_spoiler(self, text): + lines = text.splitlines(keepends=True) + firstline = re.sub('<.*?>', '', lines[0]) + + root_element = self.structured_macro("expand") + title_param = self.parameter(name="title", value=firstline) + root_element.append(title_param) + + root_element.append(self.rich_text_body(''.join(lines[1:]))) + return root_element.render() + + + def mark(self, text): + return f"""{text}""" + + def insert(self, text): + return f"""{text}""" + + def admonition(self, text: str, name: str, **attrs) -> str: + confluence_mapping = {"tip" : "tip", + "attention": "warning", + "caution": "warning", + "danger": "warning", + "error": "warning", + "hint" : "tip", + "important": "note", + "note": "info", + "warning": "warning"} + + adm_class = confluence_mapping.get(name, "info") + root_element = self.structured_macro(name=adm_class, text=text) + return root_element.render() + + def admonition_title(self, text: str) -> str: + param = self.parameter(name="title", value=text) + return param.render() + + + def admonition_content(self, text: str) -> str: + body = self.rich_text_body(text) + return body.render() + + def block_image( + self, + src: str, + alt: Optional[str] = None, + width: Optional[str] = None, + height: Optional[str] = None, + **attrs: Any, + ) -> str: + return self.image(alt, src, alt, width, height) diff --git a/md2cf/upsert.py b/md2cf/upsert.py index 7d757d6..9a3b119 100644 --- a/md2cf/upsert.py +++ b/md2cf/upsert.py @@ -104,6 +104,7 @@ def upsert_page( update_message=page_message, labels=page.labels if replace_all_labels else None, minor_edit=minor_edit, + title=page.title ) action = UpsertAction.UPDATED else: diff --git a/setup.py b/setup.py index 4034063..4447a55 100644 --- a/setup.py +++ b/setup.py @@ -23,9 +23,9 @@ install_requires=[ "rich-argparse==1.0.0", "rich==13.0.1", - "mistune==0.8.4", + "mistune==3.1.2", "chardet==5.1.0", - "requests==2.31.0", + "requests==2.32.3", "PyYAML==6.0.1", "gitignorefile==1.1.2", ],