phml.utils.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.nodes import AST, All_Nodes, Element, Root 9from phml.utils.misc import heading_rank 10from phml.utils.travel import walk 11from phml.utils.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] 21 22 23def filter_nodes( 24 tree: Root | Element | AST, 25 condition: Test, 26 strict: bool = True, 27): 28 """Take a given tree and filter the nodes with the condition. 29 Only nodes passing the condition stay. If the parent node fails, 30 all children are moved up in scope. Depth first 31 32 Same as remove_nodes but keeps the nodes that match. 33 34 Args: 35 tree (Root | Element): The tree node to filter. 36 condition (Test): The condition to apply to each node. 37 38 Returns: 39 Root | Element: The given tree after being filtered. 40 """ 41 42 if tree.__class__.__name__ == "AST": 43 tree = tree.tree 44 45 def filter_children(node): 46 children = [] 47 for i, child in enumerate(node.children): 48 if child.type in ["root", "element"]: 49 node.children[i] = filter_children(node.children[i]) 50 if not check(child, condition, strict=strict): 51 for idx, _ in enumerate(child.children): 52 child.children[idx].parent = node 53 children.extend(node.children[i].children) 54 else: 55 children.append(node.children[i]) 56 elif check(child, condition, strict=strict): 57 children.append(node.children[i]) 58 59 node.children = children 60 if len(node.children) == 0 and isinstance(node, Element): 61 node.startend = True 62 return node 63 64 filter_children(tree) 65 66 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 if len(node.children) == 0 and isinstance(node, Element): 91 node.startend = True 92 93 filter_children(tree) 94 95 96def map_nodes(tree: Root | Element | AST, transform: Callable): 97 """Takes a tree and a callable that returns a node and maps each node. 98 99 Signature for the transform function should be as follows: 100 101 1. Takes a single argument that is the node. 102 2. Returns any type of node that is assigned to the original node. 103 104 ```python 105 def to_links(node): 106 return Element("a", {}, node.parent, children=node.children) 107 if node.type == "element" 108 else node 109 ``` 110 111 Args: 112 tree (Root | Element): Tree to transform. 113 transform (Callable): The Callable that returns a node that is assigned 114 to each node. 115 """ 116 117 if tree.__class__.__name__ == "AST": 118 tree = tree.tree 119 120 def recursive_map(node): 121 for i, child in enumerate(node.children): 122 if isinstance(child, Element): 123 recursive_map(node.children[i]) 124 node.children[i] = transform(child) 125 else: 126 node.children[i] = transform(child) 127 128 recursive_map(tree) 129 130 131def replace_node( 132 start: Root | Element, 133 condition: Test, 134 replacement: Optional[All_Nodes | list[All_Nodes]], 135 strict: bool = True, 136): 137 """Search for a specific node in the tree and replace it with either 138 a node or list of nodes. If replacement is None the found node is just removed. 139 140 Args: 141 start (Root | Element): The starting point. 142 condition (test): Test condition to find the correct node. 143 replacement (All_Nodes | list[All_Nodes] | None): What to replace the node with. 144 """ 145 for node in walk(start): 146 if check(node, condition, strict=strict): 147 if node.parent is not None: 148 idx = node.parent.children.index(node) 149 if replacement is not None: 150 node.parent.children = ( 151 node.parent.children[:idx] + replacement + node.parent.children[idx + 1 :] 152 if isinstance(replacement, list) 153 else node.parent.children[:idx] 154 + [replacement] 155 + node.parent.children[idx + 1 :] 156 ) 157 else: 158 node.parent.children.pop(idx) 159 160 161def find_and_replace(start: Root | Element, *replacements: tuple[str, str | Callable]) -> int: 162 """Takes a ast, root, or any node and replaces text in `text` 163 nodes with matching replacements. 164 165 First value in each replacement tuple is the regex to match and 166 the second value is what to replace it with. This can either be 167 a string or a callable that returns a string or a new node. If 168 a new node is returned then the text element will be split. 169 """ 170 from re import finditer # pylint: disable=import-outside-toplevel 171 172 for node in walk(start): 173 if node.type == "text": 174 for replacement in replacements: 175 if isinstance(replacement[1], str): 176 for match in finditer(replacement[0], node.value): 177 node.value = ( 178 node.value[: match.start()] + replacement[1] + node.value[match.end() :] 179 ) 180 else: 181 raise NotImplementedError( 182 "Callables are not yet supported for find_and_replace operations." 183 ) 184 # tada add ability to inject nodes in place of text replacement 185 # elif isinstance(replacement[1], Callable): 186 # for match in finditer(replacement[0], n.value): 187 # result = replacement[1](match.group()) 188 # if isinstance(result, str): 189 # n.value = n.value[:match.start()] 190 # + replacement[1] 191 # + n.value[match.end():] 192 # elif isinstance(result, All_Nodes): 193 # pass 194 # elif isinstance(result, list): 195 # pass 196 197 198def shift_heading(node: Element, amount: int): 199 """Shift the heading by the amount specified. 200 201 value is clamped between 1 and 6. 202 """ 203 204 rank = heading_rank(node) 205 rank += amount 206 207 node.tag = f"h{min(6, max(1, rank))}" 208 209 210def modify_children(func): 211 """Function wrapper that when called and passed an 212 AST, Root, or Element will apply the wrapped function 213 to each child. This means that whatever is returned 214 from the wrapped function will be assigned to the child. 215 216 The wrapped function will be passed the child node, 217 the index in the parents children, and the parent node 218 """ 219 from phml.utils import visit_children # pylint: disable=import-outside-toplevel 220 221 def inner(start: AST | Element | Root): 222 if isinstance(start, AST): 223 start = start.tree 224 225 for idx, child in enumerate(visit_children(start)): 226 start.children[idx] = func(child, idx, child.parent) 227 228 return inner
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)
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 (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.
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)
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 (Root | Element): The parent node to start recursively removing from.
- condition (Test): The condition to apply to each node.
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)
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 (Root | Element): Tree to transform.
- transform (Callable): The Callable that returns a node that is assigned
- to each node.
162def find_and_replace(start: Root | Element, *replacements: tuple[str, str | Callable]) -> int: 163 """Takes a ast, root, or any node and replaces text in `text` 164 nodes with matching replacements. 165 166 First value in each replacement tuple is the regex to match and 167 the second value is what to replace it with. This can either be 168 a string or a callable that returns a string or a new node. If 169 a new node is returned then the text element will be split. 170 """ 171 from re import finditer # pylint: disable=import-outside-toplevel 172 173 for node in walk(start): 174 if node.type == "text": 175 for replacement in replacements: 176 if isinstance(replacement[1], str): 177 for match in finditer(replacement[0], node.value): 178 node.value = ( 179 node.value[: match.start()] + replacement[1] + node.value[match.end() :] 180 ) 181 else: 182 raise NotImplementedError( 183 "Callables are not yet supported for find_and_replace operations." 184 ) 185 # tada add ability to inject nodes in place of text replacement 186 # elif isinstance(replacement[1], Callable): 187 # for match in finditer(replacement[0], n.value): 188 # result = replacement[1](match.group()) 189 # if isinstance(result, str): 190 # n.value = n.value[:match.start()] 191 # + replacement[1] 192 # + n.value[match.end():] 193 # elif isinstance(result, All_Nodes): 194 # pass 195 # elif isinstance(result, list): 196 # pass
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.
199def shift_heading(node: Element, amount: int): 200 """Shift the heading by the amount specified. 201 202 value is clamped between 1 and 6. 203 """ 204 205 rank = heading_rank(node) 206 rank += amount 207 208 node.tag = f"h{min(6, max(1, rank))}"
Shift the heading by the amount specified.
value is clamped between 1 and 6.
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 node.parent.children = ( 152 node.parent.children[:idx] + replacement + node.parent.children[idx + 1 :] 153 if isinstance(replacement, list) 154 else node.parent.children[:idx] 155 + [replacement] 156 + node.parent.children[idx + 1 :] 157 ) 158 else: 159 node.parent.children.pop(idx)
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 (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.