Skip to content

Commit

Permalink
Move parser impl code out.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Mar 16, 2023
1 parent 98d3cf0 commit 1c569ca
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 85 deletions.
87 changes: 87 additions & 0 deletions src/antsibull_docs/markup/_parser_impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Author: Felix Fontein <felix@fontein.de>
# 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
91 changes: 10 additions & 81 deletions src/antsibull_docs/markup/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
58 changes: 58 additions & 0 deletions tests/units/markup/test_markup.py
Original file line number Diff line number Diff line change
@@ -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' <ansible_collections.ansible.builtin.yum_module>`\ ',
'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' <https://docs.ansible.com/user-guide.html>`__\ ',
'R(the user guide,user-guide)': r'\ :ref:`the user guide <user-guide>`\ ',
'C(/usr/bin/file)': r'\ :literal:`/usr/bin/file`\ ',
'HORIZONTALLINE': '\n\n.. raw:: html\n\n <hr>\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 <ansible_collections.ansible.builtin.yum_module>`\ module \ :strong:`MUST`\ be given the \ :literal:`package`\ parameter. See the \ :ref:`looping docs <using-loops>`\ 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 <https://docs.ansible.com/>`__\ ',
'R(the user guide, user-guide)': r'\ :ref:`the user guide <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
Original file line number Diff line number Diff line change
Expand Up @@ -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(\\,)'""",
}
Expand Down
65 changes: 65 additions & 0 deletions tests/units/markup/test_parser_impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Author: Felix Fontein <felix@fontein.de>
# 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

0 comments on commit 1c569ca

Please sign in to comment.