puppeteer-ruby 0.45.6 → 0.50.0.alpha6
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -3
- data/AGENTS.md +170 -0
- data/CLAUDE/README.md +41 -0
- data/CLAUDE/architecture.md +253 -0
- data/CLAUDE/cdp_protocol.md +230 -0
- data/CLAUDE/concurrency.md +216 -0
- data/CLAUDE/porting_puppeteer.md +605 -0
- data/CLAUDE/rbs_type_checking.md +101 -0
- data/CLAUDE/spec_migration_plans.md +1039 -0
- data/CLAUDE/testing.md +278 -0
- data/CLAUDE.md +242 -0
- data/README.md +9 -0
- data/Rakefile +7 -0
- data/Steepfile +28 -0
- data/docs/api_coverage.md +106 -57
- data/lib/puppeteer/aria_query_handler.rb +3 -2
- data/lib/puppeteer/async_utils.rb +214 -0
- data/lib/puppeteer/browser.rb +98 -56
- data/lib/puppeteer/browser_connector.rb +18 -3
- data/lib/puppeteer/browser_context.rb +196 -3
- data/lib/puppeteer/browser_runner.rb +18 -10
- data/lib/puppeteer/cdp_session.rb +67 -23
- data/lib/puppeteer/chrome_target_manager.rb +65 -40
- data/lib/puppeteer/connection.rb +55 -36
- data/lib/puppeteer/console_message.rb +9 -1
- data/lib/puppeteer/console_patch.rb +47 -0
- data/lib/puppeteer/css_coverage.rb +5 -3
- data/lib/puppeteer/custom_query_handler.rb +80 -33
- data/lib/puppeteer/define_async_method.rb +31 -37
- data/lib/puppeteer/dialog.rb +47 -14
- data/lib/puppeteer/element_handle.rb +236 -62
- data/lib/puppeteer/emulation_manager.rb +1 -1
- data/lib/puppeteer/env.rb +1 -1
- data/lib/puppeteer/errors.rb +25 -2
- data/lib/puppeteer/event_callbackable.rb +15 -0
- data/lib/puppeteer/events.rb +4 -0
- data/lib/puppeteer/execution_context.rb +148 -3
- data/lib/puppeteer/file_chooser.rb +6 -0
- data/lib/puppeteer/frame.rb +177 -91
- data/lib/puppeteer/frame_manager.rb +69 -48
- data/lib/puppeteer/http_request.rb +114 -38
- data/lib/puppeteer/http_response.rb +24 -7
- data/lib/puppeteer/isolated_world.rb +64 -41
- data/lib/puppeteer/js_coverage.rb +5 -3
- data/lib/puppeteer/js_handle.rb +77 -16
- data/lib/puppeteer/keyboard.rb +30 -17
- data/lib/puppeteer/launcher/browser_options.rb +3 -1
- data/lib/puppeteer/launcher/chrome.rb +8 -5
- data/lib/puppeteer/launcher/launch_options.rb +7 -2
- data/lib/puppeteer/launcher.rb +4 -8
- data/lib/puppeteer/lifecycle_watcher.rb +38 -22
- data/lib/puppeteer/locators.rb +733 -0
- data/lib/puppeteer/mouse.rb +273 -64
- data/lib/puppeteer/network_event_manager.rb +7 -0
- data/lib/puppeteer/network_manager.rb +393 -112
- data/lib/puppeteer/p_query_handler.rb +367 -0
- data/lib/puppeteer/p_selector_parser.rb +241 -0
- data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
- data/lib/puppeteer/page.rb +583 -226
- data/lib/puppeteer/puppeteer.rb +171 -64
- data/lib/puppeteer/query_handler_manager.rb +66 -16
- data/lib/puppeteer/reactor_runner.rb +247 -0
- data/lib/puppeteer/remote_object.rb +127 -47
- data/lib/puppeteer/target.rb +74 -27
- data/lib/puppeteer/task_manager.rb +3 -1
- data/lib/puppeteer/timeout_helper.rb +6 -10
- data/lib/puppeteer/touch_handle.rb +39 -0
- data/lib/puppeteer/touch_screen.rb +72 -22
- data/lib/puppeteer/tracing.rb +3 -3
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +264 -101
- data/lib/puppeteer/web_socket.rb +2 -2
- data/lib/puppeteer/web_socket_transport.rb +91 -27
- data/lib/puppeteer/web_worker.rb +175 -0
- data/lib/puppeteer.rb +23 -4
- data/puppeteer-ruby.gemspec +15 -11
- data/sig/_external.rbs +8 -0
- data/sig/_supplementary.rbs +314 -0
- data/sig/puppeteer/browser.rbs +166 -0
- data/sig/puppeteer/cdp_session.rbs +64 -0
- data/sig/puppeteer/dialog.rbs +41 -0
- data/sig/puppeteer/element_handle.rbs +308 -0
- data/sig/puppeteer/execution_context.rbs +87 -0
- data/sig/puppeteer/frame.rbs +233 -0
- data/sig/puppeteer/http_request.rbs +214 -0
- data/sig/puppeteer/http_response.rbs +89 -0
- data/sig/puppeteer/js_handle.rbs +64 -0
- data/sig/puppeteer/keyboard.rbs +40 -0
- data/sig/puppeteer/locators.rbs +222 -0
- data/sig/puppeteer/mouse.rbs +113 -0
- data/sig/puppeteer/p_query_handler.rbs +73 -0
- data/sig/puppeteer/p_selector_parser.rbs +31 -0
- data/sig/puppeteer/page.rbs +522 -0
- data/sig/puppeteer/puppeteer.rbs +98 -0
- data/sig/puppeteer/remote_object.rbs +78 -0
- data/sig/puppeteer/touch_handle.rbs +21 -0
- data/sig/puppeteer/touch_screen.rbs +35 -0
- data/sig/puppeteer/web_worker.rbs +83 -0
- metadata +122 -45
- data/CHANGELOG.md +0 -397
- data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
- data/lib/puppeteer/firefox_target_manager.rb +0 -157
- data/lib/puppeteer/launcher/firefox.rb +0 -453
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
class Puppeteer::PQueryHandler
|
|
4
|
+
POLLING_DEFAULT_SECONDS = 0.1
|
|
5
|
+
|
|
6
|
+
CSS_QUERY_SELECTOR_JS = <<~'JAVASCRIPT'
|
|
7
|
+
(element, selector) => {
|
|
8
|
+
const results = [];
|
|
9
|
+
const isQueryableNode = node => {
|
|
10
|
+
return node && typeof node.querySelectorAll === 'function';
|
|
11
|
+
};
|
|
12
|
+
if (!isQueryableNode(element)) {
|
|
13
|
+
return results;
|
|
14
|
+
}
|
|
15
|
+
const trimmed = selector.trimStart();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
return results;
|
|
18
|
+
}
|
|
19
|
+
const IDENT_TOKEN_START = /[-\w\P{ASCII}*]/u;
|
|
20
|
+
if (IDENT_TOKEN_START.test(trimmed[0])) {
|
|
21
|
+
results.push(...element.querySelectorAll(selector));
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
if (!element.parentElement) {
|
|
25
|
+
results.push(...element.querySelectorAll(selector));
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
let index = 0;
|
|
29
|
+
for (const child of element.parentElement.children) {
|
|
30
|
+
++index;
|
|
31
|
+
if (child === element) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
results.push(
|
|
36
|
+
...element.parentElement.querySelectorAll(
|
|
37
|
+
`:scope>:nth-child(${index})${selector}`
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
JAVASCRIPT
|
|
43
|
+
|
|
44
|
+
PIERCE_JS = <<~JAVASCRIPT
|
|
45
|
+
node => {
|
|
46
|
+
if (!node) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
if (node.shadowRoot) {
|
|
50
|
+
return [node.shadowRoot];
|
|
51
|
+
}
|
|
52
|
+
return [node];
|
|
53
|
+
}
|
|
54
|
+
JAVASCRIPT
|
|
55
|
+
|
|
56
|
+
PIERCE_ALL_JS = <<~JAVASCRIPT
|
|
57
|
+
root => {
|
|
58
|
+
if (!root) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const results = [];
|
|
62
|
+
const rootNode = root.shadowRoot ? root.shadowRoot : root;
|
|
63
|
+
results.push(rootNode);
|
|
64
|
+
const walkers = [document.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT)];
|
|
65
|
+
for (const walker of walkers) {
|
|
66
|
+
let node;
|
|
67
|
+
while ((node = walker.nextNode())) {
|
|
68
|
+
if (!node.shadowRoot) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
results.push(node.shadowRoot);
|
|
72
|
+
walkers.push(document.createTreeWalker(node.shadowRoot, NodeFilter.SHOW_ELEMENT));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
JAVASCRIPT
|
|
78
|
+
|
|
79
|
+
DOM_SORT_JS = <<~JAVASCRIPT
|
|
80
|
+
(...elements) => {
|
|
81
|
+
const results = new Set(elements);
|
|
82
|
+
class DepthCalculator {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.cache = new WeakMap();
|
|
85
|
+
}
|
|
86
|
+
calculate(node, depth = []) {
|
|
87
|
+
if (node === null) {
|
|
88
|
+
return depth;
|
|
89
|
+
}
|
|
90
|
+
if (node instanceof ShadowRoot) {
|
|
91
|
+
node = node.host;
|
|
92
|
+
}
|
|
93
|
+
const cached = this.cache.get(node);
|
|
94
|
+
if (cached) {
|
|
95
|
+
return [...cached, ...depth];
|
|
96
|
+
}
|
|
97
|
+
let index = 0;
|
|
98
|
+
for (let prev = node.previousSibling; prev; prev = prev.previousSibling) {
|
|
99
|
+
++index;
|
|
100
|
+
}
|
|
101
|
+
const value = this.calculate(node.parentNode, [index]);
|
|
102
|
+
this.cache.set(node, value);
|
|
103
|
+
return [...value, ...depth];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const compareDepths = (a, b) => {
|
|
107
|
+
if (a.length + b.length === 0) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
const [i = -1, ...restA] = a;
|
|
111
|
+
const [j = -1, ...restB] = b;
|
|
112
|
+
if (i === j) {
|
|
113
|
+
return compareDepths(restA, restB);
|
|
114
|
+
}
|
|
115
|
+
return i < j ? -1 : 1;
|
|
116
|
+
};
|
|
117
|
+
const calculator = new DepthCalculator();
|
|
118
|
+
return [...results]
|
|
119
|
+
.map(result => [result, calculator.calculate(result)])
|
|
120
|
+
.sort(([, a], [, b]) => compareDepths(a, b))
|
|
121
|
+
.map(([result]) => result);
|
|
122
|
+
}
|
|
123
|
+
JAVASCRIPT
|
|
124
|
+
|
|
125
|
+
# @rbs query_handler_manager: untyped -- Query handler manager
|
|
126
|
+
# @rbs return: void -- No return value
|
|
127
|
+
def initialize(query_handler_manager: Puppeteer::QueryHandlerManager.instance)
|
|
128
|
+
@query_handler_manager = query_handler_manager
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @rbs element: Puppeteer::ElementHandle -- Root element
|
|
132
|
+
# @rbs selector: String -- PSelector string
|
|
133
|
+
# @rbs return: Puppeteer::ElementHandle? -- First matching element
|
|
134
|
+
def query_one(element, selector)
|
|
135
|
+
matches = query_all(element, selector)
|
|
136
|
+
return nil if matches.empty?
|
|
137
|
+
|
|
138
|
+
matches.first
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @rbs element: Puppeteer::ElementHandle -- Root element
|
|
142
|
+
# @rbs selector: String -- PSelector string
|
|
143
|
+
# @rbs return: Array[Puppeteer::ElementHandle] -- Matching elements
|
|
144
|
+
def query_all(element, selector)
|
|
145
|
+
selectors, = Puppeteer::PSelectorParser.parse(selector)
|
|
146
|
+
return [] if selectors.empty?
|
|
147
|
+
|
|
148
|
+
results = selectors.flat_map do |complex|
|
|
149
|
+
run_complex_selector(element, complex)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
return [] if results.empty?
|
|
153
|
+
|
|
154
|
+
dom_sort(results)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @rbs element: Puppeteer::ElementHandle -- Root element
|
|
158
|
+
# @rbs selector: String -- PSelector string
|
|
159
|
+
# @rbs return: Puppeteer::JSHandle -- Handle to array of elements
|
|
160
|
+
def query_all_array(element, selector)
|
|
161
|
+
elements = query_all(element, selector)
|
|
162
|
+
return element.execution_context.evaluate_handle('() => []') if elements.empty?
|
|
163
|
+
|
|
164
|
+
element.execution_context.evaluate_handle('(...elements) => elements', *elements)
|
|
165
|
+
ensure
|
|
166
|
+
elements&.each(&:dispose)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @rbs element_or_frame: Puppeteer::ElementHandle | Puppeteer::Frame -- Root element or frame
|
|
170
|
+
# @rbs selector: String -- PSelector string
|
|
171
|
+
# @rbs visible: bool? -- Wait for element to be visible
|
|
172
|
+
# @rbs hidden: bool? -- Wait for element to be hidden
|
|
173
|
+
# @rbs timeout: Numeric? -- Timeout in milliseconds
|
|
174
|
+
# @rbs polling: (String | Numeric | nil) -- Polling strategy
|
|
175
|
+
# @rbs return: Puppeteer::ElementHandle? -- Matching element
|
|
176
|
+
def wait_for(element_or_frame, selector, visible:, hidden:, timeout:, polling: nil)
|
|
177
|
+
case element_or_frame
|
|
178
|
+
when Puppeteer::Frame
|
|
179
|
+
frame = element_or_frame
|
|
180
|
+
root = nil
|
|
181
|
+
when Puppeteer::ElementHandle
|
|
182
|
+
frame = element_or_frame.frame
|
|
183
|
+
root = frame.puppeteer_world.adopt_handle(element_or_frame)
|
|
184
|
+
else
|
|
185
|
+
raise ArgumentError.new("element_or_frame must be a Frame or ElementHandle. #{element_or_frame.inspect}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
timeout_ms = timeout.nil? ? frame.default_timeout : timeout
|
|
189
|
+
deadline = timeout_ms && timeout_ms > 0 ? monotonic_time + (timeout_ms / 1000.0) : nil
|
|
190
|
+
polling_interval = polling_interval_seconds(polling, visible: visible, hidden: hidden)
|
|
191
|
+
|
|
192
|
+
begin
|
|
193
|
+
loop do
|
|
194
|
+
resolved_root = root || frame.puppeteer_world.document
|
|
195
|
+
handle = query_one(resolved_root, selector)
|
|
196
|
+
|
|
197
|
+
if handle
|
|
198
|
+
if visible
|
|
199
|
+
return transfer_handle(frame, handle) if handle.visible?
|
|
200
|
+
elsif hidden
|
|
201
|
+
return transfer_handle(frame, handle) if handle.hidden?
|
|
202
|
+
else
|
|
203
|
+
return transfer_handle(frame, handle)
|
|
204
|
+
end
|
|
205
|
+
handle.dispose
|
|
206
|
+
elsif hidden
|
|
207
|
+
return nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
raise_timeout(selector) if deadline && monotonic_time >= deadline
|
|
211
|
+
Puppeteer::AsyncUtils.sleep_seconds(polling_interval)
|
|
212
|
+
end
|
|
213
|
+
rescue => err
|
|
214
|
+
if err.is_a?(Puppeteer::TimeoutError)
|
|
215
|
+
raise
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
wait_for_selector_error =
|
|
219
|
+
if err.is_a?(Puppeteer::TimeoutError)
|
|
220
|
+
Puppeteer::TimeoutError.new("Waiting for selector `#{selector}` failed")
|
|
221
|
+
else
|
|
222
|
+
Puppeteer::Error.new("Waiting for selector `#{selector}` failed")
|
|
223
|
+
end
|
|
224
|
+
wait_for_selector_error.cause = err
|
|
225
|
+
raise wait_for_selector_error
|
|
226
|
+
ensure
|
|
227
|
+
root&.dispose
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
def run_complex_selector(root, complex)
|
|
233
|
+
elements = [root]
|
|
234
|
+
|
|
235
|
+
complex.each do |part|
|
|
236
|
+
case part
|
|
237
|
+
when '>>>'
|
|
238
|
+
elements = pierce_all(elements)
|
|
239
|
+
when '>>>>'
|
|
240
|
+
elements = pierce(elements)
|
|
241
|
+
else
|
|
242
|
+
elements = apply_compound(elements, part)
|
|
243
|
+
end
|
|
244
|
+
return [] if elements.empty?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
elements
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def apply_compound(elements, compound)
|
|
251
|
+
compound.reduce(elements) do |current, selector|
|
|
252
|
+
next [] if current.empty?
|
|
253
|
+
|
|
254
|
+
if selector.is_a?(String)
|
|
255
|
+
apply_css_selector(current, selector)
|
|
256
|
+
else
|
|
257
|
+
apply_pseudo_selector(current, selector)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def apply_css_selector(elements, selector)
|
|
263
|
+
elements.flat_map do |element|
|
|
264
|
+
query_css_selector(element, selector)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def query_css_selector(element, selector)
|
|
269
|
+
return [] if selector.strip.empty?
|
|
270
|
+
|
|
271
|
+
array_handle = element.evaluate_handle(CSS_QUERY_SELECTOR_JS, selector)
|
|
272
|
+
array_handle_to_elements(array_handle)
|
|
273
|
+
ensure
|
|
274
|
+
array_handle&.dispose
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def apply_pseudo_selector(elements, pseudo)
|
|
278
|
+
handler = query_handler_for_pseudo(pseudo.name)
|
|
279
|
+
target_context = elements.first&.execution_context
|
|
280
|
+
|
|
281
|
+
elements.flat_map do |element|
|
|
282
|
+
handles = handler.query_all(element, pseudo.value) || []
|
|
283
|
+
handles.map do |handle|
|
|
284
|
+
ensure_context(handle, target_context)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def query_handler_for_pseudo(name)
|
|
290
|
+
handler = @query_handler_manager.query_handlers[name.to_sym]
|
|
291
|
+
return handler if handler
|
|
292
|
+
|
|
293
|
+
raise Puppeteer::Error.new("Unknown selector type: #{name}")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def pierce(elements)
|
|
297
|
+
elements.flat_map do |element|
|
|
298
|
+
array_handle = element.evaluate_handle(PIERCE_JS)
|
|
299
|
+
array_handle_to_elements(array_handle)
|
|
300
|
+
ensure
|
|
301
|
+
array_handle&.dispose
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def pierce_all(elements)
|
|
306
|
+
elements.flat_map do |element|
|
|
307
|
+
array_handle = element.evaluate_handle(PIERCE_ALL_JS)
|
|
308
|
+
array_handle_to_elements(array_handle)
|
|
309
|
+
ensure
|
|
310
|
+
array_handle&.dispose
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def dom_sort(elements)
|
|
315
|
+
return [] if elements.empty?
|
|
316
|
+
|
|
317
|
+
context = elements.first.execution_context
|
|
318
|
+
sorted_handle = context.evaluate_handle(DOM_SORT_JS, *elements)
|
|
319
|
+
sorted = array_handle_to_elements(sorted_handle)
|
|
320
|
+
elements.each(&:dispose)
|
|
321
|
+
sorted
|
|
322
|
+
ensure
|
|
323
|
+
sorted_handle&.dispose
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def array_handle_to_elements(array_handle)
|
|
327
|
+
properties = array_handle.properties
|
|
328
|
+
properties.keys.sort_by(&:to_i).filter_map do |key|
|
|
329
|
+
properties[key].as_element
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def ensure_context(handle, context)
|
|
334
|
+
return handle if context.nil? || handle.execution_context == context
|
|
335
|
+
|
|
336
|
+
world = context.world
|
|
337
|
+
return handle unless world
|
|
338
|
+
|
|
339
|
+
world.transfer_handle(handle)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def transfer_handle(frame, handle)
|
|
343
|
+
if handle.execution_context == frame.main_world.execution_context
|
|
344
|
+
handle
|
|
345
|
+
else
|
|
346
|
+
frame.main_world.transfer_handle(handle)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def polling_interval_seconds(polling, visible:, hidden:)
|
|
351
|
+
return polling / 1000.0 if polling.is_a?(Numeric)
|
|
352
|
+
|
|
353
|
+
if visible || hidden
|
|
354
|
+
return 0.016
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
POLLING_DEFAULT_SECONDS
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def monotonic_time
|
|
361
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def raise_timeout(selector)
|
|
365
|
+
raise Puppeteer::TimeoutError.new("Waiting for selector `#{selector}` failed")
|
|
366
|
+
end
|
|
367
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module PSelectorParser
|
|
5
|
+
class PseudoSelector
|
|
6
|
+
attr_reader :name, :value
|
|
7
|
+
|
|
8
|
+
def initialize(name:, value:)
|
|
9
|
+
@name = name
|
|
10
|
+
@value = value
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
PSEUDO_PREFIX = '::-p-'
|
|
15
|
+
|
|
16
|
+
# @rbs selector: String -- Selector to parse
|
|
17
|
+
# @rbs return: [Array[untyped], bool, bool, bool] -- Parsed selectors and flags
|
|
18
|
+
def self.parse(selector)
|
|
19
|
+
is_pure_css = true
|
|
20
|
+
has_pseudo_classes = false
|
|
21
|
+
has_aria = false
|
|
22
|
+
|
|
23
|
+
selectors = []
|
|
24
|
+
compound = []
|
|
25
|
+
complex = [compound]
|
|
26
|
+
storage = +''
|
|
27
|
+
|
|
28
|
+
i = 0
|
|
29
|
+
length = selector.length
|
|
30
|
+
in_quote = nil
|
|
31
|
+
escaped = false
|
|
32
|
+
paren_depth = 0
|
|
33
|
+
|
|
34
|
+
while i < length
|
|
35
|
+
char = selector[i]
|
|
36
|
+
|
|
37
|
+
if escaped
|
|
38
|
+
storage << char
|
|
39
|
+
escaped = false
|
|
40
|
+
i += 1
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if char == '\\'
|
|
45
|
+
storage << char
|
|
46
|
+
escaped = true
|
|
47
|
+
i += 1
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if in_quote
|
|
52
|
+
if char == in_quote
|
|
53
|
+
in_quote = nil
|
|
54
|
+
end
|
|
55
|
+
storage << char
|
|
56
|
+
i += 1
|
|
57
|
+
next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if char == '"' || char == "'"
|
|
61
|
+
in_quote = char
|
|
62
|
+
storage << char
|
|
63
|
+
i += 1
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if char == '('
|
|
68
|
+
paren_depth += 1
|
|
69
|
+
storage << char
|
|
70
|
+
i += 1
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if char == ')'
|
|
75
|
+
paren_depth = [paren_depth - 1, 0].max
|
|
76
|
+
storage << char
|
|
77
|
+
i += 1
|
|
78
|
+
next
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if paren_depth == 0
|
|
82
|
+
if starts_with_at?(selector, i, '>>>>')
|
|
83
|
+
flush_storage(storage, compound)
|
|
84
|
+
complex << '>>>>'
|
|
85
|
+
compound = []
|
|
86
|
+
complex << compound
|
|
87
|
+
is_pure_css = false
|
|
88
|
+
i += 4
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if starts_with_at?(selector, i, '>>>')
|
|
93
|
+
flush_storage(storage, compound)
|
|
94
|
+
complex << '>>>'
|
|
95
|
+
compound = []
|
|
96
|
+
complex << compound
|
|
97
|
+
is_pure_css = false
|
|
98
|
+
i += 3
|
|
99
|
+
next
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if starts_with_at?(selector, i, PSEUDO_PREFIX)
|
|
103
|
+
flush_storage(storage, compound)
|
|
104
|
+
i += PSEUDO_PREFIX.length
|
|
105
|
+
name, i = parse_name(selector, i)
|
|
106
|
+
raise ArgumentError.new('Invalid PSelector name') if name.empty?
|
|
107
|
+
|
|
108
|
+
i = skip_spaces(selector, i)
|
|
109
|
+
value = ''
|
|
110
|
+
if selector[i] == '('
|
|
111
|
+
value, i = parse_argument(selector, i)
|
|
112
|
+
end
|
|
113
|
+
value = unquote(value.strip)
|
|
114
|
+
|
|
115
|
+
compound << PseudoSelector.new(name: name, value: value)
|
|
116
|
+
is_pure_css = false
|
|
117
|
+
has_aria = true if name == 'aria'
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if char == ','
|
|
122
|
+
flush_storage(storage, compound)
|
|
123
|
+
selectors << complex
|
|
124
|
+
compound = []
|
|
125
|
+
complex = [compound]
|
|
126
|
+
i += 1
|
|
127
|
+
next
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if char == ':' && selector[i, PSEUDO_PREFIX.length] != PSEUDO_PREFIX
|
|
131
|
+
if selector[i + 1] != ':'
|
|
132
|
+
has_pseudo_classes = true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
storage << char
|
|
138
|
+
i += 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
raise ArgumentError.new('Unterminated string in PSelector') if in_quote
|
|
142
|
+
raise ArgumentError.new('Unterminated parentheses in PSelector') if paren_depth != 0
|
|
143
|
+
|
|
144
|
+
flush_storage(storage, compound)
|
|
145
|
+
selectors << complex if complex.any?
|
|
146
|
+
|
|
147
|
+
[selectors, is_pure_css, has_pseudo_classes, has_aria]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.flush_storage(storage, compound)
|
|
151
|
+
css = storage.strip
|
|
152
|
+
compound << css unless css.empty?
|
|
153
|
+
storage.clear
|
|
154
|
+
end
|
|
155
|
+
private_class_method :flush_storage
|
|
156
|
+
|
|
157
|
+
def self.parse_name(selector, index)
|
|
158
|
+
name = +''
|
|
159
|
+
while index < selector.length && selector[index] =~ /[A-Za-z0-9_-]/
|
|
160
|
+
name << selector[index]
|
|
161
|
+
index += 1
|
|
162
|
+
end
|
|
163
|
+
[name, index]
|
|
164
|
+
end
|
|
165
|
+
private_class_method :parse_name
|
|
166
|
+
|
|
167
|
+
def self.skip_spaces(selector, index)
|
|
168
|
+
index += 1 while index < selector.length && selector[index] =~ /\s/
|
|
169
|
+
index
|
|
170
|
+
end
|
|
171
|
+
private_class_method :skip_spaces
|
|
172
|
+
|
|
173
|
+
def self.parse_argument(selector, index)
|
|
174
|
+
raise ArgumentError.new('Expected opening parenthesis') unless selector[index] == '('
|
|
175
|
+
|
|
176
|
+
index += 1
|
|
177
|
+
start = index
|
|
178
|
+
depth = 1
|
|
179
|
+
in_quote = nil
|
|
180
|
+
escaped = false
|
|
181
|
+
|
|
182
|
+
while index < selector.length
|
|
183
|
+
char = selector[index]
|
|
184
|
+
|
|
185
|
+
if escaped
|
|
186
|
+
escaped = false
|
|
187
|
+
index += 1
|
|
188
|
+
next
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
if char == '\\'
|
|
192
|
+
escaped = true
|
|
193
|
+
index += 1
|
|
194
|
+
next
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if in_quote
|
|
198
|
+
if char == in_quote
|
|
199
|
+
in_quote = nil
|
|
200
|
+
end
|
|
201
|
+
index += 1
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if char == '"' || char == "'"
|
|
206
|
+
in_quote = char
|
|
207
|
+
index += 1
|
|
208
|
+
next
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
if char == '('
|
|
212
|
+
depth += 1
|
|
213
|
+
elsif char == ')'
|
|
214
|
+
depth -= 1
|
|
215
|
+
if depth == 0
|
|
216
|
+
value = selector[start...index]
|
|
217
|
+
return [value, index + 1]
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
index += 1
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
raise ArgumentError.new('Unterminated PSelector argument')
|
|
225
|
+
end
|
|
226
|
+
private_class_method :parse_argument
|
|
227
|
+
|
|
228
|
+
def self.starts_with_at?(selector, index, token)
|
|
229
|
+
selector[index, token.length] == token
|
|
230
|
+
end
|
|
231
|
+
private_class_method :starts_with_at?
|
|
232
|
+
|
|
233
|
+
def self.unquote(text)
|
|
234
|
+
if text.length > 1 && (text.start_with?('"') || text.start_with?("'")) && text.end_with?(text[0])
|
|
235
|
+
text = text[1...-1]
|
|
236
|
+
end
|
|
237
|
+
text.gsub(/\\([\s\S])/m, '\\1')
|
|
238
|
+
end
|
|
239
|
+
private_class_method :unquote
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
class Puppeteer::Page
|
|
2
2
|
class ScreenshotTaskQueue
|
|
3
3
|
def initialize
|
|
4
|
-
@chain =
|
|
4
|
+
@chain = Async::Promise.new.tap { |promise| promise.resolve(nil) }
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def post_task(&block)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
previous = @chain
|
|
9
|
+
result_promise = Async::Promise.new
|
|
10
|
+
@chain = result_promise
|
|
11
|
+
|
|
12
|
+
Async do
|
|
13
|
+
previous.wait
|
|
14
|
+
result = block.call
|
|
15
|
+
result_promise.resolve(result)
|
|
16
|
+
rescue => err
|
|
17
|
+
result_promise.reject(err)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
result_promise.wait
|
|
11
21
|
end
|
|
12
22
|
end
|
|
13
23
|
end
|