unmagic-browser 0.1.0

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.
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The class is declared before its parts are required, because they reopen it.
4
+ # Its documentation lives at the bottom of this file, with the implementation.
5
+ module Unmagic
6
+ class Browser; end
7
+ end
8
+
9
+ require_relative "browser/version"
10
+ require_relative "browser/error"
11
+ require_relative "browser/failures"
12
+ require_relative "browser/configuration"
13
+ require_relative "browser/emulation"
14
+ require_relative "browser/markdown"
15
+ require_relative "browser/locator"
16
+ require_relative "browser/element"
17
+ require_relative "browser/session"
18
+ require_relative "browser/driver"
19
+ require_relative "browser/driver/connection"
20
+ require_relative "browser/driver/base"
21
+ require_relative "browser/driver/local"
22
+ require_relative "browser/driver/remote"
23
+ require_relative "browser/driver/cloudflare"
24
+
25
+ module Unmagic
26
+ # A real browser you can drive from Ruby — for one-off page grabs *and*
27
+ # multi-step automation.
28
+ #
29
+ # The same API runs against local Chrome, any remote Puppeteer endpoint, and
30
+ # Cloudflare Browser Rendering. Which one is a construction-time decision;
31
+ # nothing else in your code changes.
32
+ #
33
+ # There are two ways in. Call {#markdown}, {#html}, {#screenshot} or {#pdf}
34
+ # straight on the browser for "just get me this page", and each takes the
35
+ # cheapest path its driver offers. Call {#open} for anything with more than
36
+ # one step, and you get a {Session} — a live page with Capybara-flavoured
37
+ # verbs that auto-wait.
38
+ #
39
+ # @example One-off
40
+ # Unmagic::Browser.new.markdown("https://example.com")
41
+ #
42
+ # @example Multi-step
43
+ # Unmagic::Browser.open("https://news.ycombinator.com/login") do |session|
44
+ # session.fill_in "acct", with: "pg"
45
+ # session.click_button "login"
46
+ # session.wait_for text: "logout"
47
+ # end
48
+ class Browser
49
+ # Environment names that mean "someone is sitting at this machine", where a
50
+ # local Chrome you can watch beats a remote one you can't.
51
+ DEVELOPMENT = ["development", "dev", "test"].freeze
52
+
53
+ class << self
54
+ # Set the gem's global defaults.
55
+ #
56
+ # @yieldparam config [Configuration]
57
+ # @return [Configuration]
58
+ #
59
+ # @example
60
+ # Unmagic::Browser.configure do |config|
61
+ # config.default_driver = :cloudflare
62
+ # end
63
+ def configure
64
+ yield(configuration) if block_given?
65
+ configuration
66
+ end
67
+
68
+ # @return [Configuration] the current global defaults
69
+ def configuration
70
+ @configuration ||= Configuration.new
71
+ end
72
+
73
+ # Throw the configuration away. Mostly for tests.
74
+ #
75
+ # @return [void]
76
+ def reset!
77
+ @configuration = Configuration.new
78
+ end
79
+
80
+ # Shorthand for `Unmagic::Browser.new.open(url)`.
81
+ #
82
+ # @param url [String] the page to open
83
+ # @param options [Hash] see {Browser#open}
84
+ # @yieldparam session [Session]
85
+ # @return [Session, Object] the session, or the block's value
86
+ def open(url, **options, &block)
87
+ new.open(url, **options, &block)
88
+ end
89
+
90
+ # Whether this process looks like somebody's machine rather than a
91
+ # server. Checked only when no driver was asked for.
92
+ #
93
+ # @return [Boolean]
94
+ def development?
95
+ return Rails.env.development? || Rails.env.test? if defined?(Rails) && Rails.respond_to?(:env) && Rails.env
96
+
97
+ environment = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["APP_ENV"]
98
+ environment.nil? || DEVELOPMENT.include?(environment)
99
+ end
100
+ end
101
+
102
+ # The driver doing the real work. The mirror of the `driver:` option: you
103
+ # pick one when you construct the browser, and this hands the instance back
104
+ # for anything the curated API doesn't wrap.
105
+ #
106
+ # @return [Driver::Base]
107
+ attr_reader :driver
108
+
109
+ # @param driver [Symbol, Driver::Base, nil] `:local`, `:remote`, `:cloudflare`, or an
110
+ # instance. Defaults to `:local` in development and to
111
+ # {Configuration#default_driver} everywhere else.
112
+ # @param timeout [Numeric, nil] seconds a locator waits, defaulting to {Configuration#timeout}
113
+ # @param navigation_timeout [Numeric, nil] seconds a navigation is given,
114
+ # defaulting to {Configuration#navigation_timeout}
115
+ def initialize(driver: nil, timeout: nil, navigation_timeout: nil)
116
+ @driver = Driver.build(driver || default_driver)
117
+ @timeout = timeout
118
+ @navigation_timeout = navigation_timeout
119
+ end
120
+
121
+ # @param url [String] the page to render
122
+ # @return [String] the page as Markdown
123
+ def markdown(url)
124
+ @driver.markdown(url)
125
+ end
126
+
127
+ # @param url [String] the page to render
128
+ # @return [String] the page's HTML, after JavaScript has run
129
+ def html(url)
130
+ @driver.html(url)
131
+ end
132
+
133
+ # @param url [String] the page to render
134
+ # @param full_page [Boolean] capture the whole scrollable page
135
+ # @param selector [String, nil] capture just the first element matching this CSS
136
+ # @return [String] PNG bytes
137
+ def screenshot(url, full_page: false, selector: nil)
138
+ @driver.screenshot(url, full_page: full_page, selector: selector)
139
+ end
140
+
141
+ # @param url [String] the page to render
142
+ # @return [String] PDF bytes
143
+ def pdf(url)
144
+ @driver.pdf(url)
145
+ end
146
+
147
+ # Open a page you can drive step by step.
148
+ #
149
+ # Works like `File.open`: with a block you get a session already on that
150
+ # page, cleaned up when the block ends. Without one, the session is yours to
151
+ # keep — and yours to {Session#close}, because it holds a real browser (and
152
+ # on Cloudflare, one of a limited number of concurrent slots) until you do.
153
+ #
154
+ # @param url [String, nil] the page to start on, or nil for a blank one
155
+ # @param emulate [Emulation, Hash, nil] how the page should render
156
+ # @param timeout [Numeric, nil] seconds a locator waits in this session
157
+ # @param navigation_timeout [Numeric, nil] seconds a navigation is given in this session
158
+ # @yieldparam session [Session]
159
+ # @return [Session, Object] the session, or the block's value
160
+ def open(url = nil, emulate: nil, timeout: nil, navigation_timeout: nil)
161
+ session = start_session(emulate: emulate, timeout: timeout, navigation_timeout: navigation_timeout)
162
+ begin
163
+ session.visit(url) if url
164
+ rescue StandardError
165
+ session.close
166
+ raise
167
+ end
168
+
169
+ return session unless block_given?
170
+
171
+ begin
172
+ yield(session)
173
+ ensure
174
+ session.close
175
+ end
176
+ end
177
+
178
+ # Release the shared browser one-off grabs run on. Sessions are unaffected —
179
+ # each holds its own.
180
+ #
181
+ # @return [void]
182
+ def close
183
+ @driver.close
184
+ end
185
+
186
+ # @return [String] the browser and the driver behind it
187
+ def inspect
188
+ "#<#{self.class.name} driver=#{@driver.name}>"
189
+ end
190
+
191
+ private
192
+
193
+ def start_session(emulate:, timeout:, navigation_timeout:)
194
+ connection = @driver.open_connection
195
+ begin
196
+ Session.new(
197
+ browser: self,
198
+ connection: connection,
199
+ page: connection.new_page,
200
+ emulation: emulate,
201
+ timeout: timeout || @timeout,
202
+ navigation_timeout: navigation_timeout || @navigation_timeout,
203
+ )
204
+ rescue StandardError
205
+ connection.close
206
+ raise
207
+ end
208
+ end
209
+
210
+ def default_driver
211
+ self.class.development? ? :local : self.class.configuration.default_driver
212
+ end
213
+ end
214
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unmagic-browser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Keith Pitt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: puppeteer-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.53'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.53'
27
+ - !ruby/object:Gem::Dependency
28
+ name: reverse_markdown
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description: Drive a real browser from Ruby for one-off page grabs and multi-step
42
+ automation. Runs against local Chrome, any remote Puppeteer endpoint, or Cloudflare
43
+ Browser Rendering — behind one Capybara-flavoured API with smart locators, auto-waiting,
44
+ and sessions that outlive a single call.
45
+ email:
46
+ - keith@unreasonable-magic.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - CHANGELOG.md
52
+ - LICENSE
53
+ - README.md
54
+ - lib/unmagic/browser.rb
55
+ - lib/unmagic/browser/configuration.rb
56
+ - lib/unmagic/browser/console.rb
57
+ - lib/unmagic/browser/console/banner.rb
58
+ - lib/unmagic/browser/console/graphics.rb
59
+ - lib/unmagic/browser/console/helpers.rb
60
+ - lib/unmagic/browser/console/prompt.rb
61
+ - lib/unmagic/browser/console/renderer.rb
62
+ - lib/unmagic/browser/console/terminal.rb
63
+ - lib/unmagic/browser/console/values.rb
64
+ - lib/unmagic/browser/driver.rb
65
+ - lib/unmagic/browser/driver/base.rb
66
+ - lib/unmagic/browser/driver/cloudflare.rb
67
+ - lib/unmagic/browser/driver/cloudflare/transport.rb
68
+ - lib/unmagic/browser/driver/connection.rb
69
+ - lib/unmagic/browser/driver/local.rb
70
+ - lib/unmagic/browser/driver/remote.rb
71
+ - lib/unmagic/browser/element.rb
72
+ - lib/unmagic/browser/emulation.rb
73
+ - lib/unmagic/browser/error.rb
74
+ - lib/unmagic/browser/failures.rb
75
+ - lib/unmagic/browser/locator.rb
76
+ - lib/unmagic/browser/markdown.rb
77
+ - lib/unmagic/browser/session.rb
78
+ - lib/unmagic/browser/version.rb
79
+ homepage: https://github.com/unreasonable-magic/unmagic-browser
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ homepage_uri: https://github.com/unreasonable-magic/unmagic-browser
84
+ source_code_uri: https://github.com/unreasonable-magic/unmagic-browser
85
+ changelog_uri: https://github.com/unreasonable-magic/unmagic-browser/blob/main/CHANGELOG.md
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 3.3.7
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.5.22
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: A real browser you can drive from Ruby, locally or on Cloudflare
105
+ test_files: []