phml.utilities.misc.inspect
Logic to inspect any phml node. Outputs a tree representation of the node as a string.
1"""phml.utilities.misc.inspect 2 3Logic to inspect any phml node. Outputs a tree representation 4of the node as a string. 5""" 6 7from json import dumps, JSONEncoder 8 9from phml.core.nodes import AST, NODE, Comment, Element, Root, Text 10 11__all__ = ["inspect", "normalize_indent"] 12 13 14def inspect(start: AST | NODE, indent: int = 2): 15 """Recursively inspect the passed node or ast.""" 16 17 if isinstance(start, AST): 18 start = start.tree 19 20 def recursive_inspect(node: Element | Root, indent: int) -> list[str]: 21 """Generate signature for node then for each child recursively.""" 22 from phml import visit_children # pylint: disable=import-outside-toplevel 23 24 results = [*signature(node)] 25 26 for idx, child in enumerate(visit_children(node)): 27 if isinstance(child, (Element, Root)): 28 lines = recursive_inspect(child, indent) 29 30 child_prefix = "└" if idx == len(node.children) - 1 else "├" 31 nested_prefix = " " if idx == len(node.children) - 1 else "│" 32 33 lines[0] = f"{child_prefix}{idx} {lines[0]}" 34 if len(lines) > 1: 35 for line in range(1, len(lines)): 36 lines[line] = f"{nested_prefix} {lines[line]}" 37 results.extend(lines) 38 else: 39 lines = signature(child, indent) 40 41 child_prefix = "└" if idx == len(node.children) - 1 else "├" 42 nested_prefix = " " if idx == len(node.children) - 1 else "│" 43 44 lines[0] = f"{child_prefix}{idx} {lines[0]}" 45 if len(lines) > 1: 46 for line in range(1, len(lines)): 47 lines[line] = f"{nested_prefix} {lines[line]}" 48 49 results.extend(lines) 50 return results 51 52 if isinstance(start, (Element, Root)): 53 return "\n".join(recursive_inspect(start, indent)) 54 55 return "\n".join(signature(start)) 56 57 58def signature(node: NODE, indent: int = 2): 59 """Generate the signature or base information for a single node.""" 60 sig = f"{node.type}" 61 # element node's tag 62 if isinstance(node, Element): 63 sig += f"<{node.tag}{'/' if node.startend else ''}>" 64 65 # count of children in parent node 66 if isinstance(node, (Element, Root)) and len(node.children) > 0: 67 sig += f" [{len(node.children)}]" 68 69 # position of non generated nodes 70 if node.position is not None: 71 sig += f" {node.position}" 72 73 result = [sig] 74 75 # element node's properties 76 if hasattr(node, "properties"): 77 for line in stringify_props(node): 78 result.append(f"│{' '*indent}{line}") 79 80 # literal node's value 81 if isinstance(node, (Text, Comment)): 82 for line in build_literal_value(node): 83 result.append(f"│{' '*indent}{line}") 84 85 return result 86 87class ComplexEncoder(JSONEncoder): 88 def default(self, obj): 89 try: 90 return JSONEncoder.default(self, obj) 91 except: 92 return repr(obj) 93 94def stringify_props(node: Element) -> list[str]: 95 """Generate a list of lines from strigifying the nodes properties.""" 96 97 if len(node.properties.keys()) > 0: 98 lines = dumps(node.properties, indent=2, cls=ComplexEncoder).split("\n") 99 lines[0] = f"properties: {lines[0]}" 100 return lines 101 return [] 102 103 104def build_literal_value(node: Text | Comment) -> list[str]: 105 """Build the lines for the string value of a literal node.""" 106 107 lines = normalize_indent(node.value).split("\n") 108 109 if len(lines) == 1: 110 lines[0] = f'"{lines[0]}"' 111 else: 112 lines[0] = f'"{lines[0]}' 113 lines[-1] = f' {lines[-1]}"' 114 if len(lines) > 2: 115 for idx in range(1, len(lines) - 1): 116 lines[idx] = f' {lines[idx]}' 117 return lines 118 119 120def normalize_indent(text: str) -> str: 121 """Remove extra prefix whitespace while preserving relative indenting. 122 123 Example: 124 ```python 125 if True: 126 print("Hello World") 127 ``` 128 129 becomes 130 131 ```python 132 if True: 133 print("Hello World") 134 ``` 135 """ 136 lines = text.split("\n") 137 138 # Get min offset 139 if len(lines) > 1: 140 min_offset = len(lines[0]) 141 for line in lines: 142 offset = len(line) - len(line.lstrip()) 143 if offset < min_offset: 144 min_offset = offset 145 else: 146 return lines[0] 147 148 # Remove min_offset from each line 149 return "\n".join([line[min_offset:] for line in lines])
def
inspect( start: phml.core.nodes.AST.AST | phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.nodes.Text | phml.core.nodes.nodes.Comment | phml.core.nodes.nodes.DocType | phml.core.nodes.nodes.Parent | phml.core.nodes.nodes.Node | phml.core.nodes.nodes.Literal, indent: int = 2):
15def inspect(start: AST | NODE, indent: int = 2): 16 """Recursively inspect the passed node or ast.""" 17 18 if isinstance(start, AST): 19 start = start.tree 20 21 def recursive_inspect(node: Element | Root, indent: int) -> list[str]: 22 """Generate signature for node then for each child recursively.""" 23 from phml import visit_children # pylint: disable=import-outside-toplevel 24 25 results = [*signature(node)] 26 27 for idx, child in enumerate(visit_children(node)): 28 if isinstance(child, (Element, Root)): 29 lines = recursive_inspect(child, indent) 30 31 child_prefix = "└" if idx == len(node.children) - 1 else "├" 32 nested_prefix = " " if idx == len(node.children) - 1 else "│" 33 34 lines[0] = f"{child_prefix}{idx} {lines[0]}" 35 if len(lines) > 1: 36 for line in range(1, len(lines)): 37 lines[line] = f"{nested_prefix} {lines[line]}" 38 results.extend(lines) 39 else: 40 lines = signature(child, indent) 41 42 child_prefix = "└" if idx == len(node.children) - 1 else "├" 43 nested_prefix = " " if idx == len(node.children) - 1 else "│" 44 45 lines[0] = f"{child_prefix}{idx} {lines[0]}" 46 if len(lines) > 1: 47 for line in range(1, len(lines)): 48 lines[line] = f"{nested_prefix} {lines[line]}" 49 50 results.extend(lines) 51 return results 52 53 if isinstance(start, (Element, Root)): 54 return "\n".join(recursive_inspect(start, indent)) 55 56 return "\n".join(signature(start))
Recursively inspect the passed node or ast.
def
normalize_indent(text: str) -> str:
121def normalize_indent(text: str) -> str: 122 """Remove extra prefix whitespace while preserving relative indenting. 123 124 Example: 125 ```python 126 if True: 127 print("Hello World") 128 ``` 129 130 becomes 131 132 ```python 133 if True: 134 print("Hello World") 135 ``` 136 """ 137 lines = text.split("\n") 138 139 # Get min offset 140 if len(lines) > 1: 141 min_offset = len(lines[0]) 142 for line in lines: 143 offset = len(line) - len(line.lstrip()) 144 if offset < min_offset: 145 min_offset = offset 146 else: 147 return lines[0] 148 149 # Remove min_offset from each line 150 return "\n".join([line[min_offset:] for line in lines])
Remove extra prefix whitespace while preserving relative indenting.
Example:
if True:
print("Hello World")
becomes
if True:
print("Hello World")