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
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.
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.
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:
- Takes a single argument that is the node.
- 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.
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.
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.
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.
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.