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
data/cliff.toml ADDED
@@ -0,0 +1,126 @@
1
+ # git-cliff ~ configuration file
2
+ # https://git-cliff.org/docs/configuration
3
+
4
+ [changelog]
5
+ # template for the changelog header
6
+ header = """
7
+ <!-- generated by git-cliff start -->
8
+ # Changelog\n
9
+ All notable changes to this project will be documented in this file.
10
+
11
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
12
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n
13
+ """
14
+ # template for the changelog body
15
+ # https://keats.github.io/tera/docs/#introduction
16
+ body = """
17
+ {%- macro remote_url() -%}
18
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
19
+ {%- endmacro -%}
20
+
21
+ {% if version -%}
22
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
23
+ {% else -%}
24
+ ## [Unreleased]
25
+ {% endif -%}
26
+
27
+ {% for group, commits in commits | group_by(attribute="group") %}
28
+ ### {{ group | upper_first }}
29
+ {%- for commit in commits %}
30
+ - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
31
+ {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
32
+ {% if commit.remote.pr_number %} in \
33
+ [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
34
+ {%- endif -%}
35
+ {% endfor %}
36
+ {% endfor %}
37
+
38
+ {%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
39
+ ## New Contributors
40
+ {%- endif -%}
41
+
42
+ {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
43
+ * @{{ contributor.username }} made their first contribution
44
+ {%- if contributor.pr_number %} in \
45
+ [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
46
+ {%- endif %}
47
+ {%- endfor %}\n
48
+ """
49
+ # template for the changelog footer
50
+ footer = """
51
+ {%- macro remote_url() -%}
52
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
53
+ {%- endmacro -%}
54
+
55
+ {% for release in releases -%}
56
+ {% if release.version -%}
57
+ {% if release.previous.version -%}
58
+ [{{ release.version | trim_start_matches(pat="v") }}]: \
59
+ {{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }}
60
+ {% endif -%}
61
+ {% else -%}
62
+ [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD
63
+ {% endif -%}
64
+ {% endfor %}
65
+ <!-- generated by git-cliff end -->
66
+ """
67
+ # remove the leading and trailing whitespace from the templates
68
+ trim = true
69
+
70
+ [git]
71
+ # parse the commits based on https://www.conventionalcommits.org
72
+ conventional_commits = true
73
+ # filter out the commits that are not conventional
74
+ filter_unconventional = false
75
+ # regex for preprocessing the commit messages
76
+ commit_preprocessors = [
77
+ # remove issue numbers from commits
78
+ { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
79
+ ]
80
+ # regex for parsing and grouping commits
81
+ commit_parsers = [
82
+ # ✅ Features (additions)
83
+ { message = "^feat(?:\\([^)]+\\))?!?:", group = "🚀 Added" },
84
+ { message = "^[aA]dd", group = "🚀 Added" },
85
+ { message = "^[sS]upport", group = "🚀 Added" },
86
+ # ❌ Removals
87
+ { message = "^[rR]emove", group = "🗑️ Removed" },
88
+ { message = "^[dD]elete", group = "🗑️ Removed" },
89
+ # 🐛 Fixes
90
+ { message = "^fix(?:\\([^)]+\\))?!?:", group = "🐛 Fixed" },
91
+ { message = "^[tT]est", group = "🐛 Fixed" },
92
+ { message = "^[fF]ix", group = "🐛 Fixed" },
93
+ # 🎨 Refactors
94
+ { message = "^refactor(?:\\([^)]+\\))?!?:", group = "🎨 Refactored" },
95
+ # ⚡️ Performance
96
+ { message = "^perf(?:\\([^)]+\\))?!?:", group = "⚡️ Performance" },
97
+ # 📝 Docs
98
+ { message = "^docs(?:\\([^)]+\\))?!?:", group = "📝 Docs" },
99
+ # 💄 Style (formatting, whitespace, etc.)
100
+ { message = "^style(?:\\([^)]+\\))?!?:", group = "💄 Style" },
101
+ # 🧪 Tests
102
+ { message = "^test(?:\\([^)]+\\))?!?:", group = "🧪 Tests" },
103
+ # 🔧 Build
104
+ { message = "^build(?:\\([^)]+\\))?!?:", group = "🔧 Build" },
105
+ # 🛠️ CI
106
+ { message = "^ci(?:\\([^)]+\\))?!?:", skip = true },
107
+ # 🧹 Chores (skip)
108
+ { message = "^chore\\(release\\): prepare for", skip = true },
109
+ { message = "^chore\\(deps.*\\)", skip = true },
110
+ { message = "^chore\\(pr\\)", skip = true },
111
+ { message = "^chore\\(pull\\)", skip = true },
112
+ { message = "^chore(?:\\([^)]+\\))?!?:", skip = true },
113
+ { message = "^\\s*chore", skip = true },
114
+ # ⏪ Reverts
115
+ { message = "^revert(?:\\([^)]+\\))?!?:", group = "⏪ Reverted" },
116
+ # 🌀 Catch-all (only if nothing else matched)
117
+ { message = "^.*", group = "🔄 Changed" }
118
+ ]
119
+
120
+
121
+ # filter out the commits that are not matched by commit parsers
122
+ filter_commits = false
123
+ # sort the tags topologically
124
+ topo_order = false
125
+ # sort the commits inside sections by oldest/newest order
126
+ sort_commits = "newest"
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bidi2pdf/bidi/browser_console_logger"
4
+
5
+ module Bidi2pdfRails
6
+ class BrowserConsoleLogSubscriber < ActiveSupport::LogSubscriber
7
+ def browser_console_log_received(event)
8
+ payload = event.payload
9
+ timestamp = Bidi2pdf::Bidi::BrowserConsoleLogger.format_timestamp(payload[:timestamp])
10
+
11
+ logger.tagged("bidi2pdf_rails", "browser_console", timestamp) do |tagged_logger|
12
+ verbose_logger = Bidi2pdf::VerboseLogger.new(tagged_logger, Bidi2pdfRails.config.general_options.verbosity_value)
13
+ Bidi2pdf::Bidi::BrowserConsoleLogger.new(verbose_logger)
14
+ .builder
15
+ .with_level(payload[:level])
16
+ .with_prefix("")
17
+ .with_text(payload[:text])
18
+ .with_args(payload[:args])
19
+ .with_stack_trace(payload[:stack_trace])
20
+ .log_event
21
+ end
22
+ end
23
+ end
24
+ end
@@ -5,28 +5,28 @@ module Bidi2pdfRails
5
5
  class << self
6
6
  attr_reader :manager, :session
7
7
 
8
- def initialize_manager
9
- return unless running_as_server?
8
+ def initialize_manager(force: false)
9
+ return unless running_as_server? || force
10
10
 
11
11
  @mutex ||= Mutex.new
12
12
  @mutex.synchronize do
13
13
  return if @manager && @session
14
14
 
15
- msg = Bidi2pdfRails.remote_browser_url ? "Remote session" : "ChromeDriver manager"
15
+ msg = Bidi2pdfRails.use_remote_browser? ? "Remote session" : "ChromeDriver manager"
16
16
 
17
17
  Bidi2pdfRails.logger.info "Initializing Bidi2pdf #{msg}"
18
18
 
19
- if Bidi2pdfRails.remote_browser_url
19
+ if Bidi2pdfRails.use_remote_browser?
20
20
  @session = Bidi::Session.new(
21
- session_url: Bidi2pdfRails.remote_browser_url,
22
- headless: Bidi2pdfRails.headless,
23
- chrome_args: Bidi2pdfRails.chrome_session_args
21
+ session_url: Bidi2pdfRails.config.render_remote_settings.browser_url_value,
22
+ headless: Bidi2pdfRails.config.general_options.headless_value,
23
+ chrome_args: Bidi2pdfRails.config.general_options.chrome_session_args_value
24
24
  )
25
25
  else
26
26
  @manager = Bidi2pdf::ChromedriverManager.new(
27
- port: Bidi2pdfRails.chromedriver_port,
28
- headless: Bidi2pdfRails.headless,
29
- chrome_args: Bidi2pdfRails.chrome_session_args
27
+ port: Bidi2pdfRails.config.chromedriver_settings.port_value,
28
+ headless: Bidi2pdfRails.config.general_options.headless_value,
29
+ chrome_args: Bidi2pdfRails.config.general_options.chrome_session_args_value
30
30
  )
31
31
  @manager.start
32
32
  @session = @manager.session
@@ -42,7 +42,7 @@ module Bidi2pdfRails
42
42
 
43
43
  @mutex ||= Mutex.new
44
44
  @mutex.synchronize do
45
- msg = Bidi2pdfRails.remote_browser_url ? "Remote session" : "ChromeDriver manager"
45
+ msg = Bidi2pdfRails.use_remote_browser? ? "Remote session" : "ChromeDriver manager"
46
46
  Bidi2pdfRails.logger.info "Shutting down Bidi2pdf #{msg}"
47
47
  @session&.close
48
48
  @manager&.stop
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "configurable"
4
+
5
+ module Bidi2pdfRails
6
+ class Config
7
+ include Bidi2pdfRails::Configurable
8
+
9
+ CONFIG_OPTIONS = {
10
+ general_options: {
11
+ name: "General Options",
12
+ ask: "Configure general options? (y/n)",
13
+ options: [
14
+ { name: :logger, desc: "The logger", default: -> { Rails.logger || ActiveSupport::TaggedLogging.new(Logger.new(nil)) }, default_as_str: "Rails.logger", ask: false, color: :yellow },
15
+ { name: :verbosity, desc: "How verbose to be", default: Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS.keys.sort_by { |k| Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS[k] }.first.to_s, limited_to: Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS.keys.sort_by { |k| Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS[k] }.map(&:to_s), ask: true, color: :yellow },
16
+ { name: :headless, desc: "Run Chrome in headless mode", default: -> { !Rails.env.development? }, default_as_str: "!Rails.env.development?", ask: true, color: :green },
17
+ { name: :wait_for_network_idle, desc: "Wait for network idle", default: true, ask: true, color: :green },
18
+ { name: :wait_for_page_loaded, desc: "Wait for page loaded", default: false, ask: true, color: :green },
19
+ { name: :wait_for_page_check_script, desc: "Wait for page check script", default: nil, ask: false },
20
+ { name: :notification_service, desc: "Notification service", default: -> { ActiveSupport::Notifications }, default_as_str: "-> { ActiveSupport::Notifications }", ask: false },
21
+ { name: :default_timeout, desc: "Default timeout for various Bidi commands", default: 10, ask: true, color: :yellow },
22
+ { name: :chrome_session_args, desc: "Chrome session arguments", default: Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS, ask: false }
23
+ ]
24
+ },
25
+
26
+ chromedriver_settings: {
27
+ name: "Chromedriver Settings (when chromedriver run within your app)",
28
+ ask: "Configure chromedriver settings? (y/n)",
29
+ options: [
30
+ { name: :install_dir, desc: "Chromedriver install directory", default: nil, ask: false, color: :yellow },
31
+ { name: :port, desc: "Chromedriver port", default: 0, ask: true, color: :yellow }
32
+ ]
33
+ },
34
+
35
+ proxy_settings: {
36
+ name: "Proxy Settings",
37
+ ask: "Use a proxy server? (y/n)",
38
+ options: [
39
+ { name: :addr, desc: "Proxy address (e.g., 127.0.0.1)", default: nil, ask: true, color: :yellow },
40
+ { name: :port, desc: "Proxy port (e.g., 8080)", default: nil, ask: true, color: :yellow },
41
+ { name: :user, desc: "Proxy user", default: nil, ask: true, color: :yellow },
42
+ {
43
+ name: :pass,
44
+ desc: "Proxy password",
45
+ default: -> { -> { Rails.application.credentials.dig("bidi2pdf_rails", "proxy_pass") } },
46
+ default_as_str: "-> { Rails.application.credentials.dig('bidi2pdf_rails', 'proxy_pass') }",
47
+ secret: true,
48
+ ask: true,
49
+ color: :yellow
50
+ }
51
+ ]
52
+ },
53
+
54
+ pdf_settings: {
55
+ name: "PDF Settings",
56
+ ask: "Configure custom PDF settings? (y/n)",
57
+ options: [
58
+ { name: :orientation, desc: "PDF orientation (portrait/landscape)", default: "portrait", limited_to: %w[portrait landscape], ask: true },
59
+ { name: :margins, desc: "Configure PDF margins?", default: false, ask: true, color: :green },
60
+ { name: :margin_top, desc: "PDF margin top (cm)", default: 2.5, ask: true, color: :yellow },
61
+ { name: :margin_bottom, desc: "PDF margin bottom (cm)", default: 2, ask: true, color: :yellow },
62
+ { name: :margin_left, desc: "PDF margin left (cm)", default: 2, ask: true, color: :yellow },
63
+ { name: :margin_right, desc: "PDF margin right (cm)", default: 2, ask: true, color: :yellow },
64
+ { name: :page_format, desc: "PDF page format (e.g., A4)", default: nil, limited_to: Bidi2pdf::PAPER_FORMATS_CM.keys.map(&:to_s), ask: true },
65
+ { name: :page_width, desc: "PDF page width (cm, not needed when format is specified)", default: Bidi2pdf::PAPER_FORMATS_CM[:a4][:width], ask: true, color: :yellow },
66
+ { name: :page_height, desc: "PDF page height (cm, not needed when format is specified)", default: Bidi2pdf::PAPER_FORMATS_CM[:a4][:height], ask: true, color: :yellow },
67
+ { name: :print_background, desc: "Print background graphics?", default: true, ask: true, color: :green },
68
+ { name: :scale, desc: "PDF scale (e.g., 1.0)", default: 1.0, ask: true, color: :yellow },
69
+ { name: :shrink_to_fit, desc: "Shrink to fit?", default: false, ask: true, color: :green },
70
+ { name: :custom_js, desc: "Raw JavaScript code to inject before PDF generation (without <script> tags)", default: nil, ask: false, color: :yellow },
71
+ { name: :custom_css, desc: "Raw CSS styles to inject before PDF generation (without <style> tags)", default: nil, ask: false, color: :yellow },
72
+ { name: :custom_js_url, desc: "URL to JavaScript file to load before PDF generation", default: nil, ask: false, color: :yellow },
73
+ { name: :custom_css_url, desc: "URL to CSS file to load before PDF generation", default: nil, ask: false, color: :yellow }
74
+ ]
75
+ },
76
+
77
+ render_remote_settings: {
78
+ name: "Remote URL Settings",
79
+ ask: false,
80
+ options: [
81
+ { name: :browser_url, desc: "Remote browser URL (e.g. http://localhost:3001/sesion)", default: nil, ask: true, color: :yellow },
82
+ { name: :basic_auth_user, desc: "Basic auth user", default: nil },
83
+ { name: :basic_auth_pass, desc: "Basic auth password", default: nil, default_as_str: "-> { Rails.application.credentials.dig('bidi2pdf_rails', 'basic_auth_pass') }", secret: true },
84
+ { name: :headers, desc: "Headers to be send when allong an url", default: {}, default_as_str: '{"X-API-INFO" => "my info"}' },
85
+ { name: :cookies, desc: "Cookies to be send when alling an url", default: {}, default_as_str: '{"session_id" => "my session"}' }
86
+ ]
87
+ }
88
+
89
+ }.freeze
90
+
91
+ configure_with(CONFIG_OPTIONS)
92
+
93
+ def initialize
94
+ reset_to_defaults!
95
+ end
96
+
97
+ def pdf_settings_for_bidi_cmd(overrides = {})
98
+ overrides ||= {}
99
+ opts = {}
100
+
101
+ opts[:background] = pdf_settings.print_background_value
102
+ opts[:orientation] = pdf_settings.orientation_value
103
+ opts[:scale] = pdf_settings.scale_value
104
+ opts[:shrinkToFit] = pdf_settings.shrink_to_fit_value
105
+
106
+ # Margins
107
+ margins = {
108
+ top: pdf_settings.margin_top_value,
109
+ bottom: pdf_settings.margin_bottom_value,
110
+ left: pdf_settings.margin_left_value,
111
+ right: pdf_settings.margin_right_value
112
+ }.compact
113
+
114
+ opts[:margin] = margins unless margins.empty?
115
+
116
+ # Page size
117
+ page = {
118
+ width: pdf_settings.page_width_value,
119
+ height: pdf_settings.page_height_value,
120
+ format: pdf_settings.page_format_value
121
+ }.compact
122
+
123
+ page = overrides[:page].compact if overrides[:page]&.compact.present?
124
+ page = { format: page[:format] } if page[:format]
125
+
126
+ opts[:page] = page unless page.empty?
127
+
128
+ opts.deep_merge(overrides).compact
129
+ end
130
+
131
+ def validate_print_options!
132
+ validator = Bidi2pdf::Bidi::Commands::PrintParametersValidator.new(pdf_settings_for_bidi_cmd)
133
+
134
+ validator.validate!
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Bidi2pdfRails
6
+ module Configurable
7
+ extend ActiveSupport::Concern
8
+ included do
9
+ include Singleton
10
+ end
11
+
12
+ class BaseNestedConfig
13
+ def initialize(option_defs)
14
+ @__options = option_defs
15
+ define_accessors!
16
+ reset_to_defaults!
17
+ end
18
+
19
+ def define_accessors!
20
+ @__options.each do |opt|
21
+ name = opt[:name]
22
+ unless respond_to?(name)
23
+ define_singleton_method(name) { instance_variable_get("@#{name}") }
24
+ define_singleton_method("#{name}=") { |v| instance_variable_set("@#{name}", v) }
25
+
26
+ define_singleton_method("#{name}_value") do |*args|
27
+ value = send("#{name}")
28
+ value.respond_to?(:call) ? value.call(*args) : value
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def reset_to_defaults!
35
+ @__options.each do |opt|
36
+ default = opt[:default]
37
+ value = default
38
+ send("#{opt[:name]}=", value)
39
+ end
40
+ end
41
+
42
+ def config_options
43
+ @__options
44
+ end
45
+
46
+ def configure
47
+ yield self if block_given?
48
+ end
49
+
50
+ def to_h
51
+ @__options.to_h do |opt|
52
+ [opt[:name], send(opt[:name])]
53
+ end
54
+ end
55
+
56
+ def inspect
57
+ "#<#{self.class.name || "ConfigSection"} #{to_h.inspect}>"
58
+ end
59
+ end
60
+
61
+ def configure
62
+ yield self if block_given?
63
+ end
64
+
65
+ class_methods do
66
+ def configure_with(grouped_options)
67
+ @config_groups = grouped_options
68
+
69
+ grouped_options.each do |group_key, group_data|
70
+ group_class = Class.new(BaseNestedConfig)
71
+
72
+ define_method(group_key) do
73
+ ivar = "@#{group_key}"
74
+ instance_variable_get(ivar) || instance_variable_set(ivar, group_class.new(group_data[:options]))
75
+ end
76
+
77
+ define_method("#{group_key}=") do |val|
78
+ raise ArgumentError, "Use individual accessors, not direct assignment to config groups"
79
+ end
80
+ end
81
+
82
+ define_method(:reset_to_defaults!) do
83
+ grouped_options.each_key do |group_key|
84
+ section = send(group_key)
85
+ section.reset_to_defaults!
86
+ end
87
+ end
88
+
89
+ define_method(:config_groups) do
90
+ grouped_options
91
+ end
92
+
93
+ define_method(:to_h) do
94
+ grouped_options.keys.to_h do |group_key|
95
+ section = send(group_key)
96
+ [group_key, section.to_h]
97
+ end
98
+ end
99
+
100
+ define_method(:inspect) do
101
+ "#<#{self.class.name} #{to_h.inspect}>"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdfRails
4
+ class MainLogSubscriber < ActiveSupport::LogSubscriber
5
+ @silenced_events = []
6
+
7
+ class << self
8
+ attr_accessor :silenced_events
9
+
10
+ def silence(event)
11
+ self.silenced_events << event
12
+ end
13
+
14
+ def silenced?(event)
15
+ self.silenced_events.any? { |silenced_event| silenced_event === event }
16
+ end
17
+ end
18
+
19
+ include Bidi2pdf::Notifications::LoggingSubscriberActions
20
+
21
+ def handle_printing(event)
22
+ logger.info "Page rendered and printed: #{event.duration.round(1)}ms"
23
+ end
24
+
25
+ def logger
26
+ Bidi2pdf::VerboseLogger.new(super.tagged("bidi2pdf_rails"), Bidi2pdfRails.config.general_options.verbosity_value)
27
+ end
28
+
29
+ def silenced?(event)
30
+ MainLogSubscriber.silenced?(event) || super(event)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bidi2pdf/bidi/network_event_formatters/network_event_console_formatter"
4
+
5
+ module Bidi2pdfRails
6
+ class NetworkLogSubscriber < ActiveSupport::LogSubscriber
7
+ def network_event_received(event)
8
+ payload = event.payload
9
+
10
+ return if payload[:method] == "network.responseStarted" || payload[:method] == "network.beforeRequestSent"
11
+
12
+ logger.tagged("bidi2pdf_rails", "network") do |tagged_logger|
13
+ verbose_logger = Bidi2pdf::VerboseLogger.new(tagged_logger, Bidi2pdfRails.config.general_options.verbosity_value)
14
+ formatter = Bidi2pdf::Bidi::NetworkEventFormatters::NetworkEventConsoleFormatter.new(logger: verbose_logger)
15
+
16
+ formatter.log [payload[:event]]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rails/railtie"
4
+
5
+ require_relative "services/html_renderer"
6
+ require_relative "services/pdf_renderer"
7
+ require_relative "services/pdf_browser_session"
8
+ require_relative "services/pdf_injection"
9
+ require_relative "services/url_to_pdf_converter"
10
+ require_relative "services/html_to_pdf_converter"
11
+
3
12
  module Bidi2pdfRails
4
13
  class Railtie < ::Rails::Railtie
5
14
  initializer "bidi2pdf_rails.add_mime_type" do
@@ -9,7 +18,8 @@ module Bidi2pdfRails
9
18
  initializer "bidi2pdf_rails.initialize_chromedriver_manager", after: :load_config_initializers do
10
19
  Bidi2pdfRails.apply_bidi2pdf_config
11
20
 
12
- Rails.application.config.after_initialize do
21
+ # defer the initialization of the ChromedriverManager to the ActionController::Base class load event
22
+ ActiveSupport.on_load(:action_controller_base) do
13
23
  ChromedriverManagerSingleton.initialize_manager
14
24
  end
15
25
  end
@@ -23,56 +33,14 @@ module Bidi2pdfRails
23
33
 
24
34
  initializer "bidi2pdf_rails.add_pdf_renderer" do
25
35
  ActionController::Renderers.add :pdf do |filename, options|
26
- pdf_data = ""
27
36
  options = options.dup
28
37
 
29
- original_asset_host = ActionController::Base.asset_host
30
-
31
- if options[:asset_host]
32
- ActionController::Base.asset_host = options[:asset_host]
33
- elsif !Rails.application.config.action_controller.asset_host
34
- # Use request base URL as fallback if no asset host configured
35
- ActionController::Base.asset_host = request.base_url
36
- end
37
-
38
- html = render_to_string(options.merge(formats: [ :html ]))
39
-
40
- Bidi2pdf.default_timeout = 30
41
-
42
- # without a thread, the app will be blocked in dev mode
43
- thread = Thread.new {
44
- Rails.application.executor.wrap do
45
- # subscriptions in other threads will not be inherited
46
- Bidi2pdfRails::LogSubscriber.attach_to "bidi2pdf", inherit_all: true
47
-
48
- browser = ChromedriverManagerSingleton.session.browser
49
- context = browser.create_user_context
50
- window = context.create_browser_window
51
- tab = window.create_browser_tab
38
+ pdf_content = Services::PdfRenderer.new(filename, options, self).render_pdf
52
39
 
53
- tab.render_html_content html
54
-
55
- tab.wait_until_network_idle
56
-
57
- tab.wait_until_page_loaded
58
-
59
- pdf_data = tab.print
60
- ensure
61
- tab&.close
62
- window&.close
63
- context&.close
64
- end
65
- }
66
-
67
- thread.join
68
-
69
- send_data Base64.decode64(pdf_data),
40
+ send_data pdf_content,
70
41
  type: Mime[:pdf],
71
42
  filename: "#{filename}.pdf",
72
43
  disposition: options.fetch(:disposition, "inline")
73
- ensure
74
-
75
- ActionController::Base.asset_host = original_asset_host
76
44
  end
77
45
  end
78
46
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdfRails
4
+ module Services
5
+ # app/services/html_renderer.rb
6
+ class HtmlRenderer
7
+ def initialize(controller, options)
8
+ @controller = controller
9
+ @options = options.dup
10
+ end
11
+
12
+ def render
13
+ with_overridden_asset_host { @controller.render_to_string(@options.merge(formats: [:html])) }
14
+ end
15
+
16
+ private
17
+
18
+ def with_overridden_asset_host
19
+ original_asset_host = ActionController::Base.asset_host
20
+
21
+ if @options[:asset_host]
22
+ ActionController::Base.asset_host = @options[:asset_host]
23
+ elsif !Rails.application.config.action_controller.asset_host
24
+ ActionController::Base.asset_host = @controller.request.base_url
25
+ end
26
+
27
+ yield
28
+ ensure
29
+ ActionController::Base.asset_host = original_asset_host
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdfRails
4
+ module Services
5
+ class HtmlToPdfConverter
6
+ include PdfBrowserSession
7
+ include PdfInjection
8
+
9
+ def initialize(html, 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
+ @html = html
11
+ @print_options = print_options
12
+ @wait_for_network_idle = wait_for_network_idle
13
+ @wait_for_page_loaded = wait_for_page_loaded
14
+ @wait_for_page_check_script = wait_for_page_check_script
15
+ @custom_css = custom_css
16
+ @custom_css_url = custom_css_url
17
+ @custom_js = custom_js
18
+ @custom_js_url = custom_js_url
19
+ end
20
+
21
+ def generate
22
+ run_browser_session
23
+ end
24
+
25
+ private
26
+
27
+ def prepare_tab(tab)
28
+ tab.render_html_content(@html)
29
+
30
+ inject_custom_css(tab, @custom_css, @custom_css_url)
31
+ inject_custom_js(tab, @custom_js, @custom_js_url)
32
+
33
+ wait_for_tab(tab)
34
+ end
35
+ end
36
+ end
37
+ end