phml.utilities.transform.transform

phml.utilities.transform.transform

Utility methods that revolve around transforming or manipulating the ast.

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

Same as remove_nodes but keeps the nodes that match.

Arguments:
  • 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.

67def remove_nodes(
68    tree: Root | Element | AST,
69    condition: Test,
70    strict: bool = True,
71):
72    """Take a given tree and remove the nodes that match the condition.
73    If a parent node is removed so is all the children.
74
75    Same as filter_nodes except removes nodes that match.
76
77    Args:
78        tree (Root | Element): The parent node to start recursively removing from.
79        condition (Test): The condition to apply to each node.
80    """
81    if tree.__class__.__name__ == "AST":
82        tree = tree.tree
83
84    def filter_children(node):
85        node.children = [n for n in node.children if not check(n, condition, strict=strict)]
86        for child in node.children:
87            if child.type in ["root", "element"]:
88                filter_children(child)
89
90    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.

Arguments:
  • 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.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.AST.AST, transform: Callable):
 93def map_nodes(tree: Root | Element | AST, transform: Callable):
 94    """Takes a tree and a callable that returns a node and maps each node.
 95
 96    Signature for the transform function should be as follows:
 97
 98    1. Takes a single argument that is the node.
 99    2. Returns any type of node that is assigned to the original node.
100
101    ```python
102    def to_links(node):
103        return Element("a", {}, node.parent, children=node.children)
104            if node.type == "element"
105            else node
106    ```
107
108    Args:
109        tree (Root | Element): Tree to transform.
110        transform (Callable): The Callable that returns a node that is assigned
111        to each node.
112    """
113
114    if tree.__class__.__name__ == "AST":
115        tree = tree.tree
116
117    def recursive_map(node):
118        for i, child in enumerate(node.children):
119            if isinstance(child, Element):
120                recursive_map(node.children[i])
121                node.children[i] = transform(child)
122            else:
123                node.children[i] = transform(child)
124
125    recursive_map(tree)

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
Arguments:
  • 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.core.nodes.nodes.Root | phml.core.nodes.nodes.Element, *replacements: tuple[str, typing.Union[str, typing.Callable]]) -> int:
172def find_and_replace(start: Root | Element, *replacements: tuple[str, str | Callable]) -> int:
173    """Takes a ast, root, or any node and replaces text in `text`
174    nodes with matching replacements.
175
176    First value in each replacement tuple is the regex to match and
177    the second value is what to replace it with. This can either be
178    a string or a callable that returns a string or a new node. If
179    a new node is returned then the text element will be split.
180    """
181    from re import finditer  # pylint: disable=import-outside-toplevel
182
183    for node in walk(start):
184        if node.type == "text":
185            for replacement in replacements:
186                if isinstance(replacement[1], str):
187                    for match in finditer(replacement[0], node.value):
188                        node.value = (
189                            node.value[: match.start()] + replacement[1] + node.value[match.end() :]
190                        )

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.core.nodes.nodes.Element, amount: int):
193def shift_heading(node: Element, amount: int):
194    """Shift the heading by the amount specified.
195
196    value is clamped between 1 and 6.
197    """
198
199    rank = heading_rank(node)
200    rank += amount
201
202    node.tag = f"h{min(6, max(1, rank))}"

Shift the heading by the amount specified.

value is clamped between 1 and 6.

128def replace_node(
129    start: Root | Element,
130    condition: Test,
131    replacement: Optional[NODE | list[NODE]],
132    all_nodes: bool = False,
133    strict: bool = True,
134):
135    """Search for a specific node in the tree and replace it with either
136    a node or list of nodes. If replacement is None the found node is just removed.
137
138    Args:
139        start (Root | Element): The starting point.
140        condition (test): Test condition to find the correct node.
141        replacement (NODE | list[NODE] | None): What to replace the node with.
142    """
143    for node in walk(start):
144        if check(node, condition, strict=strict):
145            if node.parent is not None:
146                idx = node.parent.children.index(node)
147                if replacement is not None:
148                    parent = node.parent
149                    if isinstance(replacement, list):
150                        for item in replacement:
151                            item.parent = node.parent
152                        parent.children = (
153                            node.parent.children[:idx]
154                            + replacement
155                            + node.parent.children[idx + 1 :]
156                        )
157                    else:
158                        replacement.parent = node.parent
159                        parent.children = (
160                            node.parent.children[:idx]
161                            + [replacement]
162                            + node.parent.children[idx + 1 :]
163                        )
164                else:
165                    parent = node.parent
166                    parent.children.pop(idx)
167
168            if not all_nodes:
169                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.

Arguments:
  • start (Root | Element): The starting point.
  • condition (test): Test condition to find the correct node.
  • replacement (NODE | list[NODE] | None): What to replace the node with.
def modify_children(func):
205def modify_children(func):
206    """Function wrapper that when called and passed an
207    AST, Root, or Element will apply the wrapped function
208    to each child. This means that whatever is returned
209    from the wrapped function will be assigned to the child.
210
211    The wrapped function will be passed the child node,
212    the index in the parents children, and the parent node
213    """
214    from phml import visit_children  # pylint: disable=import-outside-toplevel
215
216    def inner(start: AST | Element | Root):
217        if isinstance(start, AST):
218            start = start.tree
219
220        for idx, child in enumerate(visit_children(start)):
221            start.children[idx] = func(child, idx, child.parent)
222
223    return inner

Function wrapper that when called and passed an AST, Root, or Element will apply the wrapped function to each child. This means that whatever is returned from the wrapped function will be assigned to the child.

The wrapped function will be passed the child node, the index in the parents children, and the parent node