puppeteer-ruby 0.0.20 → 0.0.26
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 +37 -0
- data/CHANGELOG.md +62 -0
- data/README.md +15 -0
- data/lib/puppeteer.rb +9 -2
- data/lib/puppeteer/browser.rb +19 -28
- data/lib/puppeteer/browser_context.rb +48 -49
- data/lib/puppeteer/browser_runner.rb +19 -5
- data/lib/puppeteer/cdp_session.rb +11 -7
- data/lib/puppeteer/concurrent_ruby_utils.rb +22 -6
- data/lib/puppeteer/connection.rb +30 -11
- data/lib/puppeteer/devices.rb +998 -849
- data/lib/puppeteer/dom_world.rb +2 -2
- data/lib/puppeteer/event_callbackable.rb +4 -0
- data/lib/puppeteer/events.rb +184 -0
- data/lib/puppeteer/exception_details.rb +38 -0
- data/lib/puppeteer/frame_manager.rb +20 -16
- data/lib/puppeteer/geolocation.rb +24 -0
- data/lib/puppeteer/keyboard/us_keyboard_layout.rb +2 -2
- data/lib/puppeteer/launcher.rb +0 -1
- data/lib/puppeteer/launcher/browser_options.rb +2 -1
- data/lib/puppeteer/launcher/chrome.rb +4 -8
- data/lib/puppeteer/launcher/firefox.rb +8 -15
- data/lib/puppeteer/lifecycle_watcher.rb +6 -6
- data/lib/puppeteer/network_manager.rb +6 -6
- data/lib/puppeteer/page.rb +119 -141
- data/lib/puppeteer/page/screenshot_options.rb +2 -2
- data/lib/puppeteer/page/screenshot_task_queue.rb +13 -0
- data/lib/puppeteer/target.rb +4 -6
- data/lib/puppeteer/version.rb +1 -1
- data/puppeteer-ruby.gemspec +1 -0
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f76be710be28b32694ef8d21bcaa305f00b8ac0ada333d2992137f166614bd94
|
4
|
+
data.tar.gz: 29d4c83aaa00705db67dd1a33b62b304b125fc59bd5f826099a9f864202f3e00
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5dbf6ee744fdb70d7645bdfaf3840088d756b31a0dfcf05109058299d07ea338967c019ee4a55f834bc388b39cfc19b147ac7f08d80762d5eb44ed8517d0f1a
|
7
|
+
data.tar.gz: 05a88198b74788638619bbdef3faaede3503c61a5b6574a562180265f5f927a236a564b3f0d73dfefcaa442b492227b934f7233ad72183924b98d66e242e4850
|
data/.rubocop.yml
CHANGED
@@ -116,6 +116,43 @@ Style/DefWithParentheses:
|
|
116
116
|
Style/MethodDefParentheses:
|
117
117
|
Enabled: true
|
118
118
|
|
119
|
+
Style/MethodCallWithArgsParentheses:
|
120
|
+
Enabled: true
|
121
|
+
IgnoredMethods:
|
122
|
+
# Gemfile, gemspec
|
123
|
+
- source
|
124
|
+
- add_dependency
|
125
|
+
- add_development_dependency
|
126
|
+
|
127
|
+
# include/require
|
128
|
+
- require
|
129
|
+
- require_relative
|
130
|
+
- include
|
131
|
+
|
132
|
+
# fundamental methods
|
133
|
+
- raise
|
134
|
+
- sleep
|
135
|
+
|
136
|
+
# RSpec
|
137
|
+
- describe
|
138
|
+
- it
|
139
|
+
- to
|
140
|
+
- not_to
|
141
|
+
- be
|
142
|
+
- eq
|
143
|
+
|
144
|
+
# async/await
|
145
|
+
- define_async_method
|
146
|
+
- await
|
147
|
+
- future
|
148
|
+
|
149
|
+
# utils
|
150
|
+
- debug_print
|
151
|
+
- debug_puts
|
152
|
+
|
153
|
+
Style/MethodCallWithoutArgsParentheses:
|
154
|
+
Enabled: true
|
155
|
+
|
119
156
|
Style/RedundantFreeze:
|
120
157
|
Enabled: true
|
121
158
|
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
### master [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.25...master)]
|
2
|
+
|
3
|
+
* xxx
|
4
|
+
|
5
|
+
### 0.0.25 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.23...0.0.25)]
|
6
|
+
|
7
|
+
New feature:
|
8
|
+
|
9
|
+
* **Cookie** feature: `Page#set_cookie`, `Page#cookies`
|
10
|
+
|
11
|
+
### 0.0.23 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.22...0.0.23)]
|
12
|
+
|
13
|
+
New feature:
|
14
|
+
|
15
|
+
* **GeoLocation** feature
|
16
|
+
* grant/clear permission
|
17
|
+
|
18
|
+
Bugfix/Improvement:
|
19
|
+
|
20
|
+
* Refactoring for events ([#31](https://github.com/YusukeIwaki/puppeteer-ruby/pull/31))
|
21
|
+
* Improve SEND/RECV handling in CDPSession ([#34](https://github.com/YusukeIwaki/puppeteer-ruby/pull/34))
|
22
|
+
|
23
|
+
### 0.0.22 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.21...0.0.22)]
|
24
|
+
|
25
|
+
Bugfix
|
26
|
+
|
27
|
+
* Make `Puppeteer#default_args` to work
|
28
|
+
* Respect Firefox launch options
|
29
|
+
* Respect `default_viewport: nil`
|
30
|
+
|
31
|
+
### 0.0.21 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.20...0.0.21)]
|
32
|
+
|
33
|
+
Bugfix/Improvement:
|
34
|
+
|
35
|
+
* Update DeviceDescriptors (list of emulatable devices)
|
36
|
+
* Fix bug on inputing "(" ([#25](https://github.com/YusukeIwaki/puppeteer-ruby/pull/25))
|
37
|
+
|
38
|
+
### 0.0.20 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.19...0.0.20)]
|
39
|
+
|
40
|
+
New feature
|
41
|
+
|
42
|
+
* Dialog-handling feature
|
43
|
+
|
44
|
+
### 0.0.19 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.18...0.0.19)]
|
45
|
+
|
46
|
+
New feature
|
47
|
+
|
48
|
+
* **Firefox support**
|
49
|
+
|
50
|
+
Bugfix/Improvement
|
51
|
+
|
52
|
+
* Allow `Page#keyboard` with block ([#18](https://github.com/YusukeIwaki/puppeteer-ruby/pull/18))
|
53
|
+
|
54
|
+
### 0.0.18 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.17...0.0.18)]
|
55
|
+
|
56
|
+
New feature
|
57
|
+
|
58
|
+
* **Firefox support**
|
59
|
+
|
60
|
+
Bugfix/Improvement
|
61
|
+
|
62
|
+
* Allow `Page#keyboard` with block ([#18](https://github.com/YusukeIwaki/puppeteer-ruby/pull/18))
|
data/README.md
CHANGED
@@ -72,6 +72,21 @@ end
|
|
72
72
|
|
73
73
|
More usage examples can be found [here](https://github.com/YusukeIwaki/puppeteer-ruby-example)
|
74
74
|
|
75
|
+
## :whale: Running in Docker
|
76
|
+
|
77
|
+
Following packages are required.
|
78
|
+
|
79
|
+
* Google Chrome or Chromium
|
80
|
+
* In Debian-based images, `google-chrome-stable`
|
81
|
+
* In Alpine-based images, `chromium`
|
82
|
+
|
83
|
+
Also, CJK font will be required for Chinese, Japanese, Korean sites.
|
84
|
+
|
85
|
+
### References
|
86
|
+
|
87
|
+
* Puppeteer official README: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker
|
88
|
+
* puppeteer-ruby example: https://github.com/YusukeIwaki/puppeteer-ruby-example/tree/master/docker_chromium
|
89
|
+
|
75
90
|
## :bulb: Collaboration with Selenium or Capybara
|
76
91
|
|
77
92
|
It is really remarkable that we can use puppeteer functions in existing Selenium or Capybara codes, with a few configuration in advance.
|
data/lib/puppeteer.rb
CHANGED
@@ -6,7 +6,9 @@ require 'puppeteer/env'
|
|
6
6
|
|
7
7
|
# Custom data types.
|
8
8
|
require 'puppeteer/device'
|
9
|
+
require 'puppeteer/events'
|
9
10
|
require 'puppeteer/errors'
|
11
|
+
require 'puppeteer/geolocation'
|
10
12
|
require 'puppeteer/viewport'
|
11
13
|
|
12
14
|
# Modules
|
@@ -27,6 +29,7 @@ require 'puppeteer/devices'
|
|
27
29
|
require 'puppeteer/dialog'
|
28
30
|
require 'puppeteer/dom_world'
|
29
31
|
require 'puppeteer/emulation_manager'
|
32
|
+
require 'puppeteer/exception_details'
|
30
33
|
require 'puppeteer/execution_context'
|
31
34
|
require 'puppeteer/file_chooser'
|
32
35
|
require 'puppeteer/frame'
|
@@ -61,7 +64,11 @@ class Puppeteer
|
|
61
64
|
is_puppeteer_core: true,
|
62
65
|
)
|
63
66
|
|
64
|
-
|
67
|
+
if kwargs.empty? # for Ruby < 2.7
|
68
|
+
@puppeteer.public_send(method, *args, &block)
|
69
|
+
else
|
70
|
+
@puppeteer.public_send(method, *args, **kwargs, &block)
|
71
|
+
end
|
65
72
|
end
|
66
73
|
|
67
74
|
# @param project_root [String]
|
@@ -127,7 +134,7 @@ class Puppeteer
|
|
127
134
|
ignore_https_errors: ignore_https_errors,
|
128
135
|
default_viewport: default_viewport,
|
129
136
|
slow_mo: slow_mo,
|
130
|
-
}
|
137
|
+
}
|
131
138
|
|
132
139
|
@product_name ||= product
|
133
140
|
browser = launcher.launch(options)
|
data/lib/puppeteer/browser.rb
CHANGED
@@ -36,7 +36,6 @@ class Puppeteer::Browser
|
|
36
36
|
@ignore_https_errors = ignore_https_errors
|
37
37
|
@default_viewport = default_viewport
|
38
38
|
@process = process
|
39
|
-
# @screenshot_task_queue = TaskQueue.new
|
40
39
|
@connection = connection
|
41
40
|
@close_callback = close_callback
|
42
41
|
|
@@ -46,37 +45,30 @@ class Puppeteer::Browser
|
|
46
45
|
@contexts[context_id] = Puppeteer::BrowserContext.new(@connection, self, context_id)
|
47
46
|
end
|
48
47
|
@targets = {}
|
49
|
-
@connection.on_event
|
50
|
-
emit_event
|
48
|
+
@connection.on_event(ConnectionEmittedEvents::Disconnected) do
|
49
|
+
emit_event(BrowserEmittedEvents::Disconnected)
|
51
50
|
end
|
52
|
-
@connection.on_event
|
53
|
-
@connection.on_event
|
54
|
-
@connection.on_event
|
51
|
+
@connection.on_event('Target.targetCreated', &method(:handle_target_created))
|
52
|
+
@connection.on_event('Target.targetDestroyed', &method(:handle_target_destroyed))
|
53
|
+
@connection.on_event('Target.targetInfoChanged', &method(:handle_target_info_changed))
|
55
54
|
end
|
56
55
|
|
57
|
-
EVENT_MAPPINGS = {
|
58
|
-
disconnected: 'Events.Browser.Disconnected',
|
59
|
-
targetcreated: 'Events.Browser.TargetCreated',
|
60
|
-
targetchanged: 'Events.Browser.TargetChanged',
|
61
|
-
targetdestroyed: 'Events.Browser.TargetDestroyed',
|
62
|
-
}
|
63
|
-
|
64
56
|
# @param event_name [Symbol] either of :disconnected, :targetcreated, :targetchanged, :targetdestroyed
|
65
57
|
def on(event_name, &block)
|
66
|
-
unless
|
67
|
-
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{
|
58
|
+
unless BrowserEmittedEvents.values.include?(event_name.to_s)
|
59
|
+
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{BrowserEmittedEvents.values.to_a.join(", ")}")
|
68
60
|
end
|
69
61
|
|
70
|
-
|
62
|
+
super(event_name.to_s, &block)
|
71
63
|
end
|
72
64
|
|
73
65
|
# @param event_name [Symbol]
|
74
66
|
def once(event_name, &block)
|
75
|
-
unless
|
76
|
-
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{
|
67
|
+
unless BrowserEmittedEvents.values.include?(event_name.to_s)
|
68
|
+
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{BrowserEmittedEvents.values.to_a.join(", ")}")
|
77
69
|
end
|
78
70
|
|
79
|
-
|
71
|
+
super(event_name.to_s, &block)
|
80
72
|
end
|
81
73
|
|
82
74
|
# @return [Puppeteer::BrowserRunner::BrowserProcess]
|
@@ -132,13 +124,12 @@ class Puppeteer::Browser
|
|
132
124
|
session_factory: -> { @connection.create_session(target_info) },
|
133
125
|
ignore_https_errors: @ignore_https_errors,
|
134
126
|
default_viewport: @default_viewport,
|
135
|
-
screenshot_task_queue: @screenshot_task_queue,
|
136
127
|
)
|
137
128
|
# assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
|
138
129
|
@targets[target_info.target_id] = target
|
139
130
|
if await target.initialized_promise
|
140
|
-
emit_event
|
141
|
-
context.emit_event
|
131
|
+
emit_event(BrowserEmittedEvents::TargetCreated, target)
|
132
|
+
context.emit_event(BrowserContextEmittedEvents::TargetCreated, target)
|
142
133
|
end
|
143
134
|
end
|
144
135
|
|
@@ -150,8 +141,8 @@ class Puppeteer::Browser
|
|
150
141
|
@targets.delete(target_id)
|
151
142
|
target.closed_callback
|
152
143
|
if await target.initialized_promise
|
153
|
-
emit_event
|
154
|
-
target.browser_context.emit_event
|
144
|
+
emit_event(BrowserEmittedEvents::TargetDestroyed, target)
|
145
|
+
target.browser_context.emit_event(BrowserContextEmittedEvents::TargetDestroyed, target)
|
155
146
|
end
|
156
147
|
end
|
157
148
|
|
@@ -169,8 +160,8 @@ class Puppeteer::Browser
|
|
169
160
|
was_initialized = target.initialized?
|
170
161
|
target.handle_target_info_changed(target_info)
|
171
162
|
if was_initialized && previous_url != target.url
|
172
|
-
emit_event
|
173
|
-
target.browser_context.emit_event
|
163
|
+
emit_event(BrowserEmittedEvents::TargetChanged, target)
|
164
|
+
target.browser_context.emit_event(BrowserContextEmittedEvents::TargetChanged, target)
|
174
165
|
end
|
175
166
|
end
|
176
167
|
|
@@ -222,12 +213,12 @@ class Puppeteer::Browser
|
|
222
213
|
|
223
214
|
event_listening_ids = []
|
224
215
|
target_promise = resolvable_future
|
225
|
-
event_listening_ids << add_event_listener(
|
216
|
+
event_listening_ids << add_event_listener(BrowserEmittedEvents::TargetCreated) do |target|
|
226
217
|
if predicate.call(target)
|
227
218
|
target_promise.fulfill(target)
|
228
219
|
end
|
229
220
|
end
|
230
|
-
event_listening_ids << add_event_listener(
|
221
|
+
event_listening_ids << add_event_listener(BrowserEmittedEvents::TargetChanged) do |target|
|
231
222
|
if predicate.call(target)
|
232
223
|
target_promise.fulfill(target)
|
233
224
|
end
|
@@ -11,29 +11,22 @@ class Puppeteer::BrowserContext
|
|
11
11
|
@id = context_id
|
12
12
|
end
|
13
13
|
|
14
|
-
EVENT_MAPPINGS = {
|
15
|
-
disconnected: 'Events.BrowserContext.Disconnected',
|
16
|
-
targetcreated: 'Events.BrowserContext.TargetCreated',
|
17
|
-
targetchanged: 'Events.BrowserContext.TargetChanged',
|
18
|
-
targetdestroyed: 'Events.BrowserContext.TargetDestroyed',
|
19
|
-
}
|
20
|
-
|
21
14
|
# @param event_name [Symbol] either of :disconnected, :targetcreated, :targetchanged, :targetdestroyed
|
22
15
|
def on(event_name, &block)
|
23
|
-
unless
|
24
|
-
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{
|
16
|
+
unless BrowserContextEmittedEvents.values.include?(event_name.to_s)
|
17
|
+
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{BrowserContextEmittedEvents.values.to_a.join(", ")}")
|
25
18
|
end
|
26
19
|
|
27
|
-
|
20
|
+
super(event_name.to_s, &block)
|
28
21
|
end
|
29
22
|
|
30
23
|
# @param event_name [Symbol]
|
31
24
|
def once(event_name, &block)
|
32
|
-
unless
|
33
|
-
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{
|
25
|
+
unless BrowserContextEmittedEvents.values.include?(event_name.to_s)
|
26
|
+
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{BrowserContextEmittedEvents.values.to_a.join(", ")}")
|
34
27
|
end
|
35
28
|
|
36
|
-
|
29
|
+
super(event_name.to_s, &block)
|
37
30
|
end
|
38
31
|
|
39
32
|
# @return {!Array<!Target>} target
|
@@ -64,42 +57,48 @@ class Puppeteer::BrowserContext
|
|
64
57
|
!!@id
|
65
58
|
end
|
66
59
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
#
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
60
|
+
WEB_PERMISSION_TO_PROTOCOL = {
|
61
|
+
'geolocation' => 'geolocation',
|
62
|
+
'midi' => 'midi',
|
63
|
+
'notifications' => 'notifications',
|
64
|
+
# TODO: push isn't a valid type?
|
65
|
+
# 'push' => 'push',
|
66
|
+
'camera' => 'videoCapture',
|
67
|
+
'microphone' => 'audioCapture',
|
68
|
+
'background-sync' => 'backgroundSync',
|
69
|
+
'ambient-light-sensor' => 'sensors',
|
70
|
+
'accelerometer' => 'sensors',
|
71
|
+
'gyroscope' => 'sensors',
|
72
|
+
'magnetometer' => 'sensors',
|
73
|
+
'accessibility-events' => 'accessibilityEvents',
|
74
|
+
'clipboard-read' => 'clipboardReadWrite',
|
75
|
+
'clipboard-write' => 'clipboardReadWrite',
|
76
|
+
'payment-handler' => 'paymentHandler',
|
77
|
+
'idle-detection' => 'idleDetection',
|
78
|
+
# chrome-specific permissions we have.
|
79
|
+
'midi-sysex' => 'midiSysex',
|
80
|
+
}.freeze
|
81
|
+
|
82
|
+
# @param origin [String]
|
83
|
+
# @param permissions [Array<String>]
|
84
|
+
def override_permissions(origin, permissions)
|
85
|
+
protocol_permissions = permissions.map do |permission|
|
86
|
+
WEB_PERMISSION_TO_PROTOCOL[permission] or raise ArgumentError.new("Unknown permission: #{permission}")
|
87
|
+
end
|
88
|
+
@connection.send_message('Browser.grantPermissions', {
|
89
|
+
origin: origin,
|
90
|
+
browserContextId: @id,
|
91
|
+
permissions: protocol_permissions,
|
92
|
+
}.compact)
|
93
|
+
end
|
94
|
+
|
95
|
+
def clear_permission_overrides
|
96
|
+
if @id
|
97
|
+
@connection.send_message('Browser.resetPermissions', browserContextId: @id)
|
98
|
+
else
|
99
|
+
@connection.send_message('Browser.resetPermissions')
|
100
|
+
end
|
101
|
+
end
|
103
102
|
|
104
103
|
# @return [Future<Puppeteer::Page>]
|
105
104
|
def new_page
|
@@ -14,16 +14,24 @@ class Puppeteer::BrowserRunner
|
|
14
14
|
@proc = nil
|
15
15
|
@connection = nil
|
16
16
|
@closed = true
|
17
|
-
@listeners = []
|
18
17
|
end
|
19
18
|
|
20
19
|
attr_reader :proc, :connection
|
21
20
|
|
22
21
|
class BrowserProcess
|
23
22
|
def initialize(env, executable_path, args)
|
23
|
+
@spawnargs =
|
24
|
+
if args && !args.empty?
|
25
|
+
[executable_path] + args
|
26
|
+
else
|
27
|
+
[executable_path]
|
28
|
+
end
|
29
|
+
|
24
30
|
stdin, @stdout, @stderr, @thread = Open3.popen3(env, executable_path, *args)
|
25
31
|
stdin.close
|
26
32
|
@pid = @thread.pid
|
33
|
+
rescue Errno::ENOENT => err
|
34
|
+
raise LaunchError.new(err.message)
|
27
35
|
end
|
28
36
|
|
29
37
|
def kill
|
@@ -37,7 +45,13 @@ class Puppeteer::BrowserRunner
|
|
37
45
|
@thread.join
|
38
46
|
end
|
39
47
|
|
40
|
-
attr_reader :stdout, :stderr
|
48
|
+
attr_reader :stdout, :stderr, :spawnargs
|
49
|
+
end
|
50
|
+
|
51
|
+
class LaunchError < StandardError
|
52
|
+
def initialize(reason)
|
53
|
+
super("Failed to launch browser! #{reason}")
|
54
|
+
end
|
41
55
|
end
|
42
56
|
|
43
57
|
# @param {!(Launcher.LaunchOptions)=} options
|
@@ -123,12 +137,12 @@ class Puppeteer::BrowserRunner
|
|
123
137
|
|
124
138
|
# @return {Promise}
|
125
139
|
def kill
|
126
|
-
unless @closed
|
127
|
-
@proc.kill
|
128
|
-
end
|
129
140
|
if @temp_directory
|
130
141
|
FileUtils.rm_rf(@temp_directory)
|
131
142
|
end
|
143
|
+
unless @closed
|
144
|
+
@proc.kill
|
145
|
+
end
|
132
146
|
end
|
133
147
|
|
134
148
|
|