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.
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:
- 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
Arguments:
- tree (Root | Element): Tree to transform.
- transform (Callable): The Callable that returns a node that is assigned
- to each node.
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.
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.
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