bidi2pdf-rails 0.0.1.pre.alpha → 0.1.1

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.idea/bidi2pdf-rails.iml +68 -84
  3. data/.rubocop.yml +14 -0
  4. data/CHANGELOG.md +54 -0
  5. data/README.md +118 -28
  6. data/Rakefile +2 -0
  7. data/cliff.toml +126 -0
  8. data/lib/bidi2pdf_rails/browser_console_log_subscriber.rb +24 -0
  9. data/lib/bidi2pdf_rails/chromedriver_manager_singleton.rb +11 -11
  10. data/lib/bidi2pdf_rails/config.rb +137 -0
  11. data/lib/bidi2pdf_rails/configurable.rb +106 -0
  12. data/lib/bidi2pdf_rails/main_log_subscriber.rb +33 -0
  13. data/lib/bidi2pdf_rails/network_log_subscriber.rb +20 -0
  14. data/lib/bidi2pdf_rails/railtie.rb +13 -45
  15. data/lib/bidi2pdf_rails/services/html_renderer.rb +33 -0
  16. data/lib/bidi2pdf_rails/services/html_to_pdf_converter.rb +37 -0
  17. data/lib/bidi2pdf_rails/services/pdf_browser_session.rb +38 -0
  18. data/lib/bidi2pdf_rails/services/pdf_injection.rb +31 -0
  19. data/lib/bidi2pdf_rails/services/pdf_renderer.rb +94 -0
  20. data/lib/bidi2pdf_rails/services/url_to_pdf_converter.rb +91 -0
  21. data/lib/bidi2pdf_rails/version.rb +1 -1
  22. data/lib/bidi2pdf_rails.rb +41 -58
  23. data/lib/generators/bidi2pdf_rails/USAGE +12 -4
  24. data/lib/generators/bidi2pdf_rails/initializer_generator.rb +136 -30
  25. data/lib/generators/bidi2pdf_rails/templates/bidi2pdf_rails.rb.tt +25 -79
  26. data/spec/acceptance/user_can_download_report_pdf_spec.rb +133 -0
  27. data/spec/acceptance/user_can_generate_pdf_from_protected_remote_url_spec.rb +173 -0
  28. data/spec/acceptance/user_can_inject_css_before_pdf_printing_spec.rb +132 -0
  29. data/spec/acceptance/user_can_inject_js_before_pdf_printing_spec.rb +158 -0
  30. data/spec/dummy/app/assets/javascripts/simple.js +12 -0
  31. data/spec/dummy/app/assets/stylesheets/simple.css +3 -0
  32. data/spec/dummy/app/controllers/reports_controller.rb +47 -0
  33. data/spec/dummy/app/controllers/secure_controller.rb +52 -0
  34. data/spec/dummy/app/views/layouts/simple.html.erb +17 -0
  35. data/spec/dummy/app/views/reports/simple.html.erb +6 -0
  36. data/spec/dummy/app/views/secure/show.html.erb +10 -0
  37. data/spec/dummy/config/environments/production.rb +1 -1
  38. data/spec/dummy/config/initializers/bidi2pdf_rails.rb +72 -54
  39. data/spec/dummy/config/initializers/cors.rb +1 -1
  40. data/spec/dummy/config/routes.rb +14 -0
  41. data/spec/dummy/log/development.log +18331 -158
  42. data/spec/dummy/log/test.log +87874 -0
  43. data/spec/dummy/tmp/pids/server.pid +1 -1
  44. data/spec/integration/generators/bidi2pdf_rails/initializer_generator_spec.rb +64 -0
  45. data/spec/rails_helper.rb +8 -1
  46. data/spec/spec_helper.rb +47 -5
  47. data/spec/support/default_dirs_helper.rb +32 -0
  48. data/spec/support/pdf_helper.rb +12 -0
  49. data/spec/support/render_setting_helpers.rb +47 -0
  50. data/spec/support/request_server_bootstrap.rb +44 -0
  51. data/spec/{bidi2pdf_rails → unit/bidi2pdf_rails}/bidi2pdf_rails_spec.rb +1 -1
  52. data/spec/unit/bidi2pdf_rails/configurable/base_nested_config_spec.rb +133 -0
  53. data/tasks/changelog.rake +29 -0
  54. data/tasks/coverage.rake +23 -0
  55. metadata +108 -27
  56. data/lib/bidi2pdf_rails/log_subscriber.rb +0 -13
  57. data/spec/dummy/spec/helpers/reports_helper_spec.rb +0 -15
  58. data/spec/dummy/spec/requests/reports_spec.rb +0 -10
  59. data/spec/dummy/spec/views/reports/show.html.erb_spec.rb +0 -5
  60. data/spec/generator/bidie2pdf_rails_initializer_generator_spec.rb +0 -5
  61. data/spec/generator/initializer_generator_spec.rb +0 -5
  62. data/spec/requests/reports_spec.rb +0 -17
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdfRails
4
+ module Services
5
+ module PdfBrowserSession
6
+ def run_browser_session
7
+ thread = Thread.new do
8
+ Rails.application.executor.wrap do
9
+ browser = ChromedriverManagerSingleton.session.browser
10
+ context = browser.create_user_context
11
+ window = context.create_browser_window
12
+ tab = window.create_browser_tab
13
+
14
+ begin
15
+ prepare_tab(tab)
16
+ tab.print(print_options: @print_options)
17
+ ensure
18
+ tab&.close
19
+ window&.close
20
+ context&.close
21
+ end
22
+ end
23
+ end
24
+
25
+ pdf_data = thread.value
26
+
27
+ Base64.decode64(pdf_data)
28
+ end
29
+
30
+ private
31
+
32
+ def wait_for_tab(tab)
33
+ tab.wait_until_network_idle if @wait_for_network_idle
34
+ tab.wait_until_page_loaded(check_script: @wait_for_page_check_script) if @wait_for_page_loaded
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdfRails
4
+ module Services
5
+ module PdfInjection
6
+ def inject_custom_css(tab, custom_css, custom_css_url)
7
+ return unless custom_css || custom_css_url
8
+
9
+ if custom_css
10
+ tab.inject_style(url: nil, content: custom_css, id: nil)
11
+ end
12
+
13
+ if custom_css_url
14
+ tab.inject_style(url: custom_css_url, content: nil, id: nil)
15
+ end
16
+ end
17
+
18
+ def inject_custom_js(tab, custom_js, custom_js_url)
19
+ return unless custom_js || custom_js_url
20
+
21
+ if custom_js
22
+ tab.inject_script(url: nil, content: custom_js, id: nil)
23
+ end
24
+
25
+ if custom_js_url
26
+ tab.inject_script(url: custom_js_url, content: nil, id: nil)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdfRails
4
+ module Services
5
+ # PdfRenderer is responsible for rendering a Rails view into a PDF using headless Chrome.
6
+ #
7
+ # It accepts a mix of HTML rendering options (passed to `render_to_string`) and PDF generation options.
8
+ #
9
+ # @example Basic usage
10
+ # renderer = Bidi2pdfRails::Services::PdfRenderer.new("invoice", { template: "invoices/show" }, controller)
11
+ # renderer.render_pdf
12
+ #
13
+ # @param filename [String] the base filename for the generated PDF (used for download naming)
14
+ # @param options [Hash] rendering options including both HTML and PDF-specific options
15
+ # @option options [String] :template The template to render (or other render_to_string keys like :partial, :layout, etc.)
16
+ # @option options [String] :asset_host Optional asset host override
17
+ # @option options [Hash] :print_options Options passed to the Chrome PDF print API
18
+ # @option options [Boolean] :wait_for_network_idle Wait for network to go idle before generating PDF (default: config default)
19
+ # @option options [Boolean] :wait_for_page_loaded Wait for page load event before generating PDF (default: config default)
20
+ # @option options [String] :wait_for_page_check_script JavaScript condition to wait for before generating PDF
21
+ #
22
+ class PdfRenderer
23
+ PDF_OPTIONS = %i[
24
+ print_options
25
+ asset_host
26
+ wait_for_network_idle
27
+ wait_for_page_loaded
28
+ wait_for_page_check_script
29
+ html
30
+ ].freeze
31
+
32
+ PRINT_URL_OPTIONS = %i[
33
+ url
34
+ auth
35
+ headers
36
+ cookies
37
+ ].freeze
38
+
39
+ def initialize(filename, options, controller)
40
+ @filename = filename
41
+ @pdf_options = options.slice(*PDF_OPTIONS)
42
+ @print_url_options = options.slice(*PRINT_URL_OPTIONS)
43
+ @html_options = options.except(*(PDF_OPTIONS + PRINT_URL_OPTIONS))
44
+ @controller = controller
45
+ end
46
+
47
+ def render_pdf
48
+ ActiveSupport::Notifications.instrument("handle_printing.bidi2pdf_rails") do
49
+ print_options = Bidi2pdfRails.config.pdf_settings_for_bidi_cmd(@pdf_options[:print_options] || {})
50
+ wait_for_network_idle = @pdf_options.fetch(:wait_for_network_idle, Bidi2pdfRails.config.general_options.wait_for_network_idle_value)
51
+ wait_for_page_loaded = @pdf_options.fetch(:wait_for_page_loaded, Bidi2pdfRails.config.general_options.wait_for_page_loaded_value)
52
+ wait_for_page_check_script = @pdf_options.fetch(:wait_for_page_check_script, Bidi2pdfRails.config.general_options.wait_for_page_check_script_value)
53
+ custom_css = @pdf_options.fetch(:custom_css, Bidi2pdfRails.config.pdf_settings.custom_css_value(@controller))
54
+ custom_css_url = @pdf_options.fetch(:custom_css_url, Bidi2pdfRails.config.pdf_settings.custom_css_url_value(@controller))
55
+ custom_js = @pdf_options.fetch(:custom_js, Bidi2pdfRails.config.pdf_settings.custom_js_value(@controller))
56
+ custom_js_url = @pdf_options.fetch(:custom_js_url, Bidi2pdfRails.config.pdf_settings.custom_js_url_value(@controller))
57
+
58
+ if @print_url_options[:url]
59
+ headers = @print_url_options[:headers] || Bidi2pdfRails.config.render_remote_settings.headers_value(@controller)
60
+ cookies = @print_url_options[:cookies] || Bidi2pdfRails.config.render_remote_settings.cookies_value(@controller)
61
+ auth = @print_url_options[:auth] || { username: Bidi2pdfRails.config.render_remote_settings.basic_auth_user_value(@controller), password: Bidi2pdfRails.config.render_remote_settings.basic_auth_pass_value(@controller) }
62
+
63
+ UrlToPdfConverter.new(@print_url_options[:url],
64
+ headers: headers,
65
+ cookies: cookies,
66
+ auth: auth,
67
+ print_options: print_options,
68
+ wait_for_network_idle: wait_for_network_idle,
69
+ wait_for_page_loaded: wait_for_page_loaded,
70
+ wait_for_page_check_script: wait_for_page_check_script,
71
+ custom_css: custom_css,
72
+ custom_css_url: custom_css_url,
73
+ custom_js: custom_js,
74
+ custom_js_url: custom_js_url,
75
+ ).generate
76
+ else
77
+ html = @html_options.fetch(:inline, HtmlRenderer.new(@controller, @html_options).render)
78
+
79
+ HtmlToPdfConverter.new(html,
80
+ print_options: print_options,
81
+ wait_for_network_idle: wait_for_network_idle,
82
+ wait_for_page_loaded: wait_for_page_loaded,
83
+ wait_for_page_check_script: wait_for_page_check_script,
84
+ custom_css: custom_css,
85
+ custom_css_url: custom_css_url,
86
+ custom_js: custom_js,
87
+ custom_js_url: custom_js_url,
88
+ ).generate
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdfRails
4
+ module Services
5
+ class UrlToPdfConverter
6
+ include PdfBrowserSession
7
+ include PdfInjection
8
+
9
+ def initialize(url, headers: {}, cookies: {}, auth: {}, print_options: {}, wait_for_network_idle: true, wait_for_page_loaded: true, wait_for_page_check_script: nil, custom_css: nil, custom_css_url: nil, custom_js: nil, custom_js_url: nil)
10
+ @url = url
11
+ @headers = headers || {}
12
+ @cookies = cookies || {}
13
+ @auth = auth || {}
14
+ @print_options = print_options
15
+ @wait_for_network_idle = wait_for_network_idle
16
+ @wait_for_page_loaded = wait_for_page_loaded
17
+ @wait_for_page_check_script = wait_for_page_check_script
18
+ @custom_css = custom_css
19
+ @custom_css_url = custom_css_url
20
+ @custom_js = custom_js
21
+ @custom_js_url = custom_js_url
22
+ end
23
+
24
+ def generate
25
+ run_browser_session
26
+ end
27
+
28
+ private
29
+
30
+ def prepare_tab(tab)
31
+ add_headers(tab)
32
+ add_basic_auth(tab)
33
+ add_cookies(tab)
34
+
35
+ tab.navigate_to(@url)
36
+
37
+ inject_custom_css(tab, @custom_css, @custom_css_url)
38
+ inject_custom_js(tab, @custom_js, @custom_js_url)
39
+
40
+ wait_for_tab(tab)
41
+ end
42
+
43
+ def add_cookies(tab)
44
+ @cookies.each do |name, value|
45
+ tab.set_cookie(
46
+ name: name,
47
+ value: value,
48
+ domain: domain,
49
+ secure: uri.scheme == "https"
50
+ )
51
+ end
52
+ end
53
+
54
+ def add_headers(tab)
55
+ @headers.each do |name, value|
56
+ tab.add_headers(
57
+ url_patterns: url_patterns,
58
+ headers: [{ name: name, value: value }]
59
+ )
60
+ end
61
+ end
62
+
63
+ def add_basic_auth(tab)
64
+ return unless @auth[:username] && @auth[:password]
65
+
66
+ tab.basic_auth(
67
+ url_patterns: url_patterns,
68
+ username: @auth[:username],
69
+ password: @auth[:password]
70
+ )
71
+ end
72
+
73
+ def uri
74
+ @uri ||= URI(@url)
75
+ end
76
+
77
+ def domain
78
+ uri.host
79
+ end
80
+
81
+ def url_patterns
82
+ [{
83
+ type: "pattern",
84
+ protocol: uri.scheme,
85
+ hostname: uri.host,
86
+ port: uri.port.to_s
87
+ }]
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,3 +1,3 @@
1
1
  module Bidi2pdfRails
2
- VERSION = "0.0.1.pre.alpha"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -1,87 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bidi2pdf"
4
- require "bidi2pdf_rails/version"
5
- require "bidi2pdf_rails/railtie"
6
- require "bidi2pdf_rails/chromedriver_manager_singleton"
7
- require "bidi2pdf_rails/log_subscriber"
4
+ require "bidi2pdf/bidi/commands/print_parameters_validator"
5
+ require_relative "bidi2pdf_rails/version"
6
+ require_relative "bidi2pdf_rails/config"
7
+ require_relative "bidi2pdf_rails/railtie"
8
+ require_relative "bidi2pdf_rails/chromedriver_manager_singleton"
9
+ require_relative "bidi2pdf_rails/main_log_subscriber"
10
+ require_relative "bidi2pdf_rails/browser_console_log_subscriber"
11
+ require_relative "bidi2pdf_rails/network_log_subscriber"
8
12
 
9
13
  module Bidi2pdfRails
10
14
  class << self
11
- attr_accessor :logger, :notification_service, :verbosity, :default_timeout, :log_network_events, :log_browser_console, :remote_browser_url, :cookies,
12
- :headers, :auth, :headless, :chromedriver_port, :chrome_session_args,
13
- :proxy_addr, :proxy_port, :proxy_user, :proxy_pass, :install_dir,
14
- :viewport_width, :viewport_height,
15
- :pdf_margin_top, :pdf_margin_bottom, :pdf_margin_left, :pdf_margin_right,
16
- :pdf_scale, :pdf_print_background, :pdf_format, :pdf_orientation, :verbosity
15
+ def config
16
+ Config.instance
17
+ end
17
18
 
18
19
  # Allow configuration through a block
19
20
  def configure
20
- yield self if block_given?
21
+ yield config if block_given?
21
22
 
22
- apply_bidi2pdf_config
23
- end
23
+ config.validate_print_options!
24
24
 
25
- def apply_bidi2pdf_config
26
- # Allow configuration through a block
27
- Bidi2pdf.configure do |config|
28
- config.notification_service = notification_service
25
+ apply_bidi2pdf_config config
26
+ end
29
27
 
30
- config.logger = logger.tagged("bidi2pdf")
31
- config.default_timeout = default_timeout
28
+ def apply_bidi2pdf_config(config = Config.instance)
29
+ Bidi2pdf.configure do |bidi2pdf_config|
30
+ bidi2pdf_config.notification_service = config.general_options.notification_service_value
32
31
 
33
- if log_network_events
34
- config.network_events_logger = logger.tagged("bidi2pdf", "network")
35
- else
36
- config.network_events_logger = Logger.new(nil) # Disable network events logging
37
- end
32
+ bidi2pdf_config.logger = config.general_options.logger_value&.tagged("bidi2pdf")
33
+ bidi2pdf_config.default_timeout = config.general_options.default_timeout_value
38
34
 
39
- if log_browser_console
40
- config.browser_console_logger = logger.tagged("bidi2pdf", "browser_console")
41
- else
42
- config.browser_console_logger = Logger.new(nil) # Disable browser console logging
43
- end
35
+ bidi2pdf_config.network_events_logger = Logger.new(nil)
36
+ bidi2pdf_config.browser_console_logger = Logger.new(nil)
44
37
 
45
- config.enable_default_logging_subscriber = false
38
+ bidi2pdf_config.enable_default_logging_subscriber = false
46
39
  end
47
40
 
48
- Chromedriver::Binary.configure do |config|
49
- config.logger = logger
41
+ Chromedriver::Binary.configure do |chromedriver_config|
42
+ chromedriver_config.logger = config.general_options.logger_value&.tagged("bidi2pdf")
50
43
 
51
- config.proxy_addr = proxy_addr
52
- config.proxy_port = proxy_port
53
- config.proxy_user = proxy_user
54
- config.proxy_pass = proxy_pass
44
+ chromedriver_config.proxy_addr = config.proxy_settings.addr_value
45
+ chromedriver_config.proxy_port = config.proxy_settings.port_value
46
+ chromedriver_config.proxy_user = config.proxy_settings.user_value
47
+ chromedriver_config.proxy_pass = config.proxy_settings.pass_value
55
48
 
56
- config.install_dir = install_dir
49
+ chromedriver_config.install_dir = config.chromedriver_settings.install_dir_value
57
50
  end
58
51
  end
59
52
 
60
- def reset_to_defaults!
61
- @logger = nil
62
- @default_timeout = 10
63
- @log_network_events = false
64
- @log_browser_console = true
65
- @verbosity = 0
66
- @remote_browser_url = nil
67
- @cookies = {}
68
- @headers = {}
69
- @auth = nil
70
- @headless = !Rails.env.development?
71
- @chromedriver_port = 0
72
- @chrome_session_args = Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS
73
-
74
- @proxy_addr = nil
75
- @proxy_port = nil
76
- @proxy_user = nil
77
- @proxy_pass = nil
78
- @install_dir = nil
53
+ def use_local_chromedriver?
54
+ !use_remote_browser?
55
+ end
56
+
57
+ def use_remote_browser?
58
+ config.render_remote_settings.browser_url_value.present?
79
59
  end
80
60
 
81
61
  def logger
82
- @logger ||= Rails.logger # lazy load the logger, after it exists
62
+ config.general_options.logger_value&.tagged("bidi2pdf-rails")
83
63
  end
84
64
  end
85
65
 
86
- self.reset_to_defaults!
66
+ that = self
67
+ ActiveSupport.on_load(:action_controller_base, run_once: true) do
68
+ that.configure # apply the default configuration at load time
69
+ end
87
70
  end
@@ -1,8 +1,16 @@
1
1
  Description:
2
- Explain the generator
2
+ Generates a configuration file for Bidi2pdfRails in config/initializers/bidi2pdf_rails.rb
3
+ and adds development environment settings.
3
4
 
4
5
  Example:
5
- bin/rails generate initializer Thing
6
+ bin/rails generate bidi2pdf_rails:initializer
6
7
 
7
- This will create:
8
- what/will/it/create
8
+ # With options
9
+ bin/rails generate bidi2pdf_rails:initializer --quiet # Skip interactive prompts
10
+ bin/rails generate bidi2pdf_rails:initializer --force # Overwrite existing config
11
+
12
+ This will create:
13
+ config/initializers/bidi2pdf_rails.rb
14
+
15
+ And add settings to:
16
+ config/environments/development.rb
@@ -1,43 +1,72 @@
1
1
  module Bidi2pdfRails
2
2
  class InitializerGenerator < Rails::Generators::Base
3
3
  source_root File.expand_path("templates", __dir__)
4
- desc "Generates a config/initializers/bidi2pdf.rb config file"
5
4
 
6
- class_option :force, type: :boolean, default: false, desc: "Overwrite existing file"
7
- class_option :quiet, type: :boolean, default: false, desc: "Skip prompts"
8
-
9
- def ask_questions
10
- return if options[:quiet]
5
+ def self.infer_option_type(default)
6
+ case default
7
+ when true, false then :boolean
8
+ when Numeric then :numeric
9
+ when Hash then :hash
10
+ when Array then :array
11
+ when Proc then :string
12
+ else :string
13
+ end
14
+ end
11
15
 
12
- @log_network_events = yes?("Log network traffic? (y/n)", :green)
13
- @log_browser_console = yes?("Log browser console output? (y/n)", :green)
14
- @headless = yes?("Run Chrome in headless mode? (y/n)", :green)
15
- @proxy = yes?("Use a proxy server? (y/n)", :green)
16
+ def self.normalize_group(key)
17
+ key.to_s.sub(/(_settings|_options)$/, "")
18
+ end
16
19
 
17
- if @proxy
18
- @proxy_addr = ask("Proxy address (e.g., 127.0.0.1):", :yellow)
19
- @proxy_port = ask("Proxy port (e.g., 8080):", :yellow)
20
+ def self.with_group_config
21
+ that = self
22
+ Bidi2pdfRails::Config::CONFIG_OPTIONS.each_pair do |group_key, group_config|
23
+ group_prefix = that.normalize_group(group_key)
24
+ yield group_key, group_prefix, group_config
20
25
  end
26
+ end
21
27
 
22
- @configure_viewport = yes?("Configure custom viewport? (y/n)", :green)
23
- if @configure_viewport
24
- @viewport_width = ask("Viewport width (e.g., 1920):", :yellow)
25
- @viewport_height = ask("Viewport height (e.g., 1080):", :yellow)
28
+ def self.with_group_option
29
+ with_group_config do |group_key, group_prefix, group_config|
30
+ group_config[:options].each do |opt|
31
+ yield group_key, group_prefix, opt
32
+ end
26
33
  end
34
+ end
35
+
36
+ with_group_option do |_group_key, group_prefix, opt|
37
+ next unless opt[:ask]
38
+
39
+ name = "#{group_prefix}_#{opt[:name]}"
40
+ type = infer_option_type(opt[:default])
41
+ default = opt[:default_as_str] || opt[:default]
42
+
43
+ class_option name,
44
+ type: type,
45
+ desc: opt[:desc],
46
+ default: default,
47
+ enum: opt[:limited_to]
48
+ end
27
49
 
28
- @configure_pdf = yes?("Configure custom PDF settings? (y/n)", :green)
29
- if @configure_pdf
30
- @pdf_orientation = ask("PDF orientation (portrait/landscape):", :yellow)
31
- @pdf_margins = yes?("Configure PDF margins? (y/n)", :green)
32
- if @pdf_margins
33
- @pdf_margin_top = ask("PDF margin top (mm):", :yellow)
34
- @pdf_margin_bottom = ask("PDF margin bottom (mm):", :yellow)
35
- @pdf_margin_left = ask("PDF margin left (mm):", :yellow)
36
- @pdf_margin_right = ask("PDF margin right (mm):", :yellow)
50
+ def ask_questions
51
+ @answers = {}
52
+
53
+ return if options[:quiet]
54
+
55
+ self.class.with_group_config do |_group_key, group_prefix, group_config|
56
+ next unless group_config[:ask]
57
+ if group_config[:ask]
58
+ next unless yes?(group_config[:ask], :green)
59
+ end
60
+
61
+ group_config[:options].select { |opt| opt[:ask] }.each do |opt|
62
+ option_key = "#{group_prefix}_#{opt[:name]}".to_sym
63
+ next if cli_option_set?(option_key, opt)
64
+
65
+ @answers[option_key] = prompt_for_option(opt)
37
66
  end
38
- @pdf_print_background = yes?("Print background graphics? (y/n)", :green)
39
- @pdf_scale = ask("PDF scale (e.g., 1.0):", :yellow)
40
67
  end
68
+
69
+ @answers.compact!
41
70
  end
42
71
 
43
72
  def generate_config
@@ -48,9 +77,10 @@ module Bidi2pdfRails
48
77
 
49
78
  def inject_environment_config
50
79
  env_file = "config/environments/development.rb"
80
+ env_path = File.join(destination_root, env_file)
51
81
 
52
- if File.exist?(env_file)
53
- if File.read(env_file).include?("config.x.bidi2pdf_rails")
82
+ if File.exist?(env_path)
83
+ if File.read(env_path).include?("config.x.bidi2pdf_rails")
54
84
  say_status :skipped, "Bidi2PDF settings already present in #{env_file}", :yellow
55
85
  else
56
86
  content = <<~RUBY.gsub(/^(?=[\w#])/, " ")
@@ -69,5 +99,81 @@ module Bidi2pdfRails
69
99
  say_status :error, "Could not find #{env_file}", :red
70
100
  end
71
101
  end
102
+
103
+ no_commands do
104
+ def cli_option_set?(option_key, opt_def)
105
+ return false unless options.key?(option_key.to_s)
106
+
107
+ default = opt_def[:default_as_str] || opt_def[:default]
108
+ options[option_key.to_s].to_s != default.to_s
109
+ end
110
+
111
+ def prompt_for_option(opt)
112
+ type = self.class.infer_option_type(opt[:default])
113
+
114
+ if type == :boolean
115
+ question = opt[:desc].match?(/y\/n/i) ? opt[:desc] : "#{opt[:desc]} (y/n)?"
116
+ yes?(question, opt[:color])
117
+ else
118
+ opts = {
119
+ default: opt[:default_as_str] || opt[:default],
120
+ limited_to: opt[:limited_to],
121
+ echo: !opt[:secret]
122
+ }.compact
123
+
124
+ ask(opt[:desc], opt[:color], **opts)
125
+ end
126
+ end
127
+
128
+ def normalize_group(key)
129
+ self.class.normalize_group(key)
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def changed_by_user?(full_top_level_name, option_name)
136
+ top_level_name = normalize_group full_top_level_name
137
+
138
+ full_qual_option_name = "#{top_level_name}_#{option_name}".to_sym
139
+ return false unless options.key?(full_qual_option_name.to_s)
140
+
141
+ option_def = Bidi2pdfRails::Config::CONFIG_OPTIONS.dig(full_top_level_name.to_sym, :options).find { |opt| opt[:name] == option_name.to_sym }
142
+ default = option_def[:default_as_str] ? option_def[:default_as_str] : option_def[:default]
143
+
144
+ current_value = get_option_value(top_level_name, option_name)
145
+
146
+ return false if default.nil? && current_value.empty?
147
+ return true if default.nil? && current_value.present?
148
+
149
+ !current_value.to_s.match /#{Regexp.escape(default.to_s)}/
150
+ end
151
+
152
+ def get_option_value(top_level_name, option_name)
153
+ top_level_name = top_level_name.to_s.sub(/(_settings)|(_options)$/, "")
154
+ full_qual_option_name = "#{top_level_name}_#{option_name}".to_sym
155
+
156
+ result = if @answers.key?(full_qual_option_name)
157
+ @answers[full_qual_option_name]
158
+ elsif options.key?(full_qual_option_name.to_s)
159
+ options[full_qual_option_name.to_s]
160
+ else
161
+ nil
162
+ end
163
+
164
+ if result.is_a?(String)
165
+ if result.match(/(^\s+->\s+{)|(.*Rails\.)/) || numeric?(result)
166
+ return result
167
+ end
168
+ '"' + result + '"'
169
+ else
170
+ result
171
+ end
172
+ end
173
+
174
+ def numeric?(string)
175
+ # Float/Integer conversion with rescue catches most numeric formats
176
+ true if Float(string) rescue false
177
+ end
72
178
  end
73
179
  end