From 7e5f17b3dd36f9c0ce4d14b165853c10fe144e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Milde?= Date: Tue, 4 Nov 2025 22:45:26 +0100 Subject: [PATCH 1/5] rST in: support custom "text roles". Pass on CSS class arguments from `` elements to support "custom interpreted text roles" (https://docutils.sourceforge.io/docs/ref/rst/directives.html#role) Add special handling for "del" and "ins" roles, so that they will become `` and `` in HTML. --- src/moin/converters/_tests/test_rst_in.py | 25 ++++++++++++++++------- src/moin/converters/rst_in.py | 16 +++++++++++++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/moin/converters/_tests/test_rst_in.py b/src/moin/converters/_tests/test_rst_in.py index cf8fdc39d..7f9982ef6 100644 --- a/src/moin/converters/_tests/test_rst_in.py +++ b/src/moin/converters/_tests/test_rst_in.py @@ -37,6 +37,14 @@ def setup_class(self): ("**Text**", "

Text

"), ("*Text*", "

Text

"), ("``Text``", "

Text

"), + ( # custom role using a CSS class + ".. role:: orange\n\n:orange:`colourful` text", + '

colourful text

', + ), + ( # special custom roles for and + ".. role:: del\n.. role:: ins\n\n" ":del:`deleted` text :ins:`inserted` text", + "

deleted text inserted text

", + ), ("a _`Link`", '

a Link

'), ( "`Text `_", @@ -358,13 +366,16 @@ def test_table(self, input, output): "Author:TestVersion:1.17Copyright:cTest:

t

", ), ( - """ -.. note:: - :name: note-id - - An admonition of type "note" -""", - '

An admonition of type "note"

', + ".. note::\n" " :name: note-id\n\n" ' An admonition of type "note"', + '' + '

An admonition of type "note"

', + ), + # use an attention for a generic admonition + ( + ".. admonition:: Generic Admonition\n\n" " Be alert!", + '' + 'Generic Admonition' + "

Be alert!

", ), ] diff --git a/src/moin/converters/rst_in.py b/src/moin/converters/rst_in.py index 3f6d8f1a2..142ad4fd9 100644 --- a/src/moin/converters/rst_in.py +++ b/src/moin/converters/rst_in.py @@ -434,10 +434,21 @@ def depart_image(self, node): self.close_moin_page_node() def visit_inline(self, node): - pass + classes = node["classes"] + moin_node = moin_page.span + attrib = {} + if "ins" in classes: + moin_node = moin_page.ins + classes.remove("ins") + if "del" in classes: + moin_node = moin_page.del_ + classes.remove("del") + if classes: + attrib[html.class_] = " ".join(classes) + self.open_moin_page_node(moin_node(attrib=attrib)) def depart_inline(self, node): - pass + self.close_moin_page_node() def visit_label(self, node): if self.status[-1] == "footnote": @@ -529,6 +540,7 @@ def visit_paragraph(self, node): footnote_node = self.footnotes.get(self.footnote_lable, None) if footnote_node: # TODO: `node.astext()` ignores all markup! + # "moin" footnotes support inline markup footnote_node.append(node.astext()) raise nodes.SkipNode self.open_moin_page_node(moin_page.p(), node) From b967bf839246053e68cd651d1928eed9bfdf4c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Milde?= Date: Wed, 5 Nov 2025 13:12:00 +0100 Subject: [PATCH 2/5] rST in: support ordered lists with custom start value. Both, rST and Moin support ordered lists starting with a value other than one. Docutils also generates an INFO level system message that is usually not shown. This may have led to the missing conversion of the start value. Correct handling of system messages will be added in the next steps. --- src/moin/converters/_tests/test_rst_in.py | 12 +++++++----- src/moin/converters/rst_in.py | 3 +++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/moin/converters/_tests/test_rst_in.py b/src/moin/converters/_tests/test_rst_in.py index 7f9982ef6..0caafc33c 100644 --- a/src/moin/converters/_tests/test_rst_in.py +++ b/src/moin/converters/_tests/test_rst_in.py @@ -65,9 +65,10 @@ def test_base(self, input, output): data = [ ( "1. a\n b\n c\n\n2. b\n\n d", - """

a -b -c

b

d

""", + '' + "

a\nb\nc

" + "

b

d

" + "
", ), ( "1. a\n2. b\n\nA. c\n\na. A\n\n 1. B\n\n 2. C\n\n", @@ -81,10 +82,11 @@ def test_base(self, input, output): "what\n def\n\nhow\n to", "what

def

how

to

", ), - # starting an ordered list with a value other than 1 generates an error + # starting an ordered list with a value other than 1 + # generates an info-level system message that stays usually hidden (TODO). ( " 3. A\n #. B", - '

A

' + '

A

' "

B

" '

Enumerated list start value not ordinal-1: "3" (ordinal 3)

' "
", diff --git a/src/moin/converters/rst_in.py b/src/moin/converters/rst_in.py index 142ad4fd9..afe140d28 100644 --- a/src/moin/converters/rst_in.py +++ b/src/moin/converters/rst_in.py @@ -314,6 +314,9 @@ def visit_enumerated_list(self, node): type = enum_style.get(node["enumtype"], None) if type: new_node.set(moin_page.list_style_type, type) + startvalue = node.get("start", 1) + if startvalue > 1: + new_node.set(moin_page.list_start, str(startvalue)) self.open_moin_page_node(new_node, node) def depart_enumerated_list(self, node): From 4358e1f4bb4f065c7fd9c32e448d3d152e2ced03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Milde?= Date: Wed, 5 Nov 2025 12:28:09 +0100 Subject: [PATCH 3/5] rST in: More informative system messages. Docutils system messages (https://docutils.sourceforge.io/docs/ref/doctree.html#system-message) contain useful information that is currently ignored: * type/level (DEBUG, INFO, WARNING, ERROR, SEVERE) * source description, line number of the issue Show this info in the inserted "admonition". Use a "caution" instad of an "error admonition" for merely informative messages. --- src/moin/converters/_tests/test_rst_in.py | 25 ++++++++++++++++++++++- src/moin/converters/rst_in.py | 18 +++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/moin/converters/_tests/test_rst_in.py b/src/moin/converters/_tests/test_rst_in.py index 0caafc33c..cd58eef8b 100644 --- a/src/moin/converters/_tests/test_rst_in.py +++ b/src/moin/converters/_tests/test_rst_in.py @@ -88,7 +88,8 @@ def test_base(self, input, output): " 3. A\n #. B", '

A

' "

B

" - '

Enumerated list start value not ordinal-1: "3" (ordinal 3)

' + '

System Message: INFO/1 (rST input line 1)

' + '

Enumerated list start value not ordinal-1: "3" (ordinal 3)

' "
", ), ] @@ -363,10 +364,12 @@ def test_table(self, input, output): self.do(input, output) data = [ + # bibliographic data (visible meta-data) ( ":Author: Test\n:Version: $Revision: 1.17 $\n:Copyright: c\n:Test: t", "Author:TestVersion:1.17Copyright:cTest:

t

", ), + # admonitions (hint, info, warning, error, ...) ( ".. note::\n" " :name: note-id\n\n" ' An admonition of type "note"', '' @@ -379,6 +382,26 @@ def test_table(self, input, output): 'Generic Admonition' "

Be alert!

", ), + # Moin uses admonitions also for system messages + ( + "Unbalanced *inline markup.", + "

Unbalanced *inline markup.

" + '' + '

System Message: WARNING/2 (rST input line 1)

' + "

Inline emphasis start-string without end-string.

" + "
", + ), + # TODO: this currently fails because the parsing error is not cleared. + # ( + # "Sections must not be nested in body elements.\n\n" + # " not allowed\n" + # " -----------\n", + # "

Sections must not be nested in body elements.

" + # '

System Message: ERROR/3 (rST input line 4)

' + # "

Unexpected section title.

" + # "not allowed\n-----------" + # "
" + # ) ] @pytest.mark.parametrize("input,output", data) diff --git a/src/moin/converters/rst_in.py b/src/moin/converters/rst_in.py index afe140d28..0c5bf9bc1 100644 --- a/src/moin/converters/rst_in.py +++ b/src/moin/converters/rst_in.py @@ -599,6 +599,7 @@ def visit_reference(self, node): return if not allowed_uri_scheme(refuri): + # TODO: visit_problematic(node), append at end. self.visit_error(node) return @@ -693,9 +694,20 @@ def depart_superscript(self, node): self.close_moin_page_node() def visit_system_message(self, node): - # we have encountered a parsing error, insert an error message - # TODO: also show error level and line number. - self.visit_admonition(node, "error") + # an element reporting a parsing issue (DEBUG, INFO, WARNING, ERROR, or SEVERE) + # TODO: handle node['backrefs'] to element. + if node["level"] < 3: + self.visit_admonition(node, "caution") + else: + self.visit_admonition(node, "error") + self.open_moin_page_node(moin_page.p()) + self.open_moin_page_node(moin_page.strong(attrib={html.class_: "title"})) + title = f"{node['type']}/{node['level']}" + self.current_node.append(f"System Message: {title}") + self.close_moin_page_node() + if node.hasattr("line"): + self.current_node.append(f" ({node['source']} line {node['line']})") + self.close_moin_page_node() def depart_system_message(self, node): self.depart_admonition(node) From ccc559e4ea883f9488d91a8368a705db07739c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Milde?= Date: Wed, 5 Nov 2025 18:21:35 +0100 Subject: [PATCH 4/5] rST in: activate default "transforms". The rst_in converter is a Docutils "writer" component (exporting a "doctree" to some other format) but does not inherit `docutils.writers.Writer`. Therefore, we add the "transfroms" that are usually loaded by the Writer class to the transforms added by Moin (in the parser component). https://docutils.sourceforge.io/docs/api/transforms.html#transforms-added-by-components --- src/moin/converters/_tests/test_rst_in.py | 5 +---- src/moin/converters/rst_in.py | 10 +++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/moin/converters/_tests/test_rst_in.py b/src/moin/converters/_tests/test_rst_in.py index cd58eef8b..66816a52b 100644 --- a/src/moin/converters/_tests/test_rst_in.py +++ b/src/moin/converters/_tests/test_rst_in.py @@ -82,14 +82,11 @@ def test_base(self, input, output): "what\n def\n\nhow\n to", "what

def

how

to

", ), - # starting an ordered list with a value other than 1 - # generates an info-level system message that stays usually hidden (TODO). + # nested in a block-quote and starting with a value other than 1 ( " 3. A\n #. B", '

A

' "

B

" - '

System Message: INFO/1 (rST input line 1)

' - '

Enumerated list start value not ordinal-1: "3" (ordinal 3)

' "
", ), ] diff --git a/src/moin/converters/rst_in.py b/src/moin/converters/rst_in.py index 0c5bf9bc1..82ac32fdc 100644 --- a/src/moin/converters/rst_in.py +++ b/src/moin/converters/rst_in.py @@ -850,6 +850,8 @@ class Parser(docutils.parsers.rst.Parser): Registers a "transform__" for hyperlink references without matching target__. + Also register the "transforms" that are added by default for a Docutils writer. + __ https://docutils.sourceforge.io/docs/api/transforms.html __ https://docutils.sourceforge.io/docs/ref/doctree.html#target """ @@ -859,7 +861,13 @@ class Parser(docutils.parsers.rst.Parser): def get_transforms(self): """Add WikiReferences to the registered transforms.""" - return super().get_transforms() + [WikiReferences] + moin_parser_transforms = [ + WikiReferences, + transforms.universal.StripClassesAndElements, + transforms.universal.Messages, + transforms.universal.FilterMessages, + ] + return super().get_transforms() + moin_parser_transforms class WikiReferences(transforms.Transform): From 4f131b5b9355ffe22e8622b3029da7c4f05c7b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Milde?= Date: Thu, 6 Nov 2025 12:00:52 +0100 Subject: [PATCH 5/5] rST in: Handle `` elements. To report issues in inline content, Docutils uses a combination of a `` element and a ``. https://docutils.sourceforge.io/docs/ref/doctree.html#problematic Make "problematic" content stay out by a red background and turn it into a link to the related "system message" (if there is one). Add a backlink from the "system message" to the "problematic" element at the place of origin. --- src/moin/converters/_tests/test_rst_in.py | 5 +++-- src/moin/converters/rst_in.py | 24 ++++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/moin/converters/_tests/test_rst_in.py b/src/moin/converters/_tests/test_rst_in.py index 66816a52b..f76c1d343 100644 --- a/src/moin/converters/_tests/test_rst_in.py +++ b/src/moin/converters/_tests/test_rst_in.py @@ -382,9 +382,10 @@ def test_table(self, input, output): # Moin uses admonitions also for system messages ( "Unbalanced *inline markup.", - "

Unbalanced *inline markup.

" + '

Unbalanced *inline markup.

' '' - '

System Message: WARNING/2 (rST input line 1)

' + '

System Message: WARNING/2 (rST input line 1) ' + 'backlink

' "

Inline emphasis start-string without end-string.

" "
", ), diff --git a/src/moin/converters/rst_in.py b/src/moin/converters/rst_in.py index 82ac32fdc..7530daa1a 100644 --- a/src/moin/converters/rst_in.py +++ b/src/moin/converters/rst_in.py @@ -553,10 +553,15 @@ def depart_paragraph(self, node): self.close_moin_page_node() def visit_problematic(self, node): - pass + if node.hasattr("refid"): + refuri = f"#{node['refid']}" + attrib = {xlink.href: refuri, html.class_: "red"} + self.open_moin_page_node(moin_page.a(attrib=attrib), node) + else: + self.open_moin_page_node(moin_page.span(attrib={html.class_: "red"})) def depart_problematic(self, node): - pass + self.close_moin_page_node() def visit_reference(self, node): refuri = node.get("refuri", "") @@ -599,7 +604,7 @@ def visit_reference(self, node): return if not allowed_uri_scheme(refuri): - # TODO: visit_problematic(node), append at end. + # TODO: prepend "wiki.local" as in "moin_in"? self.visit_error(node) return @@ -696,7 +701,7 @@ def depart_superscript(self, node): def visit_system_message(self, node): # an element reporting a parsing issue (DEBUG, INFO, WARNING, ERROR, or SEVERE) # TODO: handle node['backrefs'] to element. - if node["level"] < 3: + if node.get("level", 4) < 3: self.visit_admonition(node, "caution") else: self.visit_admonition(node, "error") @@ -704,10 +709,15 @@ def visit_system_message(self, node): self.open_moin_page_node(moin_page.strong(attrib={html.class_: "title"})) title = f"{node['type']}/{node['level']}" self.current_node.append(f"System Message: {title}") - self.close_moin_page_node() + self.close_moin_page_node() # if node.hasattr("line"): - self.current_node.append(f" ({node['source']} line {node['line']})") - self.close_moin_page_node() + self.current_node.append(f" ({node['source']} line {node['line']}) ") + if node.get("backrefs", []): + backrefuri = f"#{node['backrefs'][0]}" + self.open_moin_page_node(moin_page.a(attrib={xlink.href: backrefuri}), node) + self.current_node.append("backlink") + self.close_moin_page_node() # + self.close_moin_page_node() #

def depart_system_message(self, node): self.depart_admonition(node)