phml.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.misc import heading_rank
  9from phml.nodes import AST, All_Nodes, Element, Root
 10from phml.travel.travel import walk
 11from phml.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        if len(node.children) == 0 and isinstance(node, Element):
 62            node.startend = True
 63        return node
 64
 65    filter_children(tree)
 66
 67
 68def remove_nodes(
 69    tree: Root | Element | AST,
 70    condition: Test,
 71    strict: bool = True,
 72):
 73    """Take a given tree and remove the nodes that match the condition.
 74    If a parent node is removed so is all the children.
 75
 76    Same as filter_nodes except removes nodes that match.
 77
 78    Args:
 79        tree (Root | Element): The parent node to start recursively removing from.
 80        condition (Test): The condition to apply to each node.
 81    """
 82    if tree.__class__.__name__ == "AST":
 83        tree = tree.tree
 84
 85    def filter_children(node):
 86        node.children = [n for n in node.children if not check(n, condition, strict=strict)]
 87        for child in node.children:
 88            if child.type in ["root", "element"]:
 89                filter_children(child)
 90
 91        if len(node.children) == 0 and isinstance(node, Element):
 92            node.startend = True
 93
 94    filter_children(tree)
 95
 96
 97def map_nodes(tree: Root | Element | AST, transform: Callable):
 98    """Takes a tree and a callable that returns a node and maps each node.
 99
100    Signature for the transform function should be as follows:
101
102    1. Takes a single argument that is the node.
103    2. Returns any type of node that is assigned to the original node.
104
105    ```python
106    def to_links(node):
107        return Element("a", {}, node.parent, children=node.children)
108            if node.type == "element"
109            else node
110    ```
111
112    Args:
113        tree (Root | Element): Tree to transform.
114        transform (Callable): The Callable that returns a node that is assigned
115        to each node.
116    """
117
118    if tree.__class__.__name__ == "AST":
119        tree = tree.tree
120
121    def recursive_map(node):
122        for i, child in enumerate(node.children):
123            if isinstance(child, Element):
124                recursive_map(node.children[i])
125                node.children[i] = transform(child)
126            else:
127                node.children[i] = transform(child)
128
129    recursive_map(tree)
130
131
132def replace_node(
133    start: Root | Element,
134    condition: Test,
135    replacement: Optional[All_Nodes | list[All_Nodes]],
136    strict: bool = True,
137):
138    """Search for a specific node in the tree and replace it with either
139    a node or list of nodes. If replacement is None the found node is just removed.
140
141    Args:
142        start (Root | Element): The starting point.
143        condition (test): Test condition to find the correct node.
144        replacement (All_Nodes | list[All_Nodes] | None): What to replace the node with.
145    """
146    for node in walk(start):
147        if check(node, condition, strict=strict):
148            if node.parent is not None:
149                idx = node.parent.children.index(node)
150                if replacement is not None:
151                    parent = node.parent
152                    parent.children = (
153                        node.parent.children[:idx] + replacement + node.parent.children[idx + 1 :]
154                        if isinstance(replacement, list)
155                        else node.parent.children[:idx]
156                        + [replacement]
157                        + node.parent.children[idx + 1 :]
158                    )
159                else:
160                    parent = node.parent
161                    parent.children.pop(idx)
162                    if len(parent.children) == 0 and isinstance(parent, Element):
163                        parent.startend = True
164
165
166def find_and_replace(start: Root | Element, *replacements: tuple[str, str | Callable]) -> int:
167    """Takes a ast, root, or any node and replaces text in `text`
168    nodes with matching replacements.
169
170    First value in each replacement tuple is the regex to match and
171    the second value is what to replace it with. This can either be
172    a string or a callable that returns a string or a new node. If
173    a new node is returned then the text element will be split.
174    """
175    from re import finditer  # pylint: disable=import-outside-toplevel
176
177    for node in walk(start):
178        if node.type == "text":
179            for replacement in replacements:
180                if isinstance(replacement[1], str):
181                    for match in finditer(replacement[0], node.value):
182                        node.value = (
183                            node.value[: match.start()] + replacement[1] + node.value[match.end() :]
184                        )
185
186
187def shift_heading(node: Element, amount: int):
188    """Shift the heading by the amount specified.
189
190    value is clamped between 1 and 6.
191    """
192
193    rank = heading_rank(node)
194    rank += amount
195
196    node.tag = f"h{min(6, max(1, rank))}"
197
198
199def modify_children(func):
200    """Function wrapper that when called and passed an
201    AST, Root, or Element will apply the wrapped function
202    to each child. This means that whatever is returned
203    from the wrapped function will be assigned to the child.
204
205    The wrapped function will be passed the child node,
206    the index in the parents children, and the parent node
207    """
208    from phml import visit_children  # pylint: disable=import-outside-toplevel
209
210    def inner(start: AST | Element | Root):
211        if isinstance(start, AST):
212            start = start.tree
213
214        for idx, child in enumerate(visit_children(start)):
215            start.children[idx] = func(child, idx, child.parent)
216
217    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], strict: bool = True):
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        if len(node.children) == 0 and isinstance(node, Element):
63            node.startend = True
64        return node
65
66    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.

def remove_nodes( tree: phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.AST.AST, condition: Union[NoneType, str, list, dict, Callable], strict: bool = True):
69def remove_nodes(
70    tree: Root | Element | AST,
71    condition: Test,
72    strict: bool = True,
73):
74    """Take a given tree and remove the nodes that match the condition.
75    If a parent node is removed so is all the children.
76
77    Same as filter_nodes except removes nodes that match.
78
79    Args:
80        tree (Root | Element): The parent node to start recursively removing from.
81        condition (Test): The condition to apply to each node.
82    """
83    if tree.__class__.__name__ == "AST":
84        tree = tree.tree
85
86    def filter_children(node):
87        node.children = [n for n in node.children if not check(n, condition, strict=strict)]
88        for child in node.children:
89            if child.type in ["root", "element"]:
90                filter_children(child)
91
92        if len(node.children) == 0 and isinstance(node, Element):
93            node.startend = True
94
95    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.nodes.root.Root | phml.nodes.element.Element | phml.nodes.AST.AST, transform: Callable):
 98def map_nodes(tree: Root | Element | AST, transform: Callable):
 99    """Takes a tree and a callable that returns a node and maps each node.
100
101    Signature for the transform function should be as follows:
102
103    1. Takes a single argument that is the node.
104    2. Returns any type of node that is assigned to the original node.
105
106    ```python
107    def to_links(node):
108        return Element("a", {}, node.parent, children=node.children)
109            if node.type == "element"
110            else node
111    ```
112
113    Args:
114        tree (Root | Element): Tree to transform.
115        transform (Callable): The Callable that returns a node that is assigned
116        to each node.
117    """
118
119    if tree.__class__.__name__ == "AST":
120        tree = tree.tree
121
122    def recursive_map(node):
123        for i, child in enumerate(node.children):
124            if isinstance(child, Element):
125                recursive_map(node.children[i])
126                node.children[i] = transform(child)
127            else:
128                node.children[i] = transform(child)
129
130    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.nodes.root.Root | phml.nodes.element.Element, *replacements: tuple[str, typing.Union[str, typing.Callable]]) -> int:
167def find_and_replace(start: Root | Element, *replacements: tuple[str, str | Callable]) -> int:
168    """Takes a ast, root, or any node and replaces text in `text`
169    nodes with matching replacements.
170
171    First value in each replacement tuple is the regex to match and
172    the second value is what to replace it with. This can either be
173    a string or a callable that returns a string or a new node. If
174    a new node is returned then the text element will be split.
175    """
176    from re import finditer  # pylint: disable=import-outside-toplevel
177
178    for node in walk(start):
179        if node.type == "text":
180            for replacement in replacements:
181                if isinstance(replacement[1], str):
182                    for match in finditer(replacement[0], node.value):
183                        node.value = (
184                            node.value[: match.start()] + replacement[1] + node.value[match.end() :]
185                        )

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):
188def shift_heading(node: Element, amount: int):
189    """Shift the heading by the amount specified.
190
191    value is clamped between 1 and 6.
192    """
193
194    rank = heading_rank(node)
195    rank += amount
196
197    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], strict: bool = True):
133def replace_node(
134    start: Root | Element,
135    condition: Test,
136    replacement: Optional[All_Nodes | list[All_Nodes]],
137    strict: bool = True,
138):
139    """Search for a specific node in the tree and replace it with either
140    a node or list of nodes. If replacement is None the found node is just removed.
141
142    Args:
143        start (Root | Element): The starting point.
144        condition (test): Test condition to find the correct node.
145        replacement (All_Nodes | list[All_Nodes] | None): What to replace the node with.
146    """
147    for node in walk(start):
148        if check(node, condition, strict=strict):
149            if node.parent is not None:
150                idx = node.parent.children.index(node)
151                if replacement is not None:
152                    parent = node.parent
153                    parent.children = (
154                        node.parent.children[:idx] + replacement + node.parent.children[idx + 1 :]
155                        if isinstance(replacement, list)
156                        else node.parent.children[:idx]
157                        + [replacement]
158                        + node.parent.children[idx + 1 :]
159                    )
160                else:
161                    parent = node.parent
162                    parent.children.pop(idx)
163                    if len(parent.children) == 0 and isinstance(parent, Element):
164                        parent.startend = True

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 (All_Nodes | list[All_Nodes] | None): What to replace the node with.
def modify_children(func):
200def modify_children(func):
201    """Function wrapper that when called and passed an
202    AST, Root, or Element will apply the wrapped function
203    to each child. This means that whatever is returned
204    from the wrapped function will be assigned to the child.
205
206    The wrapped function will be passed the child node,
207    the index in the parents children, and the parent node
208    """
209    from phml import visit_children  # pylint: disable=import-outside-toplevel
210
211    def inner(start: AST | Element | Root):
212        if isinstance(start, AST):
213            start = start.tree
214
215        for idx, child in enumerate(visit_children(start)):
216            start.children[idx] = func(child, idx, child.parent)
217
218    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