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

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

Parent: The given tree after being filtered.

def remove_nodes( tree: phml.nodes.Parent, condition: Union[list, str, dict, Callable[[phml.nodes.Node], bool]], strict: bool = True):
64def remove_nodes(
65    tree: Parent,
66    condition: Test,
67    strict: bool = True,
68):
69    """Take a given tree and remove the nodes that match the condition.
70    If a parent node is removed so is all the children.
71
72    Same as filter_nodes except removes nodes that match.
73
74    Args:
75        tree (Parent): The parent node to start recursively removing from.
76        condition (Test): The condition to apply to each node.
77    """
78
79    def filter_children(node):
80        if node.children is not None:
81            node.children = [n for n in node if not check(n, condition, strict=strict)]
82            for child in node:
83                if isinstance(child, Parent):
84                    filter_children(child)
85
86    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 (Parent): The parent node to start recursively removing from.
  • condition (Test): The condition to apply to each node.
def map_nodes( tree: phml.nodes.Parent, transform: Callable[[phml.nodes.Node], phml.nodes.Node]):
 89def map_nodes(tree: Parent, transform: Callable[[Node], Node]):
 90    """Takes a tree and a callable that returns a node and maps each node.
 91
 92    Signature for the transform function should be as follows:
 93
 94    1. Takes a single argument that is the node.
 95    2. Returns any type of node that is assigned to the original node.
 96
 97    ```python
 98    def to_links(node):
 99        return Element("a", {}, node.parent, children=node.children)
100            if node.type == "element"
101            else node
102    ```
103
104    Args:
105        tree (Parent): Tree to transform.
106        transform (Callable): The Callable that returns a node that is assigned
107        to each node.
108    """
109
110    def recursive_map(node: Parent):
111        for child in node:
112            idx = node.index(child)
113            node[idx] = transform(child)
114            if isinstance(node[idx], Element):
115                recursive_map(node[idx])
116
117    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
Args
  • tree (Parent): Tree to transform.
  • transform (Callable): The Callable that returns a node that is assigned
  • to each node.
def find_and_replace( start: phml.nodes.Parent, *replacements: tuple[str, typing.Union[str, typing.Callable]]):
154def find_and_replace(start: Parent, *replacements: tuple[str, str | Callable]):
155    """Takes a node and replaces text in Literal.Text
156    nodes with matching replacements.
157
158    First value in each replacement tuple is the regex to match and
159    the second value is what to replace it with. This can either be
160    a string or a callable that returns a string or a new node. If
161    a new node is returned then the text element will be split.
162    """
163    from re import finditer  # pylint: disable=import-outside-toplevel
164
165    for node in walk(start):
166        if Literal.is_text(node):
167            for replacement in replacements:
168                if isinstance(replacement[1], str):
169                    for match in finditer(replacement[0], node.content):
170                        node.content = (
171                            node.content[: match.start()]
172                            + replacement[1]
173                            + node.content[match.end() :]
174                        )

Takes a node and replaces text in Literal.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, amount: int):
177def shift_heading(node: Element, amount: int):
178    """Shift the heading by the amount specified.
179
180    value is clamped between 1 and 6.
181    """
182
183    rank = heading_rank(node)
184    rank += amount
185
186    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.Parent, condition: Union[list, str, dict, Callable[[phml.nodes.Node], bool]], replacement: phml.nodes.Node | list[phml.nodes.Node] | None, all_nodes: bool = False, strict: bool = True):
120def replace_node(
121    start: Parent,
122    condition: Test,
123    replacement: Node | list[Node] | None,
124    all_nodes: bool = False,
125    strict: bool = True,
126):
127    """Search for a specific node in the tree and replace it with either
128    a node or list of nodes. If replacement is None the found node is just removed.
129
130    Args:
131        start (Parent): The starting point.
132        condition (test): Test condition to find the correct node.
133        replacement (Node | list[Node] | None): What to replace the node with.
134    """
135
136    # Convert iterator to static list to avoid errors while editing tree
137    for node in list(walk(start)):
138        if node != start and check(node, condition, strict=strict):
139            parent = node.parent
140            if parent is not None:
141                idx = parent.index(node)
142                if replacement is not None:
143                    if isinstance(replacement, list):
144                        parent[idx:idx+1] = replacement
145                    else:
146                        parent[idx] = replacement
147                else:
148                    del node.parent[idx]
149
150            if not all_nodes:
151                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 (Parent): 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: Callable[[phml.nodes.Node, int, phml.nodes.Parent], phml.nodes.Node]):
189def modify_children(func: Callable[[Node, int, Parent], Node]):
190    """Function wrapper that when called, and passed a Parent node,
191    will apply the wrapped function to each child.
192
193    The following args are passed to the wrapped method:
194        child (Node): A child of the parent node.
195        index (int): The index of the child in the parent node.
196        parent (Parent): The starting parent node.
197
198    The wrapped method is expected to return a new or modified node.
199    """
200
201    @wraps(func)
202    def inner(start: Parent):
203        for idx, child in enumerate(start):
204            start[idx] = func(child, idx, start)
205
206    return inner

Function wrapper that when called, and passed a Parent node, will apply the wrapped function to each child.

The following args are passed to the wrapped method

child (Node): A child of the parent node. index (int): The index of the child in the parent node. parent (Parent): The starting parent node.

The wrapped method is expected to return a new or modified node.