from functools import partial
from typing import TYPE_CHECKING, Callable, List, Union, cast, overload
from ._predicate import Predicate, blank
from ._single_selection import Selection, everything
if TYPE_CHECKING:
from typing_extensions import TypeAlias # import from stdlib once Python >= 3.10
Edition: TypeAlias = Callable[[List[str], slice], List[str]]
else:
Edition = "Edition"
[docs]def replace(fn: Callable[[str], str]) -> Edition:
"""Replace a chunk of text.
The provided function will be called with the selected text as argument
and its return value will be used as replacement.
"""
return cast(Edition, partial(_replace, fn))
def _replace(fn: Callable[[str], str], lines: List[str], select: slice) -> List[str]:
selected_lines = lines[select]
if len(selected_lines) < 1:
return lines
replacement = _splitlines(fn("\n".join(selected_lines)))
start = select.start or 0
stop = select.stop or len(lines)
return lines[:start] + replacement + lines[stop:]
[docs]def add_prefix(prefix: str, skip: Union[Predicate, None] = blank) -> Edition:
"""Add a fixed prefix string to every line in the selection for which the
``skip`` function does not evaluate to ``True``.
When no ``skip`` function is provided, blank lines are skipped.
"""
pred = skip or (lambda _: False)
return replace(lambda text: _indent(text, prefix, pred))
[docs]def remove_prefix(prefix, skip: Union[Predicate, None] = blank) -> Edition:
"""Remove a fixed prefix string to every line in the selection for which the
``skip`` function does not evaluate to ``True``.
Please note that if the line does not start with the prefix, it is skipped.
"""
pred = skip or (lambda _: False)
return replace(lambda text: _dedent(text, prefix, pred))
def _dedent(text: str, prefix: str, skip: Predicate) -> str:
i = len(prefix)
return "\n".join(
line[i:] if not skip(line) and line.startswith(prefix) else line
for line in _splitlines(text)
)
def _indent(text: str, prefix: str, skip: Predicate) -> str:
return "\n".join(
(prefix + line) if not skip(line) else line for line in _splitlines(text)
)
def _splitlines(text: str):
return [""] if text == "" else text.splitlines()
@overload
def edit(text: str, edition: Edition) -> str:
...
@overload
def edit(text: str, select: Selection, edition: Edition) -> str:
...
[docs]def edit(text, select, edition=None):
r"""Apply the operations to a text. You can stack a series of select operations,
but only one edit operation is allowed.
>>> from texted import edit, remove_prefix, add_prefix, find, blank, contains
>>> new_text = edit(
... "hello\n* world",
... remove_prefix("* "),
... )
>>> print(new_text)
hello
world
>>> new_text = edit(
... "hello\n* world",
... find(blank),
... add_prefix("% "),
... ) # No match, no change
>>> print(new_text)
hello
* world
>>> new_text = edit(
... "hello\n\nworld",
... add_prefix("%", skip=contains("hello")),
... )
>>> print(new_text)
hello
%
%world
>>> new_text = edit(
... "hello\n\nworld",
... add_prefix("%", skip=None),
... )
>>> print(new_text)
%hello
%
%world
"""
if edition is None:
edition = select
select = everything
lines = text.splitlines()
all_text = slice(len(lines))
new_select = select(lines, all_text)
return "\n".join(edition(lines, new_select))