pax_global_header00006660000000000000000000000064135506741310014517gustar00rootroot0000000000000052 comment=f1b5c502504960e6d5c8917e22a5d19799d0962d roadie-4.0.0/000077500000000000000000000000001355067413100127635ustar00rootroot00000000000000roadie-4.0.0/.autotest000066400000000000000000000003641355067413100146370ustar00rootroot00000000000000# 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-4.0.0/.gitignore000066400000000000000000000001331355067413100147500ustar00rootroot00000000000000.DS_Store .yardoc .rspec .ruby-version Gemfile.lock doc tmp pkg .bundle .history vendor/ roadie-4.0.0/.rubocop.yml000066400000000000000000000001251355067413100152330ustar00rootroot00000000000000AllCops: DisabledByDefault: true Style/FrozenStringLiteralComment: Enabled: trueroadie-4.0.0/.travis.yml000066400000000000000000000007101355067413100150720ustar00rootroot00000000000000sudo: false language: ruby rvm: - 2.4 - 2.5 - 2.6 - jruby - rbx before_install: - gem install bundler -v '1.17.3' 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 script: "rake" roadie-4.0.0/.yardopts000066400000000000000000000000421355067413100146250ustar00rootroot00000000000000--no-private --files Changelog.md roadie-4.0.0/Changelog.md000066400000000000000000000336301355067413100152010ustar00rootroot00000000000000### dev [full changelog](https://github.com/Mange/roadie/compare/v4.0.0...master) Nothing yet. ### 4.0.0 [full changelog](https://github.com/Mange/roadie/compare/v3.5.1...v4.0.0) * Drop support for Ruby 2.1, 2.2, and 2.3 and add support for frozen string literals and Ruby 2.6 - [adamkiczula (Adam Kiczula)](https://github.com/adamkiczula) (#164) * `Roadie::Stylesheet#each_inlinable_block` is now removed. ### 3.5.1 [full changelog](https://github.com/Mange/roadie/compare/v3.5.0...v3.5.1) * Gracefully handle empty string email body, such as those provided by `ActionMailer::Base::NullMail` objects - [adamkiczula (Adam Kiczula)](https://github.com/adamkiczula) (#163). ### 3.5.0 [full changelog](https://github.com/Mange/roadie/compare/v3.4.0...v3.5.0) * Drop support for Nokogiri before 1.8. ### 3.4.0 [full changelog](https://github.com/Mange/roadie/compare/v3.3.0...v3.4.0) * Enhancements * Better support for media queries - [BroiSatse (Stanislaw Klajn)](https://github.com/BroiSatse) and [jeznag (Jeremy Nagel)](https://github.com/jeznag) (#157). Media queries should no longer be inlined in your HTML; instead they should appear in `` if `keep_uninlinable_css` is enabled. By default different media query blocks with the same conditions will be merged together (which might change specificity), but this behavior can be disabled by setting `merge_media_queries` to `false`. * Remove Guard as a development dependency. Tests run fast anyway, and I never liked that this tool "infected" the dependencies of the project when it should remain a user-specific tool unless the project wants to enforce it. ### 3.3.0 [full changelog](https://github.com/Mange/roadie/compare/v3.2.2...v3.3.0) * Enhancements * Allow transforming to XHTML instead of HTML - [Zhivko Draganov](https://github.com/zdraganov) (#144). * Support partial HTML documents (fragments) - #147 * With the help of [andfx](https://github.com/andfx) - #115 * With the help of [Frida Sjoholm](https://github.com/FridaSjoholm) - #146 * Skip URL rewriting on elements with `data-roadie-ignore` - #154. * With the help of [Hamed Asghari](https://github.com/hasghari) - #138. * Bug fixes: * Apply correct string encoding / charset in `NetHttpProvider` - [Jeremy Nagel](https://github.com/jeznag) (#152). ### 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-4.0.0/Gemfile000066400000000000000000000003031355067413100142520ustar00rootroot00000000000000# frozen_string_literal: true source '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 roadie-4.0.0/LICENSE000066400000000000000000000021171355067413100137710ustar00rootroot00000000000000Copyright (c) 2009-2016 Magnus Bergmark, Jim Neath / Purify, and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. roadie-4.0.0/README.md000066400000000000000000000610161355067413100142460ustar00rootroot00000000000000Roadie ====== [![Build history and status](https://travis-ci.org/Mange/roadie.svg?branch=master)](http://travis-ci.org/#!/Mange/roadie) [![Code Climate](https://codeclimate.com/github/Mange/roadie.png)](https://codeclimate.com/github/Mange/roadie) [![Code coverage status](https://codecov.io/github/Mange/roadie/coverage.svg?branch=master)](https://codecov.io/github/Mange/roadie?branch=master) [![Gem](https://img.shields.io/gem/v/roadie.svg)](https://rubygems.org/gems/roadie) [![Passive maintenance](https://img.shields.io/badge/maintenance-Passive-yellow.svg)][passive] ||| |---|---| | :warning: | This gem is now in [passive maintenance mode][passive]. [(more)][passive] | > Making HTML emails comfortable for the Ruby rockstars Roadie tries to make sending HTML emails a little less painful by inlining stylesheets and rewriting relative URLs for you inside your emails. How does it work? ----------------- Email clients have bad support for stylesheets, and some of them blocks stylesheets from downloading. The easiest way to handle this is to work with inline styles (`style="..."`), but that is error prone and hard to work with as you cannot use classes and/or reuse styling over your HTML. This gem makes this easier by automatically inlining stylesheets into the document. You give Roadie your CSS, or let it find it by itself from the `` and ` ``` 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 = ->(dom, document) { logger.debug "Inlining document with title #{dom.at_css('head > title').try(:text)}" } document.after_transformation = TrackNewsletterLinks.new ``` ### XHTML vs HTML ### You can configure the underlying HTML/XML engine to output XHTML or HTML (which is the default). One usecase for this is that `{` tokens usually gets escaped to `{`, which would be a problem if you then pass the resulting HTML on to some other templating engine that uses those tokens (like Handlebars or Mustache). ```ruby document.mode = :xhtml ``` This will also affect the emitted `` if transforming a full document. Partial documents does not have a ``. 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 ` 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 "does not change URLs of ignored elements, but still inlines styles on them" do document = Roadie::Document.new <<-HTML About us Unsubscribe HTML document.url_options = {host: "myapp.com", scheme: "https", path: "rails/app/"} result = parse_html document.transform expect(result.at_css("a.one")["href"]).to eq("https://myapp.com/rails/app/about_us") # Nokogiri still treats the attribute as an URL and escapes it. expect(result.at_css("a.two")["href"]).to eq("%24UNSUBSCRIBE_URL") expect(result).to have_styling("color" => "green").at_selector("a.one") expect(result).to have_styling("color" => "green").at_selector("a.two") 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 it "doesn't inline styles in media queries with features" do document = Roadie::Document.new <<-HTML

HTML document.asset_providers = TestProvider.new( "/style.css" => <<-CSS .colorful { color: green; } @media screen and (max-width 600px) { .colorful { color: red; } } CSS ) result = parse_html document.transform expect(result).to have_styling('color' => 'green').at_selector('.colorful') end it 'puts non-inlineable media queries in the head' do document = Roadie::Document.new <<-HTML
HTML document.asset_providers = TestProvider.new( "/style.css" => <<-CSS .colorful { color: green; } @media screen and (max-width 800px) { .colorful { color: blue; } } @media screen, print and (max-width 800px) { .colorful { color: blue; } } CSS ) result = parse_html document.transform styles = result.at_css('html > head > style').text expected_result = <<-CSS @media screen and (max-width 800px) { .colorful{color:blue} } @media screen, print and (max-width 800px) { .colorful{color:blue} } CSS expected_result = expected_result.gsub(/[\s]+/, ' ').strip actual_result = styles.gsub(/[\s]+/, ' ').strip expect(actual_result).to eq(expected_result) end it 'groups non-inlineable media queries in the head by default' do document = Roadie::Document.new <<-HTML
HTML document.asset_providers = TestProvider.new( "/style.css" => <<-CSS .colorful { color: green; } @media screen and (max-width 600px) { .colorful { color: red; width: 600px; } } @media screen and (max-width 600px) { .colorful-2 { color: red; width: 600px; } } CSS ) result = parse_html document.transform styles = result.at_css('html > head > style').text expected_result = <<-CSS @media screen and (max-width 600px) { .colorful{color:red;width:600px} .colorful-2{color:red;width:600px} } CSS expected_result = expected_result.gsub(/[\s]+/, ' ').strip actual_result = styles.gsub(/[\s]+/, ' ').strip expect(actual_result).to eq(expected_result) end describe 'if merge_media_queries is set to false' do it "doesn't group non-inlineable media queries in the head" do document = Roadie::Document.new <<-HTML
HTML document.merge_media_queries = false document.asset_providers = TestProvider.new( "/style.css" => <<-CSS .colorful { color: green; } @media screen and (max-width 600px) { .colorful { color: red; width: 600px; } } @media screen and (max-width 600px) { .colorful-2 { color: red; width: 600px; } } CSS ) result = parse_html document.transform styles = result.at_css('html > head > style').text expected_result = <<-CSS @media screen and (max-width 600px) { .colorful{color:red;width:600px} } @media screen and (max-width 600px) { .colorful-2{color:red;width:600px} } CSS expected_result = expected_result.gsub(/[\s]+/, ' ').strip actual_result = styles.gsub(/[\s]+/, ' ').strip expect(actual_result).to eq(expected_result) end end end describe "on partial documents" do def parse_html(html) Nokogiri::HTML.fragment(html) end it "does not add structure" do html = "

Hello world!

".encode("Shift_JIS") document = Roadie::Document.new(html) result = document.transform_partial expect(result).to eq(html) end it "inlines given css" do document = Roadie::Document.new <<-HTML

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_partial 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 a new
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_partial 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("div") 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("p")["class"] = "roadie" } document.after_transformation = proc { |dom| dom.at_css("span").remove } result = parse_html document.transform_partial expect(result.at_css("p")["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_partial expect(result).to include("One1") expect(result).to include("Two2") end end end roadie-4.0.0/spec/lib/000077500000000000000000000000001355067413100144635ustar00rootroot00000000000000roadie-4.0.0/spec/lib/roadie/000077500000000000000000000000001355067413100157265ustar00rootroot00000000000000roadie-4.0.0/spec/lib/roadie/asset_scanner_spec.rb000066400000000000000000000161541355067413100221240ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Roadie describe AssetScanner do let(:normal_provider) { TestProvider.new } let(:external_provider) { ProviderList.empty } let(:dom) { dom_document "" } def dom_fragment(html); Nokogiri::HTML.fragment html; end def dom_document(html); Nokogiri::HTML.parse html; end it "is initialized with a DOM tree, a normal asset provider set, and an external asset provider set" do scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.dom).to eq(dom) expect(scanner.normal_asset_provider).to eq(normal_provider) expect(scanner.external_asset_provider).to eq(external_provider) end describe "finding" do it "returns nothing when no stylesheets are referenced" do scanner = AssetScanner.new dom, normal_provider, external_provider expect(scanner.find_css).to eq([]) end it "finds all embedded stylesheets" do dom = dom_document <<-HTML 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 end end end roadie-4.0.0/spec/lib/roadie/cached_provider_spec.rb000066400000000000000000000030521355067413100224060ustar00rootroot00000000000000# frozen_string_literal: true require '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-4.0.0/spec/lib/roadie/css_not_found_spec.rb000066400000000000000000000014541355067413100221340ustar00rootroot00000000000000# frozen_string_literal: true require '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-4.0.0/spec/lib/roadie/deduplicator_spec.rb000066400000000000000000000011231355067413100217410ustar00rootroot00000000000000# frozen_string_literal: true require "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-4.0.0/spec/lib/roadie/document_spec.rb000066400000000000000000000167041355067413100211130ustar00rootroot00000000000000# frozen_string_literal: true 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 "defaults to HTML mode" do expect(document.mode).to eq(:html) 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 "allows changes to the mode setting" do document.mode = :xhtml expect(document.mode).to eq(:xhtml) document.mode = :html expect(document.mode).to eq(:html) end it "does not allow unknown modes" do expect { document.mode = :other }.to raise_error(ArgumentError, /:other/) 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 context "in HTML mode" do it "does not escape curly braces" do document = Document.new "Hello" document.mode = :xhtml expect(document.transform).to include("{{hello}}") end end end describe "partial 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::DocumentFragment), document, ).ordered expect(Inliner).to receive(:new).ordered.and_return double.as_null_object expect(after).to receive(:call).with( instance_of(Nokogiri::HTML::DocumentFragment), document, ).ordered document.transform_partial end context "in HTML mode" do it "does not escape curly braces" do document = Document.new "Hello" document.mode = :xhtml expect(document.transform_partial).to include("{{hello}}") end 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 it "removes data-roadie-ignore markers" do document = Document.new <<-HTML Hello world! HTML result = Nokogiri::HTML.parse document.transform expect(result).to have_selector("body > a > span") expect(result).not_to have_selector("[data-roadie-ignore]") end end end roadie-4.0.0/spec/lib/roadie/filesystem_provider_spec.rb000066400000000000000000000052261355067413100233700ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' require 'roadie/rspec' require 'shared_examples/asset_provider' module Roadie describe FilesystemProvider do let(:fixtures_path) { File.expand_path "../../../fixtures", __FILE__ } subject(:provider) { FilesystemProvider.new(fixtures_path) } it_behaves_like "roadie asset provider", valid_name: "stylesheets/green.css", invalid_name: "foo" it "takes a path" do expect(FilesystemProvider.new("/tmp").path).to eq("/tmp") end it "defaults to the current working directory" do expect(FilesystemProvider.new.path).to eq(Dir.pwd) end it "shows the given path in string representation" do expect(provider.to_s).to include provider.path.to_s expect(provider.inspect).to include provider.path.to_s end describe "finding stylesheets" do it "finds files in the path" do full_path = File.join(fixtures_path, "stylesheets", "green.css") file_contents = File.read full_path stylesheet = provider.find_stylesheet("stylesheets/green.css") expect(stylesheet).not_to be_nil expect(stylesheet.name).to eq(full_path) expect(stylesheet.to_s).to eq(Stylesheet.new("", file_contents).to_s) end it "returns nil on non-existant files" do expect(provider.find_stylesheet("non/existant.css")).to be_nil end it "finds files inside the base path when using absolute paths" do full_path = File.join(fixtures_path, "stylesheets", "green.css") expect(provider.find_stylesheet("/stylesheets/green.css").name).to eq(full_path) end it "does not read files above the base directory" do expect { provider.find_stylesheet("../#{File.basename(__FILE__)}") }.to raise_error FilesystemProvider::InsecurePathError end end describe "finding stylesheets with query strings" do it "ignores the query string" do full_path = File.join(fixtures_path, "stylesheets", "green.css") file_contents = File.read full_path stylesheet = provider.find_stylesheet("/stylesheets/green.css?time=111") expect(stylesheet).not_to be_nil expect(stylesheet.name).to eq(full_path) expect(stylesheet.to_s).to eq(Stylesheet.new("", file_contents).to_s) end it "shows that the query string is ignored inside raised errors" do begin provider.find_stylesheet!("/foo.css?query-string") fail "No error was raised" rescue CssNotFound => error expect(error.css_name).to eq("foo.css") expect(error.to_s).to include("/foo.css?query-string") end end end end end roadie-4.0.0/spec/lib/roadie/inliner_spec.rb000066400000000000000000000212721355067413100207310ustar00rootroot00000000000000# frozen_string_literal: true 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