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