require 'grover/version'
require 'grover/utils'
require 'grover/html_preprocessor'
require 'grover/middleware'
require 'grover/configuration'
require 'schmooze'
require 'nokogiri'
#
# Grover interface for converting HTML to PDF
#
class Grover
#
# Processor helper class for calling out to Puppeteer NodeJS library
#
class Processor < Schmooze::Base
dependencies puppeteer: 'puppeteer'
def self.launch_params
ENV['CI'] == 'true' ? "{args: ['--no-sandbox', '--disable-setuid-sandbox']}" : ''
end
method :convert_pdf, Utils.squish(<<-FUNCTION)
async (url, options) => {
let browser;
try {
browser = await puppeteer.launch(#{launch_params});
const page = await browser.newPage();
if (url.match(/^http/i)) {
await page.goto(url, { waitUntil: 'networkidle2' });
} else {
await page.goto(`data:text/html,${url}`, { waitUntil: 'networkidle0' });
}
const emulateMedia = options.emulateMedia; delete options.emulateMedia;
if (emulateMedia) {
await page.emulateMedia(emulateMedia);
}
return await page.pdf(options);
} finally {
if (browser) {
await browser.close();
}
}
}
FUNCTION
end
private_constant :Processor
DISPLAY_URL_PLACEHOLDER = '{{display_url}}'.freeze
DEFAULT_HEADER_TEMPLATE = "
".freeze
DEFAULT_FOOTER_TEMPLATE = Utils.strip_heredoc(<<-HTML).freeze
#{DISPLAY_URL_PLACEHOLDER}
/
HTML
#
# @param [String] url URL of the page to convert
# @param [Hash] options Optional parameters to pass to PDF processor
# see https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagepdfoptions
#
def initialize(url, options = {})
@url = url
@options = Grover.configuration.options.merge options
@root_path = @options.delete :root_path
end
#
# Request URL with provided options and create PDF
#
# @param [String] path Optional path to write the PDF to
# @return [String] The resulting PDF data
#
def to_pdf(path = nil)
result = processor.convert_pdf @url, normalized_options(path)
result['data'].pack('c*')
end
#
# Instance inspection
#
def inspect
format(
'#<%s:0x%p @url="%s">',
class_name: self.class.name,
object_id: object_id,
url: @url
)
end
#
# Configuration for the conversion
#
def self.configuration
@configuration ||= Configuration.new
end
def self.configure
yield(configuration)
end
private
def root_path
@root_path ||= Dir.pwd
end
def processor
Processor.new(root_path)
end
def base_options
options = @options.dup
options.merge! meta_options unless url_source?
options
end
def options_with_template_fix
options = base_options
display_url = options.delete :display_url
if display_url
options[:footer_template] ||= DEFAULT_FOOTER_TEMPLATE
%i[header_template footer_template].each do |key|
next unless options[key].is_a? String
options[key] = options[key].gsub(DISPLAY_URL_PLACEHOLDER, display_url)
end
end
options
end
def normalized_options(path)
options = Utils.normalize_object options_with_template_fix
fix_boolean_options! options
fix_numeric_options! options
options['path'] = path if path
options
end
def fix_boolean_options!(options)
%w[displayHeaderFooter printBackground landscape preferCSSPageSize].each do |opt|
next unless options.key? opt
options[opt] = !FALSE_VALUES.include?(options[opt])
end
end
FALSE_VALUES = [nil, false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].freeze
def fix_numeric_options!(options)
return unless options.key? 'scale'
options['scale'] = options['scale'].to_f
end
#
# Extract out options from meta tags in the source - based on code from PDFKit project
#
def meta_options
meta_opts = {}
meta_tags.each do |meta|
tag_name = meta['name'] && meta['name'][/#{Grover.configuration.meta_tag_prefix}([a-z_-]+)/, 1]
next unless tag_name
Utils.deep_assign meta_opts, tag_name.split('-'), meta['content']
end
meta_opts
end
def meta_tags
Nokogiri::HTML(@url).xpath('//meta')
end
def url_source?
@url.match(/^http/i)
end
end