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.utilities 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 = ( 31 "\x1b[38;5;8m└\x1b[39m" 32 if idx == len(node.children) - 1 33 else "\x1b[38;5;8m├\x1b[39m" 34 ) 35 nested_prefix = ( 36 " " if idx == len(node.children) - 1 else "\x1b[38;5;8m│\x1b[39m" 37 ) 38 39 lines[0] = f"{child_prefix}\x1b[38;5;8m{idx}\x1b[39m {lines[0]}" 40 if len(lines) > 1: 41 for line in range(1, len(lines)): 42 lines[line] = f"{nested_prefix} {lines[line]}" 43 results.extend(lines) 44 else: 45 lines = signature(child, indent) 46 47 child_prefix = ( 48 "\x1b[38;5;8m└\x1b[39m" 49 if idx == len(node.children) - 1 50 else "\x1b[38;5;8m├\x1b[39m" 51 ) 52 nested_prefix = ( 53 " " if idx == len(node.children) - 1 else "\x1b[38;5;8m│\x1b[39m" 54 ) 55 56 lines[0] = f"{child_prefix}\x1b[38;5;8m{idx}\x1b[39m {lines[0]}" 57 if len(lines) > 1: 58 for line in range(1, len(lines)): 59 lines[line] = f"{nested_prefix} {lines[line]}" 60 61 results.extend(lines) 62 return results 63 64 if isinstance(start, (Element, Root)): 65 return "\n".join(recursive_inspect(start, indent)) 66 67 return "\n".join(signature(start)) 68 69 70def signature(node: NODE, indent: int = 2): 71 """Generate the signature or base information for a single node.""" 72 sig = "" 73 # element node's tag 74 if isinstance(node, Element): 75 sig += f"\x1b[34m{node.tag}\x1b[39m" 76 else: 77 sig = f"\x1b[34m{node.type}\x1b[39m" 78 79 # count of children in parent node 80 if isinstance(node, (Element, Root)): 81 sig += f" \x1b[38;5;8m[{len(node.children) if len(node.children) > 0 else '/'}]\x1b[39m" 82 83 # position of non generated nodes 84 if node.position is not None: 85 sig += f" {node.position}" 86 87 result = [sig] 88 89 # element node's properties 90 if hasattr(node, "properties"): 91 for line in stringify_props(node): 92 result.append(f"\x1b[38;5;8m│\x1b[39m{' '*indent}{line}") 93 94 # literal node's value 95 if isinstance(node, (Text, Comment)): 96 for line in build_literal_value(node): 97 result.append(f"\x1b[38;5;8m│\x1b[39m{' '*indent}\x1b[32m{line}\x1b[39m") 98 99 return result 100 101 102class ComplexEncoder(JSONEncoder): 103 def default(self, obj): 104 try: 105 return JSONEncoder.default(self, obj) 106 except: 107 return repr(obj) 108 109 110def stringify_props(node: Element) -> list[str]: 111 """Generate a list of lines from strigifying the nodes properties.""" 112 113 if len(node.properties.keys()) > 0: 114 lines = ["properties: \x1b[38;5;8m{\x1b[39m"] 115 for key, value in node.properties.items(): 116 if value is None or isinstance(value, bool): 117 value = f"\x1b[38;5;104m{value}\x1b[39m" 118 elif isinstance(value, str): 119 value = f'\x1b[32m"{value}"\x1b[39m' 120 lines.append(f' \x1b[32m"{key}"\x1b[39m: {value},') 121 lines.append("\x1b[38;5;8m}\x1b[39m") 122 return lines 123 return [] 124 125 126def build_literal_value(node: Text | Comment) -> list[str]: 127 """Build the lines for the string value of a literal node.""" 128 129 lines = normalize_indent(node.value).split("\n") 130 131 if len(lines) == 1: 132 lines[0] = f'"{lines[0]}"' 133 else: 134 lines[0] = f'"{lines[0]}' 135 lines[-1] = f' {lines[-1]}"' 136 if len(lines) > 2: 137 for idx in range(1, len(lines) - 1): 138 lines[idx] = f" {lines[idx]}" 139 return lines 140 141 142def normalize_indent(text: str) -> str: 143 """Remove extra prefix whitespace while preserving relative indenting. 144 145 Example: 146 ```python 147 if True: 148 print("Hello World") 149 ``` 150 151 becomes 152 153 ```python 154 if True: 155 print("Hello World") 156 ``` 157 """ 158 lines = text.split("\n") 159 160 # Get min offset 161 if len(lines) > 1: 162 min_offset = len(lines[0]) 163 for line in lines: 164 offset = len(line) - len(line.lstrip()) 165 if offset < min_offset: 166 min_offset = offset 167 else: 168 return lines[0] 169 170 # Remove min_offset from each line 171 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.utilities 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 = ( 32 "\x1b[38;5;8m└\x1b[39m" 33 if idx == len(node.children) - 1 34 else "\x1b[38;5;8m├\x1b[39m" 35 ) 36 nested_prefix = ( 37 " " if idx == len(node.children) - 1 else "\x1b[38;5;8m│\x1b[39m" 38 ) 39 40 lines[0] = f"{child_prefix}\x1b[38;5;8m{idx}\x1b[39m {lines[0]}" 41 if len(lines) > 1: 42 for line in range(1, len(lines)): 43 lines[line] = f"{nested_prefix} {lines[line]}" 44 results.extend(lines) 45 else: 46 lines = signature(child, indent) 47 48 child_prefix = ( 49 "\x1b[38;5;8m└\x1b[39m" 50 if idx == len(node.children) - 1 51 else "\x1b[38;5;8m├\x1b[39m" 52 ) 53 nested_prefix = ( 54 " " if idx == len(node.children) - 1 else "\x1b[38;5;8m│\x1b[39m" 55 ) 56 57 lines[0] = f"{child_prefix}\x1b[38;5;8m{idx}\x1b[39m {lines[0]}" 58 if len(lines) > 1: 59 for line in range(1, len(lines)): 60 lines[line] = f"{nested_prefix} {lines[line]}" 61 62 results.extend(lines) 63 return results 64 65 if isinstance(start, (Element, Root)): 66 return "\n".join(recursive_inspect(start, indent)) 67 68 return "\n".join(signature(start))
Recursively inspect the passed node or ast.
def
normalize_indent(text: str) -> str:
143def normalize_indent(text: str) -> str: 144 """Remove extra prefix whitespace while preserving relative indenting. 145 146 Example: 147 ```python 148 if True: 149 print("Hello World") 150 ``` 151 152 becomes 153 154 ```python 155 if True: 156 print("Hello World") 157 ``` 158 """ 159 lines = text.split("\n") 160 161 # Get min offset 162 if len(lines) > 1: 163 min_offset = len(lines[0]) 164 for line in lines: 165 offset = len(line) - len(line.lstrip()) 166 if offset < min_offset: 167 min_offset = offset 168 else: 169 return lines[0] 170 171 # Remove min_offset from each line 172 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")