phml.utils.transform.transform

phml.utils.transform.transform

Utility methods that revolve around transforming or manipulating the ast.

  1"""phml.utils.transform.transform
  2
  3Utility methods that revolve around transforming or manipulating the ast.
  4"""
  5
  6from typing import Callable, Optional
  7
  8from phml.nodes import AST, All_Nodes, Element, Root
  9from phml.utils.misc import heading_rank
 10from phml.utils.travel import walk
 11from phml.utils.validate.test import Test, test
 12
 13__all__ = [
 14    "filter_nodes",
 15    "remove_nodes",
 16    "map_nodes",
 17    "find_and_replace",
 18    "shift_heading",
 19    "replace_node",
 20]
 21
 22
 23def filter_nodes(tree: Root | Element | AST, condition: Test):
 24    """Take a given tree and filter the nodes with the condition.
 25    Only nodes passing the condition stay. If the parent node fails,
 26    then all children are removed.
 27
 28    Same as remove_nodes but keeps the nodes that match.
 29
 30    Args:
 31        tree (Root | Element): The tree node to filter.
 32        condition (Test): The condition to apply to each node.
 33
 34    Returns:
 35        Root | Element: The given tree after being filtered.
 36    """
 37
 38    if tree.__class__.__name__ == "AST":
 39        tree = tree.tree
 40
 41    def filter_children(node):
 42        node.children = [n for n in node.children if test(n, condition)]
 43        for child in node.children:
 44            if child.type in ["root", "element"]:
 45                filter_children(child)
 46
 47    filter_children(tree)
 48
 49
 50def remove_nodes(tree: Root | Element | AST, condition: Test):
 51    """Take a given tree and remove the nodes that match the condition.
 52    If a parent node is removed so is all the children.
 53
 54    Same as filter_nodes except removes nodes that match.
 55
 56    Args:
 57        tree (Root | Element): The parent node to start recursively removing from.
 58        condition (Test): The condition to apply to each node.
 59    """
 60    if tree.__class__.__name__ == "AST":
 61        tree = tree.tree
 62
 63    def filter_children(node):
 64        node.children = [n for n in node.children if not test(n, condition)]
 65        for child in node.children:
 66            if child.type in ["root", "element"]:
 67                filter_children(child)
 68
 69    filter_children(tree)
 70
 71
 72def map_nodes(tree: Root | Element | AST, transform: Callable):
 73    """Takes a tree and a callable that returns a node and maps each node.
 74
 75    Signature for the transform function should be as follows:
 76
 77    1. Takes a single argument that is the node.
 78    2. Returns any type of node that is assigned to the original node.
 79
 80    ```python
 81    def to_links(node):
 82        return Element("a", {}, node.parent, children=node.children)
 83            if node.type == "element"
 84            else node
 85    ```
 86
 87    Args:
 88        tree (Root | Element): Tree to transform.
 89        transform (Callable): The Callable that returns a node that is assigned
 90        to each node.
 91    """
 92
 93    if tree.__class__.__name__ == "AST":
 94        tree = tree.tree
 95
 96    for node in walk(tree):
 97        if not isinstance(node, Root):
 98            node = transform(node)
 99
100
101def replace_node(
102    start: Root | Element, condition: Test, replacement: Optional[All_Nodes | list[All_Nodes]]
103):
104    """Search for a specific node in the tree and replace it with either
105    a node or list of nodes. If replacement is None the found node is just removed.
106
107    Args:
108        start (Root | Element): The starting point.
109        condition (test): Test condition to find the correct node.
110        replacement (All_Nodes | list[All_Nodes] | None): What to replace the node with.
111    """
112    for node in walk(start):
113        if test(node, condition):
114            if node.parent is not None:
115                idx = node.parent.children.index(node)
116                if replacement is not None:
117                    node.parent.children = (
118                        node.parent.children[:idx] + replacement + node.parent.children[idx + 1 :]
119                        if isinstance(replacement, list)
120                        else node.parent.children[:idx]
121                        + [replacement]
122                        + node.parent.children[idx + 1 :]
123                    )
124                else:
125                    node.parent.children.pop(idx)
126                break
127
128
129def find_and_replace(start: Root | Element, *replacements: tuple[str, str | Callable]) -> int:
130    """Takes a ast, root, or any node and replaces text in `text`
131    nodes with matching replacements.
132
133    First value in each replacement tuple is the regex to match and
134    the second value is what to replace it with. This can either be
135    a string or a callable that returns a string or a new node. If
136    a new node is returned then the text element will be split.
137    """
138    from re import finditer  # pylint: disable=import-outside-toplevel
139
140    for node in walk(start):
141        if node.type == "text":
142            for replacement in replacements:
143                if isinstance(replacement[1], str):
144                    for match in finditer(replacement[0], node.value):
145                        node.value = (
146                            node.value[: match.start()] + replacement[1] + node.value[match.end() :]
147                        )
148                else:
149                    raise NotImplementedError(
150                        "Callables are not yet supported for find_and_replace operations."
151                    )
152                # tada add ability to inject nodes in place of text replacement
153                # elif isinstance(replacement[1], Callable):
154                #     for match in finditer(replacement[0], n.value):
155                #         result = replacement[1](match.group())
156                #         if isinstance(result, str):
157                #             n.value = n.value[:match.start()]
158                #             + replacement[1]
159                #             + n.value[match.end():]
160                #         elif isinstance(result, All_Nodes):
161                #             pass
162                #         elif isinstance(result, list):
163                #             pass
164
165
166def shift_heading(node: Element, amount: int):
167    """Shift the heading by the amount specified.
168
169    value is clamped between 1 and 6.
170    """
171
172    rank = heading_rank(node)
173    rank += amount
174
175    node.tag = f"h{min(6, max(1, rank))}"
176
177
178def modify_children(func):
179    """Function wrapper that when called and passed an
180    AST, Root, or Element will apply the wrapped function
181    to each child. This means that whatever is returned
182    from the wrapped function will be assigned to the child.
183
184    The wrapped function will be passed the child node,
185    the index in the parents children, and the parent node
186    """
187    from phml.utils import visit_children  # pylint: disable=import-outside-toplevel
188
189    def inner(start: AST | Element | Root):
190        if isinstance(start, AST):
191            start = start.tree
192
193        for idx, child in enumerate(visit_children(start)):
194            start.children[idx] = func(child, idx, child.parent)
195
196    return inner
def filter_nodes( tree: phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.AST.AST, condition: Union[NoneType, str, list, dict, Callable]):
24def filter_nodes(tree: Root | Element | AST, condition: Test):
25    """Take a given tree and filter the nodes with the condition.
26    Only nodes passing the condition stay. If the parent node fails,
27    then all children are removed.
28
29    Same as remove_nodes but keeps the nodes that match.
30
31    Args:
32        tree (Root | Element): The tree node to filter.
33        condition (Test): The condition to apply to each node.
34
35    Returns:
36        Root | Element: The given tree after being filtered.
37    """
38
39    if tree.__class__.__name__ == "AST":
40        tree = tree.tree
41
42    def filter_children(node):
43        node.children = [n for n in node.children if test(n, condition)]
44        for child in node.children:
45            if child.type in ["root", "element"]:
46                filter_children(child)
47
48    filter_children(tree)

Take a given tree and filter the nodes with the condition. Only nodes passing the condition stay. If the parent node fails, then all children are removed.

Same as remove_nodes but keeps the nodes that match.

Args
  • tree (Root | Element): The tree node to filter.
  • condition (Test): The condition to apply to each node.
Returns

Root | Element: The given tree after being filtered.

def remove_nodes( tree: phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.AST.AST, condition: Union[NoneType, str, list, dict, Callable]):
51def remove_nodes(tree: Root | Element | AST, condition: Test):
52    """Take a given tree and remove the nodes that match the condition.
53    If a parent node is removed so is all the children.
54
55    Same as filter_nodes except removes nodes that match.
56
57    Args:
58        tree (Root | Element): The parent node to start recursively removing from.
59        condition (Test): The condition to apply to each node.
60    """
61    if tree.__class__.__name__ == "AST":
62        tree = tree.tree
63
64    def filter_children(node):
65        node.children = [n for n in node.children if not test(n, condition)]
66        for child in node.children:
67            if child.type in ["root", "element"]:
68                filter_children(child)
69
70    filter_children(tree)

Take a given tree and remove the nodes that match the condition. If a parent node is removed so is all the children.

Same as filter_nodes except removes nodes that match.

Args
  • tree (Root | Element): The parent node to start recursively removing from.
  • condition (Test): The condition to apply to each node.
def map_nodes( tree: phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.AST.AST, transform: Callable):
73def map_nodes(tree: Root | Element | AST, transform: Callable):
74    """Takes a tree and a callable that returns a node and maps each node.
75
76    Signature for the transform function should be as follows:
77
78    1. Takes a single argument that is the node.
79    2. Returns any type of node that is assigned to the original node.
80
81    ```python
82    def to_links(node):
83        return Element("a", {}, node.parent, children=node.children)
84            if node.type == "element"
85            else node
86    ```
87
88    Args:
89        tree (Root | Element): Tree to transform.
90        transform (Callable): The Callable that returns a node that is assigned
91        to each node.
92    """
93
94    if tree.__class__.__name__ == "AST":
95        tree = tree.tree
96
97    for node in walk(tree):
98        if not isinstance(node, Root):
99            node = transform(node)

Takes a tree and a callable that returns a node and maps each node.

Signature for the transform function should be as follows:

  1. Takes a single argument that is the node.
  2. Returns any type of node that is assigned to the original node.
def to_links(node):
    return Element("a", {}, node.parent, children=node.children)
        if node.type == "element"
        else node
Args
  • tree (Root | Element): Tree to transform.
  • transform (Callable): The Callable that returns a node that is assigned
  • to each node.
def find_and_replace( start: phml.nodes.root.Root | phml.nodes.element.Element, *replacements: tuple[str, typing.Union[str, typing.Callable]]) -> int:
130def find_and_replace(start: Root | Element, *replacements: tuple[str, str | Callable]) -> int:
131    """Takes a ast, root, or any node and replaces text in `text`
132    nodes with matching replacements.
133
134    First value in each replacement tuple is the regex to match and
135    the second value is what to replace it with. This can either be
136    a string or a callable that returns a string or a new node. If
137    a new node is returned then the text element will be split.
138    """
139    from re import finditer  # pylint: disable=import-outside-toplevel
140
141    for node in walk(start):
142        if node.type == "text":
143            for replacement in replacements:
144                if isinstance(replacement[1], str):
145                    for match in finditer(replacement[0], node.value):
146                        node.value = (
147                            node.value[: match.start()] + replacement[1] + node.value[match.end() :]
148                        )
149                else:
150                    raise NotImplementedError(
151                        "Callables are not yet supported for find_and_replace operations."
152                    )
153                # tada add ability to inject nodes in place of text replacement
154                # elif isinstance(replacement[1], Callable):
155                #     for match in finditer(replacement[0], n.value):
156                #         result = replacement[1](match.group())
157                #         if isinstance(result, str):
158                #             n.value = n.value[:match.start()]
159                #             + replacement[1]
160                #             + n.value[match.end():]
161                #         elif isinstance(result, All_Nodes):
162                #             pass
163                #         elif isinstance(result, list):
164                #             pass

Takes a ast, root, or any node and replaces text in text nodes with matching replacements.

First value in each replacement tuple is the regex to match and the second value is what to replace it with. This can either be a string or a callable that returns a string or a new node. If a new node is returned then the text element will be split.

def shift_heading(node: phml.nodes.element.Element, amount: int):
167def shift_heading(node: Element, amount: int):
168    """Shift the heading by the amount specified.
169
170    value is clamped between 1 and 6.
171    """
172
173    rank = heading_rank(node)
174    rank += amount
175
176    node.tag = f"h{min(6, max(1, rank))}"

Shift the heading by the amount specified.

value is clamped between 1 and 6.

def replace_node( start: phml.nodes.root.Root | phml.nodes.element.Element, condition: Union[NoneType, str, list, dict, Callable], replacement: Union[phml.nodes.root.Root, phml.nodes.element.Element, phml.nodes.text.Text, phml.nodes.comment.Comment, phml.nodes.doctype.DocType, phml.nodes.parent.Parent, phml.nodes.node.Node, phml.nodes.literal.Literal, list[phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal], NoneType]):
102def replace_node(
103    start: Root | Element, condition: Test, replacement: Optional[All_Nodes | list[All_Nodes]]
104):
105    """Search for a specific node in the tree and replace it with either
106    a node or list of nodes. If replacement is None the found node is just removed.
107
108    Args:
109        start (Root | Element): The starting point.
110        condition (test): Test condition to find the correct node.
111        replacement (All_Nodes | list[All_Nodes] | None): What to replace the node with.
112    """
113    for node in walk(start):
114        if test(node, condition):
115            if node.parent is not None:
116                idx = node.parent.children.index(node)
117                if replacement is not None:
118                    node.parent.children = (
119                        node.parent.children[:idx] + replacement + node.parent.children[idx + 1 :]
120                        if isinstance(replacement, list)
121                        else node.parent.children[:idx]
122                        + [replacement]
123                        + node.parent.children[idx + 1 :]
124                    )
125                else:
126                    node.parent.children.pop(idx)
127                break

Search for a specific node in the tree and replace it with either a node or list of nodes. If replacement is None the found node is just removed.

Args
  • start (Root | Element): The starting point.
  • condition (test): Test condition to find the correct node.
  • replacement (All_Nodes | list[All_Nodes] | None): What to replace the node with.