Coverage for phml\builder.py: 44%
105 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-30 09:38 -0600
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-30 09:38 -0600
1from __future__ import annotations
3from typing import Optional
5from phml.nodes import *
8def p(
9 selector: Optional[str] = None,
10 *args: str | list | int | All_Nodes,
11):
12 def __process_children(node, children: list[str | list | int | All_Nodes]):
13 for child in children:
14 if isinstance(child, str):
15 node.children.append(Text(child, node))
16 elif isinstance(child, int):
17 node.children.append(Text(str(child), node))
18 elif isinstance(child, All_Nodes):
19 child.parent = node
20 node.children.append(child)
21 elif isinstance(child, list):
22 for c in child:
23 if isinstance(c, str):
24 node.children.append(Text(c, node))
25 elif isinstance(c, int):
26 node.children.append(Text(str(c), node))
27 elif isinstance(c, All_Nodes):
28 c.parent = node
29 node.children.append(c)
30 else:
31 raise TypeError(f"Unkown type <{type(c).__name__}> in {child}: {c}")
33 if isinstance(selector, str) and selector.startswith("<!--"):
34 return Comment(selector.replace("<!--", "").replace("-->", ""))
35 if selector is not None and (
36 not isinstance(selector, str) or len(selector.replace("\n", " ").split(" ")) > 1
37 ):
38 if (
39 isinstance(selector, str)
40 and (len(selector.split(" ")) > 1 or selector.split("\n"))
41 and len(args) == 0
42 ):
43 return Text(selector)
44 args = [selector, *args]
45 selector = None
47 children = [child for child in args if isinstance(child, (str, list, int, All_Nodes))]
48 props = [prop for prop in args if isinstance(prop, dict)]
50 if selector is not None:
51 node = __parse_specifiers(selector)
52 if len(node) > 1:
53 raise Exception("Selector can not be a complex selector")
54 if not isinstance(node[0], dict) or len(node[0]["attributes"]) > 0:
55 raise EncodingWarning("Selector must be of the format `tag?[#id][.classes...]`")
57 node = node[0]
59 node["tag"] = "div" if node["tag"] == "*" else node["tag"]
61 if node["tag"].lower() == "doctype":
62 str_children = [child for child in children if isinstance(child, str)]
63 if len(str_children) > 0:
64 return DocType(str_children[0])
65 else:
66 return DocType()
67 elif node["tag"].lower() == "text":
68 return Text(" ".join([child for child in children if isinstance(child, str)]))
69 else:
70 properties = {}
71 for prop in props:
72 properties.update(prop)
74 if len(node["classList"]) > 0:
75 properties["class"] = properties["class"] or ""
76 properties["class"] += " ".join(node["classList"])
77 if node["id"] is not None:
78 properties["id"] = node["id"]
80 children = [child for child in args if isinstance(child, (str, list, int, All_Nodes))]
82 node = Element(
83 node["tag"],
84 properties=properties,
85 startend=len(children) == 0,
86 )
87 else:
88 node = Root()
90 if len(children) > 0:
91 __process_children(node, children)
93 return node
96def __parse_specifiers(specifier: str) -> dict:
97 """
98 Rules:
99 * `*` = any element
100 * `>` = Everything with certain parent child relationship
101 * `+` = first sibling
102 * `~` = All after
103 * `.` = class
104 * `#` = id
105 * `[attribute]` = all elements with attribute
106 * `[attribute=value]` = all elements with attribute=value
107 * `[attribute~=value]` = all elements with attribute containing value
108 * `[attribute|=value]` = all elements with attribute=value or attribute starting with value-
109 * `node[attribute^=value]` = all elements with attribute starting with value
110 * `node[attribute$=value]` = all elements with attribute ending with value
111 * `node[attribute*=value]` = all elements with attribute containing value
113 """
114 from re import compile
116 splitter = compile(r"([~>*+])|(([.#]?[a-zA-Z0-9_-]+)+((\[[^\[\]]+\]))*)|(\[[^\[\]]+\])+")
118 el_with_attr = compile(r"([.#]?[a-zA-Z0-9_-]+)+(\[[^\[\]]+\])*")
119 el_only_attr = compile(r"((\[[^\[\]]+\]))+")
121 el_classid_from_attr = compile(r"([a-zA-Z0-9_#.-]+)((\[.*\])*)")
122 el_from_class_from_id = compile(r"(#|\.)?([a-zA-Z0-9_-]+)")
123 attr_compare_val = compile(r"\[([a-zA-Z0-9_-]+)([~|^$*]?=)?(\"[^\"]+\"|'[^']+'|[^'\"]+)?\]")
125 tokens = []
126 for token in splitter.finditer(specifier):
128 if token in ["*", ">", "+", "~"]:
129 tokens.append(token.group())
130 elif el_with_attr.match(token.group()):
131 element = {
132 "tag": None,
133 "classList": [],
134 "id": None,
135 "attributes": [],
136 }
138 res = el_classid_from_attr.match(token.group())
140 el_class_id, attrs = res.group(1), res.group(2)
142 if attrs not in ["", None]:
143 for attr in attr_compare_val.finditer(attrs):
144 name, compare, value = attr.groups()
145 if value is not None:
146 value = value.lstrip("'\"").rstrip("'\"")
147 element["attributes"].append(
148 {
149 "name": name,
150 "compare": compare,
151 "value": value,
152 }
153 )
155 if el_class_id not in ["", None]:
156 for item in el_from_class_from_id.finditer(el_class_id):
157 if item.group(1) == ".":
158 if item.group(2) not in element["classList"]:
159 element["classList"].append(item.group(2))
160 elif item.group(1) == "#":
161 if element["id"] is None:
162 element["id"] = item.group(2)
163 else:
164 raise Exception(
165 f"There may only be one id per element specifier.\n{token.group()}"
166 )
167 else:
168 element["tag"] = item.group(2)
170 tokens.append(element)
171 elif el_only_attr.match(token.group()):
172 element = {
173 "tag": None,
174 "classList": [],
175 "id": None,
176 "attributes": [],
177 }
179 element["tag"] = "*"
181 if token.group() not in ["", None]:
182 for attr in attr_compare_val.finditer(token.group()):
183 name, compare, value = attr.groups()
184 if value is not None:
185 value = value.lstrip("'\"").rstrip("'\"")
186 element["attributes"].append(
187 {
188 "name": name,
189 "compare": compare,
190 "value": value,
191 }
192 )
194 tokens.append(element)
196 return tokens