require 'set' require 'nokogiri' require 'uri' require 'css_parser' module Roadie # @api private # The Inliner inlines stylesheets to the elements of the DOM. # # Inlining means that {StyleBlock}s and a DOM tree are combined: # a { color: red; } # StyleBlock # # DOM # # becomes # # class Inliner # @param [Array] stylesheets the stylesheets to use in the inlining # @param [Nokogiri::HTML::Document] dom def initialize(stylesheets, dom) @stylesheets = stylesheets @dom = dom end # Start the inlining, mutating the DOM tree. # # @param [true, false] keep_extra_blocks # @return [nil] def inline(keep_extra_blocks = true) style_map, extra_blocks = consume_stylesheets apply_style_map(style_map) add_styles_to_head(extra_blocks) if keep_extra_blocks nil end protected attr_reader :stylesheets, :dom private def consume_stylesheets style_map = StyleMap.new extra_blocks = [] each_style_block do |stylesheet, block| if (elements = selector_elements(stylesheet, block)) style_map.add elements, block.properties else extra_blocks << block end end [style_map, extra_blocks] end def each_style_block stylesheets.each do |stylesheet| stylesheet.blocks.each do |block| yield stylesheet, block end end end def selector_elements(stylesheet, block) block.inlinable? && elements_matching_selector(stylesheet, block.selector) end def apply_style_map(style_map) style_map.each_element { |element, builder| apply_element_style(element, builder) } end def apply_element_style(element, builder) element["style"] = [builder.attribute_string, element["style"]].compact.join(";") end def elements_matching_selector(stylesheet, selector) dom.css(selector.to_s) # There's no way to get a list of supported pseudo selectors, so we're left # with having to rescue errors. # Pseudo selectors that are known to be bad are skipped automatically but # this will catch the rest. rescue Nokogiri::XML::XPath::SyntaxError, Nokogiri::CSS::SyntaxError => error Utils.warn "Cannot inline #{selector.inspect} from \"#{stylesheet.name}\" stylesheet. If this is valid CSS, please report a bug." nil rescue => error Utils.warn "Got error when looking for #{selector.inspect} (from \"#{stylesheet.name}\" stylesheet): #{error}" raise unless error.message.include?('XPath') nil end def add_styles_to_head(blocks) unless blocks.empty? create_style_element(blocks, find_head) end end def find_head dom.at_xpath('html/head') end def create_style_element(style_blocks, head) return unless head element = Nokogiri::XML::Node.new("style", head.document) element.content = style_blocks.join("\n") head.add_child(element) end # @api private # StyleMap is a map between a DOM element and {StyleAttributeBuilder}. Basically, # it's an accumulator for properties, scoped on specific elements. class StyleMap def initialize @map = Hash.new { |hash, key| hash[key] = StyleAttributeBuilder.new } end def add(elements, new_properties) Array(elements).each do |element| new_properties.each do |property| @map[element] << property end end end def each_element(&block) @map.each_pair(&block) end end end end