roadie-3.2.2/0000755000004100000410000000000013123110273013002 5ustar www-datawww-dataroadie-3.2.2/roadie.gemspec0000644000004100000410000000212313123110273015610 0ustar www-datawww-data# roadie.gemspec # -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require 'roadie/version' Gem::Specification.new do |s| s.name = 'roadie' s.version = Roadie::VERSION s.platform = Gem::Platform::RUBY s.authors = ['Magnus Bergmark'] s.email = ['magnus.bergmark@gmail.com'] s.homepage = 'http://github.com/Mange/roadie' s.summary = %q{Making HTML emails comfortable for the Ruby rockstars} s.description = %q{Roadie tries to make sending HTML emails a little less painful by inlining stylesheets and rewriting relative URLs for you.} s.license = "MIT" s.required_ruby_version = ">= 1.9" s.add_dependency 'nokogiri', '~> 1.5' s.add_dependency 'css_parser', '~> 1.4' s.add_development_dependency 'rspec', '~> 3.0' s.add_development_dependency 'rspec-collection_matchers', '~> 1.0' s.add_development_dependency 'webmock', '~> 3.0' s.extra_rdoc_files = %w[README.md Changelog.md] s.require_paths = %w[lib] s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- spec/*`.split("\n") end roadie-3.2.2/Rakefile0000644000004100000410000000030713123110273014447 0ustar www-datawww-data# encoding: utf-8 require 'bundler/setup' Bundler::GemHelper.install_tasks desc "Run specs" task :spec do sh "bundle exec rspec -f progress" end desc "Default: Run specs" task :default => :spec roadie-3.2.2/Gemfile0000644000004100000410000000033313123110273014274 0ustar www-datawww-datasource 'https://rubygems.org' gemspec # Added here so it does not show up on the Gemspec; I only want it for CI builds gem 'codecov', group: :test, require: false group :guard do gem 'guard' gem 'guard-rspec' end roadie-3.2.2/.autotest0000644000004100000410000000036413123110273014656 0ustar www-datawww-data# Override autotest default magic to rerun all tests every time a # change is detected on the file system. class Autotest def get_to_green begin rerun_all_tests wait_for_changes unless all_good end until all_good end endroadie-3.2.2/Changelog.md0000644000004100000410000002727713123110273015232 0ustar www-datawww-data### dev [full changelog](https://github.com/Mange/roadie/compare/v3.2.2...master) * Nothing yet. ### 3.2.2 [full changelog](https://github.com/Mange/roadie/compare/v3.2.1...v3.2.2) * Enhancements * Support Nokogiri 1.x. * Support `css_parser` 1.x. * Make tests pass on Ruby 2.4.0 (upgrade Webmock). ### 3.2.1 [full changelog](https://github.com/Mange/roadie/compare/v3.2.0...v3.2.1) * Enhancements * Support Nokogiri 1.7.x. ### 3.2.0 [full changelog](https://github.com/Mange/roadie/compare/v3.1.1...v3.2.0) * Deprecations: * Dropped support for MRI 1.9.3. * Dropped support for MRI 2.0. * Upgrades: * Use `css_parser` 1.4.x instead of 1.3.x. * Bug fixes: * Strip UTF-8 BOM (Byte Order Mark) from stylesheets before parsing / concatenating - [Bartłomiej Wójtowicz](https://github.com/qbart) (#128) * Enhancements: * Build against Ruby MRI 2.3.0 too. * Don't add extra whitespace between table cells. ### 3.1.1 [full changelog](https://github.com/Mange/roadie/compare/v3.1.0...v3.1.1) * Enhancements: * Duplicate style properties are now removed when inlining. * This means that `color: green; color: red; color: green` will now be `color: red; color: green`. * The size of your emails should be the same, or smaller. ### 3.1.0 [full changelog](https://github.com/Mange/roadie/compare/v3.1.0.rc1...v3.1.0) * Enchancements: * `NetHttpProvider` validates the whitelist hostnames; passing an invalid hostname will raise `ArgumentError`. * `NetHttpProvider` supports scheme-less URLs (`//foo.com/`), defaulting to `https`. ### 3.1.0.rc1 [full changelog](https://github.com/Mange/roadie/compare/v3.0.5...v3.1.0.rc1) * Enhancements: * Allow user to specify asset providers for referenced assets with full URLs and inline them (#107) * Pass `Document` instance to transformation callbacks (#86) * Made `nokogiri` dependency more forgiving. * Supports `1.5.0`...`1.7.0` now instead of `1.6.0`...`1.7.0`. Some people out there are stuck on this older version of Nokogiri, and I don't want to leave them out. * Output better errors when no assets can be found. * The error will now show which providers were tried and in which order, along with the error message from the specific providers. * `Roadie::FilesystemProvider` shows the given path when inspected. * `data-roadie-ignore` attributes will now be removed from markup; hiding "development markers" in the final email. * Add a `Roadie::CachedProvider` asset provider that wraps other providers and cache them. * Add a `Roadie::PathRewriterProvider` asset provider that rewrites asset names for other providers. * This saves you from having to create custom providers if you require small tweaks to the lookup in order to use an official provider. * **Deprecations:** * `Roadie::Stylesheet#each_inlinable_block` is now deprecated. You can iterate and filter the `blocks` at your own discresion. ### 3.0.5 [full changelog](https://github.com/Mange/roadie/compare/v3.0.4...v3.0.5) * Bug fixes: * Don't try to inline external stylesheets. (#106) * Don't generate absolute URLs for anchor links. (Mange/roadie-rails#40) ### 3.0.4 [full changelog](https://github.com/Mange/roadie/compare/v3.0.3...v3.0.4) * Bug fixes: * Schemeless URLs was accepted as-is, which isn't supported in a lot of email clients. (#104) ### 3.0.3 [full changelog](https://github.com/Mange/roadie/compare/v3.0.2...v3.0.3) * Bug fixes: * CSS was mutated when parsed, breaking caches and memoized sources - [Brendan Mulholland (bmulholland)](https://github.com/bmulholland) (Mange/roadie-rails#32) ### 3.0.2 [full changelog](https://github.com/Mange/roadie/compare/v3.0.1...v3.0.2) * Bug fixes: * Some `data:` URLs could cause exceptions. (#97) * Correctly parse properties with semicolons in their values - [Aidan Feldman (afeld)](https://github.com/afeld) (#100) ### 3.0.1 [full changelog](https://github.com/Mange/roadie/compare/v3.0.0...v3.0.1) * Enhancements: * `CssNotFound` can take a provider which will be shown in error messages. * Bug fixes: * URL rewriter no longer raises on absolute URLs that cannot be parsed by `URI`. Absolute URLs are completely ignored. * URL rewriter supports urls without a scheme (like `//assets.myapp.com/foo`). * URL rewriter no longer crashes on absolute URLs without a path (like `myapp://`). ### 3.0.0 [full changelog](https://github.com/Mange/roadie/compare/v3.0.0.pre1...v3.0.0) * Enhancements: * `Roadie::ProviderList` responds to `#empty?` and `#last` * `Roadie::FilesystemProvider` ignores query string in filename. Older versions of Rails generated `` tags with query strings in their URLs, like such: `/stylesheets/email.css?1380694096` * Blacklist `:enabled`, `:disabled` and `:checked` pseudo functions - [Tyler Hunt (tylerhunt)](https://github.com/tylerhunt). * Add MRI 2.1.2 to Travis build matrix - [Grey Baker (greysteil)](https://github.com/greysteil). * Try to detect an upgrade from Roadie 2 and mention how to make it work with the new version. * Styles emitted in the `style` attribute should now be ordered as they were in the source CSS. ### 3.0.0.pre1 [full changelog](https://github.com/Mange/roadie/compare/v2.4.2...v3.0.0.pre1) Complete rewrite of most of the code and a new direction for the gem. * Breaking changes: * Removed Rails support into a separate Gem (`roadie-rails`). * Removed Sprockets dependency and AssetPipelineProvider. * Changed the entire public API. * Changed the API of custom providers. * Dropped support for Ruby 1.8.7. * Change `data-immutable` to `data-roadie-ignore`. * New features: * Rewriting the URLs of `img[src]`. * A way to inject stylesheets without having to adjust template. * A before callback to compliment the after callback. * Enhancements: * Better support for stylesheets using CSS fallbacks. This means that styles like this is now inlined: `width: 5em; width: 3rem;`, while Roadie would previously remove the first of the two. This sadly means that the HTML file will be much larger than before if you're using a non-optimized stylesheet (for example including your application stylesheet to the email). This was a bad idea even before this change, and this might get you to change. * Using HTML5 doctype instead of XHTML * Full support for JRuby * Experimental support for Rubinius ### 2.4.2 [full changelog](https://github.com/Mange/roadie/compare/v2.4.1...v2.4.2) * Bug fixes: * Fix Nokogiri version to allow only 1.5.x on ruby 1.8.7 * Blacklist :before, :after, :-ms-input-placeholder, :-moz-placeholder selectors – [Brian Bauer (bbauer)][https://github.com/bbauer]. * Build failed on 1.8.7 due to a change in `css_parser` ### 2.4.1 [full changelog](https://github.com/Mange/roadie/compare/v2.4.0...v2.4.1) * Bug fixes: * Allow Nokogiri 1.5.x again; 1.6.x is unsupported in Ruby 1.8.7. ### 2.4.0 [full changelog](https://github.com/Mange/roadie/compare/v2.3.4...v2.4.0) * Enhancements: * Support Rails 4.0, with the help of: * [Ryunosuke SATO (tricknotes)](https://github.com/tricknotes) * [Dylan Markow](https://github.com/dmarkow) * Keep `!important` when outputting styles to help combat web mail styles being `!important` * Support `:nth-child`, `:last-child`, etc. * To make this work, Roadie have to catch errors from Nokogiri and ignore them. A warning will be printed when this happens so users can open issues with the project and tests can be expanded. * Support for custom inliner (#58) — [Harish Shetty (kandadaboggu)](https://github.com/kandadaboggu) with friends * Bug fixes: * Don't crash when URL options have protocols with "://" in them (#52). * Other: * Be more specific on which versions are required; require newer `css_parser` * Officially support MRI 2.0.0 * Add experimental support for JRuby * Remove documentation that talks about passing CSS filenames as symbols; unsupported in Rails 4. (Thanks to [PikachuEXE](https://github.com/PikachuEXE)) ### 2.3.4 [full changelog](https://github.com/Mange/roadie/compare/v2.3.3...v2.3.4) * Enhancements: * Add `config.roadie.enabled` that can be set to `false` to disable Roadie completely. * Bug fixes: * Proc objects to the `:css` option is now run in the context of the mailer instance, mirroring similar options from ActionMailer. * Fix some tests that would always pass * Improve JRuby compatibility * Update Gemfile.lock and fix issues with newer gem versions ### 2.3.3 [full changelog](https://github.com/Mange/roadie/compare/v2.3.2...v2.3.3) * Enhancements: * Allow proc objects to the `:css` option * Bug fixes: * Ignore HTML comments and CDATA sections in CSS (support TinyMCE) ### 2.3.2 [full changelog](https://github.com/Mange/roadie/compare/v2.3.1...v2.3.2) * Bug fixes: * Don't fail on selectors which start with @ (#28) — [Roman Shterenzon (romanbsd)](https://github.com/romanbsd) ### 2.3.1 [full changelog](https://github.com/Mange/roadie/compare/v2.3.0...v2.3.1) * Bug fixes: * Does not work with Rails 3.0 unless provider set specifically (#23) ### 2.3.0 [full changelog](https://github.com/Mange/roadie/compare/v2.3.0.pre1...v2.3.0) * Nothing, really ### 2.3.0.pre1 [full changelog](https://github.com/Mange/roadie/compare/v2.2.0...v2.3.0.pre1) * Enhancements: * Support Rails 3.2.pre1 - [Morton Jonuschat (yabawock)](https://github.com/yabawock) * Sped up the Travis builds * Official support for Rails 3.0 again * Dependencies allow 3.0 * Travis builds 3.0 among the others ### 2.2.0 [full changelog](https://github.com/Mange/roadie/compare/v2.1.0...v2.2.0) * Enhancements: * Support for the `url_options` method inside mailer instances * You can now dynamically alter the URL options on a per-email basis ### 2.1.0 [full changelog](https://github.com/Mange/roadie/compare/v2.1.0.pre2...v2.1.0) * Full release! ### 2.1.0.pre2 [full changelog](https://github.com/Mange/roadie/compare/v2.1.0.pre1...v2.1.0.pre2) * Bug: Roadie broke `url_for` inside mailer views ### 2.1.0.pre1 [full changelog](https://github.com/Mange/roadie/compare/v2.0.0...v2.1.0.pre1) * Enhancements: * Support normal filesystem instead of only Asset pipeline * Enable users to create their own way of fetching CSS * Improve test coverage a bit * Use a railtie to hook into Rails * Use real Rails for testing integration ### 2.0.0 [full changelog](https://github.com/Mange/roadie/compare/v1.1.3...v2.0.0) * Enhancements: * Support the Asset pipeline - [Arttu Tervo (arttu)](https://github.com/arttu) * Dependencies: * Requires Rails 3.1 to work. You can keep on using the 1.x series in Rails 3.0 ### 1.1.3 [full changelog](https://github.com/Mange/roadie/compare/v1.1.2...v1.1.3) * Do not add another ".css" to filenames if already present - [Aliaxandr (saks)](https://github.com/saks) ### 1.1.2 [full changelog](https://github.com/Mange/roadie/compare/v1.1.1...v1.1.2) * Support for Rails 3.1.0 and later inside gemspec ### 1.1.1 [full changelog](https://github.com/Mange/roadie/compare/v1.1.0...v1.1.1) * Support for Rails 3.1.x (up to and including RC4) * Rails 3.0.x is still supported * Added CI via [Travis CI](http://travis-ci.org) ### 1.1.0 [full changelog](https://github.com/Mange/roadie/compare/v1.0.1...v1.1.0) * Enhancements: * Support for inlining `` elements (thanks to [aliix](https://github.com/aliix)) ### 1.0.1 [full changelog](https://github.com/Mange/roadie/compare/v1.0.0...v1.0.1) * Enhancements: * Full, official support for Ruby 1.9.2 (in addition to 1.8.7) * Dependencies: * Explicilty depend on nokogiri >= 1.4.4 ### 1.0.0 [full changelog](https://github.com/Mange/roadie/compare/legacy...v1.0.0) Roadie fork! * Enhancements: * Support for Rails 3.0 * Code cleanup * Support `!important` * Tests * + some other enhancements * Deprecations: * Removed support for Rails 2.x roadie-3.2.2/spec/0000755000004100000410000000000013123110273013734 5ustar www-datawww-dataroadie-3.2.2/spec/spec_helper.rb0000644000004100000410000000061213123110273016551 0ustar www-datawww-datarequire 'rspec/collection_matchers' require 'webmock/rspec' if ENV['CI'] require 'simplecov' SimpleCov.start require 'codecov' SimpleCov.formatter = SimpleCov::Formatter::Codecov end $: << File.dirname(__FILE__) + '/../lib' require 'roadie' RSpec.configure do |config| config.run_all_when_everything_filtered = true end Dir['./spec/support/**/*.rb'].each { |file| require file } roadie-3.2.2/spec/lib/0000755000004100000410000000000013123110273014502 5ustar www-datawww-dataroadie-3.2.2/spec/lib/roadie/0000755000004100000410000000000013123110273015745 5ustar www-datawww-dataroadie-3.2.2/spec/lib/roadie/selector_spec.rb0000644000004100000410000000343013123110273021124 0ustar www-datawww-data# encoding: UTF-8 require 'spec_helper' module Roadie describe Selector do it "can be coerced into String" do expect("I love " + Selector.new("html")).to eq("I love html") end it "can be inlined when simple" do expect(Selector.new("html body #main p.class")).to be_inlinable end it "cannot be inlined when containing pseudo functions" do %w[ p:active p:focus p:hover p:link p:target p:visited p:-ms-input-placeholder p:-moz-placeholder p:before p:after p:enabled p:disabled p:checked ].each do |bad_selector| expect(Selector.new(bad_selector)).not_to be_inlinable end expect(Selector.new('p.active')).to be_inlinable end it "cannot be inlined when containing pseudo elements" do expect(Selector.new('p::some-element')).not_to be_inlinable end it "cannot be inlined when selector is an at-rule" do expect(Selector.new('@keyframes progress-bar-stripes')).not_to be_inlinable end it "has a calculated specificity" do selector = "html p.active.nice #main.deep-selector" expect(Selector.new(selector).specificity).to eq(CssParser.calculate_specificity(selector)) end it "can be told about the specificity at initialization" do selector = "html p.active.nice #main.deep-selector" expect(Selector.new(selector, 1337).specificity).to eq(1337) end it "is equal to other selectors when they match the same things" do expect(Selector.new("foo")).to eq(Selector.new("foo ")) expect(Selector.new("foo")).not_to eq("foo") end it "strips the given selector" do expect(Selector.new(" foo \n").to_s).to eq(Selector.new("foo").to_s) end end end roadie-3.2.2/spec/lib/roadie/markup_improver_spec.rb0000644000004100000410000000547713123110273022543 0ustar www-datawww-data# encoding: UTF-8 require 'spec_helper' module Roadie describe MarkupImprover do def improve(html) dom = Nokogiri::HTML.parse html MarkupImprover.new(dom, html).improve dom end # JRuby up to at least 1.6.0 has a bug where the doctype of a document cannot be changed. # See https://github.com/sparklemotion/nokogiri/issues/984 def pending_for_buggy_jruby # No reason to check for version yet since no existing version has a fix. skip "Pending until Nokogiri issue #984 is fixed and released" if defined?(JRuby) end describe "automatic doctype" do it "inserts a HTML5 doctype if no doctype is present" do pending_for_buggy_jruby expect(improve("").internal_subset.to_xml).to eq("") end it "does not insert duplicate doctypes" do html = improve('').to_html expect(html.scan('DOCTYPE').size).to eq(1) end it "leaves other doctypes alone" do dtd = "" html = "#{dtd}" expect(improve(html).internal_subset.to_xml.strip).to eq(dtd) end end describe "basic HTML structure" do it "inserts a element as the root" do expect(improve("

Hey!

")).to have_selector("html h1") expect(improve("").css('html').size).to eq(1) end it "inserts if not present" do expect(improve('')).to have_selector('html > head + body') expect(improve('')).to have_selector('html > head') expect(improve('Foo')).to have_selector('html > head') expect(improve('').css('head').size).to eq(1) end it "inserts if not present" do expect(improve('

Hey!

')).to have_selector('html > body > h1') expect(improve('

Hey!

')).to have_selector('html > body > h1') expect(improve('

Hey!

').css('body').size).to eq(1) end end describe "charset declaration" do it "is inserted if missing" do dom = improve('') expect(dom).to have_selector('head meta') meta = dom.at_css('head meta') expect(meta['http-equiv']).to eq('Content-Type') expect(meta['content']).to eq('text/html; charset=UTF-8') end it "is left alone when predefined" do expect(improve(<<-HTML).xpath('//meta')).to have(1).item HTML end end end end roadie-3.2.2/spec/lib/roadie/document_spec.rb0000644000004100000410000001215613123110273021127 0ustar www-datawww-data# encoding: UTF-8 require 'spec_helper' module Roadie describe Document do sample_html = "

Hello world!

" subject(:document) { described_class.new sample_html } it "is initialized with HTML" do doc = Document.new "" expect(doc.html).to eq("") end it "has an accessor for URL options" do document.url_options = {host: "foo.bar"} expect(document.url_options).to eq({host: "foo.bar"}) end it "has a setting for keeping uninlinable styles" do expect(document.keep_uninlinable_css).to be true document.keep_uninlinable_css = false expect(document.keep_uninlinable_css).to be false end it "has a ProviderList for normal and external providers" do expect(document.asset_providers).to be_instance_of(ProviderList) expect(document.external_asset_providers).to be_instance_of(ProviderList) end it "defaults to having just a FilesystemProvider in the normal provider list" do expect(document).to have(1).asset_providers expect(document).to have(0).external_asset_providers provider = document.asset_providers.first expect(provider).to be_instance_of(FilesystemProvider) end it "allows changes to the normal asset providers" do other_provider = double "Other proider" old_list = document.asset_providers document.asset_providers = [other_provider] expect(document.asset_providers).to be_instance_of(ProviderList) expect(document.asset_providers.each.to_a).to eq([other_provider]) document.asset_providers = old_list expect(document.asset_providers).to eq(old_list) end it "allows changes to the external asset providers" do other_provider = double "Other proider" old_list = document.external_asset_providers document.external_asset_providers = [other_provider] expect(document.external_asset_providers).to be_instance_of(ProviderList) expect(document.external_asset_providers.each.to_a).to eq([other_provider]) document.external_asset_providers = old_list expect(document.external_asset_providers).to eq(old_list) end it "can store callbacks for inlining" do callable = double "Callable" document.before_transformation = callable document.after_transformation = callable expect(document.before_transformation).to eq(callable) expect(document.after_transformation).to eq(callable) end describe "transforming" do it "runs the before and after callbacks" do document = Document.new "" before = ->{} after = ->{} document.before_transformation = before document.after_transformation = after expect(before).to receive(:call).with(instance_of(Nokogiri::HTML::Document), document).ordered expect(Inliner).to receive(:new).ordered.and_return double.as_null_object expect(after).to receive(:call).with(instance_of(Nokogiri::HTML::Document), document).ordered document.transform end # TODO: Remove on next major version. it "works on callables that don't expect more than one argument" do document = Document.new "" document.before_transformation = ->(first) { } document.after_transformation = ->(first = nil) { } expect { document.transform }.to_not raise_error # It still supplies the second argument, if possible. document.after_transformation = ->(first, second = nil) { raise "Oops" unless second } expect { document.transform }.to_not raise_error end end end describe Document, "(integration)" do it "can transform the document" do document = Document.new <<-HTML Greetings

Hello, world!

HTML document.add_css "p { color: green; }" result = Nokogiri::HTML.parse document.transform expect(result).to have_selector('html > head > title') expect(result.at_css('title').text).to eq("Greetings") expect(result).to have_selector('html > body > p') paragraph = result.at_css('p') expect(paragraph.text).to eq("Hello, world!") expect(paragraph.to_xml).to eq('

Hello, world!

') end it "extracts styles from the HTML" do document = Document.new <<-HTML Greetings

Hello, world!

HTML document.asset_providers = TestProvider.new({ "/sample.css" => "p { color: red; text-align: right; }", }) document.add_css "p { color: green; text-size: 2em; }" result = Nokogiri::HTML.parse document.transform expect(result).to have_styling([ %w[color red], %w[text-align right], %w[color green], %w[text-size 2em] ]).at_selector("p") end end end roadie-3.2.2/spec/lib/roadie/stylesheet_spec.rb0000644000004100000410000000450213123110273021476 0ustar www-datawww-data# encoding: UTF-8 require 'spec_helper' module Roadie describe Stylesheet do it "is initialized with a name and CSS" do stylesheet = Stylesheet.new("foo.css", "body { color: green; }") expect(stylesheet.name).to eq("foo.css") end it "has a list of blocks" do stylesheet = Stylesheet.new("foo.css", <<-CSS) body { color: green !important; font-size: 200%; } a, i { color: red; } CSS expect(stylesheet).to have(3).blocks expect(stylesheet.blocks.map(&:to_s)).to eq([ "body{color:green !important;font-size:200%}", "a{color:red}", "i{color:red}", ]) end if VERSION < "4.0" it "can iterate all inlinable blocks" do inlinable = double(inlinable?: true, selector: "good", properties: "props") bad = double(inlinable?: false, selector: "bad", properties: "props") stylesheet = Stylesheet.new("example.css", "") allow(stylesheet).to receive_messages blocks: [bad, inlinable, bad] expect(stylesheet.each_inlinable_block.to_a).to eq([ ["good", "props"], ]) end else it "should no longer have #each_inlinable_block" do fail "Remove #each_inlinable_block" end end it "has a string representation of the contents" do stylesheet = Stylesheet.new("example.css", "body { color: green;}a{ color: red; font-size: small }") expect(stylesheet.to_s).to eq("body{color:green}\na{color:red;font-size:small}") end it "understands data URIs" do # http://css-tricks.com/data-uris/ stylesheet = Stylesheet.new("foo.css", <<-CSS) h1 { background-image: url(data:image/gif;base64,R0lGODl) } CSS expect(stylesheet).to have(1).blocks expect(stylesheet.blocks.map(&:to_s)).to eq([ "h1{background-image:url(data:image/gif;base64,R0lGODl)}" ]) end it "does not mutate the input CSS" do input = "/* comment */ body { color: green; }" input_copy = input.dup expect { Stylesheet.new("name", input) }.to_not change { input }.from(input_copy) end it "strips UTF-8 Byte Order Mark" do input = "\xEF\xBB\xBFbody { color: green; }" stylesheet = Stylesheet.new("bom.css", input) expect(stylesheet.to_s).to eq "body{color:green}" end end end roadie-3.2.2/spec/lib/roadie/deduplicator_spec.rb0000644000004100000410000000106413123110273021764 0ustar www-datawww-datarequire "spec_helper" module Roadie describe Deduplicator do it "removes identical pairs, keeping the last one" do input = [ ["a", "1"], ["b", "2"], ["a", "3"], ["a", "1"], ] expect(Deduplicator.apply(input)).to eq [ ["b", "2"], ["a", "3"], ["a", "1"], ] end it "returns input when no duplicates are present" do input = [ ["a", "1"], ["a", "3"], ["a", "2"], ] expect(Deduplicator.apply(input)).to eq input end end end roadie-3.2.2/spec/lib/roadie/style_block_spec.rb0000644000004100000410000000211313123110273021613 0ustar www-datawww-data# encoding: UTF-8 require 'spec_helper' module Roadie describe StyleBlock do it "has a selector and a list of properties" do properties = [] selector = double "Selector" block = StyleBlock.new(selector, properties) expect(block.selector).to eq(selector) expect(block.properties).to eq(properties) end it "delegates #specificity to the selector" do selector = double "Selector", specificity: 45 expect(StyleBlock.new(selector, []).specificity).to eq(45) end it "delegates #inlinable? to the selector" do selector = double "Selector", inlinable?: "maybe" expect(StyleBlock.new(selector, []).inlinable?).to eq("maybe") end it "delegates #selector_string to selector#to_s" do selector = double "Selector", to_s: "yey" expect(StyleBlock.new(selector, []).selector_string).to eq("yey") end it "has a string representation" do properties = [double(to_s: "bar"), double(to_s: "baz")] expect(StyleBlock.new(double(to_s: "foo"), properties).to_s).to eq("foo{bar;baz}") end end end roadie-3.2.2/spec/lib/roadie/style_property_spec.rb0000644000004100000410000000344213123110273022413 0ustar www-datawww-datarequire 'spec_helper' module Roadie describe StyleProperty do it "is initialized with a property, value, if it is marked as important, and the specificity" do StyleProperty.new('color', 'green', true, 45).tap do |declaration| expect(declaration.property).to eq('color') expect(declaration.value).to eq('green') expect(declaration).to be_important expect(declaration.specificity).to eq(45) end end describe "string representation" do it "is the property and the value joined with a colon" do expect(StyleProperty.new('color', 'green', false, 1).to_s).to eq('color:green') expect(StyleProperty.new('font-size', '1.1em', false, 1).to_s).to eq('font-size:1.1em') end it "contains the !important flag when set" do expect(StyleProperty.new('color', 'green', true, 1).to_s).to eq('color:green !important') end end describe "comparing" do def declaration(specificity, important = false) StyleProperty.new('color', 'green', important, specificity) end it "compares on specificity" do expect(declaration(5)).to eq(declaration(5)) expect(declaration(4)).to be < declaration(5) expect(declaration(6)).to be > declaration(5) end context "with an important declaration" do it "is less than the important declaration regardless of the specificity" do expect(declaration(99, false)).to be < declaration(1, true) end it "compares like normal when both declarations are important" do expect(declaration(5, true)).to eq(declaration(5, true)) expect(declaration(4, true)).to be < declaration(5, true) expect(declaration(6, true)).to be > declaration(5, true) end end end end end roadie-3.2.2/spec/lib/roadie/null_url_rewriter_spec.rb0000644000004100000410000000071413123110273023065 0ustar www-datawww-data# encoding: UTF-8 require 'spec_helper' require 'shared_examples/url_rewriter' module Roadie describe NullUrlRewriter do let(:generator) { double "URL generator" } subject(:rewriter) { NullUrlRewriter.new(generator) } it_behaves_like "url rewriter" it "does nothing when transforming DOM" do dom = double "DOM tree" expect { NullUrlRewriter.new(generator).transform_dom dom }.to_not raise_error end end end roadie-3.2.2/spec/lib/roadie/css_not_found_spec.rb0000644000004100000410000000141513123110273022150 0ustar www-datawww-datarequire 'spec_helper' module Roadie describe CssNotFound do it "is initialized with a name" do error = CssNotFound.new('style.css') expect(error.css_name).to eq('style.css') expect(error.message).to eq('Could not find stylesheet "style.css"') end it "can be initialized with an extra message" do expect(CssNotFound.new('file.css', "directory is missing").message).to eq( 'Could not find stylesheet "file.css": directory is missing' ) end it "shows information about used provider when given" do provider = double("Some cool provider") expect(CssNotFound.new('style.css', nil, provider).message).to eq( %(Could not find stylesheet "style.css"\nUsed provider:\n#{provider}) ) end end end roadie-3.2.2/spec/lib/roadie/cached_provider_spec.rb0000644000004100000410000000301313123110273022422 0ustar www-datawww-datarequire 'spec_helper' require 'roadie/rspec' require 'shared_examples/asset_provider' module Roadie describe CachedProvider do let(:upstream) { TestProvider.new("good.css" => "body { color: green; }") } let(:cache) { Hash.new } subject(:provider) { CachedProvider.new(upstream, cache) } it_behaves_like "roadie asset provider", valid_name: "good.css", invalid_name: "bad.css" it "stores retrieved stylesheets in the cache" do found = nil expect { found = provider.find_stylesheet("good.css") }.to change(cache, :keys).to(["good.css"]) expect(cache["good.css"]).to eq found end it "reads from the cache first" do found = upstream.find_stylesheet!("good.css") cache["good.css"] = found expect(upstream).to_not receive(:find_stylesheet) expect(provider.find_stylesheet("good.css")).to eq found expect(provider.find_stylesheet!("good.css")).to eq found end it "stores failed lookups in the cache" do expect { provider.find_stylesheet("foo.css") }.to change(cache, :keys).to(["foo.css"]) expect(cache["foo.css"]).to be_nil end it "stores failed lookups even when raising errors" do expect { provider.find_stylesheet!("bar.css") }.to raise_error CssNotFound expect(cache.keys).to include "bar.css" expect(cache["bar.css"]).to be_nil end it "defaults to a hash for cache storage" do expect(CachedProvider.new(upstream).cache).to be_kind_of Hash end end end roadie-3.2.2/spec/lib/roadie/inliner_spec.rb0000644000004100000410000001736613123110273020761 0ustar www-datawww-data# encoding: UTF-8 require 'spec_helper' module Roadie describe Inliner do before { @stylesheet = "".freeze } def use_css(css) @stylesheet = Stylesheet.new("example", css) end def rendering(html, stylesheet = @stylesheet) dom = Nokogiri::HTML.parse html Inliner.new([stylesheet], dom).inline dom end describe "inlining styles" do it "inlines simple attributes" do use_css 'p { color: green }' expect(rendering('

')).to have_styling('color' => 'green') end it "keeps multiple versions of the same property to support progressive enhancement" do # https://github.com/premailer/css_parser/issues/44 pending "css_parser issue #44" use_css 'p { color: #eee; color: rgba(255, 255, 255, 0.9); }' expect(rendering('

')).to have_styling( [['color', 'green'], ['color', 'rgba(255, 255, 255, 0.9)']] ) end it "de-duplicates identical styles" do use_css ' p { color: green; } .message { color: blue; } .positive { color: green; } ' expect(rendering('

')).to have_styling( [['color', 'blue'], ['color', 'green']] ) end it "inlines browser-prefixed attributes" do use_css 'p { -vendor-color: green }' expect(rendering('

')).to have_styling('-vendor-color' => 'green') end it "inlines CSS3 attributes" do use_css 'p { border-radius: 2px; }' expect(rendering('

')).to have_styling('border-radius' => '2px') end it "keeps the order of the styles that are inlined" do use_css 'h1 { padding: 2px; margin: 5px; }' expect(rendering('

')).to have_styling([['padding', '2px'], ['margin', '5px']]) end it "combines multiple selectors into one" do use_css 'p { color: green; } .tip { float: right; }' expect(rendering('

')).to have_styling([['color', 'green'], ['float', 'right']]) end it "uses the attributes with the highest specificity when conflicts arises" do use_css ".safe { color: green; } p { color: red; }" expect(rendering('

')).to have_styling([['color', 'red'], ['color', 'green']]) end it "sorts styles by specificity order" do use_css 'p { important: no; } #important { important: very; } .important { important: yes; }' expect(rendering('

')).to have_styling([ %w[important no], %w[important yes] ]) expect(rendering('

')).to have_styling([ %w[important no], %w[important yes], %w[important very] ]) end it "supports multiple selectors for the same rules" do use_css 'p, a { color: green; }' rendering('

').tap do |document| expect(document).to have_styling('color' => 'green').at_selector('p') expect(document).to have_styling('color' => 'green').at_selector('a') end end it "keeps !important properties" do use_css "a { text-decoration: underline !important; } a.hard-to-spot { text-decoration: none; }" expect(rendering('')).to have_styling([ ['text-decoration', 'none'], ['text-decoration', 'underline !important'] ]) end it "combines with already present inline styles" do use_css "p { color: green }" expect(rendering('

')).to have_styling([['color', 'green'], ['font-size', '1.1em']]) end it "does not override inline styles" do use_css "p { text-transform: uppercase; color: red }" # The two color properties are kept to make css fallbacks work correctly expect(rendering('

')).to have_styling([ ['text-transform', 'uppercase'], ['color', 'red'], ['color', 'green'], ]) end it "does not apply link and dynamic pseudo selectors" do use_css " p:active { color: red } p:focus { color: red } p:hover { color: red } p:link { color: red } p:target { color: red } p:visited { color: red } p.active { width: 100%; } " expect(rendering('

')).to have_styling('width' => '100%') end it "does not crash on any pseudo element selectors" do use_css " p.some-element { width: 100%; } p::some-element { color: red; } " expect(rendering('

')).to have_styling('width' => '100%') end it "warns on selectors that crash Nokogiri" do dom = Nokogiri::HTML.parse "

" stylesheet = Stylesheet.new "foo.css", "p[%^=foo] { color: red; }" inliner = Inliner.new([stylesheet], dom) expect(Utils).to receive(:warn).with( %{Cannot inline "p[%^=foo]" from "foo.css" stylesheet. If this is valid CSS, please report a bug.} ) inliner.inline end it "works with nth-child" do use_css " p { color: red; } p:nth-child(2n) { color: green; } " result = rendering("

") expect(result).to have_styling([['color', 'red']]).at_selector('p:first') expect(result).to have_styling([['color', 'red'], ['color', 'green']]).at_selector('p:last') end context "with uninlinable selectors" do before do allow(Roadie::Utils).to receive(:warn) end it "puts them in a new HTML scanner = AssetScanner.new dom, normal_provider, external_provider stylesheets = scanner.find_css expect(stylesheets).to have(2).stylesheets expect(stylesheets[0].to_s).to include("green") expect(stylesheets[1].to_s).to include("red") expect(stylesheets.first.name).to eq("(inline)") end it "does not find any embedded stylesheets marked for ignoring" do dom = dom_document <<-HTML HTML scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.find_css).to have(1).stylesheet end it "finds normal referenced stylesheets through the normal provider" do stylesheet = double "A stylesheet" expect(normal_provider).to receive(:find_stylesheet!).with("/some/url.css").and_return stylesheet dom = dom_fragment %() scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.find_css).to eq([stylesheet]) end it "finds external referenced stylesheets through the external provider" do stylesheet = double "A stylesheet" external_provider = TestProvider.new expect(external_provider).to receive(:find_stylesheet!).with("//example.com/style.css").and_return stylesheet dom = dom_fragment %() scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.find_css).to eq([stylesheet]) end it "ignores referenced print stylesheets" do dom = dom_fragment %() expect(normal_provider).not_to receive(:find_stylesheet!) scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.find_css).to eq([]) end it "does not look for externally referenced stylesheets from empty ProviderList" do external_provider = ProviderList.empty dom = dom_fragment %() expect(external_provider).not_to receive(:find_stylesheet!) scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.find_css).to eq([]) end it "does not look for ignored referenced stylesheets" do dom = dom_fragment %() expect(normal_provider).not_to receive(:find_stylesheet!) scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.find_css).to eq([]) end it 'ignores HTML comments and CDATA sections' do # TinyMCE posts invalid CSS. We support that just to be pragmatic. dom = dom_fragment %() scanner = AssetScanner.new dom, normal_provider, external_provider stylesheet = scanner.find_css.first expect(stylesheet.to_s).to include("green") expect(stylesheet.to_s).not_to include("!--") expect(stylesheet.to_s).not_to include("CDATA") end it "does not pick up scripts generating styles" do dom = dom_fragment <<-HTML HTML scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.find_css).to eq([]) end end describe "extracting" do it "returns the stylesheets found, and removes them from the DOM" do dom = dom_document <<-HTML Hello world! HTML normal_provider = TestProvider.new "/some/url.css" => "body { color: green; }" scanner = AssetScanner.new dom, normal_provider, external_provider stylesheets = scanner.extract_css expect(stylesheets).to have(2).stylesheets expect(stylesheets[0].to_s).to include("span") expect(stylesheets[1].to_s).to include("body") expect(dom).to have_selector("html > head > title") expect(dom).to have_selector("html > body > style.totally-ignored") expect(dom).to have_selector("link.totally-ignored") expect(dom).to have_selector("link[media=print]") expect(dom).not_to have_selector("html > head > style") expect(dom).not_to have_selector("html > head > link[href='/some/url.css']") end it "removes external references if provider is not empty" do dom = dom_document <<-HTML HTML external_provider = ProviderList.wrap(NullProvider.new) scanner = AssetScanner.new dom, normal_provider, external_provider stylesheets = scanner.extract_css expect(stylesheets).to have(1).stylesheets expect(dom).to_not have_selector("link[href*=some]") expect(dom).to have_selector("link[href*=other]") end it "removes the data-roadie-ignore markers" do dom = dom_document <<-HTML HTML scanner = AssetScanner.new dom, TestProvider.new, external_provider scanner.extract_css expect(dom.at_css("#first").attributes).to_not include("data-roadie-ignore") expect(dom.at_css("#second").attributes).to_not include("data-roadie-ignore") end end end end roadie-3.2.2/spec/lib/roadie/null_provider_spec.rb0000644000004100000410000000115113123110273022166 0ustar www-datawww-data# encoding: UTF-8 require 'spec_helper' require 'shared_examples/asset_provider' module Roadie describe NullProvider do it_behaves_like "asset provider role" def expect_empty_stylesheet(stylesheet) expect(stylesheet).not_to be_nil expect(stylesheet.name).to eq("(null)") expect(stylesheet).to have(0).blocks expect(stylesheet.to_s).to be_empty end it "finds an empty stylesheet for every name" do expect_empty_stylesheet NullProvider.new.find_stylesheet("omg wtf bbq") expect_empty_stylesheet NullProvider.new.find_stylesheet!("omg wtf bbq") end end end roadie-3.2.2/spec/lib/roadie/path_rewriter_provider_spec.rb0000644000004100000410000000236113123110273024077 0ustar www-datawww-datarequire 'spec_helper' require 'roadie/rspec' require 'shared_examples/asset_provider' module Roadie describe PathRewriterProvider do let(:upstream) { TestProvider.new "good.css" => "body { color: green; }" } subject(:provider) do PathRewriterProvider.new(upstream) do |path| path.gsub('well', 'good') end end it_behaves_like "roadie asset provider", valid_name: "well.css", invalid_name: "bad" it "does not call the upstream provider if block returns nil" do provider = PathRewriterProvider.new(upstream) { nil } expect(upstream).to_not receive(:find_stylesheet) expect(upstream).to_not receive(:find_stylesheet!) expect(provider.find_stylesheet("foo")).to be_nil expect { provider.find_stylesheet!("foo") }.to raise_error(CssNotFound, /nil/) end it "does not call the upstream provider if block returns false" do provider = PathRewriterProvider.new(upstream) { false } expect(upstream).to_not receive(:find_stylesheet) expect(upstream).to_not receive(:find_stylesheet!) expect(provider.find_stylesheet("foo")).to be_nil expect { provider.find_stylesheet!("foo") }.to raise_error(CssNotFound, /false/) end end end roadie-3.2.2/spec/fixtures/0000755000004100000410000000000013123110273015605 5ustar www-datawww-dataroadie-3.2.2/spec/fixtures/big_em.css0000644000004100000410000000003013123110273017532 0ustar www-datawww-dataem { font-size: 200%; } roadie-3.2.2/spec/fixtures/stylesheets/0000755000004100000410000000000013123110273020161 5ustar www-datawww-dataroadie-3.2.2/spec/fixtures/stylesheets/green.css0000644000004100000410000000002713123110273021772 0ustar www-datawww-databody { color: green; } roadie-3.2.2/spec/hash_as_cache_store_spec.rb0000644000004100000410000000024113123110273021235 0ustar www-datawww-datarequire "spec_helper" require "roadie/rspec" describe "Using Hash as a cache store" do subject(:hash) { Hash.new } it_behaves_like "roadie cache store" end roadie-3.2.2/spec/shared_examples/0000755000004100000410000000000013123110273017100 5ustar www-datawww-dataroadie-3.2.2/spec/shared_examples/asset_provider.rb0000644000004100000410000000055713123110273022465 0ustar www-datawww-datashared_examples_for "asset provider role" do it "responds to #find_stylesheet" do expect(subject).to respond_to(:find_stylesheet) expect(subject.method(:find_stylesheet).arity).to eq(1) end it "responds to #find_stylesheet!" do expect(subject).to respond_to(:find_stylesheet!) expect(subject.method(:find_stylesheet!).arity).to eq(1) end end roadie-3.2.2/spec/shared_examples/url_rewriter.rb0000644000004100000410000000127413123110273022156 0ustar www-datawww-datashared_examples_for "url rewriter" do it "is constructed with a generator" do generator = double "URL generator" expect { described_class.new(generator) }.to_not raise_error end it "has a #transform_dom(dom) method that returns nil" do expect(subject).to respond_to(:transform_dom) expect(subject.method(:transform_dom).arity).to eq(1) dom = Nokogiri::HTML.parse "" expect(subject.transform_dom(dom)).to be_nil end it "has a #transform_css(css) method that returns nil" do expect(subject).to respond_to(:transform_css) expect(subject.method(:transform_css).arity).to eq(1) expect(subject.transform_css("")).to be_nil end end roadie-3.2.2/spec/integration_spec.rb0000644000004100000410000001741513123110273017626 0ustar www-datawww-datarequire 'spec_helper' describe "Roadie functionality" do def parse_html(html) Nokogiri::HTML.parse(html) end it "adds missing structure" do html = "

Hello world!

".encode("Shift_JIS") document = Roadie::Document.new(html) result = document.transform unless defined?(JRuby) # JRuby has a bug that makes DTD manipulation impossible # See Nokogiri bugs #984 and #985 # https://github.com/sparklemotion/nokogiri/issues/984 # https://github.com/sparklemotion/nokogiri/issues/985 expect(result).to include("") end expect(result).to include("") expect(result).to include("") expect(result).to include("") expect(result).to include(" Hello world!

Hello world!

Check out these awesome prices!

HTML document.add_css <<-CSS em { color: red; } h1 { text-align: center; } CSS result = parse_html document.transform expect(result).to have_styling('text-align' => 'center').at_selector('h1') expect(result).to have_styling('color' => 'red').at_selector('p > em') end it "stores styles that cannot be inlined in the " do document = Roadie::Document.new <<-HTML

Hello world!

Check out these awesome prices!

HTML css = <<-CSS em:hover { color: red; } p:fung-shuei { color: spirit; } CSS document.add_css css expect(Roadie::Utils).to receive(:warn).with(/fung-shuei/) result = parse_html document.transform expect(result).to have_selector("html > head > style") styles = result.at_css("html > head > style").text expect(styles).to include Roadie::Stylesheet.new("", css).to_s end it "can be configured to skip styles that cannot be inlined" do document = Roadie::Document.new <<-HTML

Hello world!

Check out these awesome prices!

HTML css = <<-CSS em:hover { color: red; } p:fung-shuei { color: spirit; } CSS document.add_css css document.keep_uninlinable_css = false expect(Roadie::Utils).to receive(:warn).with(/fung-shuei/) result = parse_html document.transform expect(result).to_not have_selector("html > head > style") end it "inlines css from disk" do document = Roadie::Document.new <<-HTML Hello world!

Hello world!

Check out these awesome prices!

HTML result = parse_html document.transform expect(result).to have_styling('font-size' => '200%').at_selector('p > em') end it "crashes when stylesheets cannot be found, unless using NullProvider" do document = Roadie::Document.new <<-HTML HTML expect { document.transform }.to raise_error(Roadie::CssNotFound, /does_not_exist\.css/) document.asset_providers << Roadie::NullProvider.new expect { document.transform }.to_not raise_error end it "ignores external css if no external providers are added" do document = Roadie::Document.new <<-HTML Hello world!

Hello world!

Check out these awesome prices!

HTML document.external_asset_providers = [] result = parse_html document.transform expect(result).to have_selector('head > link') expect(result).to have_styling([]).at_selector('p > em') end it "inlines external css if configured" do document = Roadie::Document.new <<-HTML Hello world!

Hello world!

Check out these awesome prices!

HTML document.external_asset_providers = TestProvider.new( "http://example.com/big_em.css" => "em { font-size: 200%; }" ) result = parse_html document.transform expect(result).to have_styling('font-size' => '200%').at_selector('p > em') expect(result).to_not have_selector('head > link') end it "does not inline the same properties several times" do document = Roadie::Document.new <<-HTML

Hello world

HTML document.asset_providers = TestProvider.new("hello.css" => <<-CSS) p { color: red; } .hello { color: red; } .world { color: red; } CSS result = parse_html document.transform expect(result).to have_styling([ ['color', 'red'] ]).at_selector('p') end it "makes URLs absolute" do document = Roadie::Document.new <<-HTML About us HTML document.asset_providers = TestProvider.new( "/style.css" => "a { background: url(/assets/link-abcdef1234567890.png); }" ) document.url_options = {host: "myapp.com", scheme: "https", path: "rails/app/"} result = parse_html document.transform expect(result.at_css("a")["href"]).to eq("https://myapp.com/rails/app/about_us") expect(result.at_css("img")["src"]).to eq("https://myapp.com/rails/app/assets/about_us-abcdef1234567890.png") expect(result).to have_styling( "background" => 'url("https://myapp.com/rails/app/assets/bg-abcdef1234567890.png")' ).at_selector("body") expect(result).to have_styling( "background" => 'url(https://myapp.com/rails/app/assets/link-abcdef1234567890.png)' ).at_selector("a") end it "allows custom callbacks during inlining" do document = Roadie::Document.new <<-HTML Hello world HTML document.before_transformation = proc { |dom| dom.at_css("body")["class"] = "roadie" } document.after_transformation = proc { |dom| dom.at_css("span").remove } result = parse_html document.transform expect(result.at_css("body")["class"]).to eq("roadie") expect(result.at_css("span")).to be_nil end it "does not add whitespace between table cells" do document = Roadie::Document.new <<-HTML
One1
Two2
HTML result = document.transform expect(result).to include("One1") expect(result).to include("Two2") end end roadie-3.2.2/spec/support/0000755000004100000410000000000013123110273015450 5ustar www-datawww-dataroadie-3.2.2/spec/support/have_styling_matcher.rb0000644000004100000410000000300613123110273022173 0ustar www-datawww-dataRSpec::Matchers.define :have_styling do |rules| normalized_rules = StylingExpectation.new(rules) chain(:at_selector) { |selector| @selector = selector } match { |document| @selector ||= 'body > *:first' normalized_rules == styles_at_selector(document) } description { "have styles #{normalized_rules.inspect} at selector #{@selector.inspect}" } failure_message { |document| "expected styles at #{@selector.inspect} to be:\n#{normalized_rules}\nbut was:\n#{styles_at_selector(document)}" } failure_message_when_negated { "expected styles at #{@selector.inspect} to not be:\n#{normalized_rules}" } def styles_at_selector(document) expect(document).to have_selector(@selector) StylingExpectation.new document.at_css(@selector)['style'] end end class StylingExpectation def initialize(styling) case styling when String then @rules = parse_rules(styling) when Array then @rules = styling when Hash then @rules = styling.to_a when nil then @rules = [] else fail "I don't understand #{styling.inspect}!" end end def ==(other) rules == other.rules end def to_s() rules.to_s end protected attr_reader :rules private def parse_rules(css) css.split(';').map { |property| parse_property(property) } end def parse_property(property) rule, value = property.split(':', 2).map(&:strip) [rule, normalize_quotes(value)] end # JRuby's Nokogiri encodes quotes def normalize_quotes(string) string.gsub '%22', '"' end end roadie-3.2.2/spec/support/have_node_matcher.rb0000644000004100000410000000117613123110273021435 0ustar www-datawww-dataRSpec::Matchers.define :have_node do |selector| chain(:with_attributes) { |attributes| @attributes = attributes } match do |document| node = document.at_css(selector) if @attributes node && match_attributes(node.attributes) else node end end failure_message { "expected document to #{name_to_sentence}#{expected_to_sentence}"} failure_message_when_negated { "expected document to not #{name_to_sentence}#{expected_to_sentence}"} def match_attributes(node_attributes) attributes = Hash[node_attributes.map { |name, attribute| [name, attribute.value] }] @attributes == attributes end end roadie-3.2.2/spec/support/have_attribute_matcher.rb0000644000004100000410000000153013123110273022505 0ustar www-datawww-dataRSpec::Matchers.define :have_attribute do |attribute| @selector = 'body > *:first' chain :at_selector do |selector| @selector = selector end match do |document| name, expected = attribute.first expected == attribute(document, name) end description { "have attribute #{attribute.inspect} at selector #{@selector.inspect}" } failure_message do |document| name, expected = attribute.first "expected #{name} attribute at #{@selector.inspect} to be #{expected.inspect} but was #{attribute(document, name).inspect}" end failure_message_when_negated do |document| name, expected = attribute.first "expected #{name} attribute at #{@selector.inspect} to not be #{expected.inspect}" end def attribute(document, attribute_name) node = document.css(@selector).first node && node[attribute_name] end end roadie-3.2.2/spec/support/have_selector_matcher.rb0000644000004100000410000000045613123110273022330 0ustar www-datawww-dataRSpec::Matchers.define :have_selector do |selector| match { |document| !document.css(selector).empty? } failure_message { "expected document to #{name_to_sentence}#{to_sentence selector}"} failure_message_when_negated { "expected document to not #{name_to_sentence}#{to_sentence selector}"} end roadie-3.2.2/spec/support/test_provider.rb0000644000004100000410000000042213123110273020664 0ustar www-datawww-dataclass TestProvider include Roadie::AssetProvider def initialize(files = {}) @files = files @default = files[:default] end def find_stylesheet(name) contents = @files.fetch(name, @default) Roadie::Stylesheet.new name, contents if contents end end roadie-3.2.2/.travis.yml0000644000004100000410000000067213123110273015120 0ustar www-datawww-datasudo: false language: ruby rvm: - 2.1 - 2.2 - 2.3 - 2.4 - jruby - rbx matrix: allow_failures: # Rubinius and JRuby have a lot of trouble and no large following, so I'm going to # allow failures on it until it gets more stable on Travis / Real Life(tm). # Let me know if you need it. Patches are welcome! - rvm: jruby - rvm: rbx fast_finish: true cache: bundler bundler_args: --without guard script: "rake" roadie-3.2.2/lib/0000755000004100000410000000000013123110273013550 5ustar www-datawww-dataroadie-3.2.2/lib/roadie.rb0000644000004100000410000000133313123110273015340 0ustar www-datawww-datamodule Roadie end require 'roadie/version' require 'roadie/errors' require 'roadie/utils' require 'roadie/deduplicator' require 'roadie/stylesheet' require 'roadie/selector' require 'roadie/style_property' require 'roadie/style_attribute_builder' require 'roadie/style_block' require 'roadie/asset_provider' require 'roadie/provider_list' require 'roadie/filesystem_provider' require 'roadie/null_provider' require 'roadie/net_http_provider' require 'roadie/cached_provider' require 'roadie/path_rewriter_provider' require 'roadie/asset_scanner' require 'roadie/markup_improver' require 'roadie/url_generator' require 'roadie/url_rewriter' require 'roadie/null_url_rewriter' require 'roadie/inliner' require 'roadie/document' roadie-3.2.2/lib/roadie/0000755000004100000410000000000013123110273015013 5ustar www-datawww-dataroadie-3.2.2/lib/roadie/selector.rb0000644000004100000410000000401513123110273017160 0ustar www-datawww-datamodule Roadie # @api private # # A selector is a domain object for a CSS selector, such as: # body # a:hover # input::placeholder # p:nth-of-child(4n+1) .important a img # # "Selectors" such as "strong, em" are actually two selectors and should be # represented as two instances of this class. # # This class can also calculate specificity for the selector and answer a few # questions about them. # # Selectors can be coerced into Strings, so they should be transparent to use # anywhere a String is expected. class Selector def initialize(selector, specificity = nil) @selector = selector.to_s.strip @specificity = specificity end # Returns the specificity of the selector, calculating it if needed. def specificity @specificity ||= CssParser.calculate_specificity selector end # Returns whenever or not a selector can be inlined. # It's impossible to inline properties that applies to a pseudo element # (like +::placeholder+, +::before+) or a pseudo function (like +:active+). # # We cannot inline styles that appear inside "@" constructs, like +@keyframes+. def inlinable? !(pseudo_element? || at_rule? || pseudo_function?) end def to_s() selector end def to_str() to_s end def inspect() selector.inspect end # {Selector}s are equal to other {Selector}s if, and only if, their string # representations are equal. def ==(other) if other.is_a?(self.class) other.selector == selector else super end end protected attr_reader :selector private BAD_PSEUDO_FUNCTIONS = %w[ :active :focus :hover :link :target :visited :-ms-input-placeholder :-moz-placeholder :before :after :enabled :disabled :checked ].freeze def pseudo_element? selector.include? '::' end def at_rule? selector[0, 1] == '@' end def pseudo_function? BAD_PSEUDO_FUNCTIONS.any? { |bad| selector.include?(bad) } end end end roadie-3.2.2/lib/roadie/utils.rb0000644000004100000410000000152113123110273016477 0ustar www-datawww-datamodule Roadie module Utils # @api private def path_is_absolute?(path) # Ruby's URI is pretty unforgiving, but roadie aims to be. Don't involve # URI for URLs that's easy to determine to be absolute. # URLs starting with a scheme (http:, data:) are absolute. # # URLs that start with double slashes (//css/app.css) are also absolute # in modern browsers, but most email clients do not understand them. return true if path =~ %r{^(\w+:|//)} begin !URI.parse(path).relative? rescue URI::InvalidURIError => error raise InvalidUrlPath.new(path, error) end end # @api private module_function :path_is_absolute? # @api private def warn(message) Kernel.warn("Roadie: #{message}") end # @api private module_function :warn end end roadie-3.2.2/lib/roadie/deduplicator.rb0000644000004100000410000000167513123110273020030 0ustar www-datawww-datamodule Roadie class Deduplicator def self.apply(input) new(input).apply end def initialize(input) @input = input @duplicates = false end def apply # Bail early for very small inputs input if input.size < 2 calculate_latest_occurance # Another early bail in case we never even have a duplicate value if has_duplicates? strip_out_duplicates else input end end private attr_reader :input, :latest_occurance def has_duplicates? @duplicates end def calculate_latest_occurance @latest_occurance = input.each_with_index.each_with_object({}) do |(value, index), map| @duplicates = true if map.has_key?(value) map[value] = index end end def strip_out_duplicates input.each_with_index.select { |value, index| latest_occurance[value] == index }.map(&:first) end end end roadie-3.2.2/lib/roadie/stylesheet.rb0000644000004100000410000000421413123110273017532 0ustar www-datawww-datamodule Roadie # Domain object that represents a stylesheet (from disc, perhaps). # # It has a name and a list of {StyleBlock}s. # # @attr_reader [String] name the name of the stylesheet ("stylesheets/main.css", "Admin user styles", etc.). The name of the stylesheet will be visible if any errors occur. # @attr_reader [Array] blocks class Stylesheet BOM = "\xEF\xBB\xBF".force_encoding('UTF-8').freeze attr_reader :name, :blocks # Parses the CSS string into a {StyleBlock}s and stores it. # # @param [String] name # @param [String] css def initialize(name, css) @name = name @blocks = parse_blocks(css.sub(BOM, "")) end # @yield [selector, properties] # @yieldparam [Selector] selector # @yieldparam [Array] properties # @deprecated Iterate over the #{blocks} instead. Will be removed on version 4.0. def each_inlinable_block(&block) # #map and then #each in order to support chained enumerations, etc. if # no block is provided inlinable_blocks.map { |style_block| [style_block.selector, style_block.properties] }.each(&block) end def to_s blocks.join("\n") end private def inlinable_blocks blocks.select(&:inlinable?) end def parse_blocks(css) blocks = [] parser = setup_parser(css) parser.each_rule_set do |rule_set, media_types| rule_set.selectors.each do |selector_string| blocks << create_style_block(selector_string, rule_set) end end blocks end def create_style_block(selector_string, rule_set) specificity = CssParser.calculate_specificity(selector_string) selector = Selector.new(selector_string, specificity) properties = [] rule_set.each_declaration do |prop, val, important| properties << StyleProperty.new(prop, val, important, specificity) end StyleBlock.new(selector, properties) end def setup_parser(css) parser = CssParser::Parser.new # CssParser::Parser#add_block! mutates input parameter parser.add_block! css.dup parser end end end roadie-3.2.2/lib/roadie/style_attribute_builder.rb0000644000004100000410000000125213123110273022271 0ustar www-datawww-datamodule Roadie class StyleAttributeBuilder def initialize @styles = [] end def <<(style) @styles << style end def attribute_string Deduplicator.apply(stable_sort(@styles).map(&:to_s)).join(';') end private def stable_sort(list) # Ruby's sort is unstable for performance reasons. We need it to be # stable, e.g. to preserve order of elements that are compared equal in # the sorting. # We can accomplish this by using the original array index as a second # comparator for when the first one is equal. list.each_with_index.sort_by { |item, index| [item, index] }.map(&:first) end end end roadie-3.2.2/lib/roadie/cached_provider.rb0000644000004100000410000000463113123110273020465 0ustar www-datawww-datamodule Roadie # @api public # The {CachedProvider} wraps another provider (or {ProviderList}) and caches # the response from it. # # The default cache store is a instance-specific hash that lives for the # entire duration of the instance. If you want to share hash between # instances, pass your own hash-like object. Just remember to not allow this # cache to grow without bounds, which a shared hash would do. # # Not found assets are not cached currently, but it's possible to extend this # class in the future if there is a need for it. Remember this if you have # providers with very slow failures. # # The cache store must accept {Roadie::Stylesheet} instances, and return such # instances when fetched. It must respond to `#[name]` and `#[name]=` to # retrieve and set entries, respectively. The `#[name]=` method also needs to # return the instance again. # # @example Global cache # Application.asset_cache = Hash.new # slow_provider = MyDatabaseProvider.new(Application) # provider = Roadie::CachedProvider.new(slow_provider, Application.asset_cache) # # @example Custom cache store # class MyRoadieMemcacheStore # def initialize(memcache) # @memcache = memcache # end # # def [](path) # css = memcache.read("assets/#{path}/css") # if css # name = memcache.read("assets/#{path}/name") || "cached #{path}" # Roadie::Stylesheet.new(name, css) # end # end # # def []=(path, stylesheet) # memcache.write("assets/#{path}/css", stylesheet.to_s) # memcache.write("assets/#{path}/name", stylesheet.name) # stylesheet # You need to return the set Stylesheet # end # end # class CachedProvider # The cache store used by this instance. attr_reader :cache # @param upstream [an asset provider] The wrapped asset provider # @param cache [#[], #[]=] The cache store to use. def initialize(upstream, cache = {}) @upstream = upstream @cache = cache end def find_stylesheet(name) cache_fetch(name) do @upstream.find_stylesheet(name) end end def find_stylesheet!(name) cache_fetch(name) do @upstream.find_stylesheet!(name) end end private def cache_fetch(name) cache[name] || cache[name] = yield rescue CssNotFound cache[name] = nil raise end end end roadie-3.2.2/lib/roadie/errors.rb0000644000004100000410000000536313123110273016663 0ustar www-datawww-datamodule Roadie # Base class for all Roadie errors. Rescue this if you want to catch errors # from Roadie. # # If Roadie raises an error that does not inherit this class, please report # it as a bug. class Error < RuntimeError; end # Raised when Roadie encounters an invalid URL which cannot be parsed by # Ruby's +URI+ class. # # This could be a hint that something in your HTML or CSS is broken. class InvalidUrlPath < Error # The original error, raised from +URI+. attr_reader :cause def initialize(given_path, cause = nil) @cause = cause if cause cause_message = " Caused by: #{cause}" else cause_message = "" end super "Cannot use path \"#{given_path}\" in URL generation.#{cause_message}" end end # Raised when an asset provider cannot find a stylesheet. # # If you are writing your own asset provider, make sure to raise this in the # +#find_stylesheet!+ method. # # @see AssetProvider class CssNotFound < Error # The name of the stylesheet that cannot be found attr_reader :css_name # Provider used when finding attr_reader :provider # Extra message attr_reader :extra_message # TODO: Change signature in the next major version of Roadie. def initialize(css_name, extra_message = nil, provider = nil) @css_name = css_name @provider = provider @extra_message = extra_message super build_message(extra_message) end protected def error_row "#{provider || "Unknown provider"}: #{extra_message || message}" end private # Redundant method argument is to keep API compatability without major version bump. # TODO: Remove argument on version 4.0. def build_message(extra_message = @extra_message) message = %(Could not find stylesheet "#{css_name}") message << ": #{extra_message}" if extra_message message << "\nUsed provider:\n#{provider}" if provider message end end class ProvidersFailed < CssNotFound attr_reader :errors def initialize(css_name, provider_list, errors) @errors = errors super(css_name, "All providers failed", provider_list) end private def build_message(extra_message) message = %(Could not find stylesheet "#{css_name}": #{extra_message}\nUsed providers:\n) each_error_row(errors) do |row| message << "\t" << row << "\n" end message end def each_error_row(errors) errors.each do |error| case error when ProvidersFailed each_error_row(error.errors) { |row| yield row } when CssNotFound yield error.error_row else yield "Unknown provider (#{error.class}): #{error}" end end end end end roadie-3.2.2/lib/roadie/filesystem_provider.rb0000644000004100000410000000237313123110273021443 0ustar www-datawww-datamodule Roadie # Asset provider that looks for files on your local filesystem. # # It will be locked to a specific path and it will not access files above # that directory. class FilesystemProvider # Raised when FilesystemProvider is asked to access a file that lies above # the base path. class InsecurePathError < Error; end attr_reader :path def initialize(path = Dir.pwd) @path = path end # @return [Stylesheet, nil] def find_stylesheet(name) file_path = build_file_path(name) if File.exist? file_path Stylesheet.new file_path, File.read(file_path) end end # @raise InsecurePathError # @return [Stylesheet] def find_stylesheet!(name) file_path = build_file_path(name) if File.exist? file_path Stylesheet.new file_path, File.read(file_path) else basename = File.basename file_path raise CssNotFound.new(basename, %{#{file_path} does not exist. (Original name was "#{name}")}, self) end end def to_s() inspect end def inspect() "#<#{self.class} #@path>" end private def build_file_path(name) raise InsecurePathError, name if name.include?("..") File.join(@path, name[/^([^?]+)/]) end end end roadie-3.2.2/lib/roadie/markup_improver.rb0000644000004100000410000000536013123110273020566 0ustar www-datawww-datamodule Roadie # @api private # Class that improves the markup of a HTML DOM tree # # This class will improve the following aspects of the DOM: # * A HTML5 doctype will be added if missing, other doctypes will be left as-is. # * Basic HTML elements will be added if missing. # * ++ # * ++ # * ++ # * ++ declaring charset and content-type (text/html) # # @note Due to a Nokogiri bug, the HTML5 doctype cannot be added under JRuby. No doctype is outputted under JRuby. # See https://github.com/sparklemotion/nokogiri/issues/984 class MarkupImprover # The original HTML must also be passed in in order to handle the doctypes # since a +Nokogiri::HTML::Document+ will always have a doctype, no matter if # the original source had it or not. Reading the raw HTML is the only way to # determine if we want to add a HTML5 doctype or not. def initialize(dom, original_html) @dom = dom @html = original_html end # @return [nil] passed DOM will be mutated def improve ensure_doctype_present head = ensure_head_element_present ensure_declared_charset head end protected attr_reader :dom private def ensure_doctype_present return if uses_buggy_jruby? return if @html.include?('] every found stylesheet def find_css @dom.css(STYLE_ELEMENT_QUERY).map { |element| read_stylesheet(element) }.compact end # Looks for all non-ignored stylesheets, removes their references from the # DOM and then returns them. # # This will mutate the DOM tree. # # The order of the array corresponds with the document order in the DOM. # # @see #find_css # @return [Enumerable] every extracted stylesheet def extract_css stylesheets = @dom.css(STYLE_ELEMENT_QUERY).map { |element| stylesheet = read_stylesheet(element) element.remove if stylesheet stylesheet }.compact remove_ignore_markers stylesheets end private STYLE_ELEMENT_QUERY = ( "style:not([data-roadie-ignore]), " + # TODO: When using Nokogiri 1.6.1 and later; we may use a double :not here # instead of the extra code inside #read_stylesheet, and the #compact # call in #find_css. "link[rel=stylesheet][href]:not([data-roadie-ignore])" ).freeze # Cleans out stupid CDATA and/or HTML comments from the style text # TinyMCE causes this, allegedly CLEANING_MATCHER = / (^\s* # Beginning-of-lines matches ()| (\]\]>) $) /x.freeze def read_stylesheet(element) if element.name == "style" read_style_element element elsif element.name == "link" && element['media'] != "print" && element["href"] read_link_element element end end def read_style_element(element) Stylesheet.new "(inline)", clean_css(element.text.strip) end def read_link_element(element) if Utils.path_is_absolute?(element["href"]) external_asset_provider.find_stylesheet! element['href'] if should_find_external? else normal_asset_provider.find_stylesheet! element['href'] end end def clean_css(css) css.gsub(CLEANING_MATCHER, '') end def should_find_external? return false unless external_asset_provider # If external_asset_provider is empty list; don't use it. return false if external_asset_provider.respond_to?(:empty?) && external_asset_provider.empty? true end def remove_ignore_markers @dom.css("[data-roadie-ignore]").each do |node| node.remove_attribute "data-roadie-ignore" end end end end roadie-3.2.2/lib/roadie/document.rb0000644000004100000410000001072313123110273017161 0ustar www-datawww-datamodule Roadie # The main entry point for Roadie. A document represents a working unit and # is built with the input HTML and the configuration options you need. # # A Document must never be used from two threads at the same time. Reusing # Documents is discouraged. # # Stylesheets are added to the HTML from three different sources: # 1. Stylesheets inside the document ( + ``` Roadie will use the given asset providers to look for the actual CSS that is referenced. If you don't change the default, it will use the `Roadie::FilesystemProvider` which looks for stylesheets on the filesystem, relative to the current working directory. Example: ```ruby # /home/user/foo/stylesheets/primary.css body { color: green; } # /home/user/foo/script.rb html = <<-HTML HTML Dir.pwd # => "/home/user/foo" document = Roadie::Document.new html document.transform # => # # # # # ``` If a referenced stylesheet cannot be found, the `#transform` method will raise an `Roadie::CssNotFound` error. If you instead want to ignore missing stylesheets, you can use the `NullProvider`. ### Configuring providers ### You can write your own providers if you need very specific behavior for your app, or you can use the built-in providers. Providers come in two groups: normal and external. Normal providers handle paths without host information (`/style/foo.css`) while external providers handle URLs with host information (`//example.com/foo.css`, `localhost:3001/bar.css`, and so on). The default configuration is to not have any external providers configured, which will cause those referenced stylesheets to be ignored. Adding one or more providers for external assets causes all of them to be searched and inlined, so if you only want this to happen to specific stylesheets you need to add ignore markers to every other styleshheet (see above). Included providers: * `FilesystemProvider` – Looks for files on the filesystem, relative to the given directory unless otherwise specified. * `ProviderList` – Wraps a list of other providers and searches them in order. The `asset_providers` setting is an instance of this. It behaves a lot like an array, so you can push, pop, shift and unshift to it. * `NullProvider` – Does not actually provide anything, it always finds empty stylesheets. Use this in tests or if you want to ignore stylesheets that cannot be found by your other providers (or if you want to force the other providers to never run). * `NetHttpProvider` – Downloads stylesheets using `Net::HTTP`. Can be given a whitelist of hosts to download from. * `CachedProvider` – Wraps another provider (or `ProviderList`) and caches responses inside the provided cache store. * `PathRewriterProvider` – Rewrites the passed path and then passes it on to another provider (or `ProviderList`). If you want to search several locations on the filesystem, you can declare that: ```ruby document.asset_providers = [ Roadie::FilesystemProvider.new(App.root.join("resources", "stylesheets")), Roadie::FilesystemProvider.new(App.root.join("system", "uploads", "stylesheets")), ] ``` #### `NullProvider` #### If you want to ignore stylesheets that cannot be found instead of crashing, push the `NullProvider` to the end: ```ruby # Don't crash on missing assets document.asset_providers << Roadie::NullProvider.new # Don't download assets in tests document.external_asset_providers.unshift Roadie::NullProvider.new ``` **Note:** This will cause the referenced stylesheet to be removed from the source code, so email client will never see it either. #### `NetHttpProvider` #### The `NetHttpProvider` will download the URLs that is is given using Ruby's standard `Net::HTTP` library. You can give it a whitelist of hosts that downloads are allowed from: ```ruby document.external_asset_providers << Roadie::NetHttpProvider.new(whitelist: ["myapp.com", "assets.myapp.com", "cdn.cdnnetwork.co.jp"]) document.external_asset_providers << Roadie::NetHttpProvider.new # Allows every host ``` #### `CachedProvider` #### You might want to cache providers from working several times. If you are sending several emails quickly from the same process, this might also save a lot of time on parsing the stylesheets if you use in-memory storage such as a hash. You can wrap any other kind of providers with it, even a `ProviderList`: ```ruby document.external_asset_providers = Roadie::CachedProvider.new(document.external_asset_providers, my_cache) ``` If you don't pass a cache backend, it will use a normal `Hash`. The cache store must follow this protocol: ```ruby my_cache["key"] = some_stylesheet_instance # => # my_cache["key"] # => # my_cache["missing"] # => nil ``` **Warning:** The default `Hash` store will never be cleared, so make sure you don't allow the number of unique asset paths to grow too large in a single run. This is especially important if you run Roadie in a daemon that accepts arbritary documents, and/or if you use hash digests in your filenames. Making a new instance of `CachedProvider` will use a new `Hash` instance. You can implement your own custom cache store by implementing the `[]` and `[]=` methods. ```ruby class MyRoadieMemcacheStore def initialize(memcache) @memcache = memcache end def [](path) css = memcache.read("assets/#{path}/css") if css name = memcache.read("assets/#{path}/name") || "cached #{path}" Roadie::Stylesheet.new(name, css) end end def []=(path, stylesheet) memcache.write("assets/#{path}/css", stylesheet.to_s) memcache.write("assets/#{path}/name", stylesheet.name) stylesheet # You need to return the set Stylesheet end end document.external_asset_providers = Roadie::CachedProvider.new( document.external_asset_providers, MyRoadieMemcacheStore.new(MemcacheClient.instance) ) ``` If you are using Rspec, you can test your implementation by using the shared examples for the "roadie cache store" role: ```ruby require "roadie/rspec" describe MyRoadieMemcacheStore do let(:memcache_client) { MemcacheClient.instance } subject { MyRoadieMemcacheStore.new(memcache_client) } it_behaves_like "roadie cache store" do before { memcache_client.clear } end end ``` #### `PathRewriterProvider` #### With this provider, you can rewrite the paths that are searched in order to more easily support another provider. Examples could include rewriting absolute URLs into something that can be found on the filesystem, or to access internal hosts instead of external ones. ```ruby filesystem = Roadie::FilesystemProvider.new("assets") document.asset_providers << Roadie::PathRewriterProvider.new(filesystem) do |path| path.sub('stylesheets', 'css').downcase end document.external_asset_providers = Roadie::PathRewriterProvider.new(filesystem) do |url| if url =~ /myapp\.com/ URI.parse(url).path.sub(%r{^/assets}, '') else url end end ``` You can also wrap a list, for example to implement `external_asset_providers` by composing the normal `asset_providers`: ```ruby document.external_asset_providers = Roadie::PathRewriterProvider.new(document.asset_providers) do |url| URI.parse(url).path end ``` ### Writing your own provider ### Writing your own provider is also easy. You need to provide: * `#find_stylesheet(name)`, returning either a `Roadie::Stylesheet` or `nil`. * `#find_stylesheet!(name)`, returning either a `Roadie::Stylesheet` or raising `Roadie::CssNotFound`. ```ruby class UserAssetsProvider def initialize(user_collection) @user_collection = user_collection end def find_stylesheet(name) if name =~ %r{^/users/(\d+)\.css$} user = @user_collection.find_user($1) Roadie::Stylesheet.new("user #{user.id} stylesheet", user.stylesheet) end end def find_stylesheet!(name) find_stylesheet(name) or raise Roadie::CssNotFound.new(name, "does not match a user stylesheet", self) end # Instead of implementing #find_stylesheet!, you could also: # include Roadie::AssetProvider # That will give you a default implementation without any error message. If # you have multiple error cases, it's recommended that you implement # #find_stylesheet! without #find_stylesheet and raise with an explanatory # error message. end # Try to look for a user stylesheet first, then fall back to normal filesystem lookup. document.asset_providers = [ UserAssetsProvider.new(app), Roadie::FilesystemProvider.new('./stylesheets'), ] ``` You can test for compliance by using the built-in RSpec examples: ```ruby require 'spec_helper' require 'roadie/rspec' describe MyOwnProvider do # Will use the default `subject` (MyOwnProvider.new) it_behaves_like "roadie asset provider", valid_name: "found.css", invalid_name: "does_not_exist.css" # Extra setup just for these tests: it_behaves_like "roadie asset provider", valid_name: "found.css", invalid_name: "does_not_exist.css" do subject { MyOwnProvider.new(...) } before { stub_dependencies } end end ``` ### Keeping CSS that is impossible to inline Some CSS is impossible to inline properly. `:hover` and `::after` comes to mind. Roadie tries its best to keep these around by injecting them inside a new `

Hello world

``` When hovering over this `

`, the color will not change as the `color: green` rule takes precedence. You can get it to work by adding `!important` to the `:hover` rule. It would be foolish to try to automatically inject `!important` on every rule automatically, so this is a manual process. #### Turning it off #### If you'd rather skip this and have the styles not possible to inline disappear, you can turn off this feature by setting the `keep_uninlinable_css` option to false. ```ruby document.keep_uninlinable_css = false ``` ### Callbacks ### Callbacks allow you to do custom work on documents before they are transformed. The Nokogiri document tree is passed to the callable along with the `Roadie::Document` instance: ```ruby class TrackNewsletterLinks def call(dom, document) dom.css("a").each { |link| fix_link(link) } end def fix_link(link) divider = (link['href'] =~ /?/ ? '&' : '?') link['href'] = link['href'] + divider + 'source=newsletter' end end document.before_transformation = proc { |dom, document| logger.debug "Inlining document with title #{dom.at_css('head > title').try(:text)}" } document.after_transformation = TrackNewsletterLinks.new ``` Build Status ------------ Tested with [Travis CI](http://travis-ci.org) using: * MRI 2.1 * MRI 2.2 * MRI 2.3 * MRI 2.4 * JRuby (latest) * Rubinius (failures on Rubinius will not fail the build due to a long history of instability in `rbx`) [(Build status)](http://travis-ci.org/#!/Mange/roadie) Let me know if you want any other VM supported officially. ### Versioning ### This project follows [Semantic Versioning](http://semver.org/) and has been since version 1.0.0. FAQ --- ### Why is my markup changed in subtle ways? Roadie uses Nokogiri to parse and regenerate the HTML of your email, which means that some unintentional changes might show up. One example would be that Nokogiri might remove your ` `s in some cases. Another example is Nokogiri's lack of HTML5 support, so certain new element might have spaces removed. I recommend you don't use HTML5 in emails anyway because of bad email client support (that includes web mail!). ### I'm getting segmentation faults (or other C-like problems)! What should I do? ### Roadie uses Nokogiri to parse the HTML of your email, so any C-like problems like segfaults are likely in that end. The best way to fix this is to first upgrade libxml2 on your system and then reinstall Nokogiri. Instructions on how to do this on most platforms, see [Nokogiri's official install guide](http://nokogiri.org/tutorials/installing_nokogiri.html). ### What happened to my `@keyframes`? The CSS Parser used in Roadie does not handle keyframes. I don't think any email clients do either, but if you want to keep on trying you can add them manually to a `