bidi2pdf 0.1.22 → 0.1.23

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '008d5b0569ba4b7f8bbcd4092da72ce14e5d0e42589a97813f7c64c8bc6a0d9f'
4
- data.tar.gz: 8266f91bfa7606d843bed392cee203060cc3b13d98435cc9a0c4a679b5041285
3
+ metadata.gz: 9bff5f993e54cb0f70e6a2c83e022bf29fdef7b0fafc6ce8b641d5ec33aea7ea
4
+ data.tar.gz: e2466bb66703e3a7ac4ca16bfdea22f836c8fff07492e9f44904ecf5bc9c9c63
5
5
  SHA512:
6
- metadata.gz: 27ce2475fa8d30bc9c5447353767c6d2c06db59f57daa7fab7e7a337e26703cc1a0df04439d2b8b68455e5a872138775321a7f31988beb02dba47df0dd06d48d
7
- data.tar.gz: acb216b0c656916ee8e8ea797b1fccd026c67fa6260ac5f230f1c44190ffb00455fd83efb5238826c97af0f0051cf68a7335d09e51e0aaa25b85c2efdc802290
6
+ metadata.gz: 6958ef3cc24d5350fdb090598940410f5cd3ab9abef4003f118d52ab61142b45c6427e0225ed4d37a9fa3a23d0dfd033e67a07cc981f891cb2f1f95f7af67612
7
+ data.tar.gz: 74a366dc1c4953bb6e17e8c7578d3ec79cd7a0ea9f1f058f9d353ebdab12b2b046d7e25424b22745081895b09f60a5ca7a701eb29763472b9320e6a564142ad7
data/CHANGELOG.md CHANGED
@@ -8,10 +8,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
- [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.22..HEAD
11
+ [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.23..HEAD
12
12
 
13
13
  <!-- generated by git-cliff end -->
14
14
 
15
+ ## [0.1.23] - 2026-10-18
16
+
17
+ ### 🐛 Fixed
18
+ - Spell the plugin version as semver
19
+ - Keep screenshot names apart and stop browsers
20
+ - Stop every browser even when one stop fails
21
+ - Keep stdout to the JSON document or the PDF
22
+ - Lay a mobile print out at the intended width
23
+
24
+ ### 📝 Docs
25
+ - Explain how to install, pin and update the Claude Code skills
26
+ - Fix the script path and weigh the Chrome sandbox
27
+
28
+ ### 🔄 Changed
29
+ - Merge pull request #170 from dieter-medium/feat/claude-skills
30
+ - Merge pull request #169 from dieter-medium/fix/clean-stdout
31
+ - Merge pull request #160 from dieter-medium/dependabot/bundler/main/simplecov-1.3.2
32
+ - Merge pull request #167 from dieter-medium/dependabot/bundler/main/rubocop-1.92.0
33
+ - Merge pull request #168 from dieter-medium/feat/emulate-media
34
+
35
+ ### 🔧 Build
36
+ - Bump simplecov from 1.3.1 to 1.3.2
37
+ - Bump rubocop from 1.91.0 to 1.92.0
38
+
39
+ ### 🚀 Added
40
+ - Ship Claude Code skills for bidi2pdf
41
+ - Let a tab emulate a CSS media type
42
+
15
43
  ## [0.1.22] - 2026-10-11
16
44
 
17
45
  ### 🐛 Fixed
@@ -568,7 +596,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
568
596
 
569
597
  ### 🔄 Released
570
598
 
571
- - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.22..HEAD)
599
+ - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.23..HEAD)
600
+ - [0.1.23](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.22..v0.1.23)
572
601
  - [0.1.22](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.21..v0.1.22)
573
602
  - [0.1.21](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.20..v0.1.21)
574
603
  - [0.1.20](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.19..v0.1.20)
data/README.md CHANGED
@@ -110,6 +110,26 @@ bidi2pdf render \
110
110
  --log-level debug
111
111
  ```
112
112
 
113
+ #### The page as it looks on screen, dark mode, or its mobile version
114
+
115
+ By default Chrome prints with the page's print stylesheet. `--media screen` prints its screen styles
116
+ instead, `--color_scheme dark` its dark mode, `--reduced_motion reduce` with animations off.
117
+
118
+ A narrow page adds the page's mobile layout. A printed page is laid out at the width between its side
119
+ margins, not at the paper's width: page width minus `--margin_left` and `--margin_right`, at about
120
+ 37.8 CSS px per cm (96 / 2.54). Without margins of your own Chrome uses 1 cm a side, so a 9.5 cm page
121
+ lays out at about 283 px. For a 360 px phone layout, set the side margins explicitly:
122
+
123
+ ```bash
124
+ bidi2pdf render --url https://example.com --output mobile.pdf \
125
+ --media screen --color_scheme dark \
126
+ --page_width 9.525 --page_height 20 --margin_left 0 --margin_right 0
127
+ ```
128
+
129
+ 8.467 cm without side margins gives 320 px. All page sizes and margins are in centimetres.
130
+
131
+ `bidi2pdf diagnose` takes the same three options, so its screenshot shows the same emulation.
132
+
113
133
  ---
114
134
 
115
135
  ## 🤖 Agent and Automation Usage
@@ -243,7 +263,7 @@ bidi2pdf run invoice.yml --json # actions, then the PDF, then assertions a
243
263
  ```
244
264
 
245
265
  Actions (`wait_for`, `click`, `evaluate`, `inject_script`, `inject_style`, `set_viewport`,
246
- `wait_network_idle`) and page assertions (`selector_exists`, `text_present`, `no_console_errors`,
266
+ `wait_network_idle`, `emulate_media`) and page assertions (`selector_exists`, `text_present`, `no_console_errors`,
247
267
  `no_network_failures`, `fonts_loaded`) are a thin layer over the same `BrowserTab` methods the
248
268
  [Programmatic API](#-programmatic-api) below uses directly. PDF assertions (`page_count`,
249
269
  `pdf_text_present`, `pdf_not_blank`) need the `pdf-reader` gem - a recipe using one fails
@@ -256,6 +276,91 @@ rejected by both `bidi2pdf schema recipe` and `--validate` rather than being sil
256
276
  `wait_for` needs exactly one of `selector`, `paged_js`, `script` - zero or more than one is
257
277
  rejected the same way, before any browser launches.
258
278
 
279
+ ### Claude Code skills
280
+
281
+ The repository ships a [Claude Code](https://claude.com/claude-code) plugin with six skills, so an
282
+ agent uses bidi2pdf the way it is meant to be used instead of guessing options:
283
+
284
+ | Skill | For |
285
+ |---|---|
286
+ | `render-pdf` | a URL or HTML file to PDF with the CLI: order of commands, waits, screen/dark/mobile view, reading the JSON result and exit codes |
287
+ | `pdf-recipe` | a recipe for pages that need steps before printing, validated without a browser |
288
+ | `diagnose-pdf` | a blank, unstyled or wrongly paged PDF: a checklist around `bidi2pdf diagnose` |
289
+ | `remote-chrome` | a shared chromedriver container: settings, sizing, session cleanup, untrusted HTML |
290
+ | `page-screenshots` | review screenshots per page at phone and desktop width, light and dark (with a script) |
291
+ | `pdf-specs` | RSpec specs with the gem's PDF matchers and Chrome container helpers |
292
+
293
+ #### Installing the skills
294
+
295
+ The plugin is not in a central store: this repository is its own Claude Code marketplace
296
+ (`.claude-plugin/marketplace.json`, marketplace name `bidi2pdf`). You add the repository as a
297
+ marketplace once, then install the plugin from it. In a Claude Code session:
298
+
299
+ ```text
300
+ /plugin marketplace add dieter-medium/bidi2pdf
301
+ /plugin install bidi2pdf@bidi2pdf
302
+ ```
303
+
304
+ `/plugin install` does not install right away: it opens the plugin's details, where you choose the scope
305
+ (just you, the project, or only this checkout) and confirm the installation.
306
+
307
+ Or from a shell, where the install happens at once:
308
+
309
+ ```bash
310
+ claude plugin marketplace add dieter-medium/bidi2pdf
311
+ claude plugin install bidi2pdf@bidi2pdf # --scope user (default), project or local
312
+ ```
313
+
314
+ The skills then load on their own when a task fits them; nothing else needs configuring.
315
+
316
+ **Which version you get.** The plugin's version is the gem's (`claude-plugin/.claude-plugin/plugin.json`),
317
+ and Claude Code installs an update only when that version changes - not with every commit. Adding the
318
+ marketplace as above follows the default branch, so after a release the skills there can already
319
+ describe the next, unreleased gem (the version then reads e.g. `0.1.24-pre`). To stay on the skills of
320
+ the gem you actually use, add the marketplace at its release tag:
321
+
322
+ ```bash
323
+ claude plugin marketplace add dieter-medium/bidi2pdf#v0.1.23
324
+ ```
325
+
326
+ **Updating and removing.** Auto-update is off for this marketplace unless you turn it on (`/plugin` >
327
+ Marketplaces). By hand:
328
+
329
+ ```bash
330
+ claude plugin marketplace update bidi2pdf # fetch the marketplace again
331
+ claude plugin update bidi2pdf@bidi2pdf # take the new version
332
+ claude plugin uninstall bidi2pdf@bidi2pdf # remove the plugin only
333
+ claude plugin marketplace remove bidi2pdf # remove the marketplace and every plugin installed from it
334
+ ```
335
+
336
+ **For a whole team.** To give everyone working in a project the skills, commit this to the project's
337
+ `.claude/settings.json` - each member gets them after accepting Claude Code's trust prompt for the
338
+ folder:
339
+
340
+ ```json
341
+ {
342
+ "extraKnownMarketplaces": {
343
+ "bidi2pdf": { "source": { "source": "github", "repo": "dieter-medium/bidi2pdf" } }
344
+ },
345
+ "enabledPlugins": { "bidi2pdf@bidi2pdf": true }
346
+ }
347
+ ```
348
+
349
+ `claude plugin marketplace add dieter-medium/bidi2pdf --scope project` and
350
+ `claude plugin install bidi2pdf@bidi2pdf --scope project` write the same entries for you.
351
+
352
+ **From a checkout, without installing** (to try a change to a skill):
353
+
354
+ ```bash
355
+ claude --plugin-dir ./claude-plugin # for this one session
356
+ claude plugin validate ./claude-plugin --strict # check the plugin's structure
357
+ ```
358
+
359
+ The skills point at `bidi2pdf help <command>` and `bidi2pdf schema <kind>` for exact options and
360
+ shapes; a spec (`spec/unit/claude_plugin_spec.rb`) checks every command, option, error code, recipe and
361
+ method they name against the gem, so they cannot drift from it. The plugin is not part of the gem
362
+ itself.
363
+
259
364
  ---
260
365
 
261
366
  ## 🧠 Programmatic API
@@ -350,6 +455,10 @@ tab.navigate_to "https://example.com/invoice/14432423"
350
455
  # Printing HTML you do not trust? Switch its scripts off before navigating:
351
456
  # tab.disable_script_execution
352
457
 
458
+ # Print the screen styles in dark mode instead of the print stylesheet (holds for later
459
+ # navigations in this tab; tab.emulate_media without arguments ends it):
460
+ # tab.emulate_media(media: "screen", color_scheme: "dark")
461
+
353
462
  tab.wait_until_network_idle
354
463
  tab.print("my.pdf")
355
464
 
@@ -41,6 +41,13 @@ module Bidi2pdf
41
41
  class BrowserTab
42
42
  include JsLoggerHelper
43
43
 
44
+ # The values #emulate_media accepts, by argument - what Chrome's Emulation.setEmulatedMedia knows.
45
+ EMULATED_MEDIA = {
46
+ media: %w[screen print],
47
+ color_scheme: %w[light dark],
48
+ reduced_motion: %w[reduce no-preference]
49
+ }.freeze
50
+
44
51
  # @return [Object] The WebSocket client.
45
52
  attr_reader :client
46
53
 
@@ -439,7 +446,9 @@ module Bidi2pdf
439
446
 
440
447
  block.call(pdf_base64) if block_given?
441
448
 
442
- return pdf_base64 unless outputfile || block_given?
449
+ # The block's value, not a `return`: returning out of the instrumented block would
450
+ # skip the print event, and with it every subscriber (ResultCollector, --output -).
451
+ outputfile || block_given? ? nil : pdf_base64
443
452
  else
444
453
  Bidi2pdf.logger.error "Error printing: #{response}"
445
454
  end
@@ -480,7 +489,8 @@ module Bidi2pdf
480
489
 
481
490
  block.call(png_base64) if block_given?
482
491
 
483
- return png_base64 unless outputfile || block_given?
492
+ # The block's value, not a `return` - see #print.
493
+ outputfile || block_given? ? nil : png_base64
484
494
  else
485
495
  Bidi2pdf.logger.error "Error capturing screenshot: #{response}"
486
496
  end
@@ -513,8 +523,43 @@ module Bidi2pdf
513
523
  client.send_cmd_and_wait(cmd) { |response| response }
514
524
  end
515
525
 
526
+ # Emulates a CSS media type and media features for this tab, through Chrome's own
527
+ # Emulation.setEmulatedMedia (CDP, via goog:cdp.sendCommand). It holds for every later
528
+ # navigation in the tab and for printing: +media: "screen"+ prints a page with its screen styles
529
+ # instead of its print stylesheet, +color_scheme: "dark"+ in its dark mode. On a narrow page the
530
+ # screen media type prints a page's mobile layout: the layout width is the page width minus the
531
+ # side margins (Chrome's default is 1 cm each), at 96 / 2.54 CSS px per cm - e.g.
532
+ # +page: { width: 9.525 }, margin: { left: 0, right: 0 }+ lays out at 360 px. Each call replaces
533
+ # the previous emulation, so a media type or feature it omits is reset; a call without arguments
534
+ # ends it.
535
+ #
536
+ # @param [String, Symbol, nil] media "screen" or "print"; nil leaves it to the browser.
537
+ # @param [String, Symbol, nil] color_scheme "light" or "dark" (prefers-color-scheme).
538
+ # @param [String, Symbol, nil] reduced_motion "reduce" or "no-preference" (prefers-reduced-motion).
539
+ # @raise [ArgumentError] for a value Chrome does not know, before anything is sent.
540
+ def emulate_media(media: nil, color_scheme: nil, reduced_motion: nil)
541
+ params = emulated_media_params(media: media, color_scheme: color_scheme, reduced_motion: reduced_motion)
542
+ cmd = Commands::CdpSendCommand.new(method: "Emulation.setEmulatedMedia", params: params, session: cdp_session)
543
+ client.send_cmd_and_wait(cmd) { |response| response }
544
+ end
545
+
516
546
  private
517
547
 
548
+ def emulated_media_params(**values)
549
+ values.each { |name, value| check_emulated_media(name, value) }
550
+
551
+ features = { "prefers-color-scheme" => values[:color_scheme], "prefers-reduced-motion" => values[:reduced_motion] }
552
+ .filter_map { |name, value| { name: name, value: value.to_s } unless value.nil? }
553
+ { media: values[:media].to_s, features: features }
554
+ end
555
+
556
+ def check_emulated_media(name, value)
557
+ allowed = EMULATED_MEDIA.fetch(name)
558
+ return if value.nil? || allowed.include?(value.to_s)
559
+
560
+ raise ArgumentError, "#{name} must be one of #{allowed.join(", ")}, got #{value.inspect}"
561
+ end
562
+
518
563
  def build_command_and_extractor(print_options)
519
564
  cmd_type = (print_options.delete(:cmd_type) || :bidi).to_sym
520
565
 
data/lib/bidi2pdf/cli.rb CHANGED
@@ -78,10 +78,10 @@ module Bidi2pdf
78
78
  option :manifest, type: :string, desc: "Write a render manifest (see `bidi2pdf schema manifest`) to this file"
79
79
 
80
80
  option :background, type: :boolean, default: true, desc: "Print background graphics"
81
- option :margin_top, type: :numeric, default: 1.0, desc: "Top margin in inches"
82
- option :margin_bottom, type: :numeric, default: 1.0, desc: "Bottom margin in inches"
83
- option :margin_left, type: :numeric, default: 1.0, desc: "Left margin in inches"
84
- option :margin_right, type: :numeric, default: 1.0, desc: "Right margin in inches"
81
+ option :margin_top, type: :numeric, default: 1.0, desc: "Top margin in cm"
82
+ option :margin_bottom, type: :numeric, default: 1.0, desc: "Bottom margin in cm"
83
+ option :margin_left, type: :numeric, default: 1.0, desc: "Left margin in cm"
84
+ option :margin_right, type: :numeric, default: 1.0, desc: "Right margin in cm"
85
85
  option :orientation, type: :string, default: "portrait", enum: %w[portrait landscape], desc: "Page orientation"
86
86
  option :page_width, type: :numeric, default: 21.59, desc: "Page width in cm (min 0.0352)"
87
87
  option :page_height, type: :numeric, default: 27.94, desc: "Page height in cm (min 0.0352)"
@@ -90,6 +90,9 @@ module Bidi2pdf
90
90
  option :shrink_to_fit, type: :boolean, default: true, desc: "Shrink content to fit page"
91
91
  option :generate_tagged_pdf, type: :boolean, default: false, desc: "Generate tagged PDF"
92
92
  option :generate_document_outline, type: :boolean, default: false, desc: "Generate document outline"
93
+ option :media, type: :string, enum: %w[screen print], desc: "Emulate a CSS media type; screen prints the page with its screen styles. For its mobile layout, narrow the page: the layout width is --page_width minus the side margins, 9.525 cm with --margin_left 0 --margin_right 0 = 360 CSS px"
94
+ option :color_scheme, type: :string, enum: %w[light dark], desc: "Emulate prefers-color-scheme"
95
+ option :reduced_motion, type: :string, enum: %w[reduce no-preference], desc: "Emulate prefers-reduced-motion"
93
96
 
94
97
  class << self
95
98
  def exit_on_failure?
@@ -152,6 +155,9 @@ module Bidi2pdf
152
155
  option :json, type: :boolean, default: false, desc: "Emit a single structured JSON document (see `bidi2pdf schema diagnose`) instead of a human-readable summary"
153
156
  option :json_stream, type: :boolean, default: false, desc: "Emit one JSON progress event per line to stderr as diagnostics run, with or without --json (see `bidi2pdf schema event`)"
154
157
  option :screenshot, type: :string, desc: "Also capture a screenshot to this PNG file"
158
+ option :media, type: :string, enum: %w[screen print], desc: "Emulate a CSS media type; screen prints the page with its screen styles. For its mobile layout, narrow the page: the layout width is --page_width minus the side margins, 9.525 cm with --margin_left 0 --margin_right 0 = 360 CSS px"
159
+ option :color_scheme, type: :string, enum: %w[light dark], desc: "Emulate prefers-color-scheme"
160
+ option :reduced_motion, type: :string, enum: %w[reduce no-preference], desc: "Emulate prefers-reduced-motion"
155
161
  option :pdf, type: :string, desc: "Also render a PDF to this file and report its page count / blank-check"
156
162
 
157
163
  def diagnose
@@ -307,9 +313,11 @@ module Bidi2pdf
307
313
  emit_structured_render_output(result, collector)
308
314
 
309
315
  exit_for_result(result)
316
+ ensure
317
+ stop_browsers
310
318
  end
311
319
  ensure
312
- launcher.stop if defined?(@launcher) && @launcher
320
+ stop_browsers
313
321
  stream&.unsubscribe
314
322
  cleanup_stdin_tempfile
315
323
  end
@@ -369,9 +377,11 @@ module Bidi2pdf
369
377
  $stdout.puts(diagnose_payload(result, diagnostics).to_json)
370
378
 
371
379
  exit_for_result(result)
380
+ ensure
381
+ stop_browsers
372
382
  end
373
383
  ensure
374
- launcher.stop if defined?(@launcher) && @launcher
384
+ stop_browsers
375
385
  stream&.unsubscribe
376
386
  cleanup_stdin_tempfile
377
387
  end
@@ -413,7 +423,6 @@ module Bidi2pdf
413
423
  # --- run - Loader/Validator run before any browser work, both in --validate and in a real
414
424
  # run, so an invalid recipe never launches one.
415
425
 
416
- # rubocop:disable-next Metrics/CyclomaticComplexity
417
426
  def perform_human_run(recipe_path)
418
427
  recipe = load_and_validate_recipe(recipe_path)
419
428
  return puts "Recipe '#{recipe_path}' is valid." if merged_options[:validate]
@@ -448,12 +457,12 @@ module Bidi2pdf
448
457
  rescue Bidi2pdf::Error => e
449
458
  raise Thor::Error, e.message
450
459
  ensure
451
- @run_launcher&.stop
460
+ stop_browsers
452
461
  stream&.unsubscribe
453
462
  cleanup_stdin_tempfile
454
463
  end
455
464
 
456
- # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
465
+ # rubocop:disable-next Metrics/CyclomaticComplexity
457
466
  def perform_structured_run(recipe_path)
458
467
  recipe = load_and_validate_recipe(recipe_path)
459
468
 
@@ -490,12 +499,14 @@ module Bidi2pdf
490
499
  $stdout.puts(payload.to_json)
491
500
 
492
501
  exit_for_result(result)
502
+ ensure
503
+ stop_browsers
493
504
  end
494
505
  rescue Bidi2pdf::InvalidRecipeError, Bidi2pdf::PdfInspectionUnavailableError, Bidi2pdf::InvalidPrintOptionError => e
495
506
  $stdout.puts Bidi2pdf::Result.failure(command: "run", error: ErrorCodes.describe(e)).to_json
496
507
  exit(Bidi2pdf::ExitCodes.for(ErrorCodes.for(e)))
497
508
  ensure
498
- @run_launcher&.stop
509
+ stop_browsers
499
510
  stream&.unsubscribe
500
511
  cleanup_stdin_tempfile
501
512
  end
@@ -510,6 +521,22 @@ module Bidi2pdf
510
521
  recipe.source["url"] || recipe.source["file"] || "(stdin)"
511
522
  end
512
523
 
524
+ # Stops whatever browser this command started, once. The structured paths call it inside
525
+ # reserve_stdout_for_machine_output, while the loggers still write to stderr: stopping logs
526
+ # ("WebSocket closed"), and after the block those lines would land on stdout behind the JSON
527
+ # document or the PDF bytes. Each browser is stopped on its own, and a failing stop is logged,
528
+ # not raised: this runs in an ensure, where raising would replace the command's own result and
529
+ # exit status.
530
+ def stop_browsers
531
+ launchers = [(@launcher if defined?(@launcher)), (@run_launcher if defined?(@run_launcher))].compact
532
+ @launcher = @run_launcher = nil
533
+ launchers.each do |launcher|
534
+ launcher.stop
535
+ rescue StandardError => e
536
+ Bidi2pdf.logger.error "Stopping the browser failed: #{e.class}: #{e.message}"
537
+ end
538
+ end
539
+
513
540
  def with_recipe_tab(recipe)
514
541
  @run_launcher = launcher_for_recipe(recipe)
515
542
  tab = @run_launcher.diagnose
@@ -818,11 +845,18 @@ module Bidi2pdf
818
845
  wait_network_idle: merged_options[:wait_network_idle],
819
846
  print_options: print_options,
820
847
  network_log_format: merged_options[:network_log_format],
821
- chrome_args: chrome_args
848
+ chrome_args: chrome_args,
849
+ emulate_media: emulate_media_options
822
850
  )
823
851
  end
824
852
  end
825
853
 
854
+ # The --media/--color_scheme/--reduced_motion choices for BrowserTab#emulate_media; empty when none
855
+ # is given, so the browser keeps its own.
856
+ def emulate_media_options
857
+ { media: merged_options[:media], color_scheme: merged_options[:color_scheme], reduced_motion: merged_options[:reduced_motion] }.compact
858
+ end
859
+
826
860
  def configure
827
861
  Bidi2pdf.configure do |config|
828
862
  config.logger.level = log_level
@@ -35,45 +35,31 @@ module Bidi2pdf
35
35
  # @param [Hash] print_options Options for printing the page. Defaults to an empty hash.
36
36
  # @param [String, nil] remote_browser_url The URL of a remote browser to connect to. Defaults to nil.
37
37
  # @param [Symbol] network_log_format The format for network logs. Defaults to :console.
38
+ # @param [Hash] emulate_media Media type and features for BrowserTab#emulate_media (media:,
39
+ # color_scheme:, reduced_motion:). Defaults to none.
38
40
  class Launcher
39
41
  # rubocop:disable Metrics/ParameterLists
40
42
  def initialize(url:, inputfile:, output:, cookies:, headers:, auth:, headless: true, port: 0, wait_window_loaded: false,
41
43
  wait_network_idle: false, print_options: {}, remote_browser_url: nil, network_log_format: :console,
42
- chrome_args: Bidi::Session::DEFAULT_CHROME_ARGS)
43
- @url = url
44
- @inputfile = inputfile
44
+ chrome_args: Bidi::Session::DEFAULT_CHROME_ARGS, emulate_media: {})
45
+ # What both runners get, unchanged - SessionRunner's own keyword arguments.
46
+ @runner_options = {
47
+ url: url, inputfile: inputfile, output: output, cookies: cookies, headers: headers, auth: auth,
48
+ wait_window_loaded: wait_window_loaded, wait_network_idle: wait_network_idle, print_options: print_options || {},
49
+ network_log_format: network_log_format, emulate_media: emulate_media || {}
50
+ }
45
51
  @port = port
46
52
  @headless = headless
47
- @output = output
48
- @cookies = cookies
49
- @headers = headers
50
- @auth = auth
51
53
  @manager = nil
52
- @wait_window_loaded = wait_window_loaded
53
- @wait_network_idle = wait_network_idle
54
- @print_options = print_options || {}
55
54
  @remote_browser_url = remote_browser_url
56
55
  @custom_session = nil
57
- @network_log_format = network_log_format
58
56
  @chrome_args = chrome_args
59
57
  end
60
58
 
61
59
  # rubocop:enable Metrics/ParameterLists
62
60
 
63
61
  def launch
64
- runner = SessionRunner.new(
65
- session: session,
66
- url: @url,
67
- inputfile: @inputfile,
68
- output: @output,
69
- cookies: @cookies,
70
- headers: @headers,
71
- auth: @auth,
72
- wait_window_loaded: @wait_window_loaded,
73
- wait_network_idle: @wait_network_idle,
74
- print_options: @print_options,
75
- network_log_format: @network_log_format
76
- )
62
+ runner = SessionRunner.new(session: session, **@runner_options)
77
63
  runner.run
78
64
  end
79
65
 
@@ -83,19 +69,7 @@ module Bidi2pdf
83
69
  #
84
70
  # @return [Bidi2pdf::Bidi::BrowserTab] the navigated tab.
85
71
  def diagnose
86
- @diagnose_runner = SessionRunner.new(
87
- session: session,
88
- url: @url,
89
- inputfile: @inputfile,
90
- output: @output,
91
- cookies: @cookies,
92
- headers: @headers,
93
- auth: @auth,
94
- wait_window_loaded: @wait_window_loaded,
95
- wait_network_idle: @wait_network_idle,
96
- print_options: @print_options,
97
- network_log_format: @network_log_format
98
- )
72
+ @diagnose_runner = SessionRunner.new(session: session, **@runner_options)
99
73
  @diagnose_runner.run_diagnose
100
74
  end
101
75
 
@@ -90,6 +90,7 @@ module Bidi2pdf
90
90
  when "inject_style" then @tab.inject_style(url: opts["url"], content: opts["content"], id: opts["id"])
91
91
  when "set_viewport" then @tab.set_viewport(width: opts["width"], height: opts["height"], device_pixel_ratio: opts["device_pixel_ratio"])
92
92
  when "wait_network_idle" then @tab.wait_until_network_idle(timeout: opts.fetch("timeout", 10))
93
+ when "emulate_media" then @tab.emulate_media(media: opts["media"], color_scheme: opts["color_scheme"], reduced_motion: opts["reduced_motion"])
93
94
  end
94
95
  end
95
96
 
@@ -12,7 +12,7 @@ module Bidi2pdf
12
12
  # launches) -> Validator -> Runner.
13
13
  class Recipe
14
14
  KNOWN_TOP_LEVEL_KEYS = %w[version source browser headers cookies auth actions assert print output].freeze
15
- KNOWN_ACTIONS = %w[wait_for click evaluate inject_script inject_style set_viewport wait_network_idle].freeze
15
+ KNOWN_ACTIONS = %w[wait_for click evaluate inject_script inject_style set_viewport wait_network_idle emulate_media].freeze
16
16
  PAGE_ASSERTIONS = %w[selector_exists text_present no_console_errors no_network_failures fonts_loaded].freeze
17
17
  PDF_ASSERTIONS = %w[page_count pdf_text_present pdf_not_blank].freeze
18
18
  KNOWN_ASSERTIONS = (PAGE_ASSERTIONS + PDF_ASSERTIONS).freeze
@@ -219,7 +219,7 @@ module Bidi2pdf
219
219
  }.freeze
220
220
 
221
221
  RECIPE_ACTIONS = {
222
- "description" => "one of the 7 known actions - a thin dispatch over BrowserTab's own methods, see README's " \
222
+ "description" => "one of the 8 known actions - a thin dispatch over BrowserTab's own methods, see README's " \
223
223
  "Agent and Automation Usage section",
224
224
  "oneOf" => [
225
225
  {
@@ -271,6 +271,18 @@ module Bidi2pdf
271
271
  "type" => "object", "required" => %w[wait_network_idle], "additionalProperties" => false,
272
272
  "properties" => { "wait_network_idle" => { "type" => "object", "additionalProperties" => false,
273
273
  "properties" => { "timeout" => { "type" => "number" } } } }
274
+ },
275
+ {
276
+ "type" => "object", "required" => %w[emulate_media], "additionalProperties" => false,
277
+ "properties" => {
278
+ "emulate_media" => {
279
+ "type" => "object", "additionalProperties" => false,
280
+ "description" => "the CSS media type and media features the page is shown and printed with, from here on; " \
281
+ "media: screen prints the screen styles; on a narrow print page (layout width = page width minus the side margins, " \
282
+ "9.525 cm without side margins = 360 CSS px) the mobile layout",
283
+ "properties" => Bidi2pdf::Bidi::BrowserTab::EMULATED_MEDIA.to_h { |name, values| [name.to_s, { "type" => "string", "enum" => values }] }
284
+ }
285
+ }
274
286
  }
275
287
  ]
276
288
  }.freeze
@@ -33,10 +33,12 @@ module Bidi2pdf
33
33
  # @param [Boolean] wait_network_idle Whether to wait for the network to become idle. Defaults to false.
34
34
  # @param [Hash] print_options Options for printing the page. Defaults to an empty hash.
35
35
  # @param [Symbol] network_log_format The format for network logs. Defaults to :console.
36
+ # @param [Hash] emulate_media Media type and features for BrowserTab#emulate_media (media:,
37
+ # color_scheme:, reduced_motion:), applied before navigating. Defaults to none.
36
38
  class SessionRunner
37
39
  # rubocop: disable Metrics/ParameterLists
38
40
  def initialize(session:, url:, inputfile:, output:, cookies: {}, headers: {}, auth: {}, wait_window_loaded: false,
39
- wait_network_idle: false, print_options: {}, network_log_format: :console)
41
+ wait_network_idle: false, print_options: {}, network_log_format: :console, emulate_media: {})
40
42
  @session = session
41
43
  @url = url
42
44
  @inputfile = inputfile
@@ -48,6 +50,7 @@ module Bidi2pdf
48
50
  @wait_network_idle = wait_network_idle
49
51
  @print_options = print_options || {}
50
52
  @network_log_format = network_log_format
53
+ @emulate_media = emulate_media || {}
51
54
  end
52
55
 
53
56
  # rubocop: enable Metrics/ParameterLists
@@ -100,6 +103,7 @@ module Bidi2pdf
100
103
 
101
104
  add_headers
102
105
  add_basic_auth
106
+ @tab.emulate_media(**@emulate_media) if @emulate_media.any?
103
107
  end
104
108
 
105
109
  def add_cookies(tab)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bidi2pdf
4
- VERSION = "0.1.22"
4
+ VERSION = "0.1.23"
5
5
  end
@@ -56,6 +56,9 @@ module Bidi2pdf
56
56
 
57
57
  include JsLoggerHelper
58
58
 
59
+ # The values #emulate_media accepts, by argument - what Chrome's Emulation.setEmulatedMedia knows.
60
+ EMULATED_MEDIA: { media: ::Array["screen" | "print"], color_scheme: ::Array["light" | "dark"], reduced_motion: ::Array["reduce" | "no-preference"] }
61
+
59
62
  # @return [Object] The WebSocket client.
60
63
  attr_reader client: untyped
61
64
 
@@ -238,8 +241,28 @@ module Bidi2pdf
238
241
  # @param [Boolean] disabled Defaults to true.
239
242
  def disable_script_execution: (?disabled: bool) -> untyped
240
243
 
244
+ # Emulates a CSS media type and media features for this tab, through Chrome's own
245
+ # Emulation.setEmulatedMedia (CDP, via goog:cdp.sendCommand). It holds for every later
246
+ # navigation in the tab and for printing: +media: "screen"+ prints a page with its screen styles
247
+ # instead of its print stylesheet, +color_scheme: "dark"+ in its dark mode. On a narrow page the
248
+ # screen media type prints a page's mobile layout: the layout width is the page width minus the
249
+ # side margins (Chrome's default is 1 cm each), at 96 / 2.54 CSS px per cm - e.g.
250
+ # +page: { width: 9.525 }, margin: { left: 0, right: 0 }+ lays out at 360 px. Each call replaces
251
+ # the previous emulation, so a media type or feature it omits is reset; a call without arguments
252
+ # ends it.
253
+ #
254
+ # @param [String, Symbol, nil] media "screen" or "print"; nil leaves it to the browser.
255
+ # @param [String, Symbol, nil] color_scheme "light" or "dark" (prefers-color-scheme).
256
+ # @param [String, Symbol, nil] reduced_motion "reduce" or "no-preference" (prefers-reduced-motion).
257
+ # @raise [ArgumentError] for a value Chrome does not know, before anything is sent.
258
+ def emulate_media: (?media: untyped?, ?color_scheme: untyped?, ?reduced_motion: untyped?) -> untyped
259
+
241
260
  private
242
261
 
262
+ def emulated_media_params: (**untyped values) -> { media: untyped, features: untyped }
263
+
264
+ def check_emulated_media: (untyped name, untyped value) -> (nil | untyped)
265
+
243
266
  def build_command_and_extractor: (untyped print_options) -> ::Array[untyped]
244
267
 
245
268
  def cdp_session: () -> untyped
data/sig/bidi2pdf/cli.rbs CHANGED
@@ -1,6 +1,8 @@
1
1
  module Bidi2pdf
2
2
  # rubocop:disable Metrics/AbcSize
3
3
  class CLI < Thor
4
+ @launcher: untyped
5
+
4
6
  @run_launcher: untyped
5
7
 
6
8
  @render_input_path: untyped
@@ -9,8 +11,6 @@ module Bidi2pdf
9
11
 
10
12
  @stdin_tempfile: untyped
11
13
 
12
- @launcher: untyped
13
-
14
14
  include JsonOutput
15
15
 
16
16
  include SessionCommands
@@ -55,16 +55,23 @@ module Bidi2pdf
55
55
 
56
56
  def capture_diagnose_pdf: (untyped tab) -> (nil | { path: untyped, pages: untyped, not_blank: untyped })
57
57
 
58
- # rubocop:disable-next Metrics/CyclomaticComplexity
59
58
  def perform_human_run: (untyped recipe_path) -> untyped
60
59
 
61
- # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
60
+ # rubocop:disable-next Metrics/CyclomaticComplexity
62
61
  def perform_structured_run: (untyped recipe_path) -> untyped
63
62
 
64
63
  def load_and_validate_recipe: (untyped recipe_path) -> untyped
65
64
 
66
65
  def recipe_requested_url: (untyped recipe) -> untyped
67
66
 
67
+ # Stops whatever browser this command started, once. The structured paths call it inside
68
+ # reserve_stdout_for_machine_output, while the loggers still write to stderr: stopping logs
69
+ # ("WebSocket closed"), and after the block those lines would land on stdout behind the JSON
70
+ # document or the PDF bytes. Each browser is stopped on its own, and a failing stop is logged,
71
+ # not raised: this runs in an ensure, where raising would replace the command's own result and
72
+ # exit status.
73
+ def stop_browsers: () -> untyped
74
+
68
75
  def with_recipe_tab: (untyped recipe) { (untyped) -> untyped } -> untyped
69
76
 
70
77
  # A failing action/assertion raises Recipe::Runner::StepFailure (its own entries plus the real
@@ -144,6 +151,10 @@ module Bidi2pdf
144
151
 
145
152
  def launcher: () -> untyped
146
153
 
154
+ # The --media/--color_scheme/--reduced_motion choices for BrowserTab#emulate_media; empty when none
155
+ # is given, so the browser keeps its own.
156
+ def emulate_media_options: () -> untyped
157
+
147
158
  def configure: () -> untyped
148
159
 
149
160
  def log_level: () -> untyped
@@ -29,43 +29,28 @@ module Bidi2pdf
29
29
  # @param [Hash] print_options Options for printing the page. Defaults to an empty hash.
30
30
  # @param [String, nil] remote_browser_url The URL of a remote browser to connect to. Defaults to nil.
31
31
  # @param [Symbol] network_log_format The format for network logs. Defaults to :console.
32
+ # @param [Hash] emulate_media Media type and features for BrowserTab#emulate_media (media:,
33
+ # color_scheme:, reduced_motion:). Defaults to none.
32
34
  class Launcher
33
- @url: untyped
34
-
35
- @inputfile: untyped
35
+ # What both runners get, unchanged - SessionRunner's own keyword arguments.
36
+ @runner_options: untyped
36
37
 
37
38
  @port: untyped
38
39
 
39
40
  @headless: untyped
40
41
 
41
- @output: untyped
42
-
43
- @cookies: untyped
44
-
45
- @headers: untyped
46
-
47
- @auth: untyped
48
-
49
42
  @manager: untyped
50
43
 
51
- @wait_window_loaded: untyped
52
-
53
- @wait_network_idle: untyped
54
-
55
- @print_options: untyped
56
-
57
44
  @remote_browser_url: untyped
58
45
 
59
46
  @custom_session: untyped
60
47
 
61
- @network_log_format: untyped
62
-
63
48
  @chrome_args: untyped
64
49
 
65
50
  @diagnose_runner: untyped
66
51
 
67
52
  # rubocop:disable Metrics/ParameterLists
68
- def initialize: (url: untyped, inputfile: untyped, output: untyped, cookies: untyped, headers: untyped, auth: untyped, ?headless: bool, ?port: ::Integer, ?wait_window_loaded: bool, ?wait_network_idle: bool, ?print_options: ::Hash[untyped, untyped], ?remote_browser_url: untyped?, ?network_log_format: ::Symbol, ?chrome_args: untyped) -> void
53
+ def initialize: (url: untyped, inputfile: untyped, output: untyped, cookies: untyped, headers: untyped, auth: untyped, ?headless: bool, ?port: ::Integer, ?wait_window_loaded: bool, ?wait_network_idle: bool, ?print_options: ::Hash[untyped, untyped], ?remote_browser_url: untyped?, ?network_log_format: ::Symbol, ?chrome_args: untyped, ?emulate_media: ::Hash[untyped, untyped]) -> void
69
54
 
70
55
  def launch: () -> untyped
71
56
 
@@ -10,7 +10,7 @@ module Bidi2pdf
10
10
 
11
11
  KNOWN_TOP_LEVEL_KEYS: ::Array["version" | "source" | "browser" | "headers" | "cookies" | "auth" | "actions" | "assert" | "print" | "output"]
12
12
 
13
- KNOWN_ACTIONS: ::Array["wait_for" | "click" | "evaluate" | "inject_script" | "inject_style" | "set_viewport" | "wait_network_idle"]
13
+ KNOWN_ACTIONS: ::Array["wait_for" | "click" | "evaluate" | "inject_script" | "inject_style" | "set_viewport" | "wait_network_idle" | "emulate_media"]
14
14
 
15
15
  PAGE_ASSERTIONS: ::Array["selector_exists" | "text_present" | "no_console_errors" | "no_network_failures" | "fonts_loaded"]
16
16
 
@@ -30,7 +30,7 @@ module Bidi2pdf
30
30
 
31
31
  RECIPE_SOURCE: { "description" => "exactly one of url, file, stdin", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["url"], "properties" => { "url" => { "type" => "string" } }, "additionalProperties" => false } | { "type" => "object", "required" => ::Array["file"], "properties" => { "file" => { "type" => "string" } }, "additionalProperties" => false } | { "type" => "object", "required" => ::Array["stdin"], "properties" => { "stdin" => { "const" => true } }, "additionalProperties" => false }] }
32
32
 
33
- RECIPE_ACTIONS: { "description" => "one of the 7 known actions - a thin dispatch over BrowserTab's own methods, see README's Agent and Automation Usage section", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["wait_for"], "additionalProperties" => false, "properties" => { "wait_for" => { "type" => "object", "description" => "exactly one of selector, paged_js, script; timeout is always optional", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["selector"], "additionalProperties" => false, "properties" => { "selector" => { "type" => "string" }, "timeout" => { "type" => "number" } } } | { "type" => "object", "required" => ::Array["paged_js"], "additionalProperties" => false, "properties" => { "paged_js" => { "type" => "boolean" }, "timeout" => { "type" => "number" } } } | { "type" => "object", "required" => ::Array["script"], "additionalProperties" => false, "properties" => { "script" => { "type" => "string" }, "timeout" => { "type" => "number" } } }] } } } | { "type" => "object", "required" => ::Array["click"], "additionalProperties" => false, "properties" => { "click" => { "type" => "object", "required" => ::Array["selector"], "additionalProperties" => false, "properties" => { "selector" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["evaluate"], "additionalProperties" => false, "properties" => { "evaluate" => { "type" => "object", "required" => ::Array["script"], "additionalProperties" => false, "properties" => { "script" => { "type" => "string" }, "assign" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["inject_script"], "additionalProperties" => false, "properties" => { "inject_script" => { "type" => "object", "additionalProperties" => false, "properties" => { "url" => { "type" => "string" }, "content" => { "type" => "string" }, "id" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["inject_style"], "additionalProperties" => false, "properties" => { "inject_style" => { "type" => "object", "additionalProperties" => false, "properties" => { "url" => { "type" => "string" }, "content" => { "type" => "string" }, "id" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["set_viewport"], "additionalProperties" => false, "properties" => { "set_viewport" => { "type" => "object", "required" => ::Array["width" | "height"], "additionalProperties" => false, "properties" => { "width" => { "type" => "number" }, "height" => { "type" => "number" }, "device_pixel_ratio" => { "type" => "number" } } } } } | { "type" => "object", "required" => ::Array["wait_network_idle"], "additionalProperties" => false, "properties" => { "wait_network_idle" => { "type" => "object", "additionalProperties" => false, "properties" => { "timeout" => { "type" => "number" } } } } }] }
33
+ RECIPE_ACTIONS: { "description" => "one of the 8 known actions - a thin dispatch over BrowserTab's own methods, see README's Agent and Automation Usage section", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["wait_for"], "additionalProperties" => false, "properties" => { "wait_for" => { "type" => "object", "description" => "exactly one of selector, paged_js, script; timeout is always optional", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["selector"], "additionalProperties" => false, "properties" => { "selector" => { "type" => "string" }, "timeout" => { "type" => "number" } } } | { "type" => "object", "required" => ::Array["paged_js"], "additionalProperties" => false, "properties" => { "paged_js" => { "type" => "boolean" }, "timeout" => { "type" => "number" } } } | { "type" => "object", "required" => ::Array["script"], "additionalProperties" => false, "properties" => { "script" => { "type" => "string" }, "timeout" => { "type" => "number" } } }] } } } | { "type" => "object", "required" => ::Array["click"], "additionalProperties" => false, "properties" => { "click" => { "type" => "object", "required" => ::Array["selector"], "additionalProperties" => false, "properties" => { "selector" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["evaluate"], "additionalProperties" => false, "properties" => { "evaluate" => { "type" => "object", "required" => ::Array["script"], "additionalProperties" => false, "properties" => { "script" => { "type" => "string" }, "assign" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["inject_script"], "additionalProperties" => false, "properties" => { "inject_script" => { "type" => "object", "additionalProperties" => false, "properties" => { "url" => { "type" => "string" }, "content" => { "type" => "string" }, "id" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["inject_style"], "additionalProperties" => false, "properties" => { "inject_style" => { "type" => "object", "additionalProperties" => false, "properties" => { "url" => { "type" => "string" }, "content" => { "type" => "string" }, "id" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["set_viewport"], "additionalProperties" => false, "properties" => { "set_viewport" => { "type" => "object", "required" => ::Array["width" | "height"], "additionalProperties" => false, "properties" => { "width" => { "type" => "number" }, "height" => { "type" => "number" }, "device_pixel_ratio" => { "type" => "number" } } } } } | { "type" => "object", "required" => ::Array["wait_network_idle"], "additionalProperties" => false, "properties" => { "wait_network_idle" => { "type" => "object", "additionalProperties" => false, "properties" => { "timeout" => { "type" => "number" } } } } } | { "type" => "object", "required" => ::Array["emulate_media"], "additionalProperties" => false, "properties" => { "emulate_media" => { "type" => "object", "additionalProperties" => false, "description" => "the CSS media type and media features the page is shown and printed with, from here on; media: screen prints the screen styles; on a narrow print page (layout width = page width minus the side margins, 9.525 cm without side margins = 360 CSS px) the mobile layout", "properties" => untyped } } }] }
34
34
 
35
35
  RECIPE_ASSERT: { "description" => "one of the 8 known assertions. The presence-only ones (no_console_errors, no_network_failures, fonts_loaded, pdf_not_blank) never have their value read at run time - Recipe::Runner only checks the key is there - so they must be written as `true`; `false` or any other value is rejected as misleading rather than silently ignored. The last 3 assertions also require the pdf-reader gem at --validate time - a runtime fact this schema cannot express, see the top-level description", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["selector_exists"], "additionalProperties" => false, "properties" => { "selector_exists" => { "type" => "object", "required" => ::Array["selector"], "additionalProperties" => false, "properties" => { "selector" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["text_present"], "additionalProperties" => false, "properties" => { "text_present" => { "type" => "object", "required" => ::Array["text"], "additionalProperties" => false, "properties" => { "text" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["no_console_errors"], "additionalProperties" => false, "properties" => { "no_console_errors" => { "const" => true } } } | { "type" => "object", "required" => ::Array["no_network_failures"], "additionalProperties" => false, "properties" => { "no_network_failures" => { "const" => true } } } | { "type" => "object", "required" => ::Array["fonts_loaded"], "additionalProperties" => false, "properties" => { "fonts_loaded" => { "const" => true } } } | { "type" => "object", "required" => ::Array["page_count"], "additionalProperties" => false, "properties" => { "page_count" => { "oneOf" => ::Array[{ "type" => "integer" } | { "type" => "object", "properties" => { "min" => { "type" => "integer" }, "max" => { "type" => "integer" } }, "additionalProperties" => false }] } } } | { "type" => "object", "required" => ::Array["pdf_text_present"], "additionalProperties" => false, "properties" => { "pdf_text_present" => { "type" => "object", "required" => ::Array["text"], "additionalProperties" => false, "properties" => { "text" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["pdf_not_blank"], "additionalProperties" => false, "properties" => { "pdf_not_blank" => { "const" => true } } }] }
36
36
 
@@ -31,6 +31,8 @@ module Bidi2pdf
31
31
  # @param [Boolean] wait_network_idle Whether to wait for the network to become idle. Defaults to false.
32
32
  # @param [Hash] print_options Options for printing the page. Defaults to an empty hash.
33
33
  # @param [Symbol] network_log_format The format for network logs. Defaults to :console.
34
+ # @param [Hash] emulate_media Media type and features for BrowserTab#emulate_media (media:,
35
+ # color_scheme:, reduced_motion:), applied before navigating. Defaults to none.
34
36
  class SessionRunner
35
37
  @session: untyped
36
38
 
@@ -54,6 +56,8 @@ module Bidi2pdf
54
56
 
55
57
  @network_log_format: untyped
56
58
 
59
+ @emulate_media: untyped
60
+
57
61
  @window: untyped
58
62
 
59
63
  @tab: untyped
@@ -63,7 +67,7 @@ module Bidi2pdf
63
67
  @uri: untyped
64
68
 
65
69
  # rubocop: disable Metrics/ParameterLists
66
- def initialize: (session: untyped, url: untyped, inputfile: untyped, output: untyped, ?cookies: ::Hash[untyped, untyped], ?headers: ::Hash[untyped, untyped], ?auth: ::Hash[untyped, untyped], ?wait_window_loaded: bool, ?wait_network_idle: bool, ?print_options: ::Hash[untyped, untyped], ?network_log_format: ::Symbol) -> void
70
+ def initialize: (session: untyped, url: untyped, inputfile: untyped, output: untyped, ?cookies: ::Hash[untyped, untyped], ?headers: ::Hash[untyped, untyped], ?auth: ::Hash[untyped, untyped], ?wait_window_loaded: bool, ?wait_network_idle: bool, ?print_options: ::Hash[untyped, untyped], ?network_log_format: ::Symbol, ?emulate_media: ::Hash[untyped, untyped]) -> void
67
71
 
68
72
  def run: () -> untyped
69
73
 
@@ -1,3 +1,3 @@
1
1
  module Bidi2pdf
2
- VERSION: "0.1.22"
2
+ VERSION: "0.1.23"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bidi2pdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.22
4
+ version: 0.1.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dieter S.