pyreact-framework 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyreact/__init__.py +144 -0
- pyreact/cli/__init__.py +3 -0
- pyreact/cli/main.py +512 -0
- pyreact/core/__init__.py +80 -0
- pyreact/core/component.py +372 -0
- pyreact/core/context.py +173 -0
- pyreact/core/element.py +208 -0
- pyreact/core/error_boundary.py +145 -0
- pyreact/core/hooks.py +550 -0
- pyreact/core/memo.py +221 -0
- pyreact/core/portal.py +159 -0
- pyreact/core/reconciler.py +399 -0
- pyreact/core/refs.py +213 -0
- pyreact/core/renderer.py +112 -0
- pyreact/core/scheduler.py +304 -0
- pyreact/devtools/__init__.py +18 -0
- pyreact/devtools/debugger.py +314 -0
- pyreact/devtools/profiler.py +288 -0
- pyreact/dom/__init__.py +64 -0
- pyreact/dom/attributes.py +317 -0
- pyreact/dom/dom_operations.py +333 -0
- pyreact/dom/events.py +349 -0
- pyreact/server/__init__.py +34 -0
- pyreact/server/hydration.py +216 -0
- pyreact/server/ssr.py +344 -0
- pyreact/styles/__init__.py +19 -0
- pyreact/styles/css_module.py +231 -0
- pyreact/styles/styled.py +303 -0
- pyreact/testing/__init__.py +71 -0
- pyreact/testing/fire_event.py +355 -0
- pyreact/testing/screen.py +267 -0
- pyreact/testing/test_renderer.py +232 -0
- pyreact/utils/__init__.py +17 -0
- pyreact/utils/diff.py +182 -0
- pyreact/utils/object_pool.py +216 -0
- pyreact_framework-1.0.0.dist-info/METADATA +363 -0
- pyreact_framework-1.0.0.dist-info/RECORD +41 -0
- pyreact_framework-1.0.0.dist-info/WHEEL +5 -0
- pyreact_framework-1.0.0.dist-info/entry_points.txt +2 -0
- pyreact_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyreact_framework-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Attributes Module
|
|
3
|
+
=================
|
|
4
|
+
|
|
5
|
+
This module handles mapping between HTML attributes and DOM properties.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# HTML attributes that map to different DOM properties
|
|
12
|
+
PROPERTY_NAMES: Dict[str, str] = {
|
|
13
|
+
'acceptCharset': 'accept-charset',
|
|
14
|
+
'accessKey': 'accesskey',
|
|
15
|
+
'allowFullScreen': 'allowfullscreen',
|
|
16
|
+
'autoComplete': 'autocomplete',
|
|
17
|
+
'autoFocus': 'autofocus',
|
|
18
|
+
'autoPlay': 'autoplay',
|
|
19
|
+
'cellPadding': 'cellpadding',
|
|
20
|
+
'cellSpacing': 'cellspacing',
|
|
21
|
+
'charSet': 'charset',
|
|
22
|
+
'className': 'class',
|
|
23
|
+
'colSpan': 'colspan',
|
|
24
|
+
'contentEditable': 'contenteditable',
|
|
25
|
+
'contextMenu': 'contextmenu',
|
|
26
|
+
'crossOrigin': 'crossorigin',
|
|
27
|
+
'dateTime': 'datetime',
|
|
28
|
+
'encType': 'enctype',
|
|
29
|
+
'formAction': 'formaction',
|
|
30
|
+
'formEncType': 'formenctype',
|
|
31
|
+
'formMethod': 'formmethod',
|
|
32
|
+
'formNoValidate': 'formnovalidate',
|
|
33
|
+
'formTarget': 'formtarget',
|
|
34
|
+
'frameBorder': 'frameborder',
|
|
35
|
+
'hrefLang': 'hreflang',
|
|
36
|
+
'htmlFor': 'for',
|
|
37
|
+
'httpEquiv': 'http-equiv',
|
|
38
|
+
'isMap': 'ismap',
|
|
39
|
+
'itemProp': 'itemprop',
|
|
40
|
+
'itemScope': 'itemscope',
|
|
41
|
+
'itemType': 'itemtype',
|
|
42
|
+
'keyParams': 'keyparams',
|
|
43
|
+
'keyType': 'keytype',
|
|
44
|
+
'marginHeight': 'marginheight',
|
|
45
|
+
'marginWidth': 'marginwidth',
|
|
46
|
+
'maxLength': 'maxlength',
|
|
47
|
+
'mediaGroup': 'mediagroup',
|
|
48
|
+
'noValidate': 'novalidate',
|
|
49
|
+
'playsInline': 'playsinline',
|
|
50
|
+
'radioGroup': 'radiogroup',
|
|
51
|
+
'readOnly': 'readonly',
|
|
52
|
+
'rowSpan': 'rowspan',
|
|
53
|
+
'spellCheck': 'spellcheck',
|
|
54
|
+
'srcDoc': 'srcdoc',
|
|
55
|
+
'srcLang': 'srclang',
|
|
56
|
+
'srcSet': 'srcset',
|
|
57
|
+
'tabIndex': 'tabindex',
|
|
58
|
+
'useMap': 'usemap',
|
|
59
|
+
'vocab': 'vocab',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Reverse mapping
|
|
63
|
+
ATTRIBUTE_NAMES = {v: k for k, v in PROPERTY_NAMES.items()}
|
|
64
|
+
|
|
65
|
+
# Boolean attributes
|
|
66
|
+
BOOLEAN_ATTRIBUTES = {
|
|
67
|
+
'allowfullscreen',
|
|
68
|
+
'async',
|
|
69
|
+
'autofocus',
|
|
70
|
+
'autoplay',
|
|
71
|
+
'checked',
|
|
72
|
+
'controls',
|
|
73
|
+
'default',
|
|
74
|
+
'defer',
|
|
75
|
+
'disabled',
|
|
76
|
+
'formnovalidate',
|
|
77
|
+
'hidden',
|
|
78
|
+
'ismap',
|
|
79
|
+
'itemscope',
|
|
80
|
+
'loop',
|
|
81
|
+
'multiple',
|
|
82
|
+
'muted',
|
|
83
|
+
'nomodule',
|
|
84
|
+
'novalidate',
|
|
85
|
+
'open',
|
|
86
|
+
'playsinline',
|
|
87
|
+
'readonly',
|
|
88
|
+
'required',
|
|
89
|
+
'reversed',
|
|
90
|
+
'selected',
|
|
91
|
+
'truespeed',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Attributes that should be set as properties
|
|
95
|
+
PROPERTIES = {
|
|
96
|
+
'value',
|
|
97
|
+
'checked',
|
|
98
|
+
'selected',
|
|
99
|
+
'selectedIndex',
|
|
100
|
+
'defaultValue',
|
|
101
|
+
'defaultChecked',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Attributes with units
|
|
105
|
+
UNITLESS_PROPERTIES = {
|
|
106
|
+
'animationIterationCount',
|
|
107
|
+
'borderImageOutset',
|
|
108
|
+
'borderImageSlice',
|
|
109
|
+
'borderImageWidth',
|
|
110
|
+
'boxFlex',
|
|
111
|
+
'boxFlexGroup',
|
|
112
|
+
'boxOrdinalGroup',
|
|
113
|
+
'columnCount',
|
|
114
|
+
'columns',
|
|
115
|
+
'flex',
|
|
116
|
+
'flexGrow',
|
|
117
|
+
'flexPositive',
|
|
118
|
+
'flexShrink',
|
|
119
|
+
'flexNegative',
|
|
120
|
+
'flexOrder',
|
|
121
|
+
'gridArea',
|
|
122
|
+
'gridColumn',
|
|
123
|
+
'gridColumnEnd',
|
|
124
|
+
'gridColumnStart',
|
|
125
|
+
'gridRow',
|
|
126
|
+
'gridRowEnd',
|
|
127
|
+
'gridRowStart',
|
|
128
|
+
'lineClamp',
|
|
129
|
+
'lineHeight',
|
|
130
|
+
'opacity',
|
|
131
|
+
'order',
|
|
132
|
+
'orphans',
|
|
133
|
+
'tabSize',
|
|
134
|
+
'widows',
|
|
135
|
+
'zIndex',
|
|
136
|
+
'zoom',
|
|
137
|
+
'fillOpacity',
|
|
138
|
+
'floodOpacity',
|
|
139
|
+
'stopOpacity',
|
|
140
|
+
'strokeDasharray',
|
|
141
|
+
'strokeDashoffset',
|
|
142
|
+
'strokeMiterlimit',
|
|
143
|
+
'strokeOpacity',
|
|
144
|
+
'strokeWidth',
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_custom_attribute(name: str) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Check if an attribute is custom (data-*, aria-*)
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
name: Attribute name
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
bool: True if custom attribute
|
|
157
|
+
"""
|
|
158
|
+
return name.startswith('data-') or name.startswith('aria-')
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def should_set_attribute(name: str, value: Any) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Check if attribute should be set
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name: Attribute name
|
|
167
|
+
value: Attribute value
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
bool: True if should set
|
|
171
|
+
"""
|
|
172
|
+
# Skip null/undefined
|
|
173
|
+
if value is None:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
# Skip event handlers (handled separately)
|
|
177
|
+
if name.startswith('on'):
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
# Skip style (handled separately)
|
|
181
|
+
if name == 'style':
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
# Skip ref and key
|
|
185
|
+
if name in ('ref', 'key'):
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_attribute_name(property_name: str) -> str:
|
|
192
|
+
"""
|
|
193
|
+
Get HTML attribute name from DOM property name
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
property_name: DOM property name
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
str: HTML attribute name
|
|
200
|
+
"""
|
|
201
|
+
return PROPERTY_NAMES.get(property_name, property_name.lower())
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_property_name(attribute_name: str) -> str:
|
|
205
|
+
"""
|
|
206
|
+
Get DOM property name from HTML attribute name
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
attribute_name: HTML attribute name
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
str: DOM property name
|
|
213
|
+
"""
|
|
214
|
+
return ATTRIBUTE_NAMES.get(attribute_name, attribute_name)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def is_boolean_attribute(name: str) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Check if attribute is boolean
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
name: Attribute name
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
bool: True if boolean attribute
|
|
226
|
+
"""
|
|
227
|
+
return name.lower() in BOOLEAN_ATTRIBUTES
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def is_property(name: str) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Check if attribute should be set as a property
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
name: Attribute name
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
bool: True if should be property
|
|
239
|
+
"""
|
|
240
|
+
return name in PROPERTIES
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_style_value(name: str, value: Any) -> str:
|
|
244
|
+
"""
|
|
245
|
+
Convert style value to string
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
name: CSS property name
|
|
249
|
+
value: CSS value
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
str: Style string
|
|
253
|
+
"""
|
|
254
|
+
if value is None or value == '':
|
|
255
|
+
return ''
|
|
256
|
+
|
|
257
|
+
if isinstance(value, number):
|
|
258
|
+
if name not in UNITLESS_PROPERTIES:
|
|
259
|
+
return f"{value}px"
|
|
260
|
+
return str(value)
|
|
261
|
+
|
|
262
|
+
return str(value)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def escape_html_value(value: str) -> str:
|
|
266
|
+
"""
|
|
267
|
+
Escape HTML special characters
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
value: Value to escape
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
str: Escaped value
|
|
274
|
+
"""
|
|
275
|
+
return (
|
|
276
|
+
str(value)
|
|
277
|
+
.replace('&', '&')
|
|
278
|
+
.replace('<', '<')
|
|
279
|
+
.replace('>', '>')
|
|
280
|
+
.replace('"', '"')
|
|
281
|
+
.replace("'", ''')
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def render_attributes(props: Dict[str, Any]) -> str:
|
|
286
|
+
"""
|
|
287
|
+
Render attributes as HTML string
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
props: Props dictionary
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
str: HTML attribute string
|
|
294
|
+
"""
|
|
295
|
+
result = []
|
|
296
|
+
|
|
297
|
+
for name, value in props.items():
|
|
298
|
+
if not should_set_attribute(name, value):
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
attr_name = get_attribute_name(name)
|
|
302
|
+
|
|
303
|
+
if is_boolean_attribute(attr_name):
|
|
304
|
+
if value:
|
|
305
|
+
result.append(attr_name)
|
|
306
|
+
else:
|
|
307
|
+
escaped = escape_html_value(value)
|
|
308
|
+
result.append(f'{attr_name}="{escaped}"')
|
|
309
|
+
|
|
310
|
+
return ' '.join(result)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Number type check (for Python 2/3 compatibility)
|
|
314
|
+
try:
|
|
315
|
+
number = (int, float)
|
|
316
|
+
except NameError:
|
|
317
|
+
number = (int, float)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DOM Operations Module
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
This module provides low-level DOM manipulation functions.
|
|
6
|
+
These are platform-agnostic and can be adapted for different environments.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Dict, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Platform-specific DOM implementation
|
|
13
|
+
# In a real implementation, this would use a JavaScript bridge
|
|
14
|
+
|
|
15
|
+
class DOMNode:
|
|
16
|
+
"""Base class for DOM nodes"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, node_type: str):
|
|
19
|
+
self.node_type = node_type
|
|
20
|
+
self.parent_node: Optional['DOMNode'] = None
|
|
21
|
+
self.child_nodes: list = []
|
|
22
|
+
|
|
23
|
+
def append_child(self, child: 'DOMNode') -> 'DOMNode':
|
|
24
|
+
"""Append a child node"""
|
|
25
|
+
child.parent_node = self
|
|
26
|
+
self.child_nodes.append(child)
|
|
27
|
+
return child
|
|
28
|
+
|
|
29
|
+
def remove_child(self, child: 'DOMNode') -> 'DOMNode':
|
|
30
|
+
"""Remove a child node"""
|
|
31
|
+
if child in self.child_nodes:
|
|
32
|
+
child.parent_node = None
|
|
33
|
+
self.child_nodes.remove(child)
|
|
34
|
+
return child
|
|
35
|
+
|
|
36
|
+
def insert_before(self, new_node: 'DOMNode', ref_node: Optional['DOMNode']) -> 'DOMNode':
|
|
37
|
+
"""Insert a node before a reference node"""
|
|
38
|
+
new_node.parent_node = self
|
|
39
|
+
if ref_node is None:
|
|
40
|
+
self.child_nodes.append(new_node)
|
|
41
|
+
else:
|
|
42
|
+
index = self.child_nodes.index(ref_node)
|
|
43
|
+
self.child_nodes.insert(index, new_node)
|
|
44
|
+
return new_node
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Element(DOMNode):
|
|
48
|
+
"""DOM Element"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, tag_name: str):
|
|
51
|
+
super().__init__('element')
|
|
52
|
+
self.tag_name = tag_name.lower()
|
|
53
|
+
self.attributes: Dict[str, str] = {}
|
|
54
|
+
self.style: Dict[str, str] = {}
|
|
55
|
+
self._event_listeners: Dict[str, list] = {}
|
|
56
|
+
self._text_content: str = ''
|
|
57
|
+
|
|
58
|
+
def set_attribute(self, name: str, value: str) -> None:
|
|
59
|
+
"""Set an attribute"""
|
|
60
|
+
self.attributes[name] = value
|
|
61
|
+
|
|
62
|
+
def get_attribute(self, name: str) -> Optional[str]:
|
|
63
|
+
"""Get an attribute"""
|
|
64
|
+
return self.attributes.get(name)
|
|
65
|
+
|
|
66
|
+
def remove_attribute(self, name: str) -> None:
|
|
67
|
+
"""Remove an attribute"""
|
|
68
|
+
if name in self.attributes:
|
|
69
|
+
del self.attributes[name]
|
|
70
|
+
|
|
71
|
+
def set_style(self, name: str, value: str) -> None:
|
|
72
|
+
"""Set a style property"""
|
|
73
|
+
self.style[name] = value
|
|
74
|
+
|
|
75
|
+
def add_event_listener(self, event_type: str, listener: Callable) -> None:
|
|
76
|
+
"""Add an event listener"""
|
|
77
|
+
if event_type not in self._event_listeners:
|
|
78
|
+
self._event_listeners[event_type] = []
|
|
79
|
+
self._event_listeners[event_type].append(listener)
|
|
80
|
+
|
|
81
|
+
def remove_event_listener(self, event_type: str, listener: Callable) -> None:
|
|
82
|
+
"""Remove an event listener"""
|
|
83
|
+
if event_type in self._event_listeners:
|
|
84
|
+
if listener in self._event_listeners[event_type]:
|
|
85
|
+
self._event_listeners[event_type].remove(listener)
|
|
86
|
+
|
|
87
|
+
def set_inner_html(self, html: str) -> None:
|
|
88
|
+
"""Set inner HTML"""
|
|
89
|
+
self._text_content = html
|
|
90
|
+
self.child_nodes.clear()
|
|
91
|
+
|
|
92
|
+
def insert_child(self, child: 'DOMNode', index: int) -> None:
|
|
93
|
+
"""Insert a child at a specific index"""
|
|
94
|
+
child.parent_node = self
|
|
95
|
+
self.child_nodes.insert(index, child)
|
|
96
|
+
|
|
97
|
+
def remove_child_at(self, index: int) -> None:
|
|
98
|
+
"""Remove a child at a specific index"""
|
|
99
|
+
if 0 <= index < len(self.child_nodes):
|
|
100
|
+
child = self.child_nodes.pop(index)
|
|
101
|
+
child.parent_node = None
|
|
102
|
+
|
|
103
|
+
def replace_child_at(self, new_child: 'DOMNode', index: int) -> None:
|
|
104
|
+
"""Replace a child at a specific index"""
|
|
105
|
+
if 0 <= index < len(self.child_nodes):
|
|
106
|
+
old_child = self.child_nodes[index]
|
|
107
|
+
old_child.parent_node = None
|
|
108
|
+
new_child.parent_node = self
|
|
109
|
+
self.child_nodes[index] = new_child
|
|
110
|
+
|
|
111
|
+
def move_child(self, old_index: int, new_index: int) -> None:
|
|
112
|
+
"""Move a child from old_index to new_index"""
|
|
113
|
+
if 0 <= old_index < len(self.child_nodes) and 0 <= new_index < len(self.child_nodes):
|
|
114
|
+
child = self.child_nodes.pop(old_index)
|
|
115
|
+
self.child_nodes.insert(new_index, child)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def first_child(self) -> Optional['DOMNode']:
|
|
119
|
+
"""Get first child"""
|
|
120
|
+
return self.child_nodes[0] if self.child_nodes else None
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def inner_html(self) -> str:
|
|
124
|
+
"""Get inner HTML"""
|
|
125
|
+
return self._text_content
|
|
126
|
+
|
|
127
|
+
@inner_html.setter
|
|
128
|
+
def inner_html(self, value: str):
|
|
129
|
+
"""Set inner HTML"""
|
|
130
|
+
self._text_content = value
|
|
131
|
+
self.child_nodes.clear()
|
|
132
|
+
|
|
133
|
+
def get_element_by_id(self, id: str) -> Optional['Element']:
|
|
134
|
+
"""Find element by ID"""
|
|
135
|
+
if self.attributes.get('id') == id:
|
|
136
|
+
return self
|
|
137
|
+
for child in self.child_nodes:
|
|
138
|
+
if isinstance(child, Element):
|
|
139
|
+
result = child.get_element_by_id(id)
|
|
140
|
+
if result:
|
|
141
|
+
return result
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def query_selector(self, selector: str) -> Optional['Element']:
|
|
145
|
+
"""Query selector (simplified)"""
|
|
146
|
+
# Simplified implementation
|
|
147
|
+
for child in self.child_nodes:
|
|
148
|
+
if isinstance(child, Element):
|
|
149
|
+
if selector.startswith('#'):
|
|
150
|
+
if child.attributes.get('id') == selector[1:]:
|
|
151
|
+
return child
|
|
152
|
+
elif selector.startswith('.'):
|
|
153
|
+
class_name = selector[1:]
|
|
154
|
+
if class_name in child.attributes.get('class', '').split():
|
|
155
|
+
return child
|
|
156
|
+
elif child.tag_name == selector.lower():
|
|
157
|
+
return child
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def query_selector_all(self, selector: str) -> list:
|
|
161
|
+
"""Query all matching elements"""
|
|
162
|
+
results = []
|
|
163
|
+
for child in self.child_nodes:
|
|
164
|
+
if isinstance(child, Element):
|
|
165
|
+
if selector.startswith('#'):
|
|
166
|
+
if child.attributes.get('id') == selector[1:]:
|
|
167
|
+
results.append(child)
|
|
168
|
+
elif selector.startswith('.'):
|
|
169
|
+
class_name = selector[1:]
|
|
170
|
+
if class_name in child.attributes.get('class', '').split():
|
|
171
|
+
results.append(child)
|
|
172
|
+
elif child.tag_name == selector.lower():
|
|
173
|
+
results.append(child)
|
|
174
|
+
results.extend(child.query_selector_all(selector))
|
|
175
|
+
return results
|
|
176
|
+
|
|
177
|
+
def focus(self) -> None:
|
|
178
|
+
"""Focus the element"""
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
def blur(self) -> None:
|
|
182
|
+
"""Blur the element"""
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def click(self) -> None:
|
|
186
|
+
"""Click the element"""
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
def scroll_into_view(self) -> None:
|
|
190
|
+
"""Scroll element into view"""
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
def get_bounding_client_rect(self) -> Dict[str, float]:
|
|
194
|
+
"""Get element dimensions"""
|
|
195
|
+
return {
|
|
196
|
+
'x': 0, 'y': 0,
|
|
197
|
+
'width': 0, 'height': 0,
|
|
198
|
+
'top': 0, 'right': 0,
|
|
199
|
+
'bottom': 0, 'left': 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def __repr__(self) -> str:
|
|
203
|
+
return f"<{self.tag_name}>"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TextNode(DOMNode):
|
|
207
|
+
"""Text node"""
|
|
208
|
+
|
|
209
|
+
def __init__(self, text: str):
|
|
210
|
+
super().__init__('text')
|
|
211
|
+
self.text_content = text
|
|
212
|
+
|
|
213
|
+
def __repr__(self) -> str:
|
|
214
|
+
return f"#text: {self.text_content[:20]}..."
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class CommentNode(DOMNode):
|
|
218
|
+
"""Comment node"""
|
|
219
|
+
|
|
220
|
+
def __init__(self, text: str):
|
|
221
|
+
super().__init__('comment')
|
|
222
|
+
self.text_content = text
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class Document(Element):
|
|
226
|
+
"""Document object"""
|
|
227
|
+
|
|
228
|
+
def __init__(self):
|
|
229
|
+
super().__init__('html')
|
|
230
|
+
self.node_type = 'document'
|
|
231
|
+
self.body = Element('body')
|
|
232
|
+
self.head = Element('head')
|
|
233
|
+
self.append_child(self.head)
|
|
234
|
+
self.append_child(self.body)
|
|
235
|
+
|
|
236
|
+
def create_element(self, tag_name: str) -> Element:
|
|
237
|
+
"""Create an element"""
|
|
238
|
+
return Element(tag_name)
|
|
239
|
+
|
|
240
|
+
def create_text_node(self, text: str) -> TextNode:
|
|
241
|
+
"""Create a text node"""
|
|
242
|
+
return TextNode(text)
|
|
243
|
+
|
|
244
|
+
def create_comment(self, text: str) -> CommentNode:
|
|
245
|
+
"""Create a comment node"""
|
|
246
|
+
return CommentNode(text)
|
|
247
|
+
|
|
248
|
+
def get_element_by_id(self, id: str) -> Optional[Element]:
|
|
249
|
+
"""Find element by ID"""
|
|
250
|
+
return self.body.get_element_by_id(id)
|
|
251
|
+
|
|
252
|
+
def query_selector(self, selector: str) -> Optional[Element]:
|
|
253
|
+
"""Query selector"""
|
|
254
|
+
return self.body.query_selector(selector)
|
|
255
|
+
|
|
256
|
+
def query_selector_all(self, selector: str) -> list:
|
|
257
|
+
"""Query all matching elements"""
|
|
258
|
+
return self.body.query_selector_all(selector)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Global document instance
|
|
262
|
+
document = Document()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def create_element(tag_name: str) -> Element:
|
|
266
|
+
"""Create a DOM element"""
|
|
267
|
+
return document.create_element(tag_name)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def create_text_node(text: str) -> TextNode:
|
|
271
|
+
"""Create a text node"""
|
|
272
|
+
return document.create_text_node(text)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def append_child(parent: DOMNode, child: DOMNode) -> DOMNode:
|
|
276
|
+
"""Append a child to a parent"""
|
|
277
|
+
return parent.append_child(child)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def remove_child(parent: DOMNode, child: DOMNode) -> DOMNode:
|
|
281
|
+
"""Remove a child from a parent"""
|
|
282
|
+
return parent.remove_child(child)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def insert_before(parent: DOMNode, new_node: DOMNode, ref_node: Optional[DOMNode]) -> DOMNode:
|
|
286
|
+
"""Insert a node before a reference node"""
|
|
287
|
+
return parent.insert_before(new_node, ref_node)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def set_attribute(element: Element, name: str, value: str) -> None:
|
|
291
|
+
"""Set an attribute on an element"""
|
|
292
|
+
element.attributes[name] = value
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def remove_attribute(element: Element, name: str) -> None:
|
|
296
|
+
"""Remove an attribute from an element"""
|
|
297
|
+
if name in element.attributes:
|
|
298
|
+
del element.attributes[name]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def set_style(element: Element, styles: Dict[str, str]) -> None:
|
|
302
|
+
"""Set styles on an element"""
|
|
303
|
+
element.style.update(styles)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def add_event_listener(
|
|
307
|
+
element: Element,
|
|
308
|
+
event_type: str,
|
|
309
|
+
listener: Callable,
|
|
310
|
+
options: Optional[Dict] = None
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Add an event listener to an element"""
|
|
313
|
+
if event_type not in element._event_listeners:
|
|
314
|
+
element._event_listeners[event_type] = []
|
|
315
|
+
element._event_listeners[event_type].append(listener)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def remove_event_listener(
|
|
319
|
+
element: Element,
|
|
320
|
+
event_type: str,
|
|
321
|
+
listener: Callable
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Remove an event listener from an element"""
|
|
324
|
+
if event_type in element._event_listeners:
|
|
325
|
+
if listener in element._event_listeners[event_type]:
|
|
326
|
+
element._event_listeners[event_type].remove(listener)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def dispatch_event(element: Element, event_type: str, event_data: Optional[Dict] = None) -> None:
|
|
330
|
+
"""Dispatch an event on an element"""
|
|
331
|
+
if event_type in element._event_listeners:
|
|
332
|
+
for listener in element._event_listeners[event_type]:
|
|
333
|
+
listener(event_data or {})
|