diff --git a/md2cf/confluence_renderer.py b/md2cf/confluence_renderer.py index 74b3496..db1d54f 100644 --- a/md2cf/confluence_renderer.py +++ b/md2cf/confluence_renderer.py @@ -60,7 +60,7 @@ def append(self, child): self.children.append(child) -class ConfluenceRenderer(mistune.Renderer): +class ConfluenceRenderer(mistune.HTMLRenderer): def __init__( self, strip_header=False, @@ -81,14 +81,13 @@ def reinit(self): self.relative_links = list() self.title = None - def header(self, text, level, raw=None): + def heading(self, text, level, **attrs): if self.title is None and level == 1: self.title = text - # Don't duplicate page title as a header if self.strip_header: return "" - return super(ConfluenceRenderer, self).header(text, level, raw=raw) + return super().heading(text, level, **attrs) def structured_macro(self, name): return ConfluenceTag("structured-macro", attrib={"name": name}) @@ -103,28 +102,25 @@ def plain_text_body(self, text): body_tag.text = text return body_tag - def link(self, link, title, text): - parsed_link = urlparse(link) + 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) and parsed_link.path ): - # relative link replacement_link = f"md2cf-internal-link-{uuid.uuid4()}" self.relative_links.append( RelativeLink( - # make sure to unquote the url as relative paths - # might have escape sequences 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_url(url), ) ) - link = replacement_link - return super(ConfluenceRenderer, self).link(link, title, text) + url = replacement_link + return super().link(text, url, title) def text(self, text): if self.remove_text_newlines: @@ -132,31 +128,33 @@ def text(self, text): return super().text(text) - def block_code(self, code, lang=None): + def block_code(self, code, info=None, **attrs): + lang = None + if info is not None: + lang = info.strip().split(None, 1)[0] root_element = self.structured_macro("code") - if lang is not None: + if lang: lang_parameter = self.parameter(name="language", value=lang) 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): + def image(self, text, url, title=None): attributes = {"alt": text} if title: attributes["title"] = title 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() diff --git a/md2cf/document.py b/md2cf/document.py index 5ea2592..7baaea2 100644 --- a/md2cf/document.py +++ b/md2cf/document.py @@ -276,7 +276,6 @@ def parse_page( enable_relative_links: bool = False, ) -> Page: renderer = ConfluenceRenderer( - use_xhtml=True, strip_header=strip_header, remove_text_newlines=remove_text_newlines, enable_relative_links=enable_relative_links, diff --git a/requirements-test.txt b/requirements-test.txt index c38d400..32d094d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,3 @@ -pytest-mock==1.11.1 +pytest-mock>=3.10.0,<4 pyfakefs -requests-mock==1.10.0 +requests-mock>=1.10.0,<2 diff --git a/setup.py b/setup.py index 4034063..6e12503 100644 --- a/setup.py +++ b/setup.py @@ -21,14 +21,14 @@ ], keywords="markdown confluence", install_requires=[ - "rich-argparse==1.0.0", - "rich==13.0.1", - "mistune==0.8.4", - "chardet==5.1.0", - "requests==2.31.0", - "PyYAML==6.0.1", - "gitignorefile==1.1.2", + "rich-argparse>=1.0.0,<2", + "rich>=13.0.0,<16", + "mistune>=3.0.0,<4", + "chardet>=5.1.0,<6", + "requests>=2.31.0,<3", + "PyYAML>=6.0.1,<7", + "gitignorefile>=1.1.2,<2", ], - python_requires=">=3.7", + python_requires=">=3.8", entry_points={"console_scripts": ["md2cf=md2cf.__main__:main"]}, ) diff --git a/test_package/functional/result.xml b/test_package/functional/result.xml index ca22230..080d184 100644 --- a/test_package/functional/result.xml +++ b/test_package/functional/result.xml @@ -41,16 +41,16 @@ inspiration for Markdown's syntax is the format of plain text email.

by one or more blank lines. (A blank line is any line that looks like a blank line -- a line containing nothing but spaces or tabs is considered blank.) Normal paragraphs should not be indented with spaces or tabs.

-

The implication of the "one or more consecutive lines of text" rule is -that Markdown supports "hard-wrapped" text paragraphs. This differs +

The implication of the "one or more consecutive lines of text" rule is +that Markdown supports "hard-wrapped" text paragraphs. This differs significantly from most other text-to-HTML formatters (including Movable -Type's "Convert Line Breaks" option) which translate every line break +Type's "Convert Line Breaks" option) which translate every line break character in a paragraph into a <br /> tag.

When you do want to insert a <br /> break tag using Markdown, you end a line with two or more spaces, then type return.

Headers

Markdown supports two styles of headers, [Setext] [1] and [atx] [2].

-

Optionally, you may "close" atx-style headers. This is purely +

Optionally, you may "close" atx-style headers. This is purely cosmetic -- you can use this if you think it looks better. The closing hashes don't even need to match the number of hashes used to open the header. (The number of opening hashes @@ -60,7 +60,8 @@ determines the header level.)

familiar with quoting passages of text in an email message, then you know how to create a blockquote in Markdown. It looks best if you hard wrap the text and put a > before every line:

-

This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, +

+

This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.

Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse @@ -68,22 +69,28 @@ id sem consectetuer libero luctus adipiscing.

Markdown allows you to be lazy and only put the > before the first line of a hard-wrapped paragraph:

-

This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, +

+

This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.

+
+

Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse id sem consectetuer libero luctus adipiscing.

Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by adding additional levels of >:

-

This is the first level of quoting.

-

This is nested blockquote.

+
+

This is the first level of quoting.

+
+

This is nested blockquote.

Back to the first level.

Blockquotes can contain other Markdown elements, including headers, lists, and code blocks:

-

This is a header.

+
+

This is a header.

  1. This is the first list item.
  2. This is the second list item.
  3. @@ -133,7 +140,7 @@ Markdown produces from the above list is:

  4. Parish

or even:

-
    +
    1. Bird
    2. McHale
    3. Parish
    4. @@ -188,7 +195,8 @@ sit amet, consectetuer adipiscing elit.

      delimiters need to be indented:

      • A list item with a blockquote:

        -

        This is a blockquote +

        +

        This is a blockquote inside a list item.

      • @@ -211,17 +219,13 @@ in both <pre> and <code> tags.

        block by at least 4 spaces or 1 tab.

        This is a normal paragraph:

        true - +

        Here is an example of AppleScript:

        true +end tell]]>

        A code block continues until it reaches a line that is not indented (or the end of the article).

        @@ -233,9 +237,7 @@ ampersands and angle brackets. For example, this:

        true © 2004 Foo Corporation - - -]]> +]]>

        Regular Markdown syntax is not processed within code blocks. E.g., asterisks are just literal asterisks within a code block. This means @@ -243,7 +245,8 @@ it's also easy to use Markdown to write about Markdown's own syntax.

        true +end tell +]]>

        Span Elements

        Links

        diff --git a/test_package/unit/test_renderer.py b/test_package/unit/test_renderer.py index 1ab9596..f03dbee 100644 --- a/test_package/unit/test_renderer.py +++ b/test_package/unit/test_renderer.py @@ -116,7 +116,7 @@ def test_tag_render_with_child_and_text(): def test_renderer_reinit(): renderer = ConfluenceRenderer() - renderer.header("this is a title", 1) + renderer.heading("this is a title", 1) assert renderer.title is not None renderer.reinit() @@ -150,14 +150,14 @@ def test_renderer_block_code_with_language(): renderer = ConfluenceRenderer() - assert renderer.block_code(test_code, lang=test_language) == test_markup + assert renderer.block_code(test_code, info=test_language) == test_markup -def test_renderer_header_sets_title(): +def test_renderer_heading_sets_title(): test_header = "this is a header" renderer = ConfluenceRenderer() - renderer.header(test_header, 1) + renderer.heading(test_header, 1) assert renderer.title == test_header @@ -166,38 +166,38 @@ def test_renderer_strips_header(): test_header = "this is a header" renderer = ConfluenceRenderer(strip_header=True) - result = renderer.header(test_header, 1) + result = renderer.heading(test_header, 1) assert result == "" -def test_renderer_header_lower_level_does_not_set_title(): +def test_renderer_heading_lower_level_does_not_set_title(): test_header = "this is a header" renderer = ConfluenceRenderer() - renderer.header(test_header, 2) + renderer.heading(test_header, 2) assert renderer.title is None -def test_renderer_header_later_level_sets_title(): +def test_renderer_heading_later_level_sets_title(): test_lower_header = "this is a lower header" test_header = "this is a header" renderer = ConfluenceRenderer() - renderer.header(test_lower_header, 2) - renderer.header(test_header, 1) + renderer.heading(test_lower_header, 2) + renderer.heading(test_header, 1) assert renderer.title is test_header -def test_renderer_header_only_sets_first_title(): +def test_renderer_heading_only_sets_first_title(): test_header = "this is a header" test_second_header = "this is another header" renderer = ConfluenceRenderer() - renderer.header(test_header, 1) - renderer.header(test_second_header, 1) + renderer.heading(test_header, 1) + renderer.heading(test_second_header, 1) assert renderer.title is test_header @@ -211,7 +211,7 @@ def test_renderer_image_external(): renderer = ConfluenceRenderer() - assert renderer.image(test_image_src, "", "") == test_image_markup + assert renderer.image("", test_image_src) == test_image_markup assert not renderer.attachments @@ -227,7 +227,7 @@ def test_renderer_image_external_alt_and_title(): renderer = ConfluenceRenderer() assert ( - renderer.image(test_image_src, test_image_title, test_image_alt) + renderer.image(test_image_alt, test_image_src, test_image_title) == test_image_markup ) @@ -242,7 +242,7 @@ def test_renderer_image_internal_absolute(): renderer = ConfluenceRenderer() - assert renderer.image(test_image_src, "", "") == test_image_markup + assert renderer.image("", test_image_src) == test_image_markup assert renderer.attachments == [test_image_src] @@ -256,7 +256,7 @@ def test_renderer_image_internal_relative(): renderer = ConfluenceRenderer() - assert renderer.image(test_image_src, "", "") == test_image_markup + assert renderer.image("", test_image_src) == test_image_markup assert renderer.attachments == [test_image_src] @@ -274,7 +274,7 @@ def test_renderer_normal_link(relative_links): renderer = ConfluenceRenderer(enable_relative_links=relative_links) assert ( - renderer.link(link="https://example.com", text="example link", title=None) + renderer.link(text="example link", url="https://example.com", title=None) == 'example link' ) @@ -284,7 +284,7 @@ def test_renderer_local_header_link(relative_links): renderer = ConfluenceRenderer(enable_relative_links=relative_links) assert ( - renderer.link(link="#header-name", text="example link", title=None) + renderer.link(text="example link", url="#header-name", title=None) == 'example link' ) @@ -296,7 +296,7 @@ def test_renderer_relative_link_enabled(): r"relative link" ) temporary_link = renderer.link( - link="document/../path/page.md", text="relative link", title=None + text="relative link", url="document/../path/page.md", title=None ) assert relative_link_regex.match(temporary_link) assert len(renderer.relative_links) == 1 @@ -319,7 +319,7 @@ def test_renderer_relative_link_with_fragment_enabled(): r"relative link" ) temporary_link = renderer.link( - link="document/../path/page.md#header-name", text="relative link", title=None + text="relative link", url="document/../path/page.md#header-name", title=None ) assert relative_link_regex.match(temporary_link) assert len(renderer.relative_links) == 1 @@ -339,7 +339,7 @@ def test_renderer_relative_link_disabled(): renderer = ConfluenceRenderer(enable_relative_links=False) assert ( - renderer.link(link="document/../path/page.md", text="relative link", title=None) + renderer.link(text="relative link", url="document/../path/page.md", title=None) == 'relative link' ) assert renderer.relative_links == [] @@ -350,8 +350,8 @@ def test_renderer_relative_link_with_fragment_disabled(): assert ( renderer.link( - link="document/../path/page.md#header-name", text="relative link", + url="document/../path/page.md#header-name", title=None, ) == 'relative link'