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