Created
November 6, 2009 11:01
-
-
Save kivanio/227912 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'nokogiri' | |
require 'open-uri' | |
gem 'maca-fork-csspool' | |
require 'csspool' | |
module InlineStyle | |
module Rack | |
class Middleware | |
# | |
# Options: | |
# +document_root+ | |
# File system path for app's public directory where the stylesheets are to be found, defaults to | |
# env['DOCUMENT_ROOT'] | |
# | |
# +paths+ | |
# Limit processing to the passed absolute paths | |
# Can be an array of strings or regular expressions, a single string or regular expression | |
# If not passed will process output for every path. | |
# Regexps and strings must comence with '/' | |
# | |
# +pseudo+ | |
# If set to true will inline style for pseudo classes according to the W3C specification: | |
# http://www.w3.org/TR/css-style-attr. | |
# Defaults to false and should probably be left like that because at least Safari and Firefox don't seem to | |
# comply with the specification for pseudo class style in the style attribute. | |
# | |
def initialize app, opts = {} | |
@app = app | |
@document_root = opts[:document_root] | |
@pseudo = opts[:pseudo] | |
@paths = /^(?:#{ [*opts[:paths]].join('|') })/ | |
end | |
def call env | |
response = @app.call env | |
return response unless @paths === env['PATH_INFO'] | |
status, headers, content = response | |
response = ::Rack::Response.new '', status, headers | |
body = content.respond_to?(:body) ? content.body : content | |
response.write InlineStyle.process(body, :stylesheets_path => @document_root || env['DOCUMENT_ROOT'], :pseudo => @pseudo) | |
response.finish | |
end | |
end | |
end | |
def self.process html, opts = {} | |
stylesheets_path = opts[:stylesheets_path] || '' | |
pseudo = opts[:pseudo] || false | |
nokogiri_doc_given = Nokogiri::HTML::Document === html | |
html = nokogiri_doc_given ? html : Nokogiri.HTML(html) | |
css = extract_css html, stylesheets_path | |
nodes = {} | |
css.rule_sets.each do |rule_set| | |
rule_set.selectors.each do |selector| | |
css_selector = selector.to_s | |
css_selector = "#{ 'body ' unless /^body/ === css_selector }#{ css_selector.gsub /:.*/, '' }" | |
html.css(css_selector).each do |node| | |
nodes[node] ||= [] | |
nodes[node].push selector | |
next unless node['style'] | |
path = node.css_path | |
path << "##{ node['id'] }" if node['id'] | |
path << ".#{ node['class'].scan(/\S+/).join('.') }" if node['class'] | |
CSSPool.CSS("#{ path }{#{ node['style'] }}").rule_sets.each{ |rule| nodes[node].push *rule.selectors } | |
end | |
end | |
end | |
nodes.each_pair do |node, style| | |
style = style.sort_by{ |sel| "#{ sel.specificity }%03d" % style.index(sel) } | |
sets = style.partition{ |sel| not /:\w+/ === sel.to_s } | |
sets.pop if not pseudo or sets.last.empty? | |
node['style'] = sets.collect do |selectors| | |
index = sets.index selectors | |
set = selectors.map do |selector| | |
declarations = selector.declarations.map{ |d| d.to_css.squeeze(' ') }.join | |
index == 0 ? declarations : "\n#{ selector.to_s.gsub /\w(?=:)/, '' } {#{ declarations }}" | |
end | |
index == 0 && sets.size > 1 ? "{#{ set }}" : set.join | |
end.join.strip | |
end | |
nokogiri_doc_given ? html : html.to_s | |
end | |
# Returns CSSPool::Document | |
def self.extract_css html, stylesheets_path = '' | |
CSSPool.CSS html.css('style, link').collect { |e| | |
next unless e['media'].nil? or ['screen', 'all'].include? e['media'] | |
next(e.remove and e.content) if e.name == 'style' | |
next unless e['rel'] == 'stylesheet' | |
e.remove | |
uri = %r{^https?://} === e['href'] ? e['href'] : File.join(stylesheets_path, e['href'].sub(/\?\d+$/,'')) | |
open(uri).read rescue nil | |
}.join("\n") | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment