From a7f3f942c10d0096daefdcd62b5ee9e906a7e635 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Wed, 15 Mar 2023 22:50:37 +0100
Subject: [PATCH 01/15] Began moving markup code out.
---
src/antsibull_docs/jinja2/environment.py | 4 +-
src/antsibull_docs/jinja2/filters.py | 42 +++++++++++++++++++
.../{jinja2 => markup}/htmlify.py | 32 ++++----------
.../{jinja2 => markup}/parser.py | 0
.../{jinja2 => markup}/rstify.py | 32 ++++----------
.../{ => markup}/semantic_helper.py | 0
src/sphinx_antsibull_ext/roles.py | 2 +-
tests/units/test_jinja2.py | 25 +----------
...jinja2_parser.py => test_markup_parser.py} | 2 +-
9 files changed, 63 insertions(+), 76 deletions(-)
rename src/antsibull_docs/{jinja2 => markup}/htmlify.py (91%)
rename src/antsibull_docs/{jinja2 => markup}/parser.py (100%)
rename src/antsibull_docs/{jinja2 => markup}/rstify.py (90%)
rename src/antsibull_docs/{ => markup}/semantic_helper.py (100%)
rename tests/units/{test_jinja2_parser.py => test_markup_parser.py} (98%)
diff --git a/src/antsibull_docs/jinja2/environment.py b/src/antsibull_docs/jinja2/environment.py
index e4668c9f..b29a864e 100644
--- a/src/antsibull_docs/jinja2/environment.py
+++ b/src/antsibull_docs/jinja2/environment.py
@@ -10,6 +10,8 @@
from jinja2 import BaseLoader, Environment, FileSystemLoader, PackageLoader
+from ..markup.htmlify import html_ify
+from ..markup.rstify import rst_code, rst_escape, rst_ify
from ..utils.collection_name_transformer import CollectionNameTransformer
from .filters import (
do_max,
@@ -23,8 +25,6 @@
to_ini_value,
to_json,
)
-from .htmlify import html_ify
-from .rstify import rst_code, rst_escape, rst_ify
from .tests import still_relevant, test_list
# kludge_ns gives us a kludgey way to set variables inside of loops that need to be visible outside
diff --git a/src/antsibull_docs/jinja2/filters.py b/src/antsibull_docs/jinja2/filters.py
index ee20ca51..e83b7224 100644
--- a/src/antsibull_docs/jinja2/filters.py
+++ b/src/antsibull_docs/jinja2/filters.py
@@ -13,6 +13,10 @@
from antsibull_core.logging import log
from jinja2.runtime import Context, Undefined
+from jinja2.utils import pass_context
+
+from ..markup.rstify import rst_ify as rst_ify_impl
+from ..markup.htmlify import html_ify as html_ify_impl
mlog = log.fields(mod=__name__)
@@ -127,3 +131,41 @@ def to_ini_value(data: t.Any) -> str:
return 'MAPPINGS ARE NOT SUPPORTED'
# Handle other values (booleans, integers, floats) as JSON
return json.dumps(data)
+
+
+@pass_context
+def rst_ify(context: Context, text: str,
+ *,
+ plugin_fqcn: t.Optional[str] = None,
+ plugin_type: t.Optional[str] = None) -> str:
+ ''' convert symbols like I(this is in italics) to valid restructured text '''
+ flog = mlog.fields(func='rst_ify')
+ flog.fields(text=text).debug('Enter')
+
+ plugin_fqcn, plugin_type = extract_plugin_data(
+ context, plugin_fqcn=plugin_fqcn, plugin_type=plugin_type)
+
+ text, counts = rst_ify_impl(text, plugin_fqcn=plugin_fqcn, plugin_type=plugin_type)
+
+ flog.fields(counts=counts).info('Number of macros converted to rst equivalents')
+ flog.debug('Leave')
+ return text
+
+
+@pass_context
+def html_ify(context: Context, text: str,
+ *,
+ plugin_fqcn: t.Optional[str] = None,
+ plugin_type: t.Optional[str] = None) -> str:
+ ''' convert symbols like I(this is in italics) to valid HTML '''
+ flog = mlog.fields(func='html_ify')
+ flog.fields(text=text).debug('Enter')
+
+ plugin_fqcn, plugin_type = extract_plugin_data(
+ context, plugin_fqcn=plugin_fqcn, plugin_type=plugin_type)
+
+ text, counts = html_ify_impl(text, plugin_fqcn=plugin_fqcn, plugin_type=plugin_type)
+
+ flog.fields(counts=counts).info('Number of macros converted to html equivalents')
+ flog.debug('Leave')
+ return text
diff --git a/src/antsibull_docs/jinja2/htmlify.py b/src/antsibull_docs/markup/htmlify.py
similarity index 91%
rename from src/antsibull_docs/jinja2/htmlify.py
rename to src/antsibull_docs/markup/htmlify.py
index 91c06df7..46fa67eb 100644
--- a/src/antsibull_docs/jinja2/htmlify.py
+++ b/src/antsibull_docs/markup/htmlify.py
@@ -11,16 +11,9 @@
from html import escape as html_escape
from urllib.parse import quote
-from antsibull_core.logging import log
-from jinja2.runtime import Context
-from jinja2.utils import pass_context
-
-from ..semantic_helper import parse_option, parse_return_value
-from .filters import extract_plugin_data
+from .semantic_helper import parse_option, parse_return_value
from .parser import Command, CommandSet, convert_text
-mlog = log.fields(mod=__name__)
-
_MODULE = re.compile(r"^([^).]+)\.([^).]+)\.([^)]+)$")
_PLUGIN = re.compile(r"^([^).]+)\.([^).]+)\.([^)]+)#([a-z]+)$")
@@ -37,15 +30,13 @@ def _create_error(text: str, error: str) -> str:
class _Context:
- j2_context: Context
counts: t.Dict[str, int]
plugin_fqcn: t.Optional[str]
plugin_type: t.Optional[str]
- def __init__(self, j2_context: Context,
+ def __init__(self,
plugin_fqcn: t.Optional[str] = None,
plugin_type: t.Optional[str] = None):
- self.j2_context = j2_context
self.counts = {
'italic': 0,
'bold': 0,
@@ -61,8 +52,8 @@ def __init__(self, j2_context: Context,
'return-value': 0,
'ruler': 0,
}
- self.plugin_fqcn, self.plugin_type = extract_plugin_data(
- j2_context, plugin_fqcn=plugin_fqcn, plugin_type=plugin_type)
+ self.plugin_fqcn = plugin_fqcn
+ self.plugin_type = plugin_type
# In the following, we make heavy use of escaped whitespace ("\ ") being removed from the output.
@@ -300,17 +291,12 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
])
-@pass_context
-def html_ify(context: Context, text: str,
+def html_ify(text: str,
*,
plugin_fqcn: t.Optional[str] = None,
- plugin_type: t.Optional[str] = None) -> str:
+ plugin_type: t.Optional[str] = None) -> t.Tuple[str, t.Mapping[str, int]]:
''' convert symbols like I(this is in italics) to valid HTML '''
- flog = mlog.fields(func='html_ify')
- flog.fields(text=text).debug('Enter')
-
our_context = _Context(
- context,
plugin_fqcn=plugin_fqcn,
plugin_type=plugin_type,
)
@@ -318,8 +304,6 @@ def html_ify(context: Context, text: str,
try:
text = convert_text(text, _COMMAND_SET, html_escape, our_context)
except Exception as exc: # pylint:disable=broad-except
- return _create_error(text, str(exc))
+ text = _create_error(text, str(exc))
- flog.fields(counts=our_context.counts).info('Number of macros converted to html equivalents')
- flog.debug('Leave')
- return text
+ return text, our_context.counts
diff --git a/src/antsibull_docs/jinja2/parser.py b/src/antsibull_docs/markup/parser.py
similarity index 100%
rename from src/antsibull_docs/jinja2/parser.py
rename to src/antsibull_docs/markup/parser.py
diff --git a/src/antsibull_docs/jinja2/rstify.py b/src/antsibull_docs/markup/rstify.py
similarity index 90%
rename from src/antsibull_docs/jinja2/rstify.py
rename to src/antsibull_docs/markup/rstify.py
index 865026be..6815a401 100644
--- a/src/antsibull_docs/jinja2/rstify.py
+++ b/src/antsibull_docs/markup/rstify.py
@@ -10,16 +10,9 @@
import typing as t
from urllib.parse import quote
-from antsibull_core.logging import log
-from jinja2.runtime import Context
-from jinja2.utils import pass_context
-
-from ..semantic_helper import augment_plugin_name_type
-from .filters import extract_plugin_data
+from .semantic_helper import augment_plugin_name_type
from .parser import Command, CommandSet, convert_text
-mlog = log.fields(mod=__name__)
-
_MODULE = re.compile(r"^([^).]+)\.([^).]+)\.([^)]+)$")
_PLUGIN = re.compile(r"^([^).]+)\.([^).]+)\.([^)]+)#([a-z]+)$")
@@ -65,15 +58,13 @@ def _create_error(text: str, error: str) -> str:
class _Context:
- j2_context: Context
counts: t.Dict[str, int]
plugin_fqcn: t.Optional[str]
plugin_type: t.Optional[str]
- def __init__(self, j2_context: Context,
+ def __init__(self,
plugin_fqcn: t.Optional[str] = None,
plugin_type: t.Optional[str] = None):
- self.j2_context = j2_context
self.counts = {
'italic': 0,
'bold': 0,
@@ -89,8 +80,8 @@ def __init__(self, j2_context: Context,
'return-value': 0,
'ruler': 0,
}
- self.plugin_fqcn, self.plugin_type = extract_plugin_data(
- j2_context, plugin_fqcn=plugin_fqcn, plugin_type=plugin_type)
+ self.plugin_fqcn = plugin_fqcn
+ self.plugin_type = plugin_type
# In the following, we make heavy use of escaped whitespace ("\ ") being removed from the output.
@@ -265,17 +256,12 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
])
-@pass_context
-def rst_ify(context: Context, text: str,
+def rst_ify(text: str,
*,
plugin_fqcn: t.Optional[str] = None,
- plugin_type: t.Optional[str] = None) -> str:
+ plugin_type: t.Optional[str] = None) -> t.Tuple[str, t.Mapping[str, int]]:
''' convert symbols like I(this is in italics) to valid restructured text '''
- flog = mlog.fields(func='rst_ify')
- flog.fields(text=text).debug('Enter')
-
our_context = _Context(
- context,
plugin_fqcn=plugin_fqcn,
plugin_type=plugin_type,
)
@@ -283,8 +269,6 @@ def rst_ify(context: Context, text: str,
try:
text = convert_text(text, _COMMAND_SET, rst_escape, our_context)
except Exception as exc: # pylint:disable=broad-except
- return _create_error(text, str(exc))
+ text = _create_error(text, str(exc))
- flog.fields(counts=our_context.counts).info('Number of macros converted to rst equivalents')
- flog.debug('Leave')
- return text
+ return text, our_context.counts
diff --git a/src/antsibull_docs/semantic_helper.py b/src/antsibull_docs/markup/semantic_helper.py
similarity index 100%
rename from src/antsibull_docs/semantic_helper.py
rename to src/antsibull_docs/markup/semantic_helper.py
diff --git a/src/sphinx_antsibull_ext/roles.py b/src/sphinx_antsibull_ext/roles.py
index 49c5b53b..3259a495 100644
--- a/src/sphinx_antsibull_ext/roles.py
+++ b/src/sphinx_antsibull_ext/roles.py
@@ -12,7 +12,7 @@
from docutils import nodes
from sphinx import addnodes
-from antsibull_docs.semantic_helper import parse_option, parse_return_value
+from antsibull_docs.markup.semantic_helper import parse_option, parse_return_value
# pylint:disable-next=unused-argument,dangerous-default-value
diff --git a/tests/units/test_jinja2.py b/tests/units/test_jinja2.py
index afd0c00b..2dbb434e 100644
--- a/tests/units/test_jinja2.py
+++ b/tests/units/test_jinja2.py
@@ -4,8 +4,7 @@
import pytest
-from antsibull_docs.jinja2.filters import massage_author_name, move_first, to_ini_value, to_json
-from antsibull_docs.jinja2.rstify import rst_escape, rst_ify
+from antsibull_docs.jinja2.filters import massage_author_name, move_first, to_ini_value, to_json, rst_ify
RST_IFY_DATA = {
# No substitutions
@@ -41,28 +40,6 @@ def test_rst_ify(text, expected):
assert rst_ify(context, text) == expected
-RST_ESCAPE_DATA = {
- '': '',
- 'no-op': 'no-op',
- None: 'None',
- 1: '1',
- '*': '\\*',
- '_': '\\_',
- '<': '\\<',
- '>': '\\>',
- '`': '\\`',
- '\\': '\\\\',
- '\\*': '\\\\\\*',
- '*\\': '\\*\\\\',
- ':role:`test`': ':role:\\`test\\`',
-}
-
-
-@pytest.mark.parametrize('value, expected', RST_ESCAPE_DATA.items())
-def test_escape_ify(value, expected):
- assert rst_escape(value) == expected
-
-
MOVE_FIRST_DATA = [
([], [], []),
(['a', 'b', 'c'], ['d'], ['a', 'b', 'c']),
diff --git a/tests/units/test_jinja2_parser.py b/tests/units/test_markup_parser.py
similarity index 98%
rename from tests/units/test_jinja2_parser.py
rename to tests/units/test_markup_parser.py
index 52707037..930fb603 100644
--- a/tests/units/test_jinja2_parser.py
+++ b/tests/units/test_markup_parser.py
@@ -7,7 +7,7 @@
import pytest
-from antsibull_docs.jinja2.parser import Command, CommandSet, ParsingException, convert_text
+from antsibull_docs.markup.parser import Command, CommandSet, ParsingException, convert_text
class TestCommand0(Command):
From f37fe19cdf77db3ee2c145f1432e9c0a83591e76 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Wed, 15 Mar 2023 22:57:27 +0100
Subject: [PATCH 02/15] Forgot __init__.py.
---
src/antsibull_docs/markup/__init__.py | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 src/antsibull_docs/markup/__init__.py
diff --git a/src/antsibull_docs/markup/__init__.py b/src/antsibull_docs/markup/__init__.py
new file mode 100644
index 00000000..7547f6e1
--- /dev/null
+++ b/src/antsibull_docs/markup/__init__.py
@@ -0,0 +1,4 @@
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2020, Ansible Project
From 98d3cf0ee32641b51d05db01d292e900ca04ee28 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Wed, 15 Mar 2023 23:01:42 +0100
Subject: [PATCH 03/15] Fix imports.
---
src/antsibull_docs/jinja2/environment.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/antsibull_docs/jinja2/environment.py b/src/antsibull_docs/jinja2/environment.py
index b29a864e..f255c288 100644
--- a/src/antsibull_docs/jinja2/environment.py
+++ b/src/antsibull_docs/jinja2/environment.py
@@ -10,8 +10,7 @@
from jinja2 import BaseLoader, Environment, FileSystemLoader, PackageLoader
-from ..markup.htmlify import html_ify
-from ..markup.rstify import rst_code, rst_escape, rst_ify
+from ..markup.rstify import rst_code, rst_escape
from ..utils.collection_name_transformer import CollectionNameTransformer
from .filters import (
do_max,
@@ -24,6 +23,8 @@
rst_xline,
to_ini_value,
to_json,
+ html_ify,
+ rst_ify,
)
from .tests import still_relevant, test_list
From 1c569ca54d51945808d3f2c101eaa235873fafaf Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Thu, 16 Mar 2023 07:27:05 +0100
Subject: [PATCH 04/15] Move parser impl code out.
---
src/antsibull_docs/markup/_parser_impl.py | 87 ++++++++++++++++++
src/antsibull_docs/markup/parser.py | 91 ++-----------------
tests/units/markup/test_markup.py | 58 ++++++++++++
.../test_parser.py} | 8 +-
tests/units/markup/test_parser_impl.py | 65 +++++++++++++
5 files changed, 224 insertions(+), 85 deletions(-)
create mode 100644 src/antsibull_docs/markup/_parser_impl.py
create mode 100644 tests/units/markup/test_markup.py
rename tests/units/{test_markup_parser.py => markup/test_parser.py} (89%)
create mode 100644 tests/units/markup/test_parser_impl.py
diff --git a/src/antsibull_docs/markup/_parser_impl.py b/src/antsibull_docs/markup/_parser_impl.py
new file mode 100644
index 00000000..21ed5ac4
--- /dev/null
+++ b/src/antsibull_docs/markup/_parser_impl.py
@@ -0,0 +1,87 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2022, Ansible Project
+"""
+Internal parsing code.
+"""
+
+import re
+import typing as t
+
+_ESCAPE_OR_COMMA = re.compile(r'\\(.)| *(,) *')
+_ESCAPE_OR_CLOSING = re.compile(r'\\(.)|([)])')
+
+
+def parse_parameters_escaped(text: str, index: int, parameter_count: int,
+ ) -> t.Tuple[t.List[str], int, t.Optional[str]]:
+ result: t.List[str] = []
+ parameters_left = parameter_count
+ while parameters_left > 1:
+ parameters_left -= 1
+ value: t.List[str] = []
+ while True:
+ match = _ESCAPE_OR_COMMA.search(text, pos=index)
+ if not match:
+ result.append(''.join(value))
+ return (
+ result,
+ len(text),
+ 'Cannot find comma separating parameter'
+ f' {parameter_count - parameters_left} from the next one'
+ )
+ value.append(text[index:match.start(0)])
+ index = match.end(0)
+ if match.group(1):
+ value.append(match.group(1))
+ else:
+ break
+ result.append(''.join(value))
+ value = []
+ while True:
+ match = _ESCAPE_OR_CLOSING.search(text, pos=index)
+ if not match:
+ result.append(''.join(value))
+ return result, len(text), 'Cannot find closing ")" after last parameter'
+ value.append(text[index:match.start(0)])
+ index = match.end(0)
+ if match.group(1):
+ value.append(match.group(1))
+ else:
+ break
+ result.append(''.join(value))
+ return result, index, None
+
+
+def parse_parameters_unescaped(text: str, index: int, parameter_count: int,
+ ) -> t.Tuple[t.List[str], int, t.Optional[str]]:
+ result: t.List[str] = []
+ first = True
+ parameters_left = parameter_count
+ while parameters_left > 1:
+ parameters_left -= 1
+ next_index = text.find(',', index)
+ if next_index < 0:
+ return (
+ result,
+ len(text),
+ 'Cannot find comma separating parameter'
+ f' {parameter_count - parameters_left} from the next one'
+ )
+ parameter = text[index:next_index].rstrip(' ')
+ if not first:
+ parameter = parameter.lstrip(' ')
+ else:
+ first = False
+ result.append(parameter)
+ index = next_index + 1
+ next_index = text.find(')', index)
+ if next_index < 0:
+ return result, len(text), 'Cannot find closing ")" after last parameter'
+ parameter = text[index:next_index]
+ if not first:
+ parameter = parameter.lstrip(' ')
+ result.append(parameter)
+ index = next_index + 1
+ return result, index, None
diff --git a/src/antsibull_docs/markup/parser.py b/src/antsibull_docs/markup/parser.py
index 7231592b..fd916abf 100644
--- a/src/antsibull_docs/markup/parser.py
+++ b/src/antsibull_docs/markup/parser.py
@@ -11,8 +11,7 @@
import re
import typing as t
-_ESCAPE_OR_COMMA = re.compile(r'\\(.)| *(,) *')
-_ESCAPE_OR_CLOSING = re.compile(r'\\(.)|([)])')
+from ._parser_impl import parse_parameters_escaped, parse_parameters_unescaped
class ParsingException(Exception):
@@ -72,83 +71,6 @@ class CommandData(t.NamedTuple):
Part = t.Union[str, CommandData]
-def parse_parameters_escaped(text: str, index: int, command: Command,
- command_start: int) -> t.Tuple[int, t.List[str]]:
- result = []
- parameter_count = command.parameter_count
- while parameter_count > 1:
- parameter_count -= 1
- value = []
- while True:
- match = _ESCAPE_OR_COMMA.search(text, pos=index)
- if not match:
- raise ParsingException(
- f'Cannot find comma separating '
- f'parameter {command.parameter_count - parameter_count}'
- f' from the next one for command "{command.command}"'
- f' starting at index {command_start} in {text!r}'
- )
- value.append(text[index:match.start(0)])
- index = match.end(0)
- if match.group(1):
- value.append(match.group(1))
- else:
- break
- result.append(''.join(value))
- value = []
- while True:
- match = _ESCAPE_OR_CLOSING.search(text, pos=index)
- if not match:
- raise ParsingException(
- f'Cannot find ")" closing after the last parameter for'
- f' command "{command.command}" starting at index {command_start} in {text!r}'
- )
- value.append(text[index:match.start(0)])
- index = match.end(0)
- if match.group(1):
- value.append(match.group(1))
- else:
- break
- result.append(''.join(value))
- return index, result
-
-
-def parse_parameters_unescaped(text: str, index: int, command: Command,
- command_start: int) -> t.Tuple[int, t.List[str]]:
- result = []
- first = True
- parameter_count = command.parameter_count
- while parameter_count > 1:
- parameter_count -= 1
- next_index = text.find(',', index)
- if next_index < 0:
- raise ParsingException(
- f'Cannot find comma separating '
- f'parameter {command.parameter_count - parameter_count}'
- f' from the next one for command "{command.command}"'
- f' starting at index {command_start} in {text!r}'
- )
- parameter = text[index:next_index].rstrip(' ')
- if not first:
- parameter = parameter.lstrip(' ')
- else:
- first = False
- result.append(parameter)
- index = next_index + 1
- next_index = text.find(')', index)
- if next_index < 0:
- raise ParsingException(
- f'Cannot find ")" closing after the last parameter for'
- f' command "{command.command}" starting at index {command_start} in {text!r}'
- )
- parameter = text[index:next_index]
- if not first:
- parameter = parameter.lstrip(' ')
- result.append(parameter)
- index = next_index + 1
- return index, result
-
-
def parse_text(text: str, commands: CommandSet) -> t.List[Part]:
result: t.List[Part] = []
index = 0
@@ -167,9 +89,16 @@ def parse_text(text: str, commands: CommandSet) -> t.List[Part]:
continue
index += 1
if command.escaped_content:
- index, parameters = parse_parameters_escaped(text, index, command, command_start)
+ parameters, index, error = parse_parameters_escaped(
+ text, index, command.parameter_count)
else:
- index, parameters = parse_parameters_unescaped(text, index, command, command_start)
+ parameters, index, error = parse_parameters_unescaped(
+ text, index, command.parameter_count)
+ if error is not None:
+ raise ParsingException(
+ error +
+ f' for command "{command.command}" starting at index {command_start} in {text!r}'
+ )
result.append(CommandData(command=command, parameters=parameters))
return result
diff --git a/tests/units/markup/test_markup.py b/tests/units/markup/test_markup.py
new file mode 100644
index 00000000..8bfffc8e
--- /dev/null
+++ b/tests/units/markup/test_markup.py
@@ -0,0 +1,58 @@
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2020, Ansible Project
+
+import pytest
+
+from antsibull_docs.markup.rstify import rst_escape, rst_ify
+
+RST_IFY_DATA = {
+ # No substitutions
+ 'no-op': 'no-op',
+ 'no-op Z(test)': 'no-op Z(test)',
+ # Simple cases of all substitutions
+ 'I(italic)': r'\ :emphasis:`italic`\ ',
+ 'B(bold)': r'\ :strong:`bold`\ ',
+ 'M(ansible.builtin.yum)': r'\ :ref:`ansible.builtin.yum'
+ r' `\ ',
+ 'U(https://docs.ansible.com)': r'\ https://docs.ansible.com\ ',
+ 'L(the user guide,https://docs.ansible.com/user-guide.html)': r'\ `the user guide'
+ r' `__\ ',
+ 'R(the user guide,user-guide)': r'\ :ref:`the user guide `\ ',
+ 'C(/usr/bin/file)': r'\ :literal:`/usr/bin/file`\ ',
+ 'HORIZONTALLINE': '\n\n.. raw:: html\n\n
\n\n',
+ # Multiple substitutions
+ 'The M(ansible.builtin.yum) module B(MUST) be given the C(package) parameter. See the R(looping docs,using-loops) for more info':
+ r'The \ :ref:`ansible.builtin.yum `\ module \ :strong:`MUST`\ be given the \ :literal:`package`\ parameter. See the \ :ref:`looping docs `\ for more info',
+ # Problem cases
+ 'IBM(International Business Machines)': 'IBM(International Business Machines)',
+ 'L(the user guide, https://docs.ansible.com/)': r'\ `the user guide `__\ ',
+ 'R(the user guide, user-guide)': r'\ :ref:`the user guide `\ ',
+}
+
+
+@pytest.mark.parametrize('text, expected', RST_IFY_DATA.items())
+def test_rst_ify(text, expected):
+ assert rst_ify(text, plugin_fqcn='foo.bar.baz', plugin_type='module')[0] == expected
+
+
+RST_ESCAPE_DATA = {
+ '': '',
+ 'no-op': 'no-op',
+ None: 'None',
+ 1: '1',
+ '*': '\\*',
+ '_': '\\_',
+ '<': '\\<',
+ '>': '\\>',
+ '`': '\\`',
+ '\\': '\\\\',
+ '\\*': '\\\\\\*',
+ '*\\': '\\*\\\\',
+ ':role:`test`': ':role:\\`test\\`',
+}
+
+
+@pytest.mark.parametrize('value, expected', RST_ESCAPE_DATA.items())
+def test_escape_ify(value, expected):
+ assert rst_escape(value) == expected
diff --git a/tests/units/test_markup_parser.py b/tests/units/markup/test_parser.py
similarity index 89%
rename from tests/units/test_markup_parser.py
rename to tests/units/markup/test_parser.py
index 930fb603..43ba51cc 100644
--- a/tests/units/test_markup_parser.py
+++ b/tests/units/markup/test_parser.py
@@ -119,11 +119,11 @@ def test_convert_text(text, expected):
CONVERT_TEXT_FAIL_DATA = {
- 'command1a(': """Cannot find ")" closing after the last parameter for command "command1a" starting at index 0 in 'command1a('""",
- 'command2a(,': """Cannot find ")" closing after the last parameter for command "command2a" starting at index 0 in 'command2a(,'""",
+ 'command1a(': """Cannot find closing ")" after last parameter for command "command1a" starting at index 0 in 'command1a('""",
+ 'command2a(,': """Cannot find closing ")" after last parameter for command "command2a" starting at index 0 in 'command2a(,'""",
'command2a(': """Cannot find comma separating parameter 1 from the next one for command "command2a" starting at index 0 in 'command2a('""",
- 'command1b(': """Cannot find ")" closing after the last parameter for command "command1b" starting at index 0 in 'command1b('""",
- 'command2b(,': """Cannot find ")" closing after the last parameter for command "command2b" starting at index 0 in 'command2b(,'""",
+ 'command1b(': """Cannot find closing ")" after last parameter for command "command1b" starting at index 0 in 'command1b('""",
+ 'command2b(,': """Cannot find closing ")" after last parameter for command "command2b" starting at index 0 in 'command2b(,'""",
'command2b(': """Cannot find comma separating parameter 1 from the next one for command "command2b" starting at index 0 in 'command2b('""",
r'command2b(\,)': r"""Cannot find comma separating parameter 1 from the next one for command "command2b" starting at index 0 in 'command2b(\\,)'""",
}
diff --git a/tests/units/markup/test_parser_impl.py b/tests/units/markup/test_parser_impl.py
new file mode 100644
index 00000000..ec47b09c
--- /dev/null
+++ b/tests/units/markup/test_parser_impl.py
@@ -0,0 +1,65 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2022, Ansible Project
+
+import typing as t
+
+import pytest
+
+from antsibull_docs.markup._parser_impl import parse_parameters_escaped, parse_parameters_unescaped
+
+
+ESCAPED_TESTS = [
+ ['(a)', 1, 1, ['a'], 3, None],
+ ['(a,b)', 1, 1, ['a,b'], 5, None],
+ ['(a,b,c)', 1, 1, ['a,b,c'], 7, None],
+ ['(a,b)', 1, 2, ['a', 'b'], 5, None],
+ ['(a,b,c)', 1, 2, ['a', 'b,c'], 7, None],
+ ['(a,b,c)', 1, 3, ['a', 'b', 'c'], 7, None],
+ ['(a\\,,b\\,\\),c\\))', 1, 3, ['a,', 'b,)', 'c)'], 15, None],
+ ['(a', 1, 1, [''], 2, 'Cannot find closing ")" after last parameter'],
+ ['(a', 1, 2, [''], 2, 'Cannot find comma separating parameter 1 from the next one'],
+ ['(a,b', 1, 2, ['a', ''], 4, 'Cannot find closing ")" after last parameter'],
+]
+
+
+@pytest.mark.parametrize(
+ 'text, index, parameter_count, expected_result, expected_index, expected_error',
+ ESCAPED_TESTS,
+)
+def test_parse_parameters_escaped(text: str, index: int, parameter_count: int,
+ expected_result: t.List[str], expected_index: int,
+ expected_error: t.Optional[str]) -> None:
+ result, end_index, error = parse_parameters_escaped(text, index, parameter_count)
+ print(result, end_index, error)
+ assert result == expected_result
+ assert end_index == expected_index
+ assert error == expected_error
+
+
+UNESCAPED_TESTS = [
+ ['(a)', 1, 1, ['a'], 3, None],
+ ['(a,b)', 1, 1, ['a,b'], 5, None],
+ ['(a,b,c)', 1, 1, ['a,b,c'], 7, None],
+ ['(a,b)', 1, 2, ['a', 'b'], 5, None],
+ ['(a,b,c)', 1, 2, ['a', 'b,c'], 7, None],
+ ['(a,b,c)', 1, 3, ['a', 'b', 'c'], 7, None],
+ ['(a', 1, 1, [], 2, 'Cannot find closing ")" after last parameter'],
+ ['(a', 1, 2, [], 2, 'Cannot find comma separating parameter 1 from the next one'],
+ ['(a,b', 1, 2, ['a'], 4, 'Cannot find closing ")" after last parameter'],
+]
+
+
+@pytest.mark.parametrize(
+ 'text, index, parameter_count, expected_result, expected_index, expected_error',
+ UNESCAPED_TESTS,
+)
+def test_parse_parameters_unescaped(text: str, index: int, parameter_count: int,
+ expected_result: t.List[str], expected_index: int,
+ expected_error: t.Optional[str]) -> None:
+ result, end_index, error = parse_parameters_unescaped(text, index, parameter_count)
+ print(result, end_index, error)
+ assert result == expected_result
+ assert end_index == expected_index
+ assert error == expected_error
From 4ac853267701914fc5554a695fc5fa63a1788a2e Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Thu, 16 Mar 2023 23:20:48 +0100
Subject: [PATCH 05/15] Began with new-style parser.
---
src/antsibull_docs/markup/dom.py | 143 ++++++++++++
src/antsibull_docs/markup/parser.py | 341 ++++++++++++++++++++++++++++
2 files changed, 484 insertions(+)
create mode 100644 src/antsibull_docs/markup/dom.py
diff --git a/src/antsibull_docs/markup/dom.py b/src/antsibull_docs/markup/dom.py
new file mode 100644
index 00000000..6e015958
--- /dev/null
+++ b/src/antsibull_docs/markup/dom.py
@@ -0,0 +1,143 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+"""
+DOM classes used by parser.
+"""
+
+from enum import Enum
+from typing import NamedTuple
+
+import typing as t
+
+
+ErrorType = t.Union[t.Literal['ignore'], t.Literal['message'], t.Literal['exception']]
+
+
+class PluginIdentifier(NamedTuple):
+ fqcn: str
+ type: str
+
+
+class PartType(Enum):
+ ERROR = 0
+ BOLD = 1
+ CODE = 2
+ HORIZONTAL_LINE = 3
+ ITALIC = 4
+ LINK = 5
+ MODULE = 6
+ RST_REF = 7
+ URL = 8
+ TEXT = 9
+ ENV_VARIABLE = 10
+ OPTION_NAME = 11
+ OPTION_VALUE = 12
+ PLUGIN = 13
+ RETURN_VALUE = 14
+
+
+class TextPart(NamedTuple):
+ type: t.Literal[PartType.TEXT]
+ text: str
+
+
+class ItalicPart(NamedTuple):
+ type: t.Literal[PartType.ITALIC]
+ text: str
+
+
+class BoldPart(NamedTuple):
+ type: t.Literal[PartType.BOLD]
+ text: str
+
+
+class ModulePart(NamedTuple):
+ type: t.Literal[PartType.MODULE]
+ fqcn: str
+
+
+class PluginPart(NamedTuple):
+ type: t.Literal[PartType.PLUGIN]
+ plugin: PluginIdentifier
+
+
+class URLPart(NamedTuple):
+ type: t.Literal[PartType.URL]
+ url: str
+
+
+class LinkPart(NamedTuple):
+ type: t.Literal[PartType.LINK]
+ text: str
+ url: str
+
+
+class RSTRefPart(NamedTuple):
+ type: t.Literal[PartType.RST_REF]
+ text: str
+ ref: str
+
+
+class CodePart(NamedTuple):
+ type: t.Literal[PartType.CODE]
+ text: str
+
+
+class OptionNamePart(NamedTuple):
+ type: t.Literal[PartType.OPTION_NAME]
+ plugin: t.Optional[PluginIdentifier]
+ link: t.List[str]
+ name: str
+ value: t.Optional[str]
+
+
+class OptionValuePart(NamedTuple):
+ type: t.Literal[PartType.OPTION_VALUE]
+ value: str
+
+
+class EnvVariablePart(NamedTuple):
+ type: t.Literal[PartType.ENV_VARIABLE]
+ name: str
+
+
+class ReturnValuePart(NamedTuple):
+ type: t.Literal[PartType.RETURN_VALUE]
+ plugin: t.Optional[PluginIdentifier]
+ link: t.List[str]
+ name: str
+ value: t.Optional[str]
+
+
+class HorizontalLinePart(NamedTuple):
+ type: t.Literal[PartType.HORIZONTAL_LINE]
+
+
+class ErrorPart(NamedTuple):
+ type: t.Literal[PartType.ERROR]
+ message: str
+
+
+AnyPart = t.Union[
+ TextPart,
+ ItalicPart,
+ BoldPart,
+ ModulePart,
+ PluginPart,
+ URLPart,
+ LinkPart,
+ RSTRefPart,
+ CodePart,
+ OptionNamePart,
+ OptionValuePart,
+ EnvVariablePart,
+ ReturnValuePart,
+ HorizontalLinePart,
+ ErrorPart,
+]
+
+
+Paragraph = t.List[AnyPart]
diff --git a/src/antsibull_docs/markup/parser.py b/src/antsibull_docs/markup/parser.py
index fd916abf..fc48005c 100644
--- a/src/antsibull_docs/markup/parser.py
+++ b/src/antsibull_docs/markup/parser.py
@@ -11,6 +11,7 @@
import re
import typing as t
+from . import dom
from ._parser_impl import parse_parameters_escaped, parse_parameters_unescaped
@@ -113,3 +114,343 @@ def convert_text(text: str, commands: CommandSet, process_other_text: t.Callable
else:
result.append(part.command.handle(part.parameters, context))
return ''.join(result)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+_IGNORE_MARKER = 'ignore:'
+_ARRAY_STUB_RE = re.compile(r'\[([^\]]*)\]')
+_FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([a-z]+):(.*)$')
+_FQCN = re.compile(r'^[a-z0-9_]+\.[a-z0-9_]+(?:\.[a-z0-9_]+)+$')
+_PLUGIN_TYPE = re.compile(r'^[a-z_]+$')
+
+
+def _is_fqcn(text: str) -> bool:
+ return _FQCN.match(text) is not None
+
+
+def _is_plugin_type(text: str) -> bool:
+ # We do not want to hard-code a list of valid plugin types that might be inaccurate, so we
+ # simply check whether this is a valid kind of Python identifier usually used for plugin
+ # types. If ansible-core ever adds one with digits, we'll have to update this.
+ return _PLUGIN_TYPE.match(text) is not None
+
+
+class Context(t.NamedTuple):
+ current_plugin: t.Optional[dom.PluginIdentifier]
+
+
+class CommandParser(abc.ABC):
+ command: str
+ parameters: int
+ escaped_arguments: bool
+
+ def __init__(self, command: str, parameters: int, escaped_arguments: bool = False):
+ self.command = command
+ self.parameters = parameters
+ self.escaped_arguments = escaped_arguments
+
+ @abc.abstractmethod
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ pass
+
+
+class CommandParserEx(CommandParser):
+ old_markup: bool
+
+ def __init__(self, command: str, parameters: int,
+ escaped_arguments: bool = False, old_markup: bool = False):
+ super().__init__(command, parameters, escaped_arguments)
+ self.old_markup = old_markup
+
+
+# Classic Ansible docs markup:
+
+
+class _Italics(CommandParserEx):
+ def __init__(self):
+ super().__init__('I', 1, old_markup=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ return dom.ItalicPart(dom.PartType.ITALIC, text=parameters[0])
+
+
+class _Bold(CommandParserEx):
+ def __init__(self):
+ super().__init__('B', 1, old_markup=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ return dom.BoldPart(dom.PartType.BOLD, text=parameters[0])
+
+
+class _Module(CommandParserEx):
+ def __init__(self):
+ super().__init__('M', 1, old_markup=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ fqcn = parameters[0]
+ if not _is_fqcn(fqcn):
+ raise ValueError(f'Module name "{fqcn}" is not a FQCN')
+ return dom.ModulePart(dom.PartType.MODULE, fqcn=fqcn)
+
+
+class _URL(CommandParserEx):
+ def __init__(self):
+ super().__init__('U', 1, old_markup=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ return dom.URLPart(dom.PartType.URL, url=parameters[0])
+
+
+class _Link(CommandParserEx):
+ def __init__(self):
+ super().__init__('L', 2, old_markup=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ text = parameters[0]
+ url = parameters[1]
+ return dom.LinkPart(dom.PartType.LINK, text=text, url=url)
+
+
+class _RSTRef(CommandParserEx):
+ def __init__(self):
+ super().__init__('R', 2, old_markup=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ text = parameters[0]
+ ref = parameters[1]
+ return dom.RSTRefPart(dom.PartType.RST_REF, text=text, ref=ref)
+
+
+class _Code(CommandParserEx):
+ def __init__(self):
+ super().__init__('C', 1, old_markup=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ return dom.CodePart(dom.PartType.CODE, text=parameters[0])
+
+
+class _HorizontalLine(CommandParserEx):
+ def __init__(self):
+ super().__init__('HORIZONTALLINE', 0, old_markup=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ return dom.HorizontalLinePart(dom.PartType.HORIZONTAL_LINE)
+
+
+# Semantic Ansible docs markup:
+
+
+def _parse_option_like(text: str,
+ context: Context,
+ ) -> t.Tuple[t.Optional[dom.PluginIdentifier],
+ t.List[str], str, t.Optional[str]]:
+ value = None
+ if '=' in text:
+ text, value = text.split('=', 1)
+ m = _FQCN_TYPE_PREFIX_RE.match(text)
+ if m:
+ plugin_fqcn = m.group(1)
+ plugin_type = m.group(2)
+ if not _is_fqcn(plugin_fqcn):
+ raise ValueError(f'Plugin name "{plugin_fqcn}" is not a FQCN')
+ if not _is_plugin_type(plugin_type):
+ raise ValueError(f'Plugin type "{plugin_type}" is not valid')
+ plugin_identifier = dom.PluginIdentifier(fqcn=plugin_fqcn, type=plugin_type)
+ text = m.group(3)
+ elif text.startswith(_IGNORE_MARKER):
+ plugin_identifier = None
+ text = text[len(_IGNORE_MARKER):]
+ else:
+ plugin_identifier = context.current_plugin
+ if ':' in text or '#' in text:
+ raise ValueError(f'Invalid option/return value name "{text}"')
+ return (
+ plugin_identifier,
+ _ARRAY_STUB_RE.sub('', text).split('.'),
+ text,
+ value,
+ )
+
+
+class _Plugin(CommandParserEx):
+ def __init__(self):
+ super().__init__('P', 1, escaped_arguments=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ name = parameters[0]
+ if '#' not in name:
+ raise ValueError(f'Parameter "{name}" is not of the form FQCN#type')
+ fqcn, ptype = name.split('#', 1)
+ if not _is_fqcn(fqcn):
+ raise ValueError(f'Plugin name "{fqcn}" is not a FQCN')
+ if not _is_plugin_type(ptype):
+ raise ValueError(f'Plugin type "{type}" is not valid')
+ return dom.PluginPart(
+ dom.PartType.PLUGIN, plugin=dom.PluginIdentifier(fqcn=fqcn, type=ptype))
+
+
+class _EnvVar(CommandParserEx):
+ def __init__(self):
+ super().__init__('E', 1, escaped_arguments=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ return dom.EnvVariablePart(dom.PartType.ENV_VARIABLE, name=parameters[0])
+
+
+class _OptionValue(CommandParserEx):
+ def __init__(self):
+ super().__init__('V', 1, escaped_arguments=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ return dom.OptionValuePart(dom.PartType.OPTION_VALUE, value=parameters[0])
+
+
+class _OptionName(CommandParserEx):
+ def __init__(self):
+ super().__init__('O', 1, escaped_arguments=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ plugin, link, name, value = _parse_option_like(parameters[0], context)
+ return dom.OptionNamePart(
+ dom.PartType.OPTION_NAME, plugin=plugin, link=link, name=name, value=value)
+
+
+class _ReturnValue(CommandParserEx):
+ def __init__(self):
+ super().__init__('RV', 1, escaped_arguments=True)
+
+ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
+ plugin, link, name, value = _parse_option_like(parameters[0], context)
+ return dom.ReturnValuePart(
+ dom.PartType.RETURN_VALUE, plugin=plugin, link=link, name=name, value=value)
+
+
+_COMMANDS = [
+ _Italics(),
+ _Bold(),
+ _Module(),
+ _URL(),
+ _Link(),
+ _RSTRef(),
+ _Code(),
+ _HorizontalLine(),
+ _Plugin(),
+ _EnvVar(),
+ _OptionValue(),
+ _OptionName(),
+ _ReturnValue(),
+]
+
+
+def _command_re(command: CommandParser) -> str:
+ return r'\b' + re.escape(command.command) + (r'\b' if command.parameters == 0 else r'\(')
+
+
+class Parser:
+ _group_map: t.Mapping[str, CommandParser]
+ _re: 're.Pattern' # on Python 3.6 the type is called differently
+
+ def __init__(self, commands: t.Sequence[CommandParser]):
+ self._group_map = {cmd.command + ('(' if cmd.parameters else ''): cmd for cmd in commands}
+ if commands:
+ self._re = re.compile('(' + '|'.join([_command_re(cmd) for cmd in commands]) + ')')
+ else:
+ self._re = re.compile('x^') # does not match anything
+
+ @staticmethod
+ def _parse_command(result: dom.Paragraph, text: str, cmd: CommandParser, index: int,
+ end_index: int, context: Context, errors: dom.ErrorType, where: str
+ ) -> int:
+ args: t.List[str]
+ error: t.Optional[str] = None
+ if cmd.parameters == 0:
+ args = []
+ elif cmd.escaped_arguments:
+ args, end_index, error = parse_parameters_escaped(text, end_index, cmd.parameters)
+ else:
+ args, end_index, error = parse_parameters_unescaped(
+ text, end_index, cmd.parameters)
+ if error is None:
+ try:
+ result.append(cmd.parse(args, context))
+ except Exception as exc: # pylint:disable=broad-except
+ error = f'{exc}'
+ if error is not None:
+ error = (
+ f'While parsing {cmd.command}{"()" if cmd.parameters else ""}'
+ f' at index {index + 1}{where}: {error}'
+ )
+ if errors == 'message':
+ result.append(dom.ErrorPart(dom.PartType.ERROR, message=error))
+ elif errors == 'exception':
+ raise ValueError(error)
+ return end_index
+
+ def parse_string(self, text: str, context: Context,
+ errors: dom.ErrorType = 'message', where: str = '') -> dom.Paragraph:
+ result: dom.Paragraph = []
+ length = len(text)
+ index = 0
+ while index < length:
+ m = self._re.search(text, index)
+ if m is None:
+ result.append(dom.TextPart(dom.PartType.TEXT, text=text[index:]))
+ break
+ if m.start(1) > index:
+ result.append(dom.TextPart(dom.PartType.TEXT, text=text[index:m.start(1)]))
+ index = self._parse_command(
+ result,
+ text,
+ self._group_map[m.group(1)],
+ m.start(1),
+ m.end(1),
+ context,
+ errors,
+ where,
+ )
+ return result
+
+
+_CLASSIC = Parser([cmd for cmd in _COMMANDS if cmd.old_markup])
+_SEMANTIC_MARKUP = Parser(_COMMANDS)
+
+
+def parse(text: t.Union[str, t.Sequence[str]],
+ context: Context,
+ errors: dom.ErrorType = 'message',
+ only_classic_markup: bool = False
+ ) -> t.List[dom.Paragraph]:
+ has_paragraphs = True
+ if isinstance(text, str):
+ has_paragraphs = False
+ text = [text]
+ parser = _CLASSIC if only_classic_markup else _SEMANTIC_MARKUP
+ return [
+ parser.parse_string(
+ par,
+ context,
+ errors=errors,
+ where=f' of paragraph {index + 1}' if has_paragraphs else '',
+ )
+ for index, par in enumerate(text)
+ ]
From 406cacd56f9305e23dd1040f1d58d2b2bab15765 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Fri, 17 Mar 2023 21:12:57 +0100
Subject: [PATCH 06/15] Fix typing for Python 3.6 and 3.7.
---
src/antsibull_docs/markup/dom.py | 37 ++++++++++++++++++--------------
1 file changed, 21 insertions(+), 16 deletions(-)
diff --git a/src/antsibull_docs/markup/dom.py b/src/antsibull_docs/markup/dom.py
index 6e015958..1feb1d63 100644
--- a/src/antsibull_docs/markup/dom.py
+++ b/src/antsibull_docs/markup/dom.py
@@ -9,11 +9,16 @@
from enum import Enum
from typing import NamedTuple
+import sys
import typing as t
-ErrorType = t.Union[t.Literal['ignore'], t.Literal['message'], t.Literal['exception']]
+if sys.version_info >= (3, 8):
+ ErrorType = t.Union[t.Literal['ignore'], t.Literal['message'], t.Literal['exception']]
+else:
+ # Python 3.6/3.7 do not have t.Literal
+ ErrorType = str
class PluginIdentifier(NamedTuple):
@@ -40,54 +45,54 @@ class PartType(Enum):
class TextPart(NamedTuple):
- type: t.Literal[PartType.TEXT]
+ type: 't.Literal[PartType.TEXT]'
text: str
class ItalicPart(NamedTuple):
- type: t.Literal[PartType.ITALIC]
+ type: 't.Literal[PartType.ITALIC]'
text: str
class BoldPart(NamedTuple):
- type: t.Literal[PartType.BOLD]
+ type: 't.Literal[PartType.BOLD]'
text: str
class ModulePart(NamedTuple):
- type: t.Literal[PartType.MODULE]
+ type: 't.Literal[PartType.MODULE]'
fqcn: str
class PluginPart(NamedTuple):
- type: t.Literal[PartType.PLUGIN]
+ type: 't.Literal[PartType.PLUGIN]'
plugin: PluginIdentifier
class URLPart(NamedTuple):
- type: t.Literal[PartType.URL]
+ type: 't.Literal[PartType.URL]'
url: str
class LinkPart(NamedTuple):
- type: t.Literal[PartType.LINK]
+ type: 't.Literal[PartType.LINK]'
text: str
url: str
class RSTRefPart(NamedTuple):
- type: t.Literal[PartType.RST_REF]
+ type: 't.Literal[PartType.RST_REF]'
text: str
ref: str
class CodePart(NamedTuple):
- type: t.Literal[PartType.CODE]
+ type: 't.Literal[PartType.CODE]'
text: str
class OptionNamePart(NamedTuple):
- type: t.Literal[PartType.OPTION_NAME]
+ type: 't.Literal[PartType.OPTION_NAME]'
plugin: t.Optional[PluginIdentifier]
link: t.List[str]
name: str
@@ -95,17 +100,17 @@ class OptionNamePart(NamedTuple):
class OptionValuePart(NamedTuple):
- type: t.Literal[PartType.OPTION_VALUE]
+ type: 't.Literal[PartType.OPTION_VALUE]'
value: str
class EnvVariablePart(NamedTuple):
- type: t.Literal[PartType.ENV_VARIABLE]
+ type: 't.Literal[PartType.ENV_VARIABLE]'
name: str
class ReturnValuePart(NamedTuple):
- type: t.Literal[PartType.RETURN_VALUE]
+ type: 't.Literal[PartType.RETURN_VALUE]'
plugin: t.Optional[PluginIdentifier]
link: t.List[str]
name: str
@@ -113,11 +118,11 @@ class ReturnValuePart(NamedTuple):
class HorizontalLinePart(NamedTuple):
- type: t.Literal[PartType.HORIZONTAL_LINE]
+ type: 't.Literal[PartType.HORIZONTAL_LINE]'
class ErrorPart(NamedTuple):
- type: t.Literal[PartType.ERROR]
+ type: 't.Literal[PartType.ERROR]'
message: str
From f6f66fa5e24f9630a8dd6c0d011c3ec16029dfb7 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Fri, 17 Mar 2023 21:48:08 +0100
Subject: [PATCH 07/15] Prepare formatters.
---
src/antsibull_docs/markup/dom.py | 147 +++++++++++++++++++++-
src/antsibull_docs/markup/format.py | 188 ++++++++++++++++++++++++++++
2 files changed, 334 insertions(+), 1 deletion(-)
create mode 100644 src/antsibull_docs/markup/format.py
diff --git a/src/antsibull_docs/markup/dom.py b/src/antsibull_docs/markup/dom.py
index 1feb1d63..69d13788 100644
--- a/src/antsibull_docs/markup/dom.py
+++ b/src/antsibull_docs/markup/dom.py
@@ -7,9 +7,11 @@
DOM classes used by parser.
"""
+import abc
+import sys
+
from enum import Enum
from typing import NamedTuple
-import sys
import typing as t
@@ -146,3 +148,146 @@ class ErrorPart(NamedTuple):
Paragraph = t.List[AnyPart]
+
+
+class Walker(abc.ABC):
+ @abc.abstractmethod
+ def process_error(self, part: ErrorPart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_bold(self, part: BoldPart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_code(self, part: CodePart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_horizontal_line(self, part: HorizontalLinePart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_italic(self, part: ItalicPart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_link(self, part: LinkPart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_module(self, part: ModulePart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_rst_ref(self, part: RSTRefPart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_url(self, part: URLPart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_text(self, part: TextPart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_env_variable(self, part: EnvVariablePart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_option_name(self, part: OptionNamePart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_option_value(self, part: OptionValuePart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_plugin(self, part: PluginPart) -> None:
+ pass
+
+ @abc.abstractmethod
+ def process_return_value(self, part: ReturnValuePart) -> None:
+ pass
+
+
+class NoopWalker(Walker):
+ def process_error(self, part: ErrorPart) -> None:
+ pass
+
+ def process_bold(self, part: BoldPart) -> None:
+ pass
+
+ def process_code(self, part: CodePart) -> None:
+ pass
+
+ def process_horizontal_line(self, part: HorizontalLinePart) -> None:
+ pass
+
+ def process_italic(self, part: ItalicPart) -> None:
+ pass
+
+ def process_link(self, part: LinkPart) -> None:
+ pass
+
+ def process_module(self, part: ModulePart) -> None:
+ pass
+
+ def process_rst_ref(self, part: RSTRefPart) -> None:
+ pass
+
+ def process_url(self, part: URLPart) -> None:
+ pass
+
+ def process_text(self, part: TextPart) -> None:
+ pass
+
+ def process_env_variable(self, part: EnvVariablePart) -> None:
+ pass
+
+ def process_option_name(self, part: OptionNamePart) -> None:
+ pass
+
+ def process_option_value(self, part: OptionValuePart) -> None:
+ pass
+
+ def process_plugin(self, part: PluginPart) -> None:
+ pass
+
+ def process_return_value(self, part: ReturnValuePart) -> None:
+ pass
+
+
+def walk(paragraph: Paragraph, walker: Walker) -> None: # noqa: C901, pylint:disable=too-many-branches
+ for part in paragraph:
+ if part.type == PartType.ERROR:
+ walker.process_error(t.cast(ErrorPart, part))
+ elif part.type == PartType.BOLD:
+ walker.process_bold(t.cast(BoldPart, part))
+ elif part.type == PartType.CODE:
+ walker.process_code(t.cast(CodePart, part))
+ elif part.type == PartType.HORIZONTAL_LINE:
+ walker.process_horizontal_line(t.cast(HorizontalLinePart, part))
+ elif part.type == PartType.ITALIC:
+ walker.process_italic(t.cast(ItalicPart, part))
+ elif part.type == PartType.LINK:
+ walker.process_link(t.cast(LinkPart, part))
+ elif part.type == PartType.MODULE:
+ walker.process_module(t.cast(ModulePart, part))
+ elif part.type == PartType.RST_REF:
+ walker.process_rst_ref(t.cast(RSTRefPart, part))
+ elif part.type == PartType.URL:
+ walker.process_url(t.cast(URLPart, part))
+ elif part.type == PartType.TEXT:
+ walker.process_text(t.cast(TextPart, part))
+ elif part.type == PartType.ENV_VARIABLE:
+ walker.process_env_variable(t.cast(EnvVariablePart, part))
+ elif part.type == PartType.OPTION_NAME:
+ walker.process_option_name(t.cast(OptionNamePart, part))
+ elif part.type == PartType.OPTION_VALUE:
+ walker.process_option_value(t.cast(OptionValuePart, part))
+ elif part.type == PartType.PLUGIN:
+ walker.process_plugin(t.cast(PluginPart, part))
+ elif part.type == PartType.RETURN_VALUE:
+ walker.process_return_value(t.cast(ReturnValuePart, part))
diff --git a/src/antsibull_docs/markup/format.py b/src/antsibull_docs/markup/format.py
new file mode 100644
index 00000000..f276ab6c
--- /dev/null
+++ b/src/antsibull_docs/markup/format.py
@@ -0,0 +1,188 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+"""
+Flexible formatting of DOM.
+"""
+
+import abc
+
+import typing as t
+
+from . import dom
+
+
+class LinkProvider(abc.ABC):
+ def plugin_link(self, # pylint:disable=no-self-use
+ plugin: dom.PluginIdentifier, # pylint:disable=unused-argument
+ ) -> t.Optional[str]:
+ '''Provides a link to a plugin.'''
+ return None
+
+ def plugin_option_like_link(self, # pylint:disable=no-self-use
+ plugin: dom.PluginIdentifier, # pylint:disable=unused-argument
+ # pylint:disable-next=unused-argument
+ what: "t.Union[t.Literal['option'], t.Literal['retval']]",
+ # pylint:disable-next=unused-argument
+ name: t.List[str], current_plugin: bool) -> t.Optional[str]:
+ '''Provides a link to a plugin's option or return value.'''
+ return None
+
+
+class _DefaultLinkProvider(LinkProvider):
+ pass
+
+
+class Formatter(abc.ABC):
+ @abc.abstractmethod
+ def format_error(self, part: dom.ErrorPart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_bold(self, part: dom.BoldPart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_code(self, part: dom.CodePart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_horizontal_line(self, part: dom.HorizontalLinePart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_italic(self, part: dom.ItalicPart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_link(self, part: dom.LinkPart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_module(self, part: dom.ModulePart, url: t.Optional[str]) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_rst_ref(self, part: dom.RSTRefPart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_url(self, part: dom.URLPart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_text(self, part: dom.TextPart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_env_variable(self, part: dom.EnvVariablePart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_option_name(self, part: dom.OptionNamePart, url: t.Optional[str]) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_option_value(self, part: dom.OptionValuePart) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_plugin(self, part: dom.PluginPart, url: t.Optional[str]) -> str:
+ pass
+
+ @abc.abstractmethod
+ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -> str:
+ pass
+
+
+class _FormatWalker(dom.Walker):
+ destination: t.List[str]
+ formatter: Formatter
+ link_provider: LinkProvider
+ current_plugin: t.Optional[dom.PluginIdentifier]
+
+ def __init__(self, destination: t.List[str],
+ formatter: Formatter,
+ link_provider: LinkProvider,
+ current_plugin: t.Optional[dom.PluginIdentifier]):
+ self.destination = destination
+ self.formatter = formatter
+ self.link_provider = link_provider
+ self.current_plugin = current_plugin
+
+ def process_error(self, part: dom.ErrorPart) -> None:
+ self.destination.append(self.formatter.format_error(part))
+
+ def process_bold(self, part: dom.BoldPart) -> None:
+ self.destination.append(self.formatter.format_bold(part))
+
+ def process_code(self, part: dom.CodePart) -> None:
+ self.destination.append(self.formatter.format_code(part))
+
+ def process_horizontal_line(self, part: dom.HorizontalLinePart) -> None:
+ self.destination.append(self.formatter.format_horizontal_line(part))
+
+ def process_italic(self, part: dom.ItalicPart) -> None:
+ self.destination.append(self.formatter.format_italic(part))
+
+ def process_link(self, part: dom.LinkPart) -> None:
+ self.destination.append(self.formatter.format_link(part))
+
+ def process_module(self, part: dom.ModulePart) -> None:
+ url = self.link_provider.plugin_link(dom.PluginIdentifier(fqcn=part.fqcn, type='module'))
+ self.destination.append(self.formatter.format_module(part, url))
+
+ def process_rst_ref(self, part: dom.RSTRefPart) -> None:
+ self.destination.append(self.formatter.format_rst_ref(part))
+
+ def process_url(self, part: dom.URLPart) -> None:
+ self.destination.append(self.formatter.format_url(part))
+
+ def process_text(self, part: dom.TextPart) -> None:
+ self.destination.append(self.formatter.format_text(part))
+
+ def process_env_variable(self, part: dom.EnvVariablePart) -> None:
+ self.destination.append(self.formatter.format_env_variable(part))
+
+ def process_option_name(self, part: dom.OptionNamePart) -> None:
+ url = None
+ if part.plugin:
+ url = self.link_provider.plugin_option_like_link(
+ part.plugin, 'option', part.link, part.plugin == self.current_plugin)
+ self.destination.append(self.formatter.format_option_name(part, url))
+
+ def process_option_value(self, part: dom.OptionValuePart) -> None:
+ self.destination.append(self.formatter.format_option_value(part))
+
+ def process_plugin(self, part: dom.PluginPart) -> None:
+ url = self.link_provider.plugin_link(part.plugin)
+ self.destination.append(self.formatter.format_plugin(part, url))
+
+ def process_return_value(self, part: dom.ReturnValuePart) -> None:
+ url = None
+ if part.plugin:
+ url = self.link_provider.plugin_option_like_link(
+ part.plugin, 'retval', part.link, part.plugin == self.current_plugin)
+ self.destination.append(self.formatter.format_return_value(part, url))
+
+
+def format_paragraphs(paragraphs: t.Sequence[dom.Paragraph],
+ formatter: Formatter,
+ link_provider: t.Optional[LinkProvider] = None,
+ par_start: str = '',
+ par_end: str = '',
+ par_sep: str = '',
+ current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
+ if link_provider is None:
+ link_provider = _DefaultLinkProvider()
+ result: t.List[str] = []
+ walker = _FormatWalker(result, formatter, link_provider, current_plugin)
+ for paragraph in paragraphs:
+ if result:
+ result.append(par_sep)
+ result.append(par_start)
+ dom.walk(paragraph, walker)
+ result.append(par_end)
+ return ''.join(result)
From 4b5fc5aa031e631fbe4dbc85629ad25562bf3d95 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Sat, 18 Mar 2023 14:32:09 +0100
Subject: [PATCH 08/15] Continue.
---
src/antsibull_docs/markup/dom.py | 45 ++--
src/antsibull_docs/markup/format.py | 19 ++
src/antsibull_docs/markup/html.py | 141 +++++++++++
src/antsibull_docs/markup/md.py | 113 +++++++++
src/antsibull_docs/markup/parser.py | 43 ++--
src/antsibull_docs/markup/rst.py | 130 +++++++++++
tests/units/markup/test_dom.py | 121 ++++++++++
tests/units/markup/test_format.py | 65 ++++++
tests/units/markup/test_html.py | 19 ++
tests/units/markup/test_md.py | 18 ++
tests/units/markup/test_parser.py | 348 +++++++++++++++++++++++++++-
tests/units/markup/test_rst.py | 19 ++
12 files changed, 1041 insertions(+), 40 deletions(-)
create mode 100644 src/antsibull_docs/markup/html.py
create mode 100644 src/antsibull_docs/markup/md.py
create mode 100644 src/antsibull_docs/markup/rst.py
create mode 100644 tests/units/markup/test_dom.py
create mode 100644 tests/units/markup/test_format.py
create mode 100644 tests/units/markup/test_html.py
create mode 100644 tests/units/markup/test_md.py
create mode 100644 tests/units/markup/test_rst.py
diff --git a/src/antsibull_docs/markup/dom.py b/src/antsibull_docs/markup/dom.py
index 69d13788..7beb05f6 100644
--- a/src/antsibull_docs/markup/dom.py
+++ b/src/antsibull_docs/markup/dom.py
@@ -47,85 +47,85 @@ class PartType(Enum):
class TextPart(NamedTuple):
- type: 't.Literal[PartType.TEXT]'
text: str
+ type: 't.Literal[PartType.TEXT]' = PartType.TEXT
class ItalicPart(NamedTuple):
- type: 't.Literal[PartType.ITALIC]'
text: str
+ type: 't.Literal[PartType.ITALIC]' = PartType.ITALIC
class BoldPart(NamedTuple):
- type: 't.Literal[PartType.BOLD]'
text: str
+ type: 't.Literal[PartType.BOLD]' = PartType.BOLD
class ModulePart(NamedTuple):
- type: 't.Literal[PartType.MODULE]'
fqcn: str
+ type: 't.Literal[PartType.MODULE]' = PartType.MODULE
class PluginPart(NamedTuple):
- type: 't.Literal[PartType.PLUGIN]'
plugin: PluginIdentifier
+ type: 't.Literal[PartType.PLUGIN]' = PartType.PLUGIN
class URLPart(NamedTuple):
- type: 't.Literal[PartType.URL]'
url: str
+ type: 't.Literal[PartType.URL]' = PartType.URL
class LinkPart(NamedTuple):
- type: 't.Literal[PartType.LINK]'
text: str
url: str
+ type: 't.Literal[PartType.LINK]' = PartType.LINK
class RSTRefPart(NamedTuple):
- type: 't.Literal[PartType.RST_REF]'
text: str
ref: str
+ type: 't.Literal[PartType.RST_REF]' = PartType.RST_REF
class CodePart(NamedTuple):
- type: 't.Literal[PartType.CODE]'
text: str
+ type: 't.Literal[PartType.CODE]' = PartType.CODE
class OptionNamePart(NamedTuple):
- type: 't.Literal[PartType.OPTION_NAME]'
plugin: t.Optional[PluginIdentifier]
link: t.List[str]
name: str
value: t.Optional[str]
+ type: 't.Literal[PartType.OPTION_NAME]' = PartType.OPTION_NAME
class OptionValuePart(NamedTuple):
- type: 't.Literal[PartType.OPTION_VALUE]'
value: str
+ type: 't.Literal[PartType.OPTION_VALUE]' = PartType.OPTION_VALUE
class EnvVariablePart(NamedTuple):
- type: 't.Literal[PartType.ENV_VARIABLE]'
name: str
+ type: 't.Literal[PartType.ENV_VARIABLE]' = PartType.ENV_VARIABLE
class ReturnValuePart(NamedTuple):
- type: 't.Literal[PartType.RETURN_VALUE]'
plugin: t.Optional[PluginIdentifier]
link: t.List[str]
name: str
value: t.Optional[str]
+ type: 't.Literal[PartType.RETURN_VALUE]' = PartType.RETURN_VALUE
class HorizontalLinePart(NamedTuple):
- type: 't.Literal[PartType.HORIZONTAL_LINE]'
+ type: 't.Literal[PartType.HORIZONTAL_LINE]' = PartType.HORIZONTAL_LINE
class ErrorPart(NamedTuple):
- type: 't.Literal[PartType.ERROR]'
message: str
+ type: 't.Literal[PartType.ERROR]' = PartType.ERROR
AnyPart = t.Union[
@@ -151,6 +151,10 @@ class ErrorPart(NamedTuple):
class Walker(abc.ABC):
+ '''
+ Abstract base class for walker whose methods will be called for parts of a paragraph.
+ '''
+
@abc.abstractmethod
def process_error(self, part: ErrorPart) -> None:
pass
@@ -213,6 +217,11 @@ def process_return_value(self, part: ReturnValuePart) -> None:
class NoopWalker(Walker):
+ '''
+ Concrete base class for walker whose methods will be called for parts of a paragraph.
+ The default implementation for every part will not do anything.
+ '''
+
def process_error(self, part: ErrorPart) -> None:
pass
@@ -259,7 +268,11 @@ def process_return_value(self, part: ReturnValuePart) -> None:
pass
-def walk(paragraph: Paragraph, walker: Walker) -> None: # noqa: C901, pylint:disable=too-many-branches
+# pylint:disable-next=too-many-branches
+def walk(paragraph: Paragraph, walker: Walker) -> None: # noqa: C901
+ '''
+ Call the corresponding methods of a walker object for every part of the paragraph.
+ '''
for part in paragraph:
if part.type == PartType.ERROR:
walker.process_error(t.cast(ErrorPart, part))
diff --git a/src/antsibull_docs/markup/format.py b/src/antsibull_docs/markup/format.py
index f276ab6c..19483a52 100644
--- a/src/antsibull_docs/markup/format.py
+++ b/src/antsibull_docs/markup/format.py
@@ -15,6 +15,10 @@
class LinkProvider(abc.ABC):
+ '''
+ Provide URLs for objects, if available.
+ '''
+
def plugin_link(self, # pylint:disable=no-self-use
plugin: dom.PluginIdentifier, # pylint:disable=unused-argument
) -> t.Optional[str]:
@@ -36,6 +40,10 @@ class _DefaultLinkProvider(LinkProvider):
class Formatter(abc.ABC):
+ '''
+ Abstract base class for a formatter whose functions will be called for parts of a paragraph.
+ '''
+
@abc.abstractmethod
def format_error(self, part: dom.ErrorPart) -> str:
pass
@@ -98,6 +106,10 @@ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -
class _FormatWalker(dom.Walker):
+ '''
+ Walker which calls a formatter's functions and stores the result in a list.
+ '''
+
destination: t.List[str]
formatter: Formatter
link_provider: LinkProvider
@@ -175,6 +187,13 @@ def format_paragraphs(paragraphs: t.Sequence[dom.Paragraph],
par_end: str = '',
par_sep: str = '',
current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
+ '''
+ Apply the formatter to all parts of the given paragraphs, concatenate the results,
+ and insert start and end sequences for paragraphs and sequences between paragraphs.
+
+ ``link_provider`` and ``current_plugin`` will be used to compute optional URLs
+ that will be passed to the formatter.
+ '''
if link_provider is None:
link_provider = _DefaultLinkProvider()
result: t.List[str] = []
diff --git a/src/antsibull_docs/markup/html.py b/src/antsibull_docs/markup/html.py
new file mode 100644
index 00000000..bbf07976
--- /dev/null
+++ b/src/antsibull_docs/markup/html.py
@@ -0,0 +1,141 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+"""
+HTML serialization.
+"""
+
+import typing as t
+
+from html import escape as _html_escape
+from urllib.parse import quote
+
+from . import dom
+from .format import Formatter, LinkProvider, format_paragraphs as _format_paragraphs
+
+
+def html_escape(text: str) -> str:
+ return _html_escape(text).replace('"', '"')
+
+
+def _url_escape(url: str) -> str:
+ # We include '<>[]{}' in safe to allow urls such as 'https://:[PORT]/v{version}/' to
+ # remain unmangled by percent encoding
+ return quote(url, safe=':/#?%<>[]{}')
+
+
+class HTMLFormatter(Formatter):
+ @staticmethod
+ def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
+ url: t.Optional[str]) -> str:
+ link_start = ''
+ link_end = ''
+ if url:
+ link_start = (
+ f''
+ ''
+ )
+ link_end = ''
+ strong_start = ''
+ strong_end = ''
+ if part.type == dom.PartType.OPTION_NAME:
+ if part.value is None:
+ cls = 'ansible-option'
+ strong_start = ''
+ strong_end = ''
+ else:
+ cls = 'ansible-option-value'
+ else:
+ cls = 'ansible-return-value'
+ if part.value is None:
+ text = part.name
+ else:
+ text = f'{part.name}={part.value}'
+ return (
+ f'{strong_start}{link_start}'
+ f'{html_escape(text)}{link_end}{strong_end}
'
+ )
+
+ def format_error(self, part: dom.ErrorPart) -> str:
+ return f'ERROR while parsing: {html_escape(part.message)}'
+
+ def format_bold(self, part: dom.BoldPart) -> str:
+ return f'{html_escape(part.text)}'
+
+ def format_code(self, part: dom.CodePart) -> str:
+ return f"{html_escape(part.text)}
"
+
+ def format_horizontal_line(self, part: dom.HorizontalLinePart) -> str:
+ return '
'
+
+ def format_italic(self, part: dom.ItalicPart) -> str:
+ return f'{html_escape(part.text)}'
+
+ def format_link(self, part: dom.LinkPart) -> str:
+ return f"{html_escape(part.text)}"
+
+ def format_module(self, part: dom.ModulePart, url: t.Optional[str]) -> str:
+ if not url:
+ return f"{html_escape(part.fqcn)}"
+ return (
+ f""
+ f'{html_escape(part.fqcn)}'
+ )
+
+ def format_rst_ref(self, part: dom.RSTRefPart) -> str:
+ return f"{html_escape(part.text)}"
+
+ def format_url(self, part: dom.URLPart) -> str:
+ return (
+ f""
+ f'{html_escape(_url_escape(part.url))}'
+ )
+
+ def format_text(self, part: dom.TextPart) -> str:
+ return html_escape(part.text)
+
+ def format_env_variable(self, part: dom.EnvVariablePart) -> str:
+ return (
+ ''
+ f'{html_escape(part.name)}
'
+ )
+
+ def format_option_name(self, part: dom.OptionNamePart, url: t.Optional[str]) -> str:
+ return self._format_option_like(part, url)
+
+ def format_option_value(self, part: dom.OptionValuePart) -> str:
+ return f'{html_escape(part.value)}
'
+
+ def format_plugin(self, part: dom.PluginPart, url: t.Optional[str]) -> str:
+ if not url:
+ return f"{html_escape(part.plugin.fqcn)}"
+ return (
+ f""
+ f'{html_escape(part.plugin.fqcn)}'
+ )
+
+ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -> str:
+ return self._format_option_like(part, url)
+
+
+DEFAULT_FORMATTER = HTMLFormatter()
+
+
+def to_html(paragraphs: t.Sequence[dom.Paragraph],
+ formatter: Formatter = DEFAULT_FORMATTER,
+ link_provider: t.Optional[LinkProvider] = None,
+ par_start: str = '',
+ par_end: str = '
',
+ par_sep: str = '',
+ current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
+ return _format_paragraphs(
+ paragraphs,
+ formatter=formatter,
+ link_provider=link_provider,
+ par_start=par_start,
+ par_end=par_end,
+ par_sep=par_sep,
+ current_plugin=current_plugin,
+ )
diff --git a/src/antsibull_docs/markup/md.py b/src/antsibull_docs/markup/md.py
new file mode 100644
index 00000000..6aa5f49c
--- /dev/null
+++ b/src/antsibull_docs/markup/md.py
@@ -0,0 +1,113 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+"""
+MarkDown serialization.
+"""
+
+import re
+import typing as t
+
+from . import dom
+from .format import Formatter, LinkProvider, format_paragraphs as _format_paragraphs
+from .html import _url_escape, html_escape as _html_escape
+
+
+_MD_ESCAPE = re.compile(r'''([!"#$%&'()*+,:;<=>?@[\\\]^_`{|}~-])''')
+
+
+def md_escape(text: str) -> str:
+ return _MD_ESCAPE.sub(r'\\\1', text)
+
+
+class MDFormatter(Formatter):
+ @staticmethod
+ def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
+ url: t.Optional[str]) -> str:
+ link_start = ''
+ link_end = ''
+ if url:
+ link_start = f''
+ link_end = ''
+ strong_start = ''
+ strong_end = ''
+ if part.type == dom.PartType.OPTION_NAME and part.value is None:
+ strong_start = ''
+ strong_end = ''
+ if part.value is None:
+ text = part.name
+ else:
+ text = f'{part.name}={part.value}'
+ return f'{strong_start}{link_start}{_html_escape(text)}{link_end}{strong_end}
'
+
+ def format_error(self, part: dom.ErrorPart) -> str:
+ return f'ERROR while parsing: {md_escape(part.message)}'
+
+ def format_bold(self, part: dom.BoldPart) -> str:
+ return f'{md_escape(part.text)}'
+
+ def format_code(self, part: dom.CodePart) -> str:
+ return f'{md_escape(part.text)}
'
+
+ def format_horizontal_line(self, part: dom.HorizontalLinePart) -> str:
+ return '
'
+
+ def format_italic(self, part: dom.ItalicPart) -> str:
+ return f'{md_escape(part.text)}'
+
+ def format_link(self, part: dom.LinkPart) -> str:
+ return f'[{md_escape(part.text)}]({md_escape(_url_escape(part.url))})'
+
+ def format_module(self, part: dom.ModulePart, url: t.Optional[str]) -> str:
+ if url:
+ return f'[{md_escape(part.fqcn)}]({md_escape(_url_escape(url))})'
+ return md_escape(part.fqcn)
+
+ def format_rst_ref(self, part: dom.RSTRefPart) -> str:
+ return md_escape(part.text)
+
+ def format_url(self, part: dom.URLPart) -> str:
+ return f'[{md_escape(_url_escape(part.url))}]({md_escape(_url_escape(part.url))})'
+
+ def format_text(self, part: dom.TextPart) -> str:
+ return md_escape(part.text)
+
+ def format_env_variable(self, part: dom.EnvVariablePart) -> str:
+ return f'{md_escape(part.name)}
'
+
+ def format_option_name(self, part: dom.OptionNamePart, url: t.Optional[str]) -> str:
+ return self._format_option_like(part, url)
+
+ def format_option_value(self, part: dom.OptionValuePart) -> str:
+ return f'{md_escape(part.value)}
'
+
+ def format_plugin(self, part: dom.PluginPart, url: t.Optional[str]) -> str:
+ if url:
+ return f'[{md_escape(part.plugin.fqcn)}]({md_escape(_url_escape(url))})'
+ return md_escape(part.plugin.fqcn)
+
+ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -> str:
+ return self._format_option_like(part, url)
+
+
+DEFAULT_FORMATTER = MDFormatter()
+
+
+def to_md(paragraphs: t.Sequence[dom.Paragraph],
+ formatter: Formatter = DEFAULT_FORMATTER,
+ link_provider: t.Optional[LinkProvider] = None,
+ par_start: str = '',
+ par_end: str = '',
+ par_sep: str = '\n\n',
+ current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
+ return _format_paragraphs(
+ paragraphs,
+ formatter=formatter,
+ link_provider=link_provider,
+ par_start=par_start,
+ par_end=par_end,
+ par_sep=par_sep,
+ current_plugin=current_plugin,
+ )
diff --git a/src/antsibull_docs/markup/parser.py b/src/antsibull_docs/markup/parser.py
index fc48005c..e8ef7355 100644
--- a/src/antsibull_docs/markup/parser.py
+++ b/src/antsibull_docs/markup/parser.py
@@ -139,7 +139,7 @@ def convert_text(text: str, commands: CommandSet, process_other_text: t.Callable
_IGNORE_MARKER = 'ignore:'
_ARRAY_STUB_RE = re.compile(r'\[([^\]]*)\]')
-_FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([a-z]+):(.*)$')
+_FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([^:]+):(.*)$')
_FQCN = re.compile(r'^[a-z0-9_]+\.[a-z0-9_]+(?:\.[a-z0-9_]+)+$')
_PLUGIN_TYPE = re.compile(r'^[a-z_]+$')
@@ -156,7 +156,7 @@ def _is_plugin_type(text: str) -> bool:
class Context(t.NamedTuple):
- current_plugin: t.Optional[dom.PluginIdentifier]
+ current_plugin: t.Optional[dom.PluginIdentifier] = None
class CommandParser(abc.ABC):
@@ -191,7 +191,7 @@ def __init__(self):
super().__init__('I', 1, old_markup=True)
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
- return dom.ItalicPart(dom.PartType.ITALIC, text=parameters[0])
+ return dom.ItalicPart(text=parameters[0])
class _Bold(CommandParserEx):
@@ -199,7 +199,7 @@ def __init__(self):
super().__init__('B', 1, old_markup=True)
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
- return dom.BoldPart(dom.PartType.BOLD, text=parameters[0])
+ return dom.BoldPart(text=parameters[0])
class _Module(CommandParserEx):
@@ -210,7 +210,7 @@ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
fqcn = parameters[0]
if not _is_fqcn(fqcn):
raise ValueError(f'Module name "{fqcn}" is not a FQCN')
- return dom.ModulePart(dom.PartType.MODULE, fqcn=fqcn)
+ return dom.ModulePart(fqcn=fqcn)
class _URL(CommandParserEx):
@@ -218,7 +218,7 @@ def __init__(self):
super().__init__('U', 1, old_markup=True)
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
- return dom.URLPart(dom.PartType.URL, url=parameters[0])
+ return dom.URLPart(url=parameters[0])
class _Link(CommandParserEx):
@@ -228,7 +228,7 @@ def __init__(self):
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
text = parameters[0]
url = parameters[1]
- return dom.LinkPart(dom.PartType.LINK, text=text, url=url)
+ return dom.LinkPart(text=text, url=url)
class _RSTRef(CommandParserEx):
@@ -238,7 +238,7 @@ def __init__(self):
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
text = parameters[0]
ref = parameters[1]
- return dom.RSTRefPart(dom.PartType.RST_REF, text=text, ref=ref)
+ return dom.RSTRefPart(text=text, ref=ref)
class _Code(CommandParserEx):
@@ -246,7 +246,7 @@ def __init__(self):
super().__init__('C', 1, old_markup=True)
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
- return dom.CodePart(dom.PartType.CODE, text=parameters[0])
+ return dom.CodePart(text=parameters[0])
class _HorizontalLine(CommandParserEx):
@@ -254,7 +254,7 @@ def __init__(self):
super().__init__('HORIZONTALLINE', 0, old_markup=True)
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
- return dom.HorizontalLinePart(dom.PartType.HORIZONTAL_LINE)
+ return dom.HorizontalLinePart()
# Semantic Ansible docs markup:
@@ -304,9 +304,8 @@ def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
if not _is_fqcn(fqcn):
raise ValueError(f'Plugin name "{fqcn}" is not a FQCN')
if not _is_plugin_type(ptype):
- raise ValueError(f'Plugin type "{type}" is not valid')
- return dom.PluginPart(
- dom.PartType.PLUGIN, plugin=dom.PluginIdentifier(fqcn=fqcn, type=ptype))
+ raise ValueError(f'Plugin type "{ptype}" is not valid')
+ return dom.PluginPart(plugin=dom.PluginIdentifier(fqcn=fqcn, type=ptype))
class _EnvVar(CommandParserEx):
@@ -314,7 +313,7 @@ def __init__(self):
super().__init__('E', 1, escaped_arguments=True)
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
- return dom.EnvVariablePart(dom.PartType.ENV_VARIABLE, name=parameters[0])
+ return dom.EnvVariablePart(name=parameters[0])
class _OptionValue(CommandParserEx):
@@ -322,7 +321,7 @@ def __init__(self):
super().__init__('V', 1, escaped_arguments=True)
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
- return dom.OptionValuePart(dom.PartType.OPTION_VALUE, value=parameters[0])
+ return dom.OptionValuePart(value=parameters[0])
class _OptionName(CommandParserEx):
@@ -331,8 +330,7 @@ def __init__(self):
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
plugin, link, name, value = _parse_option_like(parameters[0], context)
- return dom.OptionNamePart(
- dom.PartType.OPTION_NAME, plugin=plugin, link=link, name=name, value=value)
+ return dom.OptionNamePart(plugin=plugin, link=link, name=name, value=value)
class _ReturnValue(CommandParserEx):
@@ -341,8 +339,7 @@ def __init__(self):
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
plugin, link, name, value = _parse_option_like(parameters[0], context)
- return dom.ReturnValuePart(
- dom.PartType.RETURN_VALUE, plugin=plugin, link=link, name=name, value=value)
+ return dom.ReturnValuePart(plugin=plugin, link=link, name=name, value=value)
_COMMANDS = [
@@ -401,7 +398,7 @@ def _parse_command(result: dom.Paragraph, text: str, cmd: CommandParser, index:
f' at index {index + 1}{where}: {error}'
)
if errors == 'message':
- result.append(dom.ErrorPart(dom.PartType.ERROR, message=error))
+ result.append(dom.ErrorPart(message=error))
elif errors == 'exception':
raise ValueError(error)
return end_index
@@ -414,10 +411,10 @@ def parse_string(self, text: str, context: Context,
while index < length:
m = self._re.search(text, index)
if m is None:
- result.append(dom.TextPart(dom.PartType.TEXT, text=text[index:]))
+ result.append(dom.TextPart(text=text[index:]))
break
if m.start(1) > index:
- result.append(dom.TextPart(dom.PartType.TEXT, text=text[index:m.start(1)]))
+ result.append(dom.TextPart(text=text[index:m.start(1)]))
index = self._parse_command(
result,
text,
@@ -443,7 +440,7 @@ def parse(text: t.Union[str, t.Sequence[str]],
has_paragraphs = True
if isinstance(text, str):
has_paragraphs = False
- text = [text]
+ text = [text] if text else []
parser = _CLASSIC if only_classic_markup else _SEMANTIC_MARKUP
return [
parser.parse_string(
diff --git a/src/antsibull_docs/markup/rst.py b/src/antsibull_docs/markup/rst.py
new file mode 100644
index 00000000..3df02248
--- /dev/null
+++ b/src/antsibull_docs/markup/rst.py
@@ -0,0 +1,130 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+"""
+ReStructured Text serialization.
+"""
+
+import typing as t
+
+from . import dom
+from .format import Formatter, LinkProvider, format_paragraphs as _format_paragraphs
+from .html import _url_escape
+
+
+def rst_escape(value: t.Any, escape_ending_whitespace=False) -> str:
+ ''' make sure value is converted to a string, and RST special characters are escaped '''
+
+ if not isinstance(value, str):
+ value = str(value)
+
+ value = value.replace('\\', '\\\\')
+ value = value.replace('<', '\\<')
+ value = value.replace('>', '\\>')
+ value = value.replace('_', '\\_')
+ value = value.replace('*', '\\*')
+ value = value.replace('`', '\\`')
+
+ if escape_ending_whitespace and value.endswith(' '):
+ value = value + '\\ '
+ if escape_ending_whitespace and value.startswith(' '):
+ value = '\\ ' + value
+
+ return value
+
+
+def rst_code(value: str) -> str:
+ ''' Write value as :code:`...` RST construct. '''
+ if not isinstance(value, str):
+ value = str(value)
+ return f':code:`{rst_escape(value, escape_ending_whitespace=True)}`'
+
+
+class RSTFormatter(Formatter):
+ @staticmethod
+ def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
+ role: str) -> str:
+ result: t.List[str] = []
+ plugin = part.plugin
+ if plugin:
+ result.append(plugin.fqcn)
+ result.append('#')
+ result.append(plugin.type)
+ result.append(':')
+ result.append(part.name)
+ value = part.value
+ if value is not None:
+ result.append('=')
+ result.append(value)
+ return f'\\ :{role}:`{rst_escape("".join(result), True)}`\\ '
+
+ def format_error(self, part: dom.ErrorPart) -> str:
+ return f'\\ :strong:`ERROR while parsing`\\ : {rst_escape(part.message, True)}\\ '
+
+ def format_bold(self, part: dom.BoldPart) -> str:
+ return f'\\ :strong:`{rst_escape(part.text, True)}`\\ '
+
+ def format_code(self, part: dom.CodePart) -> str:
+ return f'\\ :literal:`{rst_escape(part.text, True)}`\\ '
+
+ def format_horizontal_line(self, part: dom.HorizontalLinePart) -> str:
+ return '\n\n.. raw:: html\n\n
\n\n'
+
+ def format_italic(self, part: dom.ItalicPart) -> str:
+ return f'\\ :emphasis:`{rst_escape(part.text, True)}`\\ '
+
+ def format_link(self, part: dom.LinkPart) -> str:
+ return f'\\ `{rst_escape(part.text)} <{_url_escape(part.url)}>`__\\ '
+
+ def format_module(self, part: dom.ModulePart, url: t.Optional[str]) -> str:
+ return f'\\ :ref:`{rst_escape(part.fqcn)} `\\ '
+
+ def format_rst_ref(self, part: dom.RSTRefPart) -> str:
+ return f'\\ :ref:`{rst_escape(part.text)} <{part.ref}>`\\ '
+
+ def format_url(self, part: dom.URLPart) -> str:
+ return f'\\ {_url_escape(part.url)}\\ '
+
+ def format_text(self, part: dom.TextPart) -> str:
+ return rst_escape(part.text)
+
+ def format_env_variable(self, part: dom.EnvVariablePart) -> str:
+ return f'\\ :envvar:`{rst_escape(part.name, True)}`\\ '
+
+ def format_option_name(self, part: dom.OptionNamePart, url: t.Optional[str]) -> str:
+ return self._format_option_like(part, 'ansopt')
+
+ def format_option_value(self, part: dom.OptionValuePart) -> str:
+ return f'\\ :ansval:`{rst_escape(part.value, True)}`\\ '
+
+ def format_plugin(self, part: dom.PluginPart, url: t.Optional[str]) -> str:
+ return (
+ f'\\ :ref:`{rst_escape(part.plugin.fqcn)} '
+ f'`\\ '
+ )
+
+ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -> str:
+ return self._format_option_like(part, 'ansretval')
+
+
+DEFAULT_FORMATTER = RSTFormatter()
+
+
+def to_rst(paragraphs: t.Sequence[dom.Paragraph],
+ formatter: Formatter = DEFAULT_FORMATTER,
+ link_provider: t.Optional[LinkProvider] = None,
+ par_start: str = '',
+ par_end: str = '',
+ par_sep: str = '\n\n',
+ current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
+ return _format_paragraphs(
+ paragraphs,
+ formatter=formatter,
+ link_provider=link_provider,
+ par_start=par_start,
+ par_end=par_end,
+ par_sep=par_sep,
+ current_plugin=current_plugin,
+ )
diff --git a/tests/units/markup/test_dom.py b/tests/units/markup/test_dom.py
new file mode 100644
index 00000000..ec2258b5
--- /dev/null
+++ b/tests/units/markup/test_dom.py
@@ -0,0 +1,121 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+
+import typing as t
+
+import pytest
+
+from antsibull_docs.markup import dom
+
+
+class _TestWalker(dom.Walker):
+ result: t.List[dom.AnyPart]
+
+ def __init__(self):
+ self.result = []
+
+ def process_error(self, part: dom.ErrorPart) -> None:
+ assert part.type == dom.PartType.ERROR
+ self.result.append(part)
+
+ def process_bold(self, part: dom.BoldPart) -> None:
+ assert part.type == dom.PartType.BOLD
+ self.result.append(part)
+
+ def process_code(self, part: dom.CodePart) -> None:
+ assert part.type == dom.PartType.CODE
+ self.result.append(part)
+
+ def process_horizontal_line(self, part: dom.HorizontalLinePart) -> None:
+ assert part.type == dom.PartType.HORIZONTAL_LINE
+ self.result.append(part)
+
+ def process_italic(self, part: dom.ItalicPart) -> None:
+ assert part.type == dom.PartType.ITALIC
+ self.result.append(part)
+
+ def process_link(self, part: dom.LinkPart) -> None:
+ assert part.type == dom.PartType.LINK
+ self.result.append(part)
+
+ def process_module(self, part: dom.ModulePart) -> None:
+ assert part.type == dom.PartType.MODULE
+ self.result.append(part)
+
+ def process_rst_ref(self, part: dom.RSTRefPart) -> None:
+ assert part.type == dom.PartType.RST_REF
+ self.result.append(part)
+
+ def process_url(self, part: dom.URLPart) -> None:
+ assert part.type == dom.PartType.URL
+ self.result.append(part)
+
+ def process_text(self, part: dom.TextPart) -> None:
+ assert part.type == dom.PartType.TEXT
+ self.result.append(part)
+
+ def process_env_variable(self, part: dom.EnvVariablePart) -> None:
+ assert part.type == dom.PartType.ENV_VARIABLE
+ self.result.append(part)
+
+ def process_option_name(self, part: dom.OptionNamePart) -> None:
+ assert part.type == dom.PartType.OPTION_NAME
+ self.result.append(part)
+
+ def process_option_value(self, part: dom.OptionValuePart) -> None:
+ assert part.type == dom.PartType.OPTION_VALUE
+ self.result.append(part)
+
+ def process_plugin(self, part: dom.PluginPart) -> None:
+ assert part.type == dom.PartType.PLUGIN
+ self.result.append(part)
+
+ def process_return_value(self, part: dom.ReturnValuePart) -> None:
+ assert part.type == dom.PartType.RETURN_VALUE
+ self.result.append(part)
+
+
+TEST_WALKER = [
+ [],
+ [dom.ErrorPart(message='foo')],
+ [
+ dom.TextPart(text='foo '),
+ dom.ItalicPart(text='bar'),
+ dom.TextPart(text=' baz '),
+ dom.CodePart(text=' bam '),
+ dom.TextPart(text=' '),
+ dom.BoldPart(text=' ( boo '),
+ dom.TextPart(text=' ) '),
+ dom.URLPart(url='https://example.com/?foo=bar'),
+ dom.HorizontalLinePart(),
+ dom.TextPart(text=' '),
+ dom.LinkPart(text='foo', url='https://bar.com'),
+ dom.TextPart(text=' '),
+ dom.RSTRefPart(text=' a', ref='b '),
+ dom.ModulePart(fqcn='foo.bar.baz'),
+ dom.TextPart(text='HORIZONTALLINEx '),
+ dom.ModulePart(fqcn='foo.bar.baz.bam'),
+ ],
+ [
+ dom.TextPart(text='foo '),
+ dom.EnvVariablePart(name='a),b'),
+ dom.TextPart(text=' '),
+ dom.PluginPart(plugin=dom.PluginIdentifier(fqcn='foo.bar.baz', type='bam')),
+ dom.TextPart(text=' baz '),
+ dom.OptionValuePart(value=' b,na)\\m, '),
+ dom.TextPart(text=' '),
+ dom.OptionNamePart(plugin=None, link=['foo'], name='foo', value=None),
+ dom.TextPart(text=' '),
+ ],
+]
+
+
+@pytest.mark.parametrize('data', TEST_WALKER)
+def test_walk(data: dom.Paragraph) -> None:
+ walker = _TestWalker()
+ dom.walk(data, walker)
+ assert walker.result == data
+
diff --git a/tests/units/markup/test_format.py b/tests/units/markup/test_format.py
new file mode 100644
index 00000000..d2db7e33
--- /dev/null
+++ b/tests/units/markup/test_format.py
@@ -0,0 +1,65 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+
+import typing as t
+
+from antsibull_docs.markup import dom
+from antsibull_docs.markup.format import Formatter, format_paragraphs
+
+
+class _TestFormatter(Formatter):
+ def format_error(self, part: dom.ErrorPart) -> str:
+ return 'format_error'
+
+ def format_bold(self, part: dom.BoldPart) -> str:
+ return 'format_bold'
+
+ def format_code(self, part: dom.CodePart) -> str:
+ return 'format_code'
+
+ def format_horizontal_line(self, part: dom.HorizontalLinePart) -> str:
+ return 'format_horizontal_line'
+
+ def format_italic(self, part: dom.ItalicPart) -> str:
+ return 'format_italic'
+
+ def format_link(self, part: dom.LinkPart) -> str:
+ return 'format_link'
+
+ def format_module(self, part: dom.ModulePart, url: t.Optional[str]) -> str:
+ return 'format_module'
+
+ def format_rst_ref(self, part: dom.RSTRefPart) -> str:
+ return 'format_rst_ref'
+
+ def format_url(self, part: dom.URLPart) -> str:
+ return 'format_url'
+
+ def format_text(self, part: dom.TextPart) -> str:
+ return 'format_text'
+
+ def format_env_variable(self, part: dom.EnvVariablePart) -> str:
+ return 'format_env_variable'
+
+ def format_option_name(self, part: dom.OptionNamePart, url: t.Optional[str]) -> str:
+ return 'format_option_name'
+
+ def format_option_value(self, part: dom.OptionValuePart) -> str:
+ return 'format_option_value'
+
+ def format_plugin(self, part: dom.PluginPart, url: t.Optional[str]) -> str:
+ return 'format_plugin'
+
+ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -> str:
+ return 'format_return_value'
+
+
+def test_format_paragraphs():
+ assert format_paragraphs([], formatter=_TestFormatter()) == ''
+ assert format_paragraphs(
+ [[dom.HorizontalLinePart(), dom.TextPart(text='foo')]],
+ formatter=_TestFormatter()
+ ) == 'format_horizontal_lineformat_text'
diff --git a/tests/units/markup/test_html.py b/tests/units/markup/test_html.py
new file mode 100644
index 00000000..5f9a427b
--- /dev/null
+++ b/tests/units/markup/test_html.py
@@ -0,0 +1,19 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+
+from antsibull_docs.markup import dom
+from antsibull_docs.markup.html import html_escape, to_html
+
+
+def test_html_escape():
+ assert html_escape('') == ''
+ assert html_escape(' foo ') == ' foo '
+ assert html_escape('<&>') == '<a href="a&b"><&></a>'
+
+def test_to_html():
+ assert to_html([]) == ''
+ assert to_html([[dom.TextPart(text='test')]]) == 'test
'
+ assert to_html([[dom.TextPart(text='test')]], par_start='', par_end='
') == 'test
'
diff --git a/tests/units/markup/test_md.py b/tests/units/markup/test_md.py
new file mode 100644
index 00000000..e023cbc3
--- /dev/null
+++ b/tests/units/markup/test_md.py
@@ -0,0 +1,18 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+
+from antsibull_docs.markup import dom
+from antsibull_docs.markup.md import md_escape, to_md
+
+
+def test_md_escape():
+ assert md_escape('') == ''
+ assert md_escape(' foo ') == ' foo '
+ assert md_escape(r'[]!.()-\@<>?[]!.()-\@<>?') == r'\[\]\!.\(\)\-\\\@\<\>\?\[\]\!.\(\)\-\\\@\<\>\?'
+
+def test_to_rst():
+ assert to_md([]) == ''
+ assert to_md([[dom.TextPart(text='test')]]) == 'test'
diff --git a/tests/units/markup/test_parser.py b/tests/units/markup/test_parser.py
index 43ba51cc..f1faada3 100644
--- a/tests/units/markup/test_parser.py
+++ b/tests/units/markup/test_parser.py
@@ -17,7 +17,7 @@ class TestCommand0(Command):
def handle(self, parameters: t.List[str], context: t.Any) -> str:
assert len(parameters) == 0
- return f'EMPTY'
+ return 'EMPTY'
class TestCommand1A(Command):
@@ -135,3 +135,349 @@ def test_convert_text_fail(text, expected_exc):
with pytest.raises(ParsingException) as exc:
convert_text(text, commands, _escape_regular, None)
assert str(exc.value) == expected_exc
+
+
+###############################################################################################
+
+# import pytest
+
+from antsibull_docs.markup import dom
+from antsibull_docs.markup.parser import parse, CommandParser, Context, Parser
+
+
+TEST_PARSE_DATA: t.List[t.Tuple[t.Union[str, t.List[str]], Context, t.Dict[str, t.Any], t.List[dom.Paragraph]]] = [
+ ('', Context(), {}, []),
+ ([''], Context(), {}, [[]]),
+ ('test', Context(), {}, [[dom.TextPart(text='test')]]),
+ ('test', Context(), {}, [[dom.TextPart(text='test')]]),
+ # classic markup:
+ (
+ 'foo I(bar) baz C( bam ) B( ( boo ) ) U(https://example.com/?foo=bar)HORIZONTALLINE L(foo , https://bar.com) R( a , b )M(foo.bar.baz)HORIZONTALLINEx M(foo.bar.baz.bam)',
+ Context(),
+ {},
+ [
+ [
+ dom.TextPart(text='foo '),
+ dom.ItalicPart(text='bar'),
+ dom.TextPart(text=' baz '),
+ dom.CodePart(text=' bam '),
+ dom.TextPart(text=' '),
+ dom.BoldPart(text=' ( boo '),
+ dom.TextPart(text=' ) '),
+ dom.URLPart(url='https://example.com/?foo=bar'),
+ dom.HorizontalLinePart(),
+ dom.TextPart(text=' '),
+ dom.LinkPart(text='foo', url='https://bar.com'),
+ dom.TextPart(text=' '),
+ dom.RSTRefPart(text=' a', ref='b '),
+ dom.ModulePart(fqcn='foo.bar.baz'),
+ dom.TextPart(text='HORIZONTALLINEx '),
+ dom.ModulePart(fqcn='foo.bar.baz.bam'),
+ ],
+ ],
+ ),
+ (
+ 'foo I(bar) baz C( bam ) B( ( boo ) ) U(https://example.com/?foo=bar)HORIZONTALLINE L(foo , https://bar.com) R( a , b )M(foo.bar.baz)HORIZONTALLINEx M(foo.bar.baz.bam)',
+ Context(),
+ dict(only_classic_markup=True),
+ [
+ [
+ dom.TextPart(text='foo '),
+ dom.ItalicPart(text='bar'),
+ dom.TextPart(text=' baz '),
+ dom.CodePart(text=' bam '),
+ dom.TextPart(text=' '),
+ dom.BoldPart(text=' ( boo '),
+ dom.TextPart(text=' ) '),
+ dom.URLPart(url='https://example.com/?foo=bar'),
+ dom.HorizontalLinePart(),
+ dom.TextPart(text=' '),
+ dom.LinkPart(text='foo', url='https://bar.com'),
+ dom.TextPart(text=' '),
+ dom.RSTRefPart(text=' a', ref='b '),
+ dom.ModulePart(fqcn='foo.bar.baz'),
+ dom.TextPart(text='HORIZONTALLINEx '),
+ dom.ModulePart(fqcn='foo.bar.baz.bam'),
+ ],
+ ],
+ ),
+ # semantic markup:
+ (
+ 'foo E(a\\),b) P(foo.bar.baz#bam) baz V( b\\,\\na\\)\\\\m\\, ) O(foo) ',
+ Context(),
+ {},
+ [
+ [
+ dom.TextPart(text='foo '),
+ dom.EnvVariablePart(name='a),b'),
+ dom.TextPart(text=' '),
+ dom.PluginPart(plugin=dom.PluginIdentifier(fqcn='foo.bar.baz', type='bam')),
+ dom.TextPart(text=' baz '),
+ dom.OptionValuePart(value=' b,na)\\m, '),
+ dom.TextPart(text=' '),
+ dom.OptionNamePart(plugin=None, link=['foo'], name='foo', value=None),
+ dom.TextPart(text=' '),
+ ],
+ ],
+ ),
+ # semantic markup option name:
+ ('O(foo)', Context(), {}, [
+ [
+ dom.OptionNamePart(plugin=None, link=['foo'], name='foo', value=None),
+ ],
+ ]),
+ ('O(ignore:foo)', Context(current_plugin=dom.PluginIdentifier('foo.bar.baz', type='bam')), {}, [
+ [
+ dom.OptionNamePart(plugin=None, link=['foo'], name='foo', value=None),
+ ],
+ ]),
+ ('O(foo)', Context(current_plugin=dom.PluginIdentifier('foo.bar.baz', type='bam')), {}, [
+ [
+ dom.OptionNamePart(plugin=dom.PluginIdentifier('foo.bar.baz', type='bam'), link=['foo'], name='foo', value=None),
+ ],
+ ]),
+ ('O(foo.bar.baz#bam:foo)', Context(), {}, [
+ [
+ dom.OptionNamePart(plugin=dom.PluginIdentifier('foo.bar.baz', type='bam'), link=['foo'], name='foo', value=None),
+ ],
+ ]),
+ ('O(foo=bar)', Context(), {}, [
+ [
+ dom.OptionNamePart(plugin=None, link=['foo'], name='foo', value='bar'),
+ ],
+ ]),
+ ('O(foo.baz=bam)', Context(), {}, [
+ [
+ dom.OptionNamePart(plugin=None, link=['foo', 'baz'], name='foo.baz', value='bam'),
+ ],
+ ]),
+ ('O(foo[1].baz[bam.bar.boing].boo)', Context(), {}, [
+ [
+ dom.OptionNamePart(plugin=None, link=['foo', 'baz', 'boo'], name='foo[1].baz[bam.bar.boing].boo', value=None),
+ ],
+ ]),
+ ('O(bar.baz.bam.boo#lookup:foo[1].baz[bam.bar.boing].boo)', Context(), {}, [
+ [
+ dom.OptionNamePart(plugin=dom.PluginIdentifier('bar.baz.bam.boo', type='lookup'), link=['foo', 'baz', 'boo'], name='foo[1].baz[bam.bar.boing].boo', value=None),
+ ],
+ ]),
+ # semantic markup return value name:
+ ('RV(foo)', Context(), {}, [
+ [
+ dom.ReturnValuePart(plugin=None, link=['foo'], name='foo', value=None),
+ ],
+ ]),
+ ('RV(ignore:foo)', Context(current_plugin=dom.PluginIdentifier('foo.bar.baz', type='bam')), {}, [
+ [
+ dom.ReturnValuePart(plugin=None, link=['foo'], name='foo', value=None),
+ ],
+ ]),
+ ('RV(foo)', Context(current_plugin=dom.PluginIdentifier('foo.bar.baz', type='bam')), {}, [
+ [
+ dom.ReturnValuePart(plugin=dom.PluginIdentifier('foo.bar.baz', type='bam'), link=['foo'], name='foo', value=None),
+ ],
+ ]),
+ ('RV(foo.bar.baz#bam:foo)', Context(), {}, [
+ [
+ dom.ReturnValuePart(plugin=dom.PluginIdentifier('foo.bar.baz', type='bam'), link=['foo'], name='foo', value=None),
+ ],
+ ]),
+ ('RV(foo=bar)', Context(), {}, [
+ [
+ dom.ReturnValuePart(plugin=None, link=['foo'], name='foo', value='bar'),
+ ],
+ ]),
+ ('RV(foo.baz=bam)', Context(), {}, [
+ [
+ dom.ReturnValuePart(plugin=None, link=['foo', 'baz'], name='foo.baz', value='bam'),
+ ],
+ ]),
+ ('RV(foo[1].baz[bam.bar.boing].boo)', Context(), {}, [
+ [
+ dom.ReturnValuePart(plugin=None, link=['foo', 'baz', 'boo'], name='foo[1].baz[bam.bar.boing].boo', value=None),
+ ],
+ ]),
+ ('RV(bar.baz.bam.boo#lookup:foo[1].baz[bam.bar.boing].boo)', Context(), {}, [
+ [
+ dom.ReturnValuePart(plugin=dom.PluginIdentifier('bar.baz.bam.boo', type='lookup'), link=['foo', 'baz', 'boo'], name='foo[1].baz[bam.bar.boing].boo', value=None),
+ ],
+ ]),
+ # bad parameter parsing (no escaping, error message):
+ ('M(', Context(), {}, [
+ [dom.ErrorPart(message='While parsing M() at index 1: Cannot find closing ")" after last parameter')],
+ ]),
+ ('M(foo', Context(), dict(errors='message'), [
+ [dom.ErrorPart(message='While parsing M() at index 1: Cannot find closing ")" after last parameter')],
+ ]),
+ ('L(foo)', Context(), dict(errors='message'), [
+ [
+ dom.ErrorPart(message='While parsing L() at index 1: Cannot find comma separating parameter 1 from the next one'),
+ ],
+ ]),
+ ('L(foo,bar', Context(), dict(errors='message'), [
+ [dom.ErrorPart(message='While parsing L() at index 1: Cannot find closing ")" after last parameter')],
+ ]),
+ ('L(foo), bar', Context(), dict(errors='message'), [
+ [dom.ErrorPart(message='While parsing L() at index 1: Cannot find closing ")" after last parameter')],
+ ]),
+ ('P(', Context(), {}, [
+ [dom.ErrorPart(message='While parsing P() at index 1: Cannot find closing ")" after last parameter')],
+ ]),
+ ('P(foo', Context(), dict(errors='message'), [
+ [dom.ErrorPart(message='While parsing P() at index 1: Cannot find closing ")" after last parameter')],
+ ]),
+ # bad module ref (error message):
+ ('M(foo)', Context(), {}, [
+ [dom.ErrorPart(message='While parsing M() at index 1: Module name "foo" is not a FQCN')],
+ ]),
+ (' M(foo.bar)', Context(), dict(errors='message'), [
+ [
+ dom.TextPart(text=' '),
+ dom.ErrorPart(message='While parsing M() at index 2: Module name "foo.bar" is not a FQCN'),
+ ],
+ ]),
+ (' M(foo. bar.baz)', Context(), dict(errors='message'), [
+ [
+ dom.TextPart(text=' '),
+ dom.ErrorPart(message='While parsing M() at index 3: Module name "foo. bar.baz" is not a FQCN'),
+ ],
+ ]),
+ (' M(foo) baz', Context(), dict(errors='message'), [
+ [
+ dom.TextPart(text=' '),
+ dom.ErrorPart(message='While parsing M() at index 4: Module name "foo" is not a FQCN'),
+ dom.TextPart(text=' baz'),
+ ],
+ ]),
+ # bad plugin ref (error message):
+ ('P(foo)', Context(), {}, [
+ [
+ dom.ErrorPart(message='While parsing P() at index 1: Parameter "foo" is not of the form FQCN#type'),
+ ],
+ ]),
+ ('P(f o.b r.b z#bar)', Context(), dict(errors='message'), [
+ [
+ dom.ErrorPart(message='While parsing P() at index 1: Plugin name "f o.b r.b z" is not a FQCN'),
+ ],
+ ]),
+ ('P(foo.bar.baz#b m)', Context(), dict(errors='message'), [
+ [
+ dom.ErrorPart(message='While parsing P() at index 1: Plugin type "b m" is not valid'),
+ ],
+ ]),
+ # bad option name/return value (error message):
+ ('O(f o.b r.b z#bam:foobar)', Context(), {}, [
+ [
+ dom.ErrorPart(message='While parsing O() at index 1: Plugin name "f o.b r.b z" is not a FQCN'),
+ ],
+ ]),
+ ('O(foo.bar.baz#b m:foobar)', Context(), dict(errors='message'), [
+ [
+ dom.ErrorPart(message='While parsing O() at index 1: Plugin type "b m" is not valid'),
+ ],
+ ]),
+ ('O(foo:bar:baz)', Context(), dict(errors='message'), [
+ [
+ dom.ErrorPart(message='While parsing O() at index 1: Invalid option/return value name "foo:bar:baz"'),
+ ],
+ ]),
+ # bad parameter parsing (no escaping, ignore error):
+ ('M(', Context(), dict(errors='ignore'), [[]]),
+ ('M(foo', Context(), dict(errors='ignore'), [[]]),
+ ('L(foo)', Context(), dict(errors='ignore'), [[]]),
+ ('L(foo,bar', Context(), dict(errors='ignore'), [[]]),
+ ('L(foo), bar', Context(), dict(errors='ignore'), [[]]),
+ ('P(', Context(), dict(errors='ignore'), [[]]),
+ ('P(foo', Context(), dict(errors='ignore'), [[]]),
+ # bad module ref (ignore error):
+ ('M(foo)', Context(), dict(errors='ignore'), [[]]),
+ (' M(foo.bar)', Context(), dict(errors='ignore'), [[dom.TextPart(text=' ')]]),
+ (' M(foo. bar.baz)', Context(), dict(errors='ignore'), [[dom.TextPart(text=' ')]]),
+ (' M(foo) baz', Context(), dict(errors='ignore'), [
+ [
+ dom.TextPart(text=' '),
+ dom.TextPart(text=' baz'),
+ ],
+ ]),
+ # bad plugin ref (ignore error):
+ ('P(foo#bar)', Context(), dict(errors='ignore'), [[]]),
+ ('P(f o.b r.b z#bar)', Context(), dict(errors='ignore'), [[]]),
+ ('P(foo.bar.baz#b m)', Context(), dict(errors='ignore'), [[]]),
+ # bad option name/return value (ignore error):
+ ('O(f o.b r.b z#bam:foobar)', Context(), dict(errors='ignore'), [[]]),
+ ('O(foo.bar.baz#b m:foobar)', Context(), dict(errors='ignore'), [[]]),
+ ('O(foo:bar:baz)', Context(), dict(errors='ignore'), [[]]),
+]
+
+
+@pytest.mark.parametrize('paragraphs, context, kwargs, expected', TEST_PARSE_DATA)
+def test_parse(paragraphs: t.Union[str, t.List[str]], context: Context, kwargs: t.Dict[str, t.Any], expected: t.List[dom.Paragraph]) -> None:
+ result = parse(paragraphs, context, **kwargs)
+ print(result)
+ assert result == expected
+
+
+TEST_PARSE_THROW_DATA: t.List[t.Tuple[t.Union[str, t.List[str]], Context, t.Dict[str, t.Any], str]] = [
+ # bad parameter parsing (no escaping, throw error):
+ ('M(', Context(), dict(errors='exception'),
+ 'While parsing M() at index 1: Cannot find closing ")" after last parameter',
+ ),
+ ('M(foo', Context(), dict(errors='exception'),
+ 'While parsing M() at index 1: Cannot find closing ")" after last parameter',
+ ),
+ ('L(foo)', Context(), dict(errors='exception'),
+ 'While parsing L() at index 1: Cannot find comma separating parameter 1 from the next one',
+ ),
+ ('L(foo,bar', Context(), dict(errors='exception'),
+ 'While parsing L() at index 1: Cannot find closing ")" after last parameter',
+ ),
+ ('L(foo), bar', Context(), dict(errors='exception'),
+ 'While parsing L() at index 1: Cannot find closing ")" after last parameter',
+ ),
+ ('P(', Context(), dict(errors='exception'),
+ 'While parsing P() at index 1: Cannot find closing ")" after last parameter',
+ ),
+ ('P(foo', Context(), dict(errors='exception'),
+ 'While parsing P() at index 1: Cannot find closing ")" after last parameter',
+ ),
+ # bad module ref (throw error):
+ ('M(foo)', Context(), dict(errors='exception'),
+ 'While parsing M() at index 1: Module name "foo" is not a FQCN',
+ ),
+ (' M(foo.bar)', Context(), dict(errors='exception'),
+ 'While parsing M() at index 2: Module name "foo.bar" is not a FQCN',
+ ),
+ (' M(foo. bar.baz)', Context(), dict(errors='exception'),
+ 'While parsing M() at index 3: Module name "foo. bar.baz" is not a FQCN',
+ ),
+ (' M(foo)', Context(), dict(errors='exception'),
+ 'While parsing M() at index 4: Module name "foo" is not a FQCN',
+ ),
+ # bad plugin ref (throw error):
+ ('P(foo)', Context(), dict(errors='exception'),
+ 'While parsing P() at index 1: Parameter "foo" is not of the form FQCN#type',
+ ),
+ ('P(f o.b r.b z#bar)', Context(), dict(errors='exception'),
+ 'While parsing P() at index 1: Plugin name "f o.b r.b z" is not a FQCN',
+ ),
+ ('P(foo.bar.baz#b m)', Context(), dict(errors='exception'),
+ 'While parsing P() at index 1: Plugin type "b m" is not valid',
+ ),
+ # bad option name/return value (throw error):
+ ('O(f o.b r.b z#bam:foobar)', Context(), dict(errors='exception'),
+ 'While parsing O() at index 1: Plugin name "f o.b r.b z" is not a FQCN',
+ ),
+ ('O(foo.bar.baz#b m:foobar)', Context(), dict(errors='exception'),
+ 'While parsing O() at index 1: Plugin type "b m" is not valid',
+ ),
+ ('O(foo:bar:baz)', Context(), dict(errors='exception'),
+ 'While parsing O() at index 1: Invalid option/return value name "foo:bar:baz"',
+ ),
+]
+
+
+@pytest.mark.parametrize('paragraphs, context, kwargs, exc_message', TEST_PARSE_THROW_DATA)
+def test_parse_bad(paragraphs: t.Union[str, t.List[str]], context: Context, kwargs: t.Dict[str, t.Any], exc_message: str) -> None:
+ with pytest.raises(ValueError) as exc:
+ parse(paragraphs, context, **kwargs)
+ assert str(exc.value) == exc_message
diff --git a/tests/units/markup/test_rst.py b/tests/units/markup/test_rst.py
new file mode 100644
index 00000000..39b6e6ea
--- /dev/null
+++ b/tests/units/markup/test_rst.py
@@ -0,0 +1,19 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+
+from antsibull_docs.markup import dom
+from antsibull_docs.markup.rst import rst_escape, to_rst
+
+
+def test_rst_escape():
+ assert rst_escape('') == ''
+ assert rst_escape(' foo ') == ' foo '
+ assert rst_escape(' foo ', True) == '\\ foo \\ '
+ assert rst_escape('\\<_>`*<_>*`\\') == '\\\\\\<\\_\\>\\`\\*\\<\\_\\>\\*\\`\\\\'
+
+def test_to_rst():
+ assert to_rst([]) == ''
+ assert to_rst([[dom.TextPart(text='test')]]) == 'test'
From d38de03c4cfe516c7b129d22e828e288918143f7 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Sat, 18 Mar 2023 16:30:28 +0100
Subject: [PATCH 09/15] Replace current markup code with new one.
---
src/antsibull_docs/markup/htmlify.py | 320 ++----------------
src/antsibull_docs/markup/parser.py | 122 -------
src/antsibull_docs/markup/rst.py | 8 +-
src/antsibull_docs/markup/rstify.py | 261 +++-----------
.../collections/ns2/col/foo2_module.rst | 4 +-
.../collections/ns2/col/foo_become.rst | 4 +-
.../collections/ns2/col/foo_module.rst | 10 +-
tests/units/markup/test_parser.py | 134 --------
8 files changed, 93 insertions(+), 770 deletions(-)
diff --git a/src/antsibull_docs/markup/htmlify.py b/src/antsibull_docs/markup/htmlify.py
index 46fa67eb..a5074a3a 100644
--- a/src/antsibull_docs/markup/htmlify.py
+++ b/src/antsibull_docs/markup/htmlify.py
@@ -6,289 +6,32 @@
htmlify Jinja2 filter for use in Ansible documentation.
"""
-import re
import typing as t
-from html import escape as html_escape
-from urllib.parse import quote
-
-from .semantic_helper import parse_option, parse_return_value
-from .parser import Command, CommandSet, convert_text
-
-_MODULE = re.compile(r"^([^).]+)\.([^).]+)\.([^)]+)$")
-_PLUGIN = re.compile(r"^([^).]+)\.([^).]+)\.([^)]+)#([a-z]+)$")
-
-
-def _escape_url(url: str) -> str:
- # We include '<>[]{}' in safe to allow urls such as 'https://:[PORT]/v{version}/' to
- # remain unmangled by percent encoding
- return quote(url, safe=':/#?%<>[]{}')
-
-
-def _create_error(text: str, error: str) -> str:
- text = f'{html_escape(text)}
'
- return f'ERROR while parsing {text}: {html_escape(error)}'
-
-
-class _Context:
- counts: t.Dict[str, int]
- plugin_fqcn: t.Optional[str]
- plugin_type: t.Optional[str]
-
- def __init__(self,
- plugin_fqcn: t.Optional[str] = None,
- plugin_type: t.Optional[str] = None):
- self.counts = {
- 'italic': 0,
- 'bold': 0,
- 'module': 0,
- 'plugin': 0,
- 'link': 0,
- 'url': 0,
- 'ref': 0,
- 'const': 0,
- 'option-name': 0,
- 'option-value': 0,
- 'environment-var': 0,
- 'return-value': 0,
- 'ruler': 0,
- }
- self.plugin_fqcn = plugin_fqcn
- self.plugin_type = plugin_type
-
-
-# In the following, we make heavy use of escaped whitespace ("\ ") being removed from the output.
-# See
-# https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#character-level-inline-markup-1
-# for further information.
-
-
-class _Italic(Command):
- command = 'I'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['italic'] += 1
- return f"{html_escape(parameters[0])}"
-
-
-class _Bold(Command):
- command = 'B'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['bold'] += 1
- return f"{html_escape(parameters[0])}"
-
-
-class _Module(Command):
- command = 'M'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['module'] += 1
- m = _MODULE.match(parameters[0])
- if m is None:
- return _create_error(
- f'M({parameters[0]!r})', f'parameter {parameters[0]!r} is not a FQCN')
- fqcn = f'{m.group(1)}.{m.group(2)}.{m.group(3)}'
- url = html_escape(f'../../{m.group(1)}/{m.group(2)}/{m.group(3)}_module.html')
- return f"{html_escape(fqcn)}"
-
-
-class _Plugin(Command):
- command = 'P'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['plugin'] += 1
- m = _PLUGIN.match(parameters[0])
- if m is None:
- return _create_error(
- f'P({parameters[0]!r})',
- f'parameter {parameters[0]!r} is not of the form FQCN#type')
- fqcn = f'{m.group(1)}.{m.group(2)}.{m.group(3)}'
- plugin_type = m.group(4)
- url = html_escape(f'../../{m.group(1)}/{m.group(2)}/{m.group(3)}_{plugin_type}.html')
- cssclass = f'plugin-{html_escape(plugin_type)}'
- return f"{html_escape(fqcn)}"
-
-
-class _URL(Command):
- command = 'U'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['url'] += 1
- url = parameters[0]
- return f"{html_escape(_escape_url(url))}"
-
-
-class _Link(Command):
- command = 'L'
- parameter_count = 2
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['link'] += 1
- url = parameters[1]
- return f"{html_escape(parameters[0])}"
-
-
-class _Ref(Command):
- command = 'R'
- parameter_count = 2
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['ref'] += 1
- return f"{html_escape(parameters[0])}"
-
-
-class _Const(Command):
- command = 'C'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['const'] += 1
- # Escaping does not work in double backticks, so we use the :literal: role instead
- return f"{html_escape(parameters[0])}
"
-
-
-class _OptionName(Command):
- command = 'O'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['option-name'] += 1
- if context.plugin_fqcn is None or context.plugin_type is None:
- raise Exception('The markup O(...) cannot be used outside a plugin or role')
- text = parameters[0]
- try:
- plugin_fqcn, plugin_type, option_link, option, value = parse_option(
- text, context.plugin_fqcn, context.plugin_type, require_plugin=False)
- except ValueError as exc:
- return _create_error(f'O({text})', str(exc))
- if value is None:
- cls = 'ansible-option'
- text = f'{option}'
- strong_start = ''
- strong_end = ''
- else:
- cls = 'ansible-option-value'
- text = f'{option}={value}'
- strong_start = ''
- strong_end = ''
- if plugin_fqcn and plugin_type and plugin_fqcn.count('.') >= 2:
- # TODO: handle role arguments (entrypoint!)
- namespace, name, plugin = plugin_fqcn.split('.', 2)
- url = f'../../{namespace}/{name}/{plugin}_{plugin_type}.html'
- fragment = f'parameter-{quote(option_link.replace(".", "/"))}'
- link_start = (
- f''
- ''
- )
- link_end = ''
- else:
- link_start = ''
- link_end = ''
- return (
- f''
- f'{strong_start}{link_start}{html_escape(text)}{link_end}{strong_end}
'
- )
-
-
-class _OptionValue(Command):
- command = 'V'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['option-value'] += 1
- text = parameters[0]
- return f'{html_escape(text)}
'
-
-
-class _EnvVariable(Command):
- command = 'E'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['environment-var'] += 1
- text = parameters[0]
- return f'{html_escape(text)}
'
-
-
-class _RetValue(Command):
- command = 'RV'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['return-value'] += 1
- if context.plugin_fqcn is None or context.plugin_type is None:
- raise Exception('The markup RV(...) cannot be used outside a plugin or role')
- text = parameters[0]
- try:
- plugin_fqcn, plugin_type, rv_link, rv, value = parse_return_value(
- text, context.plugin_fqcn, context.plugin_type, require_plugin=False)
- except ValueError as exc:
- return _create_error(f'RV({text})', str(exc))
- cls = 'ansible-return-value'
- if value is None:
- text = f'{rv}'
- else:
- text = f'{rv}={value}'
- if plugin_fqcn and plugin_type and plugin_fqcn.count('.') >= 2:
- namespace, name, plugin = plugin_fqcn.split('.', 2)
- url = f'../../{namespace}/{name}/{plugin}_{plugin_type}.html'
- fragment = f'return-{quote(rv_link.replace(".", "/"))}'
- link_start = (
- f''
- ''
- )
- link_end = ''
- else:
- link_start = ''
- link_end = ''
- return (
- f'{link_start}'
- f'{html_escape(text)}{link_end}
'
- )
+from urllib.parse import quote
-class _HorizontalLine(Command):
- command = 'HORIZONTALLINE'
- parameter_count = 0
- escaped_content = False
+from . import dom
+from .parser import parse, Context
+from .html import to_html
+from .format import LinkProvider
+from .rstify import _count
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['ruler'] += 1
- return '
'
+class _HTMLLinkProvider(LinkProvider):
+ def plugin_link(self, plugin: dom.PluginIdentifier) -> t.Optional[str]:
+ name = '/'.join(plugin.fqcn.split('.', 2))
+ return f'../../{name}_{plugin.type}.html'
-_COMMAND_SET = CommandSet([
- _Italic(),
- _Bold(),
- _Module(),
- _Plugin(),
- _URL(),
- _Link(),
- _Ref(),
- _Const(),
- _OptionName(),
- _OptionValue(),
- _EnvVariable(),
- _RetValue(),
- _HorizontalLine(),
-])
+ def plugin_option_like_link(self, # pylint:disable=no-self-use
+ plugin: dom.PluginIdentifier, # pylint:disable=unused-argument
+ # pylint:disable-next=unused-argument
+ what: "t.Union[t.Literal['option'], t.Literal['retval']]",
+ # pylint:disable-next=unused-argument
+ name: t.List[str], current_plugin: bool) -> t.Optional[str]:
+ base = '' if current_plugin else self.plugin_link(plugin)
+ w = 'parameter' if what == 'option' else 'return'
+ slug = quote('/'.join(name))
+ return f'{base}#{w}-{slug}'
def html_ify(text: str,
@@ -296,14 +39,17 @@ def html_ify(text: str,
plugin_fqcn: t.Optional[str] = None,
plugin_type: t.Optional[str] = None) -> t.Tuple[str, t.Mapping[str, int]]:
''' convert symbols like I(this is in italics) to valid HTML '''
- our_context = _Context(
- plugin_fqcn=plugin_fqcn,
- plugin_type=plugin_type,
+ current_plugin: t.Optional[dom.PluginIdentifier] = None
+ if plugin_fqcn and plugin_type:
+ current_plugin = dom.PluginIdentifier(fqcn=plugin_fqcn, type=plugin_type)
+ paragraphs = parse(text, Context(current_plugin=current_plugin), errors='message')
+ link_provider = _HTMLLinkProvider()
+ text = to_html(
+ paragraphs,
+ link_provider=link_provider,
+ current_plugin=current_plugin,
+ par_start='',
+ par_end='',
)
-
- try:
- text = convert_text(text, _COMMAND_SET, html_escape, our_context)
- except Exception as exc: # pylint:disable=broad-except
- text = _create_error(text, str(exc))
-
- return text, our_context.counts
+ counts = _count(paragraphs)
+ return text, counts
diff --git a/src/antsibull_docs/markup/parser.py b/src/antsibull_docs/markup/parser.py
index e8ef7355..9af78774 100644
--- a/src/antsibull_docs/markup/parser.py
+++ b/src/antsibull_docs/markup/parser.py
@@ -13,128 +13,6 @@
from . import dom
from ._parser_impl import parse_parameters_escaped, parse_parameters_unescaped
-
-
-class ParsingException(Exception):
- pass
-
-
-class Command(abc.ABC):
- command: str
- parameter_count: int
- escaped_content: bool
-
- @abc.abstractmethod
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- pass
-
-
-class CommandSet:
- _command_map: t.Mapping[str, Command]
- _group_map: t.Mapping[str, Command]
- _re: 're.Pattern' # on Python 3.6 the type is called differently
-
- def __init__(self, commands: t.List[Command]):
- group_map = {}
- command_map = {}
- command_names = []
- for command in commands:
- if command.command in command_map:
- raise Exception(
- f'Command "{command.command}" appears multiple times in parameter list')
- command_map[command.command] = command
- if command.parameter_count == 0:
- command_names.append(fr'{re.escape(command.command)}\b')
- group_map[command.command] = command
- else:
- command_names.append(fr'{re.escape(command.command)}\(')
- group_map[f'{command.command}('] = command
- self._command_map = command_map
- self._group_map = group_map
- if command_names:
- command_re = '|'.join(command_names)
- self._re = re.compile(fr'\b({command_re})')
- else:
- self._re = re.compile(r' ^') # never matches
-
- def find_next_command(self, text: str, start_index: int) -> t.Optional[t.Tuple[int, Command]]:
- match = self._re.search(text, pos=start_index)
- if not match:
- return None
- return match.start(1), self._group_map[match.group(1)]
-
-
-class CommandData(t.NamedTuple):
- command: Command
- parameters: t.List[str]
-
-
-Part = t.Union[str, CommandData]
-
-
-def parse_text(text: str, commands: CommandSet) -> t.List[Part]:
- result: t.List[Part] = []
- index = 0
- length = len(text)
- while index < length:
- next_command = commands.find_next_command(text, index)
- if next_command is None:
- result.append(text[index:])
- break
- command_start, command = next_command
- if command_start > index:
- result.append(text[index:command_start])
- index = command_start + len(command.command)
- if command.parameter_count == 0:
- result.append(CommandData(command=command, parameters=[]))
- continue
- index += 1
- if command.escaped_content:
- parameters, index, error = parse_parameters_escaped(
- text, index, command.parameter_count)
- else:
- parameters, index, error = parse_parameters_unescaped(
- text, index, command.parameter_count)
- if error is not None:
- raise ParsingException(
- error +
- f' for command "{command.command}" starting at index {command_start} in {text!r}'
- )
- result.append(CommandData(command=command, parameters=parameters))
- return result
-
-
-def convert_text(text: str, commands: CommandSet, process_other_text: t.Callable[[str], str],
- context: t.Any,) -> str:
- parts = parse_text(text, commands)
- result = []
- for part in parts:
- if isinstance(part, str):
- result.append(process_other_text(part))
- else:
- result.append(part.command.handle(part.parameters, context))
- return ''.join(result)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
_IGNORE_MARKER = 'ignore:'
diff --git a/src/antsibull_docs/markup/rst.py b/src/antsibull_docs/markup/rst.py
index 3df02248..c69e3059 100644
--- a/src/antsibull_docs/markup/rst.py
+++ b/src/antsibull_docs/markup/rst.py
@@ -14,12 +14,8 @@
from .html import _url_escape
-def rst_escape(value: t.Any, escape_ending_whitespace=False) -> str:
- ''' make sure value is converted to a string, and RST special characters are escaped '''
-
- if not isinstance(value, str):
- value = str(value)
-
+def rst_escape(value: str, escape_ending_whitespace=False) -> str:
+ '''Escape RST specific constructs.'''
value = value.replace('\\', '\\\\')
value = value.replace('<', '\\<')
value = value.replace('>', '\\>')
diff --git a/src/antsibull_docs/markup/rstify.py b/src/antsibull_docs/markup/rstify.py
index 6815a401..cc2645b2 100644
--- a/src/antsibull_docs/markup/rstify.py
+++ b/src/antsibull_docs/markup/rstify.py
@@ -6,65 +6,28 @@
rstify Jinja2 filter for use in Ansible documentation.
"""
-import re
import typing as t
-from urllib.parse import quote
-from .semantic_helper import augment_plugin_name_type
-from .parser import Command, CommandSet, convert_text
-
-_MODULE = re.compile(r"^([^).]+)\.([^).]+)\.([^)]+)$")
-_PLUGIN = re.compile(r"^([^).]+)\.([^).]+)\.([^)]+)#([a-z]+)$")
+from . import dom
+from .parser import parse, Context
+from .rst import rst_escape as _rst_escape, rst_code as _rst_code, to_rst
def rst_escape(value: t.Any, escape_ending_whitespace=False) -> str:
''' make sure value is converted to a string, and RST special characters are escaped '''
-
- if not isinstance(value, str):
- value = str(value)
-
- value = value.replace('\\', '\\\\')
- value = value.replace('<', '\\<')
- value = value.replace('>', '\\>')
- value = value.replace('_', '\\_')
- value = value.replace('*', '\\*')
- value = value.replace('`', '\\`')
-
- if escape_ending_whitespace and value.endswith(' '):
- value = value + '\\ '
- if escape_ending_whitespace and value.startswith(' '):
- value = '\\ ' + value
-
- return value
-
-
-def rst_code(value: str) -> str:
- ''' Write value as :code:`...` RST construct. '''
if not isinstance(value, str):
value = str(value)
- return f':code:`{rst_escape(value, escape_ending_whitespace=True)}`'
-
-def _escape_url(url: str) -> str:
- # We include '<>[]{}' in safe to allow urls such as 'https://:[PORT]/v{version}/' to
- # remain unmangled by percent encoding
- return quote(url, safe=':/#?%<>[]{}')
+ return _rst_escape(value, escape_ending_whitespace=escape_ending_whitespace)
-def _create_error(text: str, error: str) -> str:
- text = f':literal:`{rst_escape(text, escape_ending_whitespace=True)}`'
- error_msg = f':strong:`{rst_escape(error, escape_ending_whitespace=True)}`'
- return f"\\ :strong:`ERROR while parsing` {text}\\ : {error_msg}\\ "
+rst_code = _rst_code
-class _Context:
+class _Counter(dom.Walker):
counts: t.Dict[str, int]
- plugin_fqcn: t.Optional[str]
- plugin_type: t.Optional[str]
- def __init__(self,
- plugin_fqcn: t.Optional[str] = None,
- plugin_type: t.Optional[str] = None):
+ def __init__(self):
self.counts = {
'italic': 0,
'bold': 0,
@@ -80,180 +43,58 @@ def __init__(self,
'return-value': 0,
'ruler': 0,
}
- self.plugin_fqcn = plugin_fqcn
- self.plugin_type = plugin_type
-
-
-# In the following, we make heavy use of escaped whitespace ("\ ") being removed from the output.
-# See
-# https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#character-level-inline-markup-1
-# for further information.
-
-
-class _Italic(Command):
- command = 'I'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['italic'] += 1
- return f"\\ :emphasis:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
-
-
-class _Bold(Command):
- command = 'B'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['bold'] += 1
- return f"\\ :strong:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
-
-class _Module(Command):
- command = 'M'
- parameter_count = 1
- escaped_content = False
+ def process_error(self, part: dom.ErrorPart) -> None:
+ pass
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['module'] += 1
- m = _MODULE.match(parameters[0])
- if m is None:
- return _create_error(
- f'M({parameters[0]!r})', f'parameter {parameters[0]!r} is not a FQCN')
- fqcn = f'{m.group(1)}.{m.group(2)}.{m.group(3)}'
- return f"\\ :ref:`{rst_escape(fqcn)} `\\ "
+ def process_bold(self, part: dom.BoldPart) -> None:
+ self.counts['bold'] += 1
+ def process_code(self, part: dom.CodePart) -> None:
+ self.counts['const'] += 1
-class _Plugin(Command):
- command = 'P'
- parameter_count = 1
- escaped_content = False
+ def process_horizontal_line(self, part: dom.HorizontalLinePart) -> None:
+ self.counts['ruler'] += 1
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['plugin'] += 1
- m = _PLUGIN.match(parameters[0])
- if m is None:
- return _create_error(
- f'P({parameters[0]!r})',
- f'parameter {parameters[0]!r} is not of the form FQCN#type')
- fqcn = f'{m.group(1)}.{m.group(2)}.{m.group(3)}'
- plugin_type = m.group(4)
- return f"\\ :ref:`{rst_escape(fqcn)} `\\ "
+ def process_italic(self, part: dom.ItalicPart) -> None:
+ self.counts['italic'] += 1
+ def process_link(self, part: dom.LinkPart) -> None:
+ self.counts['link'] += 1
-class _URL(Command):
- command = 'U'
- parameter_count = 1
- escaped_content = False
+ def process_module(self, part: dom.ModulePart) -> None:
+ self.counts['module'] += 1
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['url'] += 1
- return f"\\ {_escape_url(parameters[0])}\\ "
+ def process_rst_ref(self, part: dom.RSTRefPart) -> None:
+ self.counts['ref'] += 1
+ def process_url(self, part: dom.URLPart) -> None:
+ self.counts['url'] += 1
-class _Link(Command):
- command = 'L'
- parameter_count = 2
- escaped_content = False
+ def process_text(self, part: dom.TextPart) -> None:
+ pass
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['link'] += 1
- return f"\\ `{rst_escape(parameters[0])} <{_escape_url(parameters[1])}>`__\\ "
+ def process_env_variable(self, part: dom.EnvVariablePart) -> None:
+ self.counts['environment-var'] += 1
+ def process_option_name(self, part: dom.OptionNamePart) -> None:
+ self.counts['option-name'] += 1
-class _Ref(Command):
- command = 'R'
- parameter_count = 2
- escaped_content = False
+ def process_option_value(self, part: dom.OptionValuePart) -> None:
+ self.counts['option-value'] += 1
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['ref'] += 1
- return f"\\ :ref:`{rst_escape(parameters[0])} <{parameters[1]}>`\\ "
+ def process_plugin(self, part: dom.PluginPart) -> None:
+ self.counts['plugin'] += 1
+ def process_return_value(self, part: dom.ReturnValuePart) -> None:
+ self.counts['return-value'] += 1
-class _Const(Command):
- command = 'C'
- parameter_count = 1
- escaped_content = False
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['const'] += 1
- # Escaping does not work in double backticks, so we use the :literal: role instead
- return f"\\ :literal:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
-
-
-class _OptionName(Command):
- command = 'O'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['option-name'] += 1
- if context.plugin_fqcn is None or context.plugin_type is None:
- raise Exception('The markup O(...) cannot be used outside a plugin or role')
- text = augment_plugin_name_type(parameters[0], context.plugin_fqcn, context.plugin_type)
- return f"\\ :ansopt:`{rst_escape(text, escape_ending_whitespace=True)}`\\ "
-
-
-class _OptionValue(Command):
- command = 'V'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['option-value'] += 1
- return f"\\ :ansval:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
-
-
-class _EnvVariable(Command):
- command = 'E'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['environment-var'] += 1
- return f"\\ :envvar:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
-
-
-class _RetValue(Command):
- command = 'RV'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['return-value'] += 1
- if context.plugin_fqcn is None or context.plugin_type is None:
- raise Exception('The markup RV(...) cannot be used outside a plugin or role')
- text = augment_plugin_name_type(parameters[0], context.plugin_fqcn, context.plugin_type)
- return f"\\ :ansretval:`{rst_escape(text, escape_ending_whitespace=True)}`\\ "
-
-
-class _HorizontalLine(Command):
- command = 'HORIZONTALLINE'
- parameter_count = 0
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- context.counts['ruler'] += 1
- return '\n\n.. raw:: html\n\n
\n\n'
-
-
-_COMMAND_SET = CommandSet([
- _Italic(),
- _Bold(),
- _Module(),
- _Plugin(),
- _URL(),
- _Link(),
- _Ref(),
- _Const(),
- _OptionName(),
- _OptionValue(),
- _EnvVariable(),
- _RetValue(),
- _HorizontalLine(),
-])
+def _count(paragraphs: t.Sequence[dom.Paragraph]) -> t.Dict[str, int]:
+ counter = _Counter()
+ for paragraph in paragraphs:
+ dom.walk(paragraph, counter)
+ return counter.counts
def rst_ify(text: str,
@@ -261,14 +102,10 @@ def rst_ify(text: str,
plugin_fqcn: t.Optional[str] = None,
plugin_type: t.Optional[str] = None) -> t.Tuple[str, t.Mapping[str, int]]:
''' convert symbols like I(this is in italics) to valid restructured text '''
- our_context = _Context(
- plugin_fqcn=plugin_fqcn,
- plugin_type=plugin_type,
- )
-
- try:
- text = convert_text(text, _COMMAND_SET, rst_escape, our_context)
- except Exception as exc: # pylint:disable=broad-except
- text = _create_error(text, str(exc))
-
- return text, our_context.counts
+ current_plugin: t.Optional[dom.PluginIdentifier] = None
+ if plugin_fqcn and plugin_type:
+ current_plugin = dom.PluginIdentifier(fqcn=plugin_fqcn, type=plugin_type)
+ paragraphs = parse(text, Context(current_plugin=current_plugin), errors='message')
+ text = to_rst(paragraphs, current_plugin=current_plugin)
+ counts = _count(paragraphs)
+ return text, counts
diff --git a/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo2_module.rst b/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo2_module.rst
index 4cdf2b96..f8fbff78 100644
--- a/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo2_module.rst
+++ b/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo2_module.rst
@@ -323,8 +323,8 @@ Common return values are documented :ref:`here `, the foll
Some bar.
- Referencing myself as bar .
- Do not confuse with bar .
+ Referencing myself as bar .
+ Do not confuse with bar .
Returned: success
Sample: "baz"
|
diff --git a/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo_become.rst b/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo_become.rst
index cd2df779..becf20af 100644
--- a/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo_become.rst
+++ b/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo_become.rst
@@ -109,8 +109,8 @@ Parameters
|
diff --git a/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo_module.rst b/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo_module.rst
index 7fae7e20..cabbfb21 100644
--- a/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo_module.rst
+++ b/tests/functional/baseline-use-html-blobs/collections/ns2/col/foo_module.rst
@@ -120,8 +120,8 @@ Parameters
A bar.
- Independent from foo .
- Do not confuse with bar .
+ Independent from foo .
+ Do not confuse with bar .
|
@@ -165,7 +165,7 @@ Parameters
A sub foo.
Whatever.
- Also required when subfoo is specified when foo=bar or baz .
+ Also required when subfoo is specified when foo=bar or baz .
|
@@ -428,8 +428,8 @@ Common return values are documented :ref:`here `, the foll
Some bar.
- Referencing myself as bar .
- Do not confuse with bar .
+ Referencing myself as bar .
+ Do not confuse with bar .
Returned: success
Sample: "baz"
|
diff --git a/tests/units/markup/test_parser.py b/tests/units/markup/test_parser.py
index f1faada3..02182152 100644
--- a/tests/units/markup/test_parser.py
+++ b/tests/units/markup/test_parser.py
@@ -7,140 +7,6 @@
import pytest
-from antsibull_docs.markup.parser import Command, CommandSet, ParsingException, convert_text
-
-
-class TestCommand0(Command):
- command = 'command0'
- parameter_count = 0
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- assert len(parameters) == 0
- return 'EMPTY'
-
-
-class TestCommand1A(Command):
- command = 'command1a'
- parameter_count = 1
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- assert len(parameters) == 1
- return f'FOOA{parameters!r}'
-
-
-class TestCommand1B(Command):
- command = 'command1b'
- parameter_count = 1
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- assert len(parameters) == 1
- return f'FOOB{parameters!r}'
-
-
-class TestCommand2A(Command):
- command = 'command2a'
- parameter_count = 2
- escaped_content = False
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- assert len(parameters) == 2
- return f'FOOC{parameters!r}'
-
-
-class TestCommand2B(Command):
- command = 'command2b'
- parameter_count = 2
- escaped_content = True
-
- def handle(self, parameters: t.List[str], context: t.Any) -> str:
- assert len(parameters) == 2
- return f'FOOD{parameters!r}'
-
-
-COMMAND_SET = CommandSet([
- TestCommand0(),
- TestCommand1A(),
- TestCommand1B(),
- TestCommand2A(),
- TestCommand2B(),
-])
-
-
-def _escape_regular(text: str) -> str:
- return f'TEXT:{text!r}'
-
-
-CONVERT_TEXT_DATA = {
- 'nothing': "TEXT:'nothing'",
- 'command0': "EMPTY",
- 'a command0 b': "TEXT:'a 'EMPTYTEXT:' b'",
- 'acommand0b': "TEXT:'acommand0b'",
- 'command1a(foo)': "FOOA['foo']",
- 'command1a( foo )': "FOOA[' foo ']",
- 'command1a(foo, bar)': "FOOA['foo, bar']",
- 'command1a(())': "FOOA['(']TEXT:')'",
- r'command1a(\(\))': r"FOOA['\\(\\']TEXT:')'",
- 'a command1a()b': "TEXT:'a 'FOOA['']TEXT:'b'",
- 'a command1a() b': "TEXT:'a 'FOOA['']TEXT:' b'",
- 'acommand1a()b': "TEXT:'acommand1a()b'",
- 'command1b(foo)': "FOOB['foo']",
- 'command1b( foo )': "FOOB[' foo ']",
- 'command1b(foo, bar)': "FOOB['foo, bar']",
- 'command1b(())': "FOOB['(']TEXT:')'",
- r'command1b(\(\))': "FOOB['()']",
- 'a command1b()b': "TEXT:'a 'FOOB['']TEXT:'b'",
- 'acommand1b()b': "TEXT:'acommand1b()b'",
- 'command2a(foo, bar)': "FOOC['foo', 'bar']",
- 'command2a(foo,bar)': "FOOC['foo', 'bar']",
- 'command2a( foo , bar , baz )': "FOOC[' foo', 'bar , baz ']",
- 'command2a((,))': "FOOC['(', '']TEXT:')'",
- r'command2a(\(,\))': r"FOOC['\\(', '\\']TEXT:')'",
- 'a command2a(,)b': "TEXT:'a 'FOOC['', '']TEXT:'b'",
- 'acommand2a(,)b': "TEXT:'acommand2a(,)b'",
- 'command2b(foo, bar)': "FOOD['foo', 'bar']",
- 'command2b(foo,bar)': "FOOD['foo', 'bar']",
- 'command2b( foo , bar , baz )': "FOOD[' foo', 'bar , baz ']",
- r'command2b( foo\ , bar , baz )': "FOOD[' foo ', 'bar , baz ']",
- 'command2b((,))': "FOOD['(', '']TEXT:')'",
- r'command2b(\(,\))': "FOOD['(', ')']",
- 'a command2b(,)b': "TEXT:'a 'FOOD['', '']TEXT:'b'",
- 'acommand2b(,)b': "TEXT:'acommand2b(,)b'",
-}
-
-
-@pytest.mark.parametrize('text, expected', CONVERT_TEXT_DATA.items())
-def test_convert_text(text, expected):
- commands = COMMAND_SET
- result = convert_text(text, commands, _escape_regular, None)
- assert result == expected
-
-
-CONVERT_TEXT_FAIL_DATA = {
- 'command1a(': """Cannot find closing ")" after last parameter for command "command1a" starting at index 0 in 'command1a('""",
- 'command2a(,': """Cannot find closing ")" after last parameter for command "command2a" starting at index 0 in 'command2a(,'""",
- 'command2a(': """Cannot find comma separating parameter 1 from the next one for command "command2a" starting at index 0 in 'command2a('""",
- 'command1b(': """Cannot find closing ")" after last parameter for command "command1b" starting at index 0 in 'command1b('""",
- 'command2b(,': """Cannot find closing ")" after last parameter for command "command2b" starting at index 0 in 'command2b(,'""",
- 'command2b(': """Cannot find comma separating parameter 1 from the next one for command "command2b" starting at index 0 in 'command2b('""",
- r'command2b(\,)': r"""Cannot find comma separating parameter 1 from the next one for command "command2b" starting at index 0 in 'command2b(\\,)'""",
-}
-
-
-@pytest.mark.parametrize('text, expected_exc', CONVERT_TEXT_FAIL_DATA.items())
-def test_convert_text_fail(text, expected_exc):
- commands = COMMAND_SET
- with pytest.raises(ParsingException) as exc:
- convert_text(text, commands, _escape_regular, None)
- assert str(exc.value) == expected_exc
-
-
-###############################################################################################
-
-# import pytest
-
from antsibull_docs.markup import dom
from antsibull_docs.markup.parser import parse, CommandParser, Context, Parser
From 547ca45b7bbbd96dff7814be48d3fc040dc93cd5 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Sat, 18 Mar 2023 17:42:31 +0100
Subject: [PATCH 10/15] Make more compatible to TS code, add same test vectors.
---
src/antsibull_docs/markup/format.py | 4 +
src/antsibull_docs/markup/html.py | 2 +
src/antsibull_docs/markup/htmlify.py | 6 +-
src/antsibull_docs/markup/md.py | 6 +-
src/antsibull_docs/markup/rst.py | 2 +
test-vectors.yaml | 700 +++++++++++++++++++++++++++
tests/units/markup/test_vectors.py | 111 +++++
7 files changed, 825 insertions(+), 6 deletions(-)
create mode 100644 test-vectors.yaml
create mode 100644 tests/units/markup/test_vectors.py
diff --git a/src/antsibull_docs/markup/format.py b/src/antsibull_docs/markup/format.py
index 19483a52..91d88e4a 100644
--- a/src/antsibull_docs/markup/format.py
+++ b/src/antsibull_docs/markup/format.py
@@ -186,6 +186,7 @@ def format_paragraphs(paragraphs: t.Sequence[dom.Paragraph],
par_start: str = '',
par_end: str = '',
par_sep: str = '',
+ par_empty: str = '',
current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
'''
Apply the formatter to all parts of the given paragraphs, concatenate the results,
@@ -202,6 +203,9 @@ def format_paragraphs(paragraphs: t.Sequence[dom.Paragraph],
if result:
result.append(par_sep)
result.append(par_start)
+ before_len = len(result)
dom.walk(paragraph, walker)
+ if before_len == len(result):
+ result.append(par_empty)
result.append(par_end)
return ''.join(result)
diff --git a/src/antsibull_docs/markup/html.py b/src/antsibull_docs/markup/html.py
index bbf07976..11abbd35 100644
--- a/src/antsibull_docs/markup/html.py
+++ b/src/antsibull_docs/markup/html.py
@@ -129,6 +129,7 @@ def to_html(paragraphs: t.Sequence[dom.Paragraph],
par_start: str = '',
par_end: str = '
',
par_sep: str = '',
+ par_empty: str = '',
current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
return _format_paragraphs(
paragraphs,
@@ -137,5 +138,6 @@ def to_html(paragraphs: t.Sequence[dom.Paragraph],
par_start=par_start,
par_end=par_end,
par_sep=par_sep,
+ par_empty=par_empty,
current_plugin=current_plugin,
)
diff --git a/src/antsibull_docs/markup/htmlify.py b/src/antsibull_docs/markup/htmlify.py
index a5074a3a..809d1677 100644
--- a/src/antsibull_docs/markup/htmlify.py
+++ b/src/antsibull_docs/markup/htmlify.py
@@ -22,11 +22,9 @@ def plugin_link(self, plugin: dom.PluginIdentifier) -> t.Optional[str]:
name = '/'.join(plugin.fqcn.split('.', 2))
return f'../../{name}_{plugin.type}.html'
- def plugin_option_like_link(self, # pylint:disable=no-self-use
- plugin: dom.PluginIdentifier, # pylint:disable=unused-argument
- # pylint:disable-next=unused-argument
+ def plugin_option_like_link(self,
+ plugin: dom.PluginIdentifier,
what: "t.Union[t.Literal['option'], t.Literal['retval']]",
- # pylint:disable-next=unused-argument
name: t.List[str], current_plugin: bool) -> t.Optional[str]:
base = '' if current_plugin else self.plugin_link(plugin)
w = 'parameter' if what == 'option' else 'return'
diff --git a/src/antsibull_docs/markup/md.py b/src/antsibull_docs/markup/md.py
index 6aa5f49c..8b0b6eb7 100644
--- a/src/antsibull_docs/markup/md.py
+++ b/src/antsibull_docs/markup/md.py
@@ -29,7 +29,7 @@ def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
link_start = ''
link_end = ''
if url:
- link_start = f''
+ link_start = f''
link_end = ''
strong_start = ''
strong_end = ''
@@ -40,7 +40,7 @@ def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
text = part.name
else:
text = f'{part.name}={part.value}'
- return f'{strong_start}{link_start}{_html_escape(text)}{link_end}{strong_end}
'
+ return f'{strong_start}{link_start}{md_escape(text)}{link_end}{strong_end}
'
def format_error(self, part: dom.ErrorPart) -> str:
return f'ERROR while parsing: {md_escape(part.message)}'
@@ -101,6 +101,7 @@ def to_md(paragraphs: t.Sequence[dom.Paragraph],
par_start: str = '',
par_end: str = '',
par_sep: str = '\n\n',
+ par_empty: str = ' ',
current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
return _format_paragraphs(
paragraphs,
@@ -109,5 +110,6 @@ def to_md(paragraphs: t.Sequence[dom.Paragraph],
par_start=par_start,
par_end=par_end,
par_sep=par_sep,
+ par_empty=par_empty,
current_plugin=current_plugin,
)
diff --git a/src/antsibull_docs/markup/rst.py b/src/antsibull_docs/markup/rst.py
index c69e3059..363e72df 100644
--- a/src/antsibull_docs/markup/rst.py
+++ b/src/antsibull_docs/markup/rst.py
@@ -114,6 +114,7 @@ def to_rst(paragraphs: t.Sequence[dom.Paragraph],
par_start: str = '',
par_end: str = '',
par_sep: str = '\n\n',
+ par_empty: str = r'\ ',
current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
return _format_paragraphs(
paragraphs,
@@ -122,5 +123,6 @@ def to_rst(paragraphs: t.Sequence[dom.Paragraph],
par_start=par_start,
par_end=par_end,
par_sep=par_sep,
+ par_empty=par_empty,
current_plugin=current_plugin,
)
diff --git a/test-vectors.yaml b/test-vectors.yaml
new file mode 100644
index 00000000..81727c22
--- /dev/null
+++ b/test-vectors.yaml
@@ -0,0 +1,700 @@
+---
+# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
+# SPDX-FileCopyrightText: Ansible Project
+# SPDX-License-Identifier: BSD-2-Clause
+
+test_vectors:
+ empty:
+ source: []
+ html: ''
+ html_plain: ''
+ rst: ''
+ md: ''
+ empty_content:
+ source: ['']
+ html:
+ html_plain:
+ md: ' '
+ rst: '\ '
+ simple:
+ source: |-
+ This is a C(test) I(module) B(markup).
+ html: |-
+ This is a test
module markup.
+ html_plain: |-
+ This is a test
module markup.
+ md: |-
+ This is a test
module markup.
+ rst: |-
+ This is a \ :literal:`test`\ \ :emphasis:`module`\ \ :strong:`markup`\ .
+ module:
+ source: |-
+ The M(a.b.c) module.
+ html: |-
+ The a.b.c module.
+ html_opts:
+ pluginLink.js: |-
+ (plugin_data) => `https://docs.ansible.com/ansible/devel/collections/${plugin_data.fqcn.replace(/\./g, '/')}_${plugin_data.type}.html`
+ pluginLink.py: |-
+ lambda plugin_data: f"https://docs.ansible.com/ansible/devel/collections/{plugin_data.fqcn.replace('.', '/')}_{plugin_data.type}.html"
+ html_plain: |-
+ The a.b.c module.
+ md: |-
+ The [a.b.c](https\://docs.ansible.com/ansible/devel/collections/a/b/c\_module.html) module.
+ md_opts:
+ pluginLink.js: |-
+ (plugin_data) => `https://docs.ansible.com/ansible/devel/collections/${plugin_data.fqcn.replace(/\./g, '/')}_${plugin_data.type}.html`
+ pluginLink.py: |-
+ lambda plugin_data: f"https://docs.ansible.com/ansible/devel/collections/{plugin_data.fqcn.replace('.', '/')}_{plugin_data.type}.html"
+ rst: |-
+ The \ :ref:`a.b.c `\ module.
+ module_no_link:
+ source: |-
+ The M(a.b.c) module.
+ html: |-
+ The a.b.c module.
+ html_plain: |-
+ The a.b.c module.
+ md: |-
+ The a.b.c module.
+ rst: |-
+ The \ :ref:`a.b.c `\ module.
+ plugin:
+ source: |-
+ The P(a.b.c#lookup) lookup plugin.
+ html: |-
+ The a.b.c lookup plugin.
+ html_opts:
+ pluginLink.js: |-
+ (plugin_data) => `https://docs.ansible.com/ansible/devel/collections/${plugin_data.fqcn.replace(/\./g, '/')}_${plugin_data.type}.html`
+ pluginLink.py: |-
+ lambda plugin_data: f"https://docs.ansible.com/ansible/devel/collections/{plugin_data.fqcn.replace('.', '/')}_{plugin_data.type}.html"
+ html_plain: |-
+ The a.b.c lookup plugin.
+ md: |-
+ The [a.b.c](https\://docs.ansible.com/ansible/devel/collections/a/b/c\_lookup.html) lookup plugin.
+ md_opts:
+ pluginLink.js: |-
+ (plugin_data) => `https://docs.ansible.com/ansible/devel/collections/${plugin_data.fqcn.replace(/\./g, '/')}_${plugin_data.type}.html`
+ pluginLink.py: |-
+ lambda plugin_data: f"https://docs.ansible.com/ansible/devel/collections/{plugin_data.fqcn.replace('.', '/')}_{plugin_data.type}.html"
+ rst: |-
+ The \ :ref:`a.b.c `\ lookup plugin.
+ plugin_no_link:
+ source: |-
+ The P(a.b.c#lookup) lookup plugin.
+ html: |-
+ The a.b.c lookup plugin.
+ html_plain: |-
+ The a.b.c lookup plugin.
+ md: |-
+ The a.b.c lookup plugin.
+ rst: |-
+ The \ :ref:`a.b.c `\ lookup plugin.
+ link_and_url:
+ source: |-
+ An URL U(https://example.com) and L(a link, https://example.org).
+ html: |-
+ An URL https://example.com and a link.
+ html_plain: |-
+ An URL https://example.com and a link.
+ md: |-
+ An URL [https\://example.com](https\://example.com) and [a link](https\://example.org).
+ rst: |-
+ An URL \ https://example.com\ and \ `a link `__\ .
+ rst_ref:
+ source: |-
+ A R(RST reference, ansible_collections.community.general.ufw_module).
+ html: |-
+ A RST reference.
+ html_plain: |-
+ A RST reference.
+ md: |-
+ A RST reference.
+ rst: |-
+ A \ :ref:`RST reference `\ .
+ horizontal_line:
+ source: |-
+ foo HORIZONTALLINE bar
+ html: |-
+ foo
bar
+ html_plain: |-
+ foo
bar
+ md: |-
+ foo
bar
+ rst: |-
+ foo
+
+ .. raw:: html
+
+
+
+ bar
+ semantic_markup:
+ source: |-
+ foo E(FOOBAR) bar P(foo.bar.baz#bam) has value V( foo\),bar\\bam ).
+ html: |-
+ foo FOOBAR
bar foo.bar.baz has value foo),bar\bam
.
+ html_plain: |-
+ foo FOOBAR
bar foo.bar.baz has value foo),bar\bam
.
+ md: |-
+ foo FOOBAR
bar foo.bar.baz has value foo\)\,bar\\bam
.
+ rst: |-
+ foo \ :envvar:`FOOBAR`\ bar \ :ref:`foo.bar.baz `\ has value \ :ansval:`\ foo),bar\\bam \ `\ .
+ option_name_no_current_plugin:
+ source:
+ - |-
+ O(foo) O(bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(foo=) O(bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(foo=bar) O(bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ O(bam.baz.foo#lookup:foo) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(bam.baz.foo#lookup:foo=) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(bam.baz.foo#lookup:foo=bar) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ O(ignore:foo) O(ignore:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(ignore:foo=) O(ignore:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(ignore:foo=bar) O(ignore:bar.baz[123].bam[len(x\) - 1]=bar)
+ html: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ html_plain: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ md: |-
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+ rst: |-
+ \ :ansopt:`foo`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`foo=`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`foo=bar`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo=`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo=bar`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansopt:`foo`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`foo=`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`foo=bar`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=bar`\
+ option_name_current_plugin:
+ source:
+ - |-
+ O(foo) O(bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(foo=) O(bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(foo=bar) O(bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ O(bam.baz.foo#lookup:foo) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(bam.baz.foo#lookup:foo=) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(bam.baz.foo#lookup:foo=bar) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ O(ignore:foo) O(ignore:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(ignore:foo=) O(ignore:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(ignore:foo=bar) O(ignore:bar.baz[123].bam[len(x\) - 1]=bar)
+ html: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ html_plain: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ md: |-
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+ rst: |-
+ \ :ansopt:`foo.bar.baz.bam#boo:foo`\ \ :ansopt:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`foo.bar.baz.bam#boo:foo=`\ \ :ansopt:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`foo.bar.baz.bam#boo:foo=bar`\ \ :ansopt:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo=`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo=bar`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansopt:`foo`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`foo=`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`foo=bar`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=bar`\
+ parse_opts:
+ current_plugin:
+ fqcn: foo.bar.baz.bam
+ type: boo
+ option_name_no_current_plugin_w_links:
+ source:
+ - |-
+ O(foo) O(bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(foo=) O(bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(foo=bar) O(bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ O(bam.baz.foo#lookup:foo) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(bam.baz.foo#lookup:foo=) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(bam.baz.foo#lookup:foo=bar) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ O(ignore:foo) O(ignore:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(ignore:foo=) O(ignore:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(ignore:foo=bar) O(ignore:bar.baz[123].bam[len(x\) - 1]=bar)
+ html: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ html_opts:
+ pluginOptionLikeLink.js: |-
+ (plugin, what, name, current_plugin) => `https://docs.ansible.com/ansible/devel/collections/${plugin.fqcn.replace(/\./g, '/')}_${plugin.type}.html#${what}-${name.join('/')}`
+ pluginOptionLikeLink.py: |-
+ lambda plugin, what, name, current_plugin: f"https://docs.ansible.com/ansible/devel/collections/{plugin.fqcn.replace('.', '/')}_{plugin.type}.html#{what}-{'/'.join(name)}"
+ html_plain: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ md: |-
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+ md_opts:
+ pluginOptionLikeLink.js: |-
+ (plugin, what, name, current_plugin) => `https://docs.ansible.com/ansible/devel/collections/${plugin.fqcn.replace(/\./g, '/')}_${plugin.type}.html#${what}-${name.join('/')}`
+ pluginOptionLikeLink.py: |-
+ lambda plugin, what, name, current_plugin: f"https://docs.ansible.com/ansible/devel/collections/{plugin.fqcn.replace('.', '/')}_{plugin.type}.html#{what}-{'/'.join(name)}"
+ rst: |-
+ \ :ansopt:`foo`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`foo=`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`foo=bar`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo=`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo=bar`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansopt:`foo`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`foo=`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`foo=bar`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=bar`\
+ option_name_current_plugin_w_links:
+ source:
+ - |-
+ O(foo) O(bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(foo=) O(bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(foo=bar) O(bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ O(bam.baz.foo#lookup:foo) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(bam.baz.foo#lookup:foo=) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(bam.baz.foo#lookup:foo=bar) O(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ O(ignore:foo) O(ignore:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ O(ignore:foo=) O(ignore:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ O(ignore:foo=bar) O(ignore:bar.baz[123].bam[len(x\) - 1]=bar)
+ html: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ html_opts:
+ pluginOptionLikeLink.js: |-
+ (plugin, what, name, current_plugin) => `https://docs.ansible.com/ansible/devel/collections/${plugin.fqcn.replace(/\./g, '/')}_${plugin.type}.html#${what}-${name.join('/')}`
+ pluginOptionLikeLink.py: |-
+ lambda plugin, what, name, current_plugin: f"https://docs.ansible.com/ansible/devel/collections/{plugin.fqcn.replace('.', '/')}_{plugin.type}.html#{what}-{'/'.join(name)}"
+ html_plain: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ md: |-
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+ md_opts:
+ pluginOptionLikeLink.js: |-
+ (plugin, what, name, current_plugin) => `https://docs.ansible.com/ansible/devel/collections/${plugin.fqcn.replace(/\./g, '/')}_${plugin.type}.html#${what}-${name.join('/')}`
+ pluginOptionLikeLink.py: |-
+ lambda plugin, what, name, current_plugin: f"https://docs.ansible.com/ansible/devel/collections/{plugin.fqcn.replace('.', '/')}_{plugin.type}.html#{what}-{'/'.join(name)}"
+ rst: |-
+ \ :ansopt:`foo.bar.baz.bam#boo:foo`\ \ :ansopt:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`foo.bar.baz.bam#boo:foo=`\ \ :ansopt:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`foo.bar.baz.bam#boo:foo=bar`\ \ :ansopt:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo=`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`bam.baz.foo#lookup:foo=bar`\ \ :ansopt:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansopt:`foo`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansopt:`foo=`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansopt:`foo=bar`\ \ :ansopt:`bar.baz[123].bam[len(x) - 1]=bar`\
+ parse_opts:
+ current_plugin:
+ fqcn: foo.bar.baz.bam
+ type: boo
+ return_value_no_current_plugin:
+ source:
+ - |-
+ RV(foo) RV(bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(foo=) RV(bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(foo=bar) RV(bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ RV(bam.baz.foo#lookup:foo) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(bam.baz.foo#lookup:foo=) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(bam.baz.foo#lookup:foo=bar) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ RV(ignore:foo) RV(ignore:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(ignore:foo=) RV(ignore:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(ignore:foo=bar) RV(ignore:bar.baz[123].bam[len(x\) - 1]=bar)
+ html: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ html_plain: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ md: |-
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+ rst: |-
+ \ :ansretval:`foo`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`foo=`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`foo=bar`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo=`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo=bar`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansretval:`foo`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`foo=`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`foo=bar`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=bar`\
+ return_value_current_plugin:
+ source:
+ - |-
+ RV(foo) RV(bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(foo=) RV(bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(foo=bar) RV(bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ RV(bam.baz.foo#lookup:foo) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(bam.baz.foo#lookup:foo=) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(bam.baz.foo#lookup:foo=bar) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ RV(ignore:foo) RV(ignore:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(ignore:foo=) RV(ignore:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(ignore:foo=bar) RV(ignore:bar.baz[123].bam[len(x\) - 1]=bar)
+ html: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ html_plain: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ md: |-
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+ rst: |-
+ \ :ansretval:`foo.bar.baz.bam#boo:foo`\ \ :ansretval:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`foo.bar.baz.bam#boo:foo=`\ \ :ansretval:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`foo.bar.baz.bam#boo:foo=bar`\ \ :ansretval:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo=`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo=bar`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansretval:`foo`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`foo=`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`foo=bar`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=bar`\
+ parse_opts:
+ current_plugin:
+ fqcn: foo.bar.baz.bam
+ type: boo
+ return_value_no_current_plugin_w_links:
+ source:
+ - |-
+ RV(foo) RV(bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(foo=) RV(bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(foo=bar) RV(bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ RV(bam.baz.foo#lookup:foo) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(bam.baz.foo#lookup:foo=) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(bam.baz.foo#lookup:foo=bar) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ RV(ignore:foo) RV(ignore:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(ignore:foo=) RV(ignore:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(ignore:foo=bar) RV(ignore:bar.baz[123].bam[len(x\) - 1]=bar)
+ html: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ html_opts:
+ pluginOptionLikeLink.js: |-
+ (plugin, what, name, current_plugin) => `https://docs.ansible.com/ansible/devel/collections/${plugin.fqcn.replace(/\./g, '/')}_${plugin.type}.html#${what}-${name.join('/')}`
+ pluginOptionLikeLink.py: |-
+ lambda plugin, what, name, current_plugin: f"https://docs.ansible.com/ansible/devel/collections/{plugin.fqcn.replace('.', '/')}_{plugin.type}.html#{what}-{'/'.join(name)}"
+ html_plain: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ md: |-
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+ md_opts:
+ pluginOptionLikeLink.js: |-
+ (plugin, what, name, current_plugin) => `https://docs.ansible.com/ansible/devel/collections/${plugin.fqcn.replace(/\./g, '/')}_${plugin.type}.html#${what}-${name.join('/')}`
+ pluginOptionLikeLink.py: |-
+ lambda plugin, what, name, current_plugin: f"https://docs.ansible.com/ansible/devel/collections/{plugin.fqcn.replace('.', '/')}_{plugin.type}.html#{what}-{'/'.join(name)}"
+ rst: |-
+ \ :ansretval:`foo`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`foo=`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`foo=bar`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo=`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo=bar`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansretval:`foo`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`foo=`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`foo=bar`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=bar`\
+ return_value_current_plugin_w_links:
+ source:
+ - |-
+ RV(foo) RV(bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(foo=) RV(bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(foo=bar) RV(bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ RV(bam.baz.foo#lookup:foo) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(bam.baz.foo#lookup:foo=) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(bam.baz.foo#lookup:foo=bar) RV(bam.baz.foo#lookup:bar.baz[123].bam[len(x\) - 1]=bar)
+ - |-
+ RV(ignore:foo) RV(ignore:bar.baz[123].bam[len(x\) - 1])
+ - |-
+ RV(ignore:foo=) RV(ignore:bar.baz[123].bam[len(x\) - 1]=)
+ - |-
+ RV(ignore:foo=bar) RV(ignore:bar.baz[123].bam[len(x\) - 1]=bar)
+ html: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ html_opts:
+ pluginOptionLikeLink.js: |-
+ (plugin, what, name, current_plugin) => `https://docs.ansible.com/ansible/devel/collections/${plugin.fqcn.replace(/\./g, '/')}_${plugin.type}.html#${what}-${name.join('/')}`
+ pluginOptionLikeLink.py: |-
+ lambda plugin, what, name, current_plugin: f"https://docs.ansible.com/ansible/devel/collections/{plugin.fqcn.replace('.', '/')}_{plugin.type}.html#{what}-{'/'.join(name)}"
+ html_plain: |-
+ foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
foo
bar.baz[123].bam[len(x) - 1]
foo=
bar.baz[123].bam[len(x) - 1]=
foo=bar
bar.baz[123].bam[len(x) - 1]=bar
+ md: |-
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+
+ foo
bar.baz\[123\].bam\[len\(x\) \- 1\]
+
+ foo\=
bar.baz\[123\].bam\[len\(x\) \- 1\]\=
+
+ foo\=bar
bar.baz\[123\].bam\[len\(x\) \- 1\]\=bar
+ md_opts:
+ pluginOptionLikeLink.js: |-
+ (plugin, what, name, current_plugin) => `https://docs.ansible.com/ansible/devel/collections/${plugin.fqcn.replace(/\./g, '/')}_${plugin.type}.html#${what}-${name.join('/')}`
+ pluginOptionLikeLink.py: |-
+ lambda plugin, what, name, current_plugin: f"https://docs.ansible.com/ansible/devel/collections/{plugin.fqcn.replace('.', '/')}_{plugin.type}.html#{what}-{'/'.join(name)}"
+ rst: |-
+ \ :ansretval:`foo.bar.baz.bam#boo:foo`\ \ :ansretval:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`foo.bar.baz.bam#boo:foo=`\ \ :ansretval:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`foo.bar.baz.bam#boo:foo=bar`\ \ :ansretval:`foo.bar.baz.bam#boo:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo=`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`bam.baz.foo#lookup:foo=bar`\ \ :ansretval:`bam.baz.foo#lookup:bar.baz[123].bam[len(x) - 1]=bar`\
+
+ \ :ansretval:`foo`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]`\
+
+ \ :ansretval:`foo=`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=`\
+
+ \ :ansretval:`foo=bar`\ \ :ansretval:`bar.baz[123].bam[len(x) - 1]=bar`\
+ parse_opts:
+ current_plugin:
+ fqcn: foo.bar.baz.bam
+ type: boo
+ errors:
+ source:
+ - P(foo)
+ - C(foo
+ - R(foo,bar
+ html: |-
+ ERROR while parsing: While parsing P() at index 1 of paragraph 1: Parameter "foo" is not of the form FQCN#type
ERROR while parsing: While parsing C() at index 1 of paragraph 2: Cannot find closing ")" after last parameter
ERROR while parsing: While parsing R() at index 1 of paragraph 3: Cannot find closing ")" after last parameter
+ html_plain: |-
+ ERROR while parsing: While parsing P() at index 1 of paragraph 1: Parameter "foo" is not of the form FQCN#type
ERROR while parsing: While parsing C() at index 1 of paragraph 2: Cannot find closing ")" after last parameter
ERROR while parsing: While parsing R() at index 1 of paragraph 3: Cannot find closing ")" after last parameter
+ md: |-
+ ERROR while parsing: While parsing P\(\) at index 1 of paragraph 1\: Parameter \"foo\" is not of the form FQCN\#type
+
+ ERROR while parsing: While parsing C\(\) at index 1 of paragraph 2\: Cannot find closing \"\)\" after last parameter
+
+ ERROR while parsing: While parsing R\(\) at index 1 of paragraph 3\: Cannot find closing \"\)\" after last parameter
+ rst: |-
+ \ :strong:`ERROR while parsing`\ : While parsing P() at index 1 of paragraph 1: Parameter "foo" is not of the form FQCN#type\
+
+ \ :strong:`ERROR while parsing`\ : While parsing C() at index 1 of paragraph 2: Cannot find closing ")" after last parameter\
+
+ \ :strong:`ERROR while parsing`\ : While parsing R() at index 1 of paragraph 3: Cannot find closing ")" after last parameter\
diff --git a/tests/units/markup/test_vectors.py b/tests/units/markup/test_vectors.py
new file mode 100644
index 00000000..7e1e350e
--- /dev/null
+++ b/tests/units/markup/test_vectors.py
@@ -0,0 +1,111 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2022, Ansible Project
+
+import typing as t
+
+import pytest
+
+from antsibull_core.yaml import load_yaml_file
+
+from antsibull_docs.markup import dom
+from antsibull_docs.markup.parser import parse, Context
+from antsibull_docs.markup.html import to_html
+from antsibull_docs.markup.md import to_md
+from antsibull_docs.markup.rst import to_rst
+from antsibull_docs.markup.format import LinkProvider
+
+
+class _TestLinkProvider(LinkProvider):
+ _plugin_link = None
+ _plugin_option_like_link = None
+
+ def plugin_link(self, plugin: dom.PluginIdentifier) -> t.Optional[str]:
+ if self._plugin_link is not None:
+ return self._plugin_link(plugin)
+ return None
+
+ def plugin_option_like_link(self,
+ plugin: dom.PluginIdentifier,
+ what: "t.Union[t.Literal['option'], t.Literal['retval']]",
+ name: t.List[str], current_plugin: bool) -> t.Optional[str]:
+ if self._plugin_option_like_link is not None:
+ return self._plugin_option_like_link(plugin, what, name, current_plugin)
+ return None
+
+ def _update(self, config: t.Mapping[str, t.Any]):
+ if 'pluginLink.py' in config:
+ self._plugin_link = eval(config['pluginLink.py'])
+ if 'pluginOptionLikeLink.py' in config:
+ self._plugin_option_like_link = eval(config['pluginOptionLikeLink.py'])
+
+
+TEST_DATA = sorted(load_yaml_file('test-vectors.yaml')['test_vectors'].items())
+
+
+@pytest.mark.parametrize('test_name, test_data', TEST_DATA, ids=[test_name for test_name, test_data in TEST_DATA])
+def test_vectors(test_name: str, test_data: t.Mapping[str, t.Any]) -> None:
+ parse_opts = {}
+ context_opts = {}
+ if test_data.get('parse_opts'):
+ if 'current_plugin' in test_data['parse_opts']:
+ context_opts['current_plugin'] = dom.PluginIdentifier(
+ fqcn=test_data['parse_opts']['current_plugin']['fqcn'],
+ type=test_data['parse_opts']['current_plugin']['type'],
+ )
+ if 'errors' in test_data['parse_opts']:
+ context_opts['errors'] = test_data['parse_opts']['errors']
+ if 'onlyClassicMarkup' in test_data['parse_opts']:
+ context_opts['only_classic_markup'] = test_data['parse_opts']['onlyClassicMarkup']
+ parsed = parse(test_data['source'], Context(**context_opts), **parse_opts)
+
+ html_opts = {}
+ html_link_provider = _TestLinkProvider()
+ if test_data.get('html_opts'):
+ if 'parStart' in test_data['html_opts']:
+ html_opts['par_start'] = test_data['html_opts']['parStart']
+ if 'parEnd' in test_data['html_opts']:
+ html_opts['par_end'] = test_data['html_opts']['parEnd']
+ if 'current_plugin' in test_data['html_opts']:
+ rst_opts['current_plugin'] = dom.PluginIdentifier(
+ fqcn=test_data['html_opts']['current_plugin']['fqcn'],
+ type=test_data['html_opts']['current_plugin']['type'],
+ )
+ html_link_provider._update(test_data['html_opts'])
+
+ md_opts = {}
+ md_link_provider = _TestLinkProvider()
+ if test_data.get('md_opts'):
+ if 'current_plugin' in test_data['md_opts']:
+ rst_opts['current_plugin'] = dom.PluginIdentifier(
+ fqcn=test_data['md_opts']['current_plugin']['fqcn'],
+ type=test_data['md_opts']['current_plugin']['type'],
+ )
+ md_link_provider._update(test_data['md_opts'])
+
+ rst_opts = {}
+ if test_data.get('rst_opts'):
+ if 'current_plugin' in test_data['rst_opts']:
+ rst_opts['current_plugin'] = dom.PluginIdentifier(
+ fqcn=test_data['rst_opts']['current_plugin']['fqcn'],
+ type=test_data['rst_opts']['current_plugin']['type'],
+ )
+
+ if 'html' in test_data:
+ result = to_html(parsed, link_provider=html_link_provider, **html_opts)
+ assert result == test_data['html']
+
+ if 'html_plain' in test_data:
+ ...
+ # TODO result = to_html(parsed, link_provider=html_link_provider, **html_opts)
+ # TODO assert result == test_data['html_plain']
+
+ if 'md' in test_data:
+ print('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')
+ result = to_md(parsed, link_provider=md_link_provider, **md_opts)
+ assert result == test_data['md']
+
+ if 'rst' in test_data:
+ result = to_rst(parsed, **rst_opts)
+ assert result == test_data['rst']
From b4104d4cedd804350a4273bd5324df0b61599219 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Sat, 18 Mar 2023 18:04:42 +0100
Subject: [PATCH 11/15] Cleanup.
---
src/antsibull_docs/markup/_counter.py | 85 ++++++++++++++++++++
src/antsibull_docs/markup/htmlify.py | 2 +-
src/antsibull_docs/markup/rst.py | 7 --
src/antsibull_docs/markup/rstify.py | 82 ++-----------------
src/antsibull_docs/markup/semantic_helper.py | 52 ++++--------
5 files changed, 110 insertions(+), 118 deletions(-)
create mode 100644 src/antsibull_docs/markup/_counter.py
diff --git a/src/antsibull_docs/markup/_counter.py b/src/antsibull_docs/markup/_counter.py
new file mode 100644
index 00000000..a2414571
--- /dev/null
+++ b/src/antsibull_docs/markup/_counter.py
@@ -0,0 +1,85 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2022, Ansible Project
+"""
+Count markup instructions in parsed markup.
+"""
+
+import typing as t
+
+from . import dom
+
+
+class _Counter(dom.Walker):
+ counts: t.Dict[str, int]
+
+ def __init__(self):
+ self.counts = {
+ 'italic': 0,
+ 'bold': 0,
+ 'module': 0,
+ 'plugin': 0,
+ 'link': 0,
+ 'url': 0,
+ 'ref': 0,
+ 'const': 0,
+ 'option-name': 0,
+ 'option-value': 0,
+ 'environment-var': 0,
+ 'return-value': 0,
+ 'ruler': 0,
+ }
+
+ def process_error(self, part: dom.ErrorPart) -> None:
+ pass
+
+ def process_bold(self, part: dom.BoldPart) -> None:
+ self.counts['bold'] += 1
+
+ def process_code(self, part: dom.CodePart) -> None:
+ self.counts['const'] += 1
+
+ def process_horizontal_line(self, part: dom.HorizontalLinePart) -> None:
+ self.counts['ruler'] += 1
+
+ def process_italic(self, part: dom.ItalicPart) -> None:
+ self.counts['italic'] += 1
+
+ def process_link(self, part: dom.LinkPart) -> None:
+ self.counts['link'] += 1
+
+ def process_module(self, part: dom.ModulePart) -> None:
+ self.counts['module'] += 1
+
+ def process_rst_ref(self, part: dom.RSTRefPart) -> None:
+ self.counts['ref'] += 1
+
+ def process_url(self, part: dom.URLPart) -> None:
+ self.counts['url'] += 1
+
+ def process_text(self, part: dom.TextPart) -> None:
+ pass
+
+ def process_env_variable(self, part: dom.EnvVariablePart) -> None:
+ self.counts['environment-var'] += 1
+
+ def process_option_name(self, part: dom.OptionNamePart) -> None:
+ self.counts['option-name'] += 1
+
+ def process_option_value(self, part: dom.OptionValuePart) -> None:
+ self.counts['option-value'] += 1
+
+ def process_plugin(self, part: dom.PluginPart) -> None:
+ self.counts['plugin'] += 1
+
+ def process_return_value(self, part: dom.ReturnValuePart) -> None:
+ self.counts['return-value'] += 1
+
+
+def count(paragraphs: t.Sequence[dom.Paragraph]) -> t.Dict[str, int]:
+ counter = _Counter()
+ for paragraph in paragraphs:
+ dom.walk(paragraph, counter)
+ return counter.counts
diff --git a/src/antsibull_docs/markup/htmlify.py b/src/antsibull_docs/markup/htmlify.py
index 809d1677..02c82baa 100644
--- a/src/antsibull_docs/markup/htmlify.py
+++ b/src/antsibull_docs/markup/htmlify.py
@@ -11,10 +11,10 @@
from urllib.parse import quote
from . import dom
+from ._counter import count as _count
from .parser import parse, Context
from .html import to_html
from .format import LinkProvider
-from .rstify import _count
class _HTMLLinkProvider(LinkProvider):
diff --git a/src/antsibull_docs/markup/rst.py b/src/antsibull_docs/markup/rst.py
index 363e72df..0d177204 100644
--- a/src/antsibull_docs/markup/rst.py
+++ b/src/antsibull_docs/markup/rst.py
@@ -31,13 +31,6 @@ def rst_escape(value: str, escape_ending_whitespace=False) -> str:
return value
-def rst_code(value: str) -> str:
- ''' Write value as :code:`...` RST construct. '''
- if not isinstance(value, str):
- value = str(value)
- return f':code:`{rst_escape(value, escape_ending_whitespace=True)}`'
-
-
class RSTFormatter(Formatter):
@staticmethod
def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
diff --git a/src/antsibull_docs/markup/rstify.py b/src/antsibull_docs/markup/rstify.py
index cc2645b2..13f6761c 100644
--- a/src/antsibull_docs/markup/rstify.py
+++ b/src/antsibull_docs/markup/rstify.py
@@ -9,8 +9,9 @@
import typing as t
from . import dom
+from ._counter import count as _count
from .parser import parse, Context
-from .rst import rst_escape as _rst_escape, rst_code as _rst_code, to_rst
+from .rst import rst_escape as _rst_escape, to_rst
def rst_escape(value: t.Any, escape_ending_whitespace=False) -> str:
@@ -21,80 +22,11 @@ def rst_escape(value: t.Any, escape_ending_whitespace=False) -> str:
return _rst_escape(value, escape_ending_whitespace=escape_ending_whitespace)
-rst_code = _rst_code
-
-
-class _Counter(dom.Walker):
- counts: t.Dict[str, int]
-
- def __init__(self):
- self.counts = {
- 'italic': 0,
- 'bold': 0,
- 'module': 0,
- 'plugin': 0,
- 'link': 0,
- 'url': 0,
- 'ref': 0,
- 'const': 0,
- 'option-name': 0,
- 'option-value': 0,
- 'environment-var': 0,
- 'return-value': 0,
- 'ruler': 0,
- }
-
- def process_error(self, part: dom.ErrorPart) -> None:
- pass
-
- def process_bold(self, part: dom.BoldPart) -> None:
- self.counts['bold'] += 1
-
- def process_code(self, part: dom.CodePart) -> None:
- self.counts['const'] += 1
-
- def process_horizontal_line(self, part: dom.HorizontalLinePart) -> None:
- self.counts['ruler'] += 1
-
- def process_italic(self, part: dom.ItalicPart) -> None:
- self.counts['italic'] += 1
-
- def process_link(self, part: dom.LinkPart) -> None:
- self.counts['link'] += 1
-
- def process_module(self, part: dom.ModulePart) -> None:
- self.counts['module'] += 1
-
- def process_rst_ref(self, part: dom.RSTRefPart) -> None:
- self.counts['ref'] += 1
-
- def process_url(self, part: dom.URLPart) -> None:
- self.counts['url'] += 1
-
- def process_text(self, part: dom.TextPart) -> None:
- pass
-
- def process_env_variable(self, part: dom.EnvVariablePart) -> None:
- self.counts['environment-var'] += 1
-
- def process_option_name(self, part: dom.OptionNamePart) -> None:
- self.counts['option-name'] += 1
-
- def process_option_value(self, part: dom.OptionValuePart) -> None:
- self.counts['option-value'] += 1
-
- def process_plugin(self, part: dom.PluginPart) -> None:
- self.counts['plugin'] += 1
-
- def process_return_value(self, part: dom.ReturnValuePart) -> None:
- self.counts['return-value'] += 1
-
-
-def _count(paragraphs: t.Sequence[dom.Paragraph]) -> t.Dict[str, int]:
- counter = _Counter()
- for paragraph in paragraphs:
- dom.walk(paragraph, counter)
- return counter.counts
+def rst_code(value: str) -> str:
+ ''' Write value as :code:`...` RST construct. '''
+ if not isinstance(value, str):
+ value = str(value)
+ return f':code:`{rst_escape(value, escape_ending_whitespace=True)}`'
def rst_ify(text: str,
diff --git a/src/antsibull_docs/markup/semantic_helper.py b/src/antsibull_docs/markup/semantic_helper.py
index bdbe102f..08e9e803 100644
--- a/src/antsibull_docs/markup/semantic_helper.py
+++ b/src/antsibull_docs/markup/semantic_helper.py
@@ -10,6 +10,7 @@
import re
import typing as t
+
_ARRAY_STUB_RE = re.compile(r'\[([^\]]*)\]')
_FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([a-z]+):(.*)$')
_IGNORE_MARKER = 'ignore:'
@@ -19,9 +20,9 @@ def _remove_array_stubs(text: str) -> str:
return _ARRAY_STUB_RE.sub('', text)
-def parse_option(text: str, plugin_fqcn: t.Optional[str], plugin_type: t.Optional[str],
- require_plugin=False
- ) -> t.Tuple[t.Optional[str], t.Optional[str], str, str, t.Optional[str]]:
+def _parse(text: str, plugin_fqcn: t.Optional[str], plugin_type: t.Optional[str], what: str,
+ require_plugin=False
+ ) -> t.Tuple[t.Optional[str], t.Optional[str], str, str, t.Optional[str]]:
"""
Given the contents of O(...) / :ansopt:`...` with potential escaping removed,
split it into plugin FQCN, plugin type, option link name, option name, and option value.
@@ -41,45 +42,26 @@ def parse_option(text: str, plugin_fqcn: t.Optional[str], plugin_type: t.Optiona
plugin_type = ''
text = text[len(_IGNORE_MARKER):]
if ':' in text or '#' in text:
- raise ValueError(f'Invalid option name "{text}"')
+ raise ValueError(f'Invalid {what} "{text}"')
return plugin_fqcn, plugin_type, _remove_array_stubs(text), text, value
-def parse_return_value(text: str, plugin_fqcn: t.Optional[str], plugin_type: t.Optional[str],
- require_plugin=False
- ) -> t.Tuple[t.Optional[str], t.Optional[str], str, str, t.Optional[str]]:
+def parse_option(text: str, plugin_fqcn: t.Optional[str], plugin_type: t.Optional[str],
+ require_plugin=False
+ ) -> t.Tuple[t.Optional[str], t.Optional[str], str, str, t.Optional[str]]:
"""
- Given the contents of RV(...) / :ansretval:`...` with potential escaping removed,
+ Given the contents of O(...) / :ansopt:`...` with potential escaping removed,
split it into plugin FQCN, plugin type, option link name, option name, and option value.
"""
- value = None
- if '=' in text:
- text, value = text.split('=', 1)
- m = _FQCN_TYPE_PREFIX_RE.match(text)
- if m:
- plugin_fqcn = m.group(1)
- plugin_type = m.group(2)
- text = m.group(3)
- elif require_plugin:
- raise ValueError('Cannot extract plugin name and type')
- elif text.startswith(_IGNORE_MARKER):
- plugin_fqcn = ''
- plugin_type = ''
- text = text[len(_IGNORE_MARKER):]
- if ':' in text or '#' in text:
- raise ValueError(f'Invalid return value name "{text}"')
- return plugin_fqcn, plugin_type, _remove_array_stubs(text), text, value
+ return _parse(text, plugin_fqcn, plugin_type, 'option name', require_plugin=require_plugin)
-def augment_plugin_name_type(text: str, plugin_fqcn: t.Optional[str], plugin_type: t.Optional[str]
- ) -> str:
+def parse_return_value(text: str, plugin_fqcn: t.Optional[str], plugin_type: t.Optional[str],
+ require_plugin=False
+ ) -> t.Tuple[t.Optional[str], t.Optional[str], str, str, t.Optional[str]]:
"""
- Given the text contents of O(...) or RV(...) and a plugin's FQCN and type, insert
- the FQCN and type if they are not already present.
+ Given the contents of RV(...) / :ansretval:`...` with potential escaping removed,
+ split it into plugin FQCN, plugin type, option link name, option name, and option value.
"""
- value = None
- if '=' in text:
- text, value = text.split('=', 1)
- if ':' not in text and plugin_fqcn and plugin_type:
- text = f'{plugin_fqcn}#{plugin_type}:{text}'
- return text if value is None else f'{text}={value}'
+ return _parse(
+ text, plugin_fqcn, plugin_type, 'return value name', require_plugin=require_plugin)
From 20241ca0c0029d8aefd541f21dccbf3e2146c762 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Sat, 18 Mar 2023 22:02:30 +0100
Subject: [PATCH 12/15] Add more tests.
---
src/antsibull_docs/markup/dom.py | 34 ++++----
src/antsibull_docs/markup/format.py | 30 +++----
src/antsibull_docs/markup/parser.py | 2 +-
tests/units/markup/test_counter.py | 122 ++++++++++++++++++++++++++++
tests/units/markup/test_dom.py | 13 +++
tests/units/markup/test_parser.py | 15 ++++
6 files changed, 184 insertions(+), 32 deletions(-)
create mode 100644 tests/units/markup/test_counter.py
diff --git a/src/antsibull_docs/markup/dom.py b/src/antsibull_docs/markup/dom.py
index 7beb05f6..52403a7e 100644
--- a/src/antsibull_docs/markup/dom.py
+++ b/src/antsibull_docs/markup/dom.py
@@ -20,7 +20,7 @@
ErrorType = t.Union[t.Literal['ignore'], t.Literal['message'], t.Literal['exception']]
else:
# Python 3.6/3.7 do not have t.Literal
- ErrorType = str
+ ErrorType = str # pragma: no cover
class PluginIdentifier(NamedTuple):
@@ -157,63 +157,63 @@ class Walker(abc.ABC):
@abc.abstractmethod
def process_error(self, part: ErrorPart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_bold(self, part: BoldPart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_code(self, part: CodePart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_horizontal_line(self, part: HorizontalLinePart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_italic(self, part: ItalicPart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_link(self, part: LinkPart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_module(self, part: ModulePart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_rst_ref(self, part: RSTRefPart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_url(self, part: URLPart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_text(self, part: TextPart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_env_variable(self, part: EnvVariablePart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_option_name(self, part: OptionNamePart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_option_value(self, part: OptionValuePart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_plugin(self, part: PluginPart) -> None:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def process_return_value(self, part: ReturnValuePart) -> None:
- pass
+ pass # pragma: no cover
class NoopWalker(Walker):
@@ -304,3 +304,5 @@ def walk(paragraph: Paragraph, walker: Walker) -> None: # noqa: C901
walker.process_plugin(t.cast(PluginPart, part))
elif part.type == PartType.RETURN_VALUE:
walker.process_return_value(t.cast(ReturnValuePart, part))
+ else:
+ raise RuntimeError(f'Internal error: unknown type {part.type!r}')
diff --git a/src/antsibull_docs/markup/format.py b/src/antsibull_docs/markup/format.py
index 91d88e4a..70da5d50 100644
--- a/src/antsibull_docs/markup/format.py
+++ b/src/antsibull_docs/markup/format.py
@@ -46,63 +46,63 @@ class Formatter(abc.ABC):
@abc.abstractmethod
def format_error(self, part: dom.ErrorPart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_bold(self, part: dom.BoldPart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_code(self, part: dom.CodePart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_horizontal_line(self, part: dom.HorizontalLinePart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_italic(self, part: dom.ItalicPart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_link(self, part: dom.LinkPart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_module(self, part: dom.ModulePart, url: t.Optional[str]) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_rst_ref(self, part: dom.RSTRefPart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_url(self, part: dom.URLPart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_text(self, part: dom.TextPart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_env_variable(self, part: dom.EnvVariablePart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_option_name(self, part: dom.OptionNamePart, url: t.Optional[str]) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_option_value(self, part: dom.OptionValuePart) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_plugin(self, part: dom.PluginPart, url: t.Optional[str]) -> str:
- pass
+ pass # pragma: no cover
@abc.abstractmethod
def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -> str:
- pass
+ pass # pragma: no cover
class _FormatWalker(dom.Walker):
diff --git a/src/antsibull_docs/markup/parser.py b/src/antsibull_docs/markup/parser.py
index 9af78774..90f5817b 100644
--- a/src/antsibull_docs/markup/parser.py
+++ b/src/antsibull_docs/markup/parser.py
@@ -49,7 +49,7 @@ def __init__(self, command: str, parameters: int, escaped_arguments: bool = Fals
@abc.abstractmethod
def parse(self, parameters: t.List[str], context: Context) -> dom.AnyPart:
- pass
+ pass # pragma: no cover
class CommandParserEx(CommandParser):
diff --git a/tests/units/markup/test_counter.py b/tests/units/markup/test_counter.py
new file mode 100644
index 00000000..2df1a156
--- /dev/null
+++ b/tests/units/markup/test_counter.py
@@ -0,0 +1,122 @@
+# Author: Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: 2023, Ansible Project
+
+import typing as t
+
+import pytest
+
+from antsibull_docs.markup import dom
+from antsibull_docs.markup._counter import count
+
+
+TEST_COUNTER = [
+ (
+ [],
+ {
+ 'italic': 0,
+ 'bold': 0,
+ 'module': 0,
+ 'plugin': 0,
+ 'link': 0,
+ 'url': 0,
+ 'ref': 0,
+ 'const': 0,
+ 'option-name': 0,
+ 'option-value': 0,
+ 'environment-var': 0,
+ 'return-value': 0,
+ 'ruler': 0,
+ },
+ ),
+ (
+ [dom.ErrorPart(message='foo')],
+ {
+ 'italic': 0,
+ 'bold': 0,
+ 'module': 0,
+ 'plugin': 0,
+ 'link': 0,
+ 'url': 0,
+ 'ref': 0,
+ 'const': 0,
+ 'option-name': 0,
+ 'option-value': 0,
+ 'environment-var': 0,
+ 'return-value': 0,
+ 'ruler': 0,
+ },
+ ),
+ (
+ [
+ dom.TextPart(text='foo '),
+ dom.ItalicPart(text='bar'),
+ dom.TextPart(text=' baz '),
+ dom.CodePart(text=' bam '),
+ dom.TextPart(text=' '),
+ dom.BoldPart(text=' ( boo '),
+ dom.TextPart(text=' ) '),
+ dom.URLPart(url='https://example.com/?foo=bar'),
+ dom.HorizontalLinePart(),
+ dom.TextPart(text=' '),
+ dom.LinkPart(text='foo', url='https://bar.com'),
+ dom.TextPart(text=' '),
+ dom.RSTRefPart(text=' a', ref='b '),
+ dom.ModulePart(fqcn='foo.bar.baz'),
+ dom.TextPart(text='HORIZONTALLINEx '),
+ dom.ModulePart(fqcn='foo.bar.baz.bam'),
+ ],
+ {
+ 'italic': 1,
+ 'bold': 1,
+ 'module': 2,
+ 'plugin': 0,
+ 'link': 1,
+ 'url': 1,
+ 'ref': 1,
+ 'const': 1,
+ 'option-name': 0,
+ 'option-value': 0,
+ 'environment-var': 0,
+ 'return-value': 0,
+ 'ruler': 1,
+ },
+ ),
+ (
+ [
+ dom.TextPart(text='foo '),
+ dom.EnvVariablePart(name='a),b'),
+ dom.TextPart(text=' '),
+ dom.PluginPart(plugin=dom.PluginIdentifier(fqcn='foo.bar.baz', type='bam')),
+ dom.TextPart(text=' baz '),
+ dom.OptionValuePart(value=' b,na)\\m, '),
+ dom.TextPart(text=' '),
+ dom.OptionNamePart(plugin=None, link=['foo'], name='foo', value=None),
+ dom.TextPart(text=' '),
+ dom.ReturnValuePart(plugin=None, link=['bar', 'baz'], name='bar.baz[1]', value=None),
+ ],
+ {
+ 'italic': 0,
+ 'bold': 0,
+ 'module': 0,
+ 'plugin': 1,
+ 'link': 0,
+ 'url': 0,
+ 'ref': 0,
+ 'const': 0,
+ 'option-name': 1,
+ 'option-value': 1,
+ 'environment-var': 1,
+ 'return-value': 1,
+ 'ruler': 0,
+ },
+ ),
+]
+
+
+@pytest.mark.parametrize('paragraph, counter', TEST_COUNTER)
+def test_count(paragraph: dom.Paragraph, counter: t.Dict[str, int]) -> None:
+ result = count([paragraph])
+ assert result == counter
diff --git a/tests/units/markup/test_dom.py b/tests/units/markup/test_dom.py
index ec2258b5..c25a3394 100644
--- a/tests/units/markup/test_dom.py
+++ b/tests/units/markup/test_dom.py
@@ -109,6 +109,7 @@ def process_return_value(self, part: dom.ReturnValuePart) -> None:
dom.TextPart(text=' '),
dom.OptionNamePart(plugin=None, link=['foo'], name='foo', value=None),
dom.TextPart(text=' '),
+ dom.ReturnValuePart(plugin=None, link=['bar', 'baz'], name='bar.baz[1]', value=None),
],
]
@@ -119,3 +120,15 @@ def test_walk(data: dom.Paragraph) -> None:
dom.walk(data, walker)
assert walker.result == data
+ # The following has no side-effect (except increasing line coverage)
+ walker = dom.NoopWalker()
+ dom.walk(data, walker)
+
+
+def test_internal_error() -> None:
+ class FakePart(t.NamedTuple):
+ type: int = 23
+
+ with pytest.raises(RuntimeError) as exc:
+ dom.walk([FakePart()], dom.NoopWalker())
+ assert str(exc.value) == 'Internal error: unknown type 23'
diff --git a/tests/units/markup/test_parser.py b/tests/units/markup/test_parser.py
index 02182152..23b4ac41 100644
--- a/tests/units/markup/test_parser.py
+++ b/tests/units/markup/test_parser.py
@@ -347,3 +347,18 @@ def test_parse_bad(paragraphs: t.Union[str, t.List[str]], context: Context, kwar
with pytest.raises(ValueError) as exc:
parse(paragraphs, context, **kwargs)
assert str(exc.value) == exc_message
+
+
+TEST_TRIVIAL_PARSER = [
+ '',
+ 'foo',
+ 'I(foo) B(bar) HORIZONTALLINE C(baz)'
+]
+
+
+@pytest.mark.parametrize('input', TEST_TRIVIAL_PARSER)
+def test_trivial_parser(input: str) -> None:
+ parser = Parser([])
+ result = parser.parse_string(input, Context())
+ expected = [dom.TextPart(text=input)] if input else []
+ assert result == expected
From 4722f470b0833410305064591d7e2ba95ffe0b7d Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Sun, 19 Mar 2023 15:17:15 +0100
Subject: [PATCH 13/15] Add changelog fragment.
---
changelogs/fragments/108-markup.yml | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 changelogs/fragments/108-markup.yml
diff --git a/changelogs/fragments/108-markup.yml b/changelogs/fragments/108-markup.yml
new file mode 100644
index 00000000..43e65174
--- /dev/null
+++ b/changelogs/fragments/108-markup.yml
@@ -0,0 +1,2 @@
+minor_change:
+ - Internal refactoring of markup code (https://github.com/ansible-community/antsibull-docs/pull/108).
From 65d256706e654dc0c6ed5406e4ee8b9aeeab7e58 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Sun, 19 Mar 2023 15:23:18 +0100
Subject: [PATCH 14/15] Add plain HTML output.
---
src/antsibull_docs/markup/html.py | 100 ++++++++++++++++++++++++++++-
tests/units/markup/test_html.py | 9 ++-
tests/units/markup/test_vectors.py | 8 +--
3 files changed, 108 insertions(+), 9 deletions(-)
diff --git a/src/antsibull_docs/markup/html.py b/src/antsibull_docs/markup/html.py
index 11abbd35..57bb4a43 100644
--- a/src/antsibull_docs/markup/html.py
+++ b/src/antsibull_docs/markup/html.py
@@ -26,7 +26,7 @@ def _url_escape(url: str) -> str:
return quote(url, safe=':/#?%<>[]{}')
-class HTMLFormatter(Formatter):
+class AntsibullHTMLFormatter(Formatter):
@staticmethod
def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
url: t.Optional[str]) -> str:
@@ -120,11 +120,85 @@ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -
return self._format_option_like(part, url)
-DEFAULT_FORMATTER = HTMLFormatter()
+class PlainHTMLFormatter(Formatter):
+ @staticmethod
+ def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
+ url: t.Optional[str]) -> str:
+ link_start = ''
+ link_end = ''
+ if url:
+ link_start = f''
+ link_end = ''
+ strong_start = ''
+ strong_end = ''
+ if part.type == dom.PartType.OPTION_NAME and part.value is None:
+ strong_start = ''
+ strong_end = ''
+ if part.value is None:
+ text = part.name
+ else:
+ text = f'{part.name}={part.value}'
+ return f'{strong_start}{link_start}{html_escape(text)}{link_end}{strong_end}
'
+
+ def format_error(self, part: dom.ErrorPart) -> str:
+ return f'ERROR while parsing: {html_escape(part.message)}'
+
+ def format_bold(self, part: dom.BoldPart) -> str:
+ return f'{html_escape(part.text)}'
+
+ def format_code(self, part: dom.CodePart) -> str:
+ return f"{html_escape(part.text)}
"
+
+ def format_horizontal_line(self, part: dom.HorizontalLinePart) -> str:
+ return '
'
+
+ def format_italic(self, part: dom.ItalicPart) -> str:
+ return f'{html_escape(part.text)}'
+
+ def format_link(self, part: dom.LinkPart) -> str:
+ return f"{html_escape(part.text)}"
+
+ def format_module(self, part: dom.ModulePart, url: t.Optional[str]) -> str:
+ if not url:
+ return f"{html_escape(part.fqcn)}"
+ return f"{html_escape(part.fqcn)}"
+
+ def format_rst_ref(self, part: dom.RSTRefPart) -> str:
+ return f"{html_escape(part.text)}"
+
+ def format_url(self, part: dom.URLPart) -> str:
+ return (
+ f""
+ f'{html_escape(_url_escape(part.url))}'
+ )
+
+ def format_text(self, part: dom.TextPart) -> str:
+ return html_escape(part.text)
+
+ def format_env_variable(self, part: dom.EnvVariablePart) -> str:
+ return f'{html_escape(part.name)}
'
+
+ def format_option_name(self, part: dom.OptionNamePart, url: t.Optional[str]) -> str:
+ return self._format_option_like(part, url)
+
+ def format_option_value(self, part: dom.OptionValuePart) -> str:
+ return f'{html_escape(part.value)}
'
+
+ def format_plugin(self, part: dom.PluginPart, url: t.Optional[str]) -> str:
+ if not url:
+ return f"{html_escape(part.plugin.fqcn)}"
+ return f"{html_escape(part.plugin.fqcn)}"
+
+ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -> str:
+ return self._format_option_like(part, url)
+
+
+DEFAULT_ANTSIBULL_FORMATTER = AntsibullHTMLFormatter()
+DEFAULT_PLAIN_FORMATTER = PlainHTMLFormatter()
def to_html(paragraphs: t.Sequence[dom.Paragraph],
- formatter: Formatter = DEFAULT_FORMATTER,
+ formatter: Formatter = DEFAULT_ANTSIBULL_FORMATTER,
link_provider: t.Optional[LinkProvider] = None,
par_start: str = '',
par_end: str = '
',
@@ -141,3 +215,23 @@ def to_html(paragraphs: t.Sequence[dom.Paragraph],
par_empty=par_empty,
current_plugin=current_plugin,
)
+
+
+def to_html_plain(paragraphs: t.Sequence[dom.Paragraph],
+ formatter: Formatter = DEFAULT_PLAIN_FORMATTER,
+ link_provider: t.Optional[LinkProvider] = None,
+ par_start: str = '',
+ par_end: str = '
',
+ par_sep: str = '',
+ par_empty: str = '',
+ current_plugin: t.Optional[dom.PluginIdentifier] = None) -> str:
+ return _format_paragraphs(
+ paragraphs,
+ formatter=formatter,
+ link_provider=link_provider,
+ par_start=par_start,
+ par_end=par_end,
+ par_sep=par_sep,
+ par_empty=par_empty,
+ current_plugin=current_plugin,
+ )
diff --git a/tests/units/markup/test_html.py b/tests/units/markup/test_html.py
index 5f9a427b..de1f8f60 100644
--- a/tests/units/markup/test_html.py
+++ b/tests/units/markup/test_html.py
@@ -5,7 +5,7 @@
# SPDX-FileCopyrightText: 2023, Ansible Project
from antsibull_docs.markup import dom
-from antsibull_docs.markup.html import html_escape, to_html
+from antsibull_docs.markup.html import html_escape, to_html, to_html_plain
def test_html_escape():
@@ -17,3 +17,10 @@ def test_to_html():
assert to_html([]) == ''
assert to_html([[dom.TextPart(text='test')]]) == 'test
'
assert to_html([[dom.TextPart(text='test')]], par_start='', par_end='
') == 'test
'
+ assert to_html([[dom.CodePart(text='test')]]) == "test
"
+
+def test_to_html_plain():
+ assert to_html_plain([]) == ''
+ assert to_html_plain([[dom.TextPart(text='test')]]) == 'test
'
+ assert to_html_plain([[dom.TextPart(text='test')]], par_start='', par_end='
') == 'test
'
+ assert to_html_plain([[dom.CodePart(text='test')]]) == 'test
'
diff --git a/tests/units/markup/test_vectors.py b/tests/units/markup/test_vectors.py
index 7e1e350e..52598b17 100644
--- a/tests/units/markup/test_vectors.py
+++ b/tests/units/markup/test_vectors.py
@@ -11,7 +11,7 @@
from antsibull_docs.markup import dom
from antsibull_docs.markup.parser import parse, Context
-from antsibull_docs.markup.html import to_html
+from antsibull_docs.markup.html import to_html, to_html_plain
from antsibull_docs.markup.md import to_md
from antsibull_docs.markup.rst import to_rst
from antsibull_docs.markup.format import LinkProvider
@@ -97,12 +97,10 @@ def test_vectors(test_name: str, test_data: t.Mapping[str, t.Any]) -> None:
assert result == test_data['html']
if 'html_plain' in test_data:
- ...
- # TODO result = to_html(parsed, link_provider=html_link_provider, **html_opts)
- # TODO assert result == test_data['html_plain']
+ result = to_html_plain(parsed, link_provider=html_link_provider, **html_opts)
+ assert result == test_data['html_plain']
if 'md' in test_data:
- print('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')
result = to_md(parsed, link_provider=md_link_provider, **md_opts)
assert result == test_data['md']
From 08be426222055ed086b5f9fe49dfb625d2726fa8 Mon Sep 17 00:00:00 2001
From: Felix Fontein
Date: Sun, 19 Mar 2023 15:23:55 +0100
Subject: [PATCH 15/15] Rename default RST formatter.
---
src/antsibull_docs/markup/rst.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/antsibull_docs/markup/rst.py b/src/antsibull_docs/markup/rst.py
index 0d177204..30d3255c 100644
--- a/src/antsibull_docs/markup/rst.py
+++ b/src/antsibull_docs/markup/rst.py
@@ -31,7 +31,7 @@ def rst_escape(value: str, escape_ending_whitespace=False) -> str:
return value
-class RSTFormatter(Formatter):
+class AntsibullRSTFormatter(Formatter):
@staticmethod
def _format_option_like(part: t.Union[dom.OptionNamePart, dom.ReturnValuePart],
role: str) -> str:
@@ -98,11 +98,11 @@ def format_return_value(self, part: dom.ReturnValuePart, url: t.Optional[str]) -
return self._format_option_like(part, 'ansretval')
-DEFAULT_FORMATTER = RSTFormatter()
+DEFAULT_ANTSIBULL_FORMATTER = AntsibullRSTFormatter()
def to_rst(paragraphs: t.Sequence[dom.Paragraph],
- formatter: Formatter = DEFAULT_FORMATTER,
+ formatter: Formatter = DEFAULT_ANTSIBULL_FORMATTER,
link_provider: t.Optional[LinkProvider] = None,
par_start: str = '',
par_end: str = '',