roadie-3.1.1/0000755000175000017500000000000012650430563013175 5ustar terceiroterceiroroadie-3.1.1/MIT-LICENSE0000644000175000017500000000204612650430563014633 0ustar terceiroterceiroCopyright (c) 2009 Jim Neath / Purify 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-3.1.1/metadata.yml0000644000175000017500000001472112650430563015505 0ustar terceiroterceiro--- !ruby/object:Gem::Specification name: roadie version: !ruby/object:Gem::Version version: 3.1.1 platform: ruby authors: - Magnus Bergmark autorequire: bindir: bin cert_chain: [] date: 2015-12-11 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: nokogiri requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.5.0 - - "<" - !ruby/object:Gem::Version version: 1.7.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.5.0 - - "<" - !ruby/object:Gem::Version version: 1.7.0 - !ruby/object:Gem::Dependency name: css_parser requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 1.3.4 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 1.3.4 - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '3.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '3.0' - !ruby/object:Gem::Dependency name: rspec-collection_matchers requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.0' - !ruby/object:Gem::Dependency name: webmock requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 1.21.0 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 1.21.0 description: Roadie tries to make sending HTML emails a little less painful by inlining stylesheets and rewriting relative URLs for you. email: - magnus.bergmark@gmail.com executables: [] extensions: [] extra_rdoc_files: - README.md - Changelog.md files: - ".autotest" - ".gitignore" - ".travis.yml" - ".yardopts" - Changelog.md - Gemfile - Guardfile - MIT-LICENSE - README.md - Rakefile - autotest/discover.rb - lib/roadie.rb - lib/roadie/asset_provider.rb - lib/roadie/asset_scanner.rb - lib/roadie/cached_provider.rb - lib/roadie/deduplicator.rb - lib/roadie/document.rb - lib/roadie/errors.rb - lib/roadie/filesystem_provider.rb - lib/roadie/inliner.rb - lib/roadie/markup_improver.rb - lib/roadie/net_http_provider.rb - lib/roadie/null_provider.rb - lib/roadie/null_url_rewriter.rb - lib/roadie/path_rewriter_provider.rb - lib/roadie/provider_list.rb - lib/roadie/rspec.rb - lib/roadie/rspec/asset_provider.rb - lib/roadie/rspec/cache_store.rb - lib/roadie/selector.rb - lib/roadie/style_attribute_builder.rb - lib/roadie/style_block.rb - lib/roadie/style_property.rb - lib/roadie/stylesheet.rb - lib/roadie/url_generator.rb - lib/roadie/url_rewriter.rb - lib/roadie/utils.rb - lib/roadie/version.rb - roadie.gemspec - spec/fixtures/big_em.css - spec/fixtures/stylesheets/green.css - spec/hash_as_cache_store_spec.rb - spec/integration_spec.rb - spec/lib/roadie/asset_scanner_spec.rb - spec/lib/roadie/cached_provider_spec.rb - spec/lib/roadie/css_not_found_spec.rb - spec/lib/roadie/deduplicator_spec.rb - spec/lib/roadie/document_spec.rb - spec/lib/roadie/filesystem_provider_spec.rb - spec/lib/roadie/inliner_spec.rb - spec/lib/roadie/markup_improver_spec.rb - spec/lib/roadie/net_http_provider_spec.rb - spec/lib/roadie/null_provider_spec.rb - spec/lib/roadie/null_url_rewriter_spec.rb - spec/lib/roadie/path_rewriter_provider_spec.rb - spec/lib/roadie/provider_list_spec.rb - spec/lib/roadie/selector_spec.rb - spec/lib/roadie/style_attribute_builder_spec.rb - spec/lib/roadie/style_block_spec.rb - spec/lib/roadie/style_property_spec.rb - spec/lib/roadie/stylesheet_spec.rb - spec/lib/roadie/test_provider_spec.rb - spec/lib/roadie/url_generator_spec.rb - spec/lib/roadie/url_rewriter_spec.rb - spec/lib/roadie/utils_spec.rb - spec/shared_examples/asset_provider.rb - spec/shared_examples/url_rewriter.rb - spec/spec_helper.rb - spec/support/have_attribute_matcher.rb - spec/support/have_node_matcher.rb - spec/support/have_selector_matcher.rb - spec/support/have_styling_matcher.rb - spec/support/test_provider.rb homepage: http://github.com/Mange/roadie licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '1.9' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.4.6 signing_key: specification_version: 4 summary: Making HTML emails comfortable for the Ruby rockstars test_files: - spec/fixtures/big_em.css - spec/fixtures/stylesheets/green.css - spec/hash_as_cache_store_spec.rb - spec/integration_spec.rb - spec/lib/roadie/asset_scanner_spec.rb - spec/lib/roadie/cached_provider_spec.rb - spec/lib/roadie/css_not_found_spec.rb - spec/lib/roadie/deduplicator_spec.rb - spec/lib/roadie/document_spec.rb - spec/lib/roadie/filesystem_provider_spec.rb - spec/lib/roadie/inliner_spec.rb - spec/lib/roadie/markup_improver_spec.rb - spec/lib/roadie/net_http_provider_spec.rb - spec/lib/roadie/null_provider_spec.rb - spec/lib/roadie/null_url_rewriter_spec.rb - spec/lib/roadie/path_rewriter_provider_spec.rb - spec/lib/roadie/provider_list_spec.rb - spec/lib/roadie/selector_spec.rb - spec/lib/roadie/style_attribute_builder_spec.rb - spec/lib/roadie/style_block_spec.rb - spec/lib/roadie/style_property_spec.rb - spec/lib/roadie/stylesheet_spec.rb - spec/lib/roadie/test_provider_spec.rb - spec/lib/roadie/url_generator_spec.rb - spec/lib/roadie/url_rewriter_spec.rb - spec/lib/roadie/utils_spec.rb - spec/shared_examples/asset_provider.rb - spec/shared_examples/url_rewriter.rb - spec/spec_helper.rb - spec/support/have_attribute_matcher.rb - spec/support/have_node_matcher.rb - spec/support/have_selector_matcher.rb - spec/support/have_styling_matcher.rb - spec/support/test_provider.rb roadie-3.1.1/roadie.gemspec0000644000175000017500000000214512650430563016007 0ustar terceiroterceiro# 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.0', '< 1.7.0' s.add_dependency 'css_parser', '~> 1.3.4' s.add_development_dependency 'rspec', '~> 3.0' s.add_development_dependency 'rspec-collection_matchers', '~> 1.0' s.add_development_dependency 'webmock', '~> 1.21.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.1.1/.travis.yml0000644000175000017500000000054412650430563015311 0ustar terceiroterceirosudo: false language: ruby rvm: - 1.9.3 - 2.0 - 2.1 - 2.2 - jruby - rbx cache: bundler bundler_args: --without guard script: "rake" env: # Setup Coveralls secure: "D1Uvi+a7W89k91zXVVwuuvv8O8qbDdyJ4g9i+3bGaSYySHxD8YuuG1QiQ4G/S2KLp/r3VPRpa8Wb1mSwb81tEBzXpzoZC7zSvgntPxRPhMg4zpodZ0O0AkK8/t1yZSkIe0V5sejFOQ1a5LJa3OorKBBjrqM5kPDOygTXtO3bQ6E=" roadie-3.1.1/Changelog.md0000644000175000017500000002555512650430563015422 0ustar terceiroterceiro### dev [full changelog](https://github.com/Mange/roadie/compare/v3.1.1...master) ### 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.1.1/.autotest0000644000175000017500000000036412650430563015051 0ustar terceiroterceiro# 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.1.1/Guardfile0000644000175000017500000000101012650430563015012 0ustar terceiroterceirorspec_options = { cmd: 'rspec -f documentation', failed_mode: :keep, all_after_pass: true, all_on_start: true, run_all: {cmd: 'rspec -f progress'} } guard 'rspec', rspec_options do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } watch('lib/roadie.rb') { "spec" } watch('lib/roadie/errors.rb') { "spec" } watch(%r{lib/roadie/rspec/.*\.rb}) { "spec" } watch(%r{spec/support/.*\.rb}) { "spec" } watch('spec/spec_helper.rb') { "spec" } end roadie-3.1.1/autotest/0000755000175000017500000000000012650430563015045 5ustar terceiroterceiroroadie-3.1.1/autotest/discover.rb0000644000175000017500000000004412650430563017206 0ustar terceiroterceiroAutotest.add_discovery { 'rspec2' } roadie-3.1.1/lib/0000755000175000017500000000000012650430563013743 5ustar terceiroterceiroroadie-3.1.1/lib/roadie/0000755000175000017500000000000012650430563015206 5ustar terceiroterceiroroadie-3.1.1/lib/roadie/markup_improver.rb0000644000175000017500000000536012650430563020761 0ustar terceiroterceiromodule 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?(' error errors << error end end raise ProvidersFailed.new(name, self, errors) end def to_s list = @providers.map { |provider| # Indent every line one level provider.to_s.split("\n").join("\n\t") } "ProviderList: [\n\t#{list.join(",\n\t")}\n]" end # ProviderList can be coerced to an array. This makes Array#flatten work # with it, among other things. def to_ary() to_a end # @!method each # @see Array#each # @!method size # @see Array#size # @!method empty? # @see Array#empty? # @!method push # @see Array#push # @!method << # @see Array#<< # @!method pop # @see Array#pop # @!method unshift # @see Array#unshift # @!method shift # @see Array#shift # @!method last # @see Array#last def_delegators :@providers, :each, :size, :empty?, :push, :<<, :pop, :unshift, :shift, :last end end roadie-3.1.1/lib/roadie/asset_scanner.rb0000644000175000017500000000703012650430563020363 0ustar terceiroterceiromodule Roadie # @api private # # The asset scanner's main usage is finding and/or extracting styles from a # DOM tree. Referenced styles will be found using the provided asset # provider. # # Any style declaration tagged with +data-roadie-ignore+ will be ignored, # except for having the attribute itself removed. class AssetScanner attr_reader :dom, :normal_asset_provider, :external_asset_provider # @param [Nokogiri::HTML::Document] dom # @param [#find_stylesheet!] normal_asset_provider # @param [#find_stylesheet!] external_asset_provider def initialize(dom, normal_asset_provider, external_asset_provider) @dom = dom @normal_asset_provider = normal_asset_provider @external_asset_provider = external_asset_provider end # Looks for all non-ignored stylesheets and returns them. # # This method will *not* mutate the DOM and is safe to call multiple times. # # The order of the array corresponds with the document order in the DOM. # # @see #extract_css # @return [Enumerable] 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.1.1/lib/roadie/url_generator.rb0000644000175000017500000001047012650430563020405 0ustar terceiroterceirorequire 'set' module Roadie # @api private # Class that handles URL generation # # URL generation is all about converting relative URLs into absolute URLS # according to the given options. It is written such as absolute URLs will # get passed right through, so all URLs could be passed through here. class UrlGenerator attr_reader :url_options # Create a new instance with the given URL options. # # Initializing without a host setting raises an error, as do unknown keys. # # @param [Hash] url_options # @option url_options [String] :host (required) # @option url_options [String, Integer] :port # @option url_options [String] :path root path # @option url_options [String] :scheme URL scheme ("http" is default) # @option url_options [String] :protocol alias for :scheme def initialize(url_options) raise ArgumentError, "No URL options were specified" unless url_options raise ArgumentError, "No :host was specified; options are: #{url_options.inspect}" unless url_options[:host] validate_options url_options @url_options = url_options @scheme = normalize_scheme(url_options[:scheme] || url_options[:protocol]) @root_uri = build_root_uri end # Generate an absolute URL from a relative URL. # # If the passed path is already an absolute URL or just an anchor # reference, it will be returned as-is. # If passed a blank path, the "root URL" will be returned. The root URL is # the URL that the {#url_options} would generate by themselves. # # An optional base can be specified. The base is another relative path from # the root that specifies an "offset" from which the path was found in. A # common use-case is to convert a relative path found in a stylesheet which # resides in a subdirectory. # # @example Normal conversions # generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https" # generator.generate_url("bar.html") # => "https://foo.com/bar.html" # generator.generate_url("/bar.html") # => "https://foo.com/bar.html" # generator.generate_url("") # => "https://foo.com" # # @example Conversions with a base # generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https" # generator.generate_url("../images/logo.png", "/css") # => "https://foo.com/images/logo.png" # generator.generate_url("../images/logo.png", "/assets/css") # => "https://foo.com/assets/images/logo.png" # # @param [String] base The base which the relative path comes from # @return [String] an absolute URL def generate_url(path, base = "/") return root_uri.to_s if path.nil? or path.empty? return path if path_is_anchor?(path) return add_scheme(path) if path_is_schemeless?(path) return path if Utils.path_is_absolute?(path) combine_segments(root_uri, base, path).to_s end protected attr_reader :root_uri, :scheme private def build_root_uri path = make_absolute url_options[:path] port = parse_port url_options[:port] URI::Generic.build(scheme: scheme, host: url_options[:host], port: port, path: path) end def add_scheme(path) [scheme, path].join(":") end def combine_segments(root, base, path) new_path = apply_base(base, path) if root.path new_path = File.join(root.path, new_path) end root.merge(new_path) end def apply_base(base, path) if path[0] == "/" path else File.join(base, path) end end # Strip :// from any scheme, if present def normalize_scheme(scheme) return 'http' unless scheme scheme.to_s[/^\w+/] end def parse_port(port) (port ? port.to_i : port) end def make_absolute(path) if path.nil? || path[0] == "/" path else "/#{path}" end end def path_is_schemeless?(path) path =~ %r{^//\w} end def path_is_anchor?(path) path.start_with? '#' end VALID_OPTIONS = Set[:host, :port, :path, :protocol, :scheme].freeze def validate_options(options) keys = Set.new(options.keys) unless keys.subset? VALID_OPTIONS raise ArgumentError, "Passed invalid options: #{(keys - VALID_OPTIONS).to_a}, valid options are: #{VALID_OPTIONS.to_a}" end end end end roadie-3.1.1/lib/roadie/errors.rb0000644000175000017500000000536312650430563017056 0ustar terceiroterceiromodule 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.1.1/lib/roadie/selector.rb0000644000175000017500000000401512650430563017353 0ustar terceiroterceiromodule 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.1.1/lib/roadie/net_http_provider.rb0000644000175000017500000000476112650430563021302 0ustar terceiroterceiro# encoding: UTF-8 require 'set' require 'uri' require 'net/http' require 'net/https' # For Ruby 1.9.3 support module Roadie # @api public # External asset provider that downloads stylesheets from some other server # using Ruby's built-in {Net::HTTP} library. # # You can pass a whitelist of hosts that downloads are allowed on. # # @example Allowing all downloads # provider = Roadie::NetHttpProvider.new # # @example Only allowing your own app domains # provider = Roadie::NetHttpProvider.new( # whitelist: ["myapp.com", "assets.myapp.com", "www.myapp.com"] # ) class NetHttpProvider attr_reader :whitelist # @option options [Array] :whitelist ([]) A list of host names that downloads are allowed from. Empty set means everything is allowed. def initialize(options = {}) @whitelist = host_set(Array(options.fetch(:whitelist, []))) end def find_stylesheet(url) find_stylesheet!(url) rescue CssNotFound nil end def find_stylesheet!(url) response = download(url) if response.kind_of? Net::HTTPSuccess Stylesheet.new url, response.body else raise CssNotFound.new(url, "Server returned #{response.code}: #{truncate response.body}", self) end rescue Timeout::Error raise CssNotFound.new(url, "Timeout", self) end def to_s() inspect end def inspect() "#<#{self.class} whitelist: #{whitelist.inspect}>" end private def host_set(hosts) hosts.each { |host| validate_host(host) }.to_set end def validate_host(host) if host.nil? || host.empty? || host == "." || host.include?("/") raise ArgumentError, "#{host.inspect} is not a valid hostname" end end def download(url) url = "https:#{url}" if url.start_with?("//") uri = URI.parse(url) if access_granted_to?(uri.host) get_response(uri) else raise CssNotFound.new(url, "#{uri.host} is not part of whitelist!", self) end end def get_response(uri) if RUBY_VERSION >= "2.0.0" Net::HTTP.get_response(uri) else Net::HTTP.start(uri.host, uri.port, use_ssl: (uri.scheme == 'https')) do |http| http.request(Net::HTTP::Get.new(uri.request_uri)) end end end def access_granted_to?(host) whitelist.empty? || whitelist.include?(host) end def truncate(string) if string.length > 50 string[0, 49] + "…" else string end end end end roadie-3.1.1/lib/roadie/deduplicator.rb0000644000175000017500000000167512650430563020223 0ustar terceiroterceiromodule 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.1.1/lib/roadie/path_rewriter_provider.rb0000644000175000017500000000434412650430563022331 0ustar terceiroterceiromodule Roadie # @api public # This provider acts a bit like a pipeline in normal UNIX parlour by enabling # you to make changes to the requested path. Some uses of this include: # # * Convert absolute URLs into local filesystem paths. # * Convert between external DNS name into internal naming. # * Changing path structure of filenames. # * Removing digests from filenames. # * Handle query string logic. # * Skipping known-bad paths. # # There might be other useful things you could use it for. The basic premise # is that a path is sent in to this provider, maybe modified and then passed # on to the "upstream" provider (or {ProviderList}). # # If the block returns {nil} or {false}, the upstream provider will not be # invoked and it will be treated as "not found". This makes it possible to # use this provider as a filter only. # # @example Simple regex # provider = Roadie::PathRewriterProvider.new(other_provider) { |path| # path.gsub(/-[a-f0-9]+\.css$/, '.css') # } # # @example Filtering assets # # Only assets containing "email" in the path will be considered by other_provider # only_email_provider = Roadie::PathRewriterProvider.new(other_provider) { |path| # path =~ /email/ ? path : nil # } # # @example Handling "external" app assets as local assets # document.external_asset_providers = [ # # Look for assets from "myapp.com" just like if we just specified a local path # Roadie::PathRewriterProvider.new(document.asset_providers) { |url| # uri = URI.parse(url) # uri.path if uri.host == "myapp.com" # }, # # Any other asset should be downloaded like normal # Roadie::NetHttpProvider.new # ] class PathRewriterProvider attr_reader :provider, :filter def initialize(provider, &filter) @provider = provider @filter = filter end def find_stylesheet(path) new_path = filter.call(path) provider.find_stylesheet(new_path) if new_path end def find_stylesheet!(path) new_path = filter.call(path) if new_path provider.find_stylesheet!(new_path) else raise CssNotFound, "Filter returned #{new_path.inspect}" end end end end roadie-3.1.1/lib/roadie/cached_provider.rb0000644000175000017500000000463112650430563020660 0ustar terceiroterceiromodule 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.1.1/lib/roadie/null_url_rewriter.rb0000644000175000017500000000047112650430563021314 0ustar terceiroterceiromodule Roadie # @api private # Null Object for the URL rewriter role. # # Used whenever client does not pass any URL options and no URL rewriting # should take place. class NullUrlRewriter def initialize(generator = nil) end def transform_dom(dom) end def transform_css(css) end end end roadie-3.1.1/lib/roadie/version.rb0000644000175000017500000000004612650430563017220 0ustar terceiroterceiromodule Roadie VERSION = '3.1.1' end roadie-3.1.1/lib/roadie/utils.rb0000644000175000017500000000152112650430563016672 0ustar terceiroterceiromodule 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.1.1/lib/roadie/null_provider.rb0000644000175000017500000000100512650430563020413 0ustar terceiroterceiromodule Roadie # An asset provider that returns empty stylesheets for any name. # # Use it to ignore missing assets or in your tests when you need a provider # but you do not care what it contains or that it is even referenced at all. class NullProvider def find_stylesheet(name) empty_stylesheet end def find_stylesheet!(name) empty_stylesheet end def to_s() inspect end def inspect() "#<#{self.class}>" end private def empty_stylesheet() Stylesheet.new "(null)", "" end end end roadie-3.1.1/lib/roadie/style_property.rb0000644000175000017500000000255412650430563020645 0ustar terceiroterceiromodule Roadie # @api private # Domain object for a CSS property such as "color: red !important". # # @attr_reader [String] property name of the property (such as "font-size"). # @attr_reader [String] value value of the property (such as "5px solid green"). # @attr_reader [Boolean] important if the property is "!important". # @attr_reader [Integer] specificity specificity of parent {Selector}. Used to compare/sort. class StyleProperty include Comparable attr_reader :value, :important, :specificity # @todo Rename #property to #name attr_reader :property def initialize(property, value, important, specificity) @property = property @value = value @important = important @specificity = specificity end def important? @important end # Compare another {StyleProperty}. Important styles are "greater than" # non-important ones; otherwise the specificity declares order. def <=>(other) if important == other.important specificity <=> other.specificity else important ? 1 : -1 end end def to_s [property, value_with_important].join(':') end def inspect "#{to_s} (#{specificity})" end private def value_with_important if important "#{value} !important" else value end end end end roadie-3.1.1/lib/roadie/filesystem_provider.rb0000644000175000017500000000237312650430563021636 0ustar terceiroterceiromodule 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.1.1/lib/roadie/rspec/0000755000175000017500000000000012650430563016322 5ustar terceiroterceiroroadie-3.1.1/lib/roadie/rspec/cache_store.rb0000644000175000017500000000143612650430563021132 0ustar terceiroterceiroshared_examples_for "roadie cache store" do it "allows storing Stylesheets" do stylesheet = Roadie::Stylesheet.new("foo.css", "body { color: green; }") expect(subject["foo"] = stylesheet).to eql stylesheet end it "allows retreiving stored stylesheets" do stylesheet = Roadie::Stylesheet.new("foo.css", "body { color: green; }") subject["foo"] = stylesheet stored_stylesheet = subject["foo"] expect(stored_stylesheet.to_s).to eq stylesheet.to_s end it "defaults to nil when cache does not contain path" do expect(subject["bar"]).to be_nil end it "accepts nil assignments to clear cache" do subject["foo"] = Roadie::Stylesheet.new("", "") expect { subject["foo"] = nil }.to_not raise_error expect(subject["foo"]).to be_nil end end roadie-3.1.1/lib/roadie/rspec/asset_provider.rb0000644000175000017500000000312312650430563021677 0ustar terceiroterceiroshared_examples_for "roadie asset provider" do |options| valid_name = options[:valid_name] or raise "You must provide a :valid_name option to the shared examples" invalid_name = options[:invalid_name] or raise "You must provide an :invalid_name option to the shared examples" def verify_stylesheet(stylesheet) expect(stylesheet).to_not be_nil # Name expect(stylesheet.name).to be_a(String) expect(stylesheet.name).to_not be_empty # We do not want to force clients to always return non-empty files. # Stylesheet#initialize should crash when given a non-valid CSS (like nil, # for example) # expect(stylesheet.blocks).to_not be_empty end 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 describe "#find_stylesheet" do it "can find a stylesheet" do verify_stylesheet subject.find_stylesheet(valid_name) end it "cannot find an invalid stylesheet" do expect(subject.find_stylesheet(invalid_name)).to be_nil end end describe "#find_stylesheet!" do it "can find a stylesheet" do verify_stylesheet subject.find_stylesheet!(valid_name) end it "raises Roadie::CssNotFound on invalid stylesheets" do expect { subject.find_stylesheet!(invalid_name) }.to raise_error Roadie::CssNotFound, Regexp.new(Regexp.quote(invalid_name)) end end end roadie-3.1.1/lib/roadie/rspec.rb0000644000175000017500000000011112650430563016640 0ustar terceiroterceirorequire 'roadie/rspec/asset_provider' require 'roadie/rspec/cache_store' roadie-3.1.1/lib/roadie/asset_provider.rb0000644000175000017500000000057412650430563020572 0ustar terceiroterceiromodule Roadie # This module can be included in your own code to help you implement the # standard behavior for asset providers. # # It helps you by declaring {#find_stylesheet!} in the terms of #find_stylesheet in your own class. module AssetProvider def find_stylesheet!(name) find_stylesheet(name) or raise CssNotFound.new(name, nil, self) end end end roadie-3.1.1/lib/roadie/stylesheet.rb0000644000175000017500000000410612650430563017725 0ustar terceiroterceiromodule 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 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) 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.1.1/lib/roadie/inliner.rb0000644000175000017500000000716412650430563017203 0ustar terceiroterceirorequire 'set' require 'nokogiri' require 'uri' require 'css_parser' module Roadie # @api private # The Inliner inlines stylesheets to the elements of the DOM. # # Inlining means that {StyleBlock}s and a DOM tree are combined: # a { color: red; } # StyleBlock # # DOM # # becomes # # class Inliner # @param [Array] stylesheets the stylesheets to use in the inlining # @param [Nokogiri::HTML::Document] dom def initialize(stylesheets, dom) @stylesheets = stylesheets @dom = dom end # Start the inlining, mutating the DOM tree. # # @param [true, false] keep_extra_blocks # @return [nil] def inline(keep_extra_blocks = true) style_map, extra_blocks = consume_stylesheets apply_style_map(style_map) add_styles_to_head(extra_blocks) if keep_extra_blocks nil end protected attr_reader :stylesheets, :dom private def consume_stylesheets style_map = StyleMap.new extra_blocks = [] each_style_block do |stylesheet, block| if (elements = selector_elements(stylesheet, block)) style_map.add elements, block.properties else extra_blocks << block end end [style_map, extra_blocks] end def each_style_block stylesheets.each do |stylesheet| stylesheet.blocks.each do |block| yield stylesheet, block end end end def selector_elements(stylesheet, block) block.inlinable? && elements_matching_selector(stylesheet, block.selector) end def apply_style_map(style_map) style_map.each_element { |element, builder| apply_element_style(element, builder) } end def apply_element_style(element, builder) element["style"] = [builder.attribute_string, element["style"]].compact.join(";") end def elements_matching_selector(stylesheet, selector) dom.css(selector.to_s) # There's no way to get a list of supported pseudo selectors, so we're left # with having to rescue errors. # Pseudo selectors that are known to be bad are skipped automatically but # this will catch the rest. rescue Nokogiri::XML::XPath::SyntaxError, Nokogiri::CSS::SyntaxError => error Utils.warn "Cannot inline #{selector.inspect} from \"#{stylesheet.name}\" stylesheet. If this is valid CSS, please report a bug." nil rescue => error Utils.warn "Got error when looking for #{selector.inspect} (from \"#{stylesheet.name}\" stylesheet): #{error}" raise unless error.message.include?('XPath') nil end def add_styles_to_head(blocks) unless blocks.empty? create_style_element(blocks, find_head) end end def find_head dom.at_xpath('html/head') end def create_style_element(style_blocks, head) return unless head element = Nokogiri::XML::Node.new("style", head.document) element.content = style_blocks.join("\n") head.add_child(element) end # @api private # StyleMap is a map between a DOM element and {StyleAttributeBuilder}. Basically, # it's an accumulator for properties, scoped on specific elements. class StyleMap def initialize @map = Hash.new { |hash, key| hash[key] = StyleAttributeBuilder.new } end def add(elements, new_properties) Array(elements).each do |element| new_properties.each do |property| @map[element] << property end end end def each_element(&block) @map.each_pair(&block) end end end end roadie-3.1.1/lib/roadie/style_attribute_builder.rb0000644000175000017500000000125212650430563022464 0ustar terceiroterceiromodule 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.1.1/lib/roadie/style_block.rb0000644000175000017500000000156412650430563020053 0ustar terceiroterceirorequire 'forwardable' module Roadie # @api private # A style block is the combination of a {Selector} and a list of {StyleProperty}. class StyleBlock extend Forwardable attr_reader :selector, :properties # @param [Selector] selector # @param [Array] properties def initialize(selector, properties) @selector = selector @properties = properties end # @!method specificity # @see Selector#specificity # @!method inlinable? # @see Selector#inlinable? def_delegators :selector, :specificity, :inlinable? # @!method selector_string # @see Selector#to_s def_delegator :selector, :to_s, :selector_string # String representation of the style block. This is valid CSS and can be # used in the DOM. def to_s "#{selector}{#{properties.map(&:to_s).join(';')}}" end end end roadie-3.1.1/lib/roadie/url_rewriter.rb0000644000175000017500000000530612650430563020264 0ustar terceiroterceiromodule Roadie # @api private # # Class that rewrites URLs in the DOM. class UrlRewriter # @param [UrlGenerator] generator def initialize(generator) @generator = generator end # Mutates the passed DOM tree, rewriting certain element's attributes. # # This will make all a[href] and img[src] into absolute URLs, as well as # all "url()" directives inside style-attributes. # # [nil] is returned so no one can misunderstand that this method mutates # the passed instance. # # @param [Nokogiri::HTML::Document] dom # @return [nil] DOM tree is mutated def transform_dom(dom) # Use only a single loop to do this dom.css("a[href], img[src], *[style]").each do |element| transform_element_style element if element.has_attribute?('style') transform_element element end nil end # Mutates passed CSS, rewriting url() directives. # # This will make all URLs inside url() absolute. # # [nil] is returned so no one can misunderstand that this method mutates # the passed string. # # @param [String] css the css to mutate # @return [nil] css is mutated def transform_css(css) css.gsub!(CSS_URL_REGEXP) do matches = Regexp.last_match "url(#{matches[:quote]}#{generate_url(matches[:url])}#{matches[:quote]})" end end private def generate_url(*args) @generator.generate_url(*args) end # Regexp matching all the url() declarations in CSS # # It matches without any quotes and with both single and double quotes # inside the parenthesis. There's much room for improvement, of course. CSS_URL_REGEXP = %r{ url\( (? (?:["']|%22)? # Optional opening quote ) (? # The URL. # We match URLs with parenthesis inside it here, # so url(foo(bar)baz) will match correctly. [^(]* # Text leading up to before opening parens (?:\([^)]*\))* # Texts containing parens pairs [^(]+ # Texts without parens - required ) \k'quote' # Closing quote \) }x def transform_element(element) case element.name when "a" then element["href"] = generate_url element["href"] when "img" then element["src"] = generate_url element["src"] end end def transform_element_style(element) # We need to use a setter for Nokogiri to detect the string mutation. # If nokogiri used "dumber" data structures, this would all be redundant. css = element["style"] transform_css css element["style"] = css end end end roadie-3.1.1/lib/roadie/document.rb0000644000175000017500000001032212650430563017347 0ustar terceiroterceiromodule 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 1.9.3 * MRI 2.0 * MRI 2.1 * MRI 2.2 * JRuby (latest) * Rubinius >= 2.1 [(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 "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 end roadie-3.1.1/spec/support/0000755000175000017500000000000012650430563015643 5ustar terceiroterceiroroadie-3.1.1/spec/support/test_provider.rb0000644000175000017500000000042212650430563021057 0ustar terceiroterceiroclass 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.1.1/spec/support/have_node_matcher.rb0000644000175000017500000000117612650430563021630 0ustar terceiroterceiroRSpec::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.1.1/spec/support/have_styling_matcher.rb0000644000175000017500000000300612650430563022366 0ustar terceiroterceiroRSpec::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.1.1/spec/support/have_attribute_matcher.rb0000644000175000017500000000153012650430563022700 0ustar terceiroterceiroRSpec::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.1.1/spec/support/have_selector_matcher.rb0000644000175000017500000000045612650430563022523 0ustar terceiroterceiroRSpec::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.1.1/spec/lib/0000755000175000017500000000000012650430563014675 5ustar terceiroterceiroroadie-3.1.1/spec/lib/roadie/0000755000175000017500000000000012650430563016140 5ustar terceiroterceiroroadie-3.1.1/spec/lib/roadie/null_url_rewriter_spec.rb0000644000175000017500000000071412650430563023260 0ustar terceiroterceiro# 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.1.1/spec/lib/roadie/net_http_provider_spec.rb0000644000175000017500000001054312650430563023241 0ustar terceiroterceirorequire 'spec_helper' require 'roadie/rspec' require 'shared_examples/asset_provider' module Roadie describe NetHttpProvider do around do |example| WebMock.disable_net_connect! example.run WebMock.allow_net_connect! end url = "http://example.com/style.css".freeze it_behaves_like( "roadie asset provider", valid_name: "http://example.com/green.css", invalid_name: "http://example.com/red.css" ) do before do stub_request(:get, "http://example.com/green.css").and_return(body: "p { color: green; }") stub_request(:get, "http://example.com/red.css").and_return(status: 404, body: "Not here!") end end it "can download over HTTPS" do stub_request(:get, "https://example.com/style.css").and_return(body: "p { color: green; }") expect { NetHttpProvider.new.find_stylesheet!("https://example.com/style.css") }.to_not raise_error end it "assumes HTTPS when given a scheme-less URL" do # Some people might re-use the same template as they use on a webpage, # and browsers support URLs without a scheme in them, replacing the # scheme with the current one. There's no "current" scheme when doing # asset inlining, but the scheme-less URL implies that there should exist # both a HTTP and a HTTPS endpoint. Let's take the secure one in that # case! stub_request(:get, "https://example.com/style.css").and_return(body: "p { color: green; }") expect { NetHttpProvider.new.find_stylesheet!("//example.com/style.css") }.to_not raise_error end describe "error handling" do it "handles timeouts" do stub_request(:get, url).and_timeout expect { NetHttpProvider.new.find_stylesheet!(url) }.to raise_error CssNotFound, /timeout/i expect { NetHttpProvider.new.find_stylesheet(url) }.to_not raise_error end it "displays response code and beginning of message body" do stub_request(:get, url).and_return( status: 503, body: "Whoah there! Didn't you see we have a service window at this time? It's kind of disrespectful not to remember everything I tell you all the time!" ) expect { NetHttpProvider.new.find_stylesheet!(url) }.to raise_error CssNotFound, /503.*whoah/i end end describe "whitelist" do it "can have a whitelist of host names" do provider = NetHttpProvider.new(whitelist: ["example.com", "foo.bar"]) expect(provider.whitelist).to eq Set["example.com", "foo.bar"] end it "defaults to empty whitelist" do expect(NetHttpProvider.new.whitelist).to eq Set[] expect(NetHttpProvider.new(whitelist: nil).whitelist).to eq Set[] end it "will not download from other hosts if set" do provider = NetHttpProvider.new(whitelist: ["whitelisted.example.com"]) whitelisted_url = "http://whitelisted.example.com/style.css" other_url = "http://www.example.com/style.css" whitelisted_request = stub_request(:get, whitelisted_url).and_return(body: "x") other_request = stub_request(:get, other_url).and_return(body: "x") expect(provider.find_stylesheet(other_url)).to be_nil expect { provider.find_stylesheet!(other_url) }.to raise_error CssNotFound, /whitelist/ expect { expect(provider.find_stylesheet(whitelisted_url)).to_not be_nil provider.find_stylesheet!(whitelisted_url) }.to_not raise_error expect(whitelisted_request).to have_been_made.twice expect(other_request).to_not have_been_made end it "is displayed in the string representation" do expect(NetHttpProvider.new(whitelist: ["bar.baz"]).to_s).to include "bar.baz" end it "raises error when given invalid hostnames" do expect { NetHttpProvider.new(whitelist: [nil]) }.to raise_error(ArgumentError) expect { NetHttpProvider.new(whitelist: [""]) }.to raise_error(ArgumentError) expect { NetHttpProvider.new(whitelist: ["."]) }.to raise_error(ArgumentError) expect { NetHttpProvider.new(whitelist: ["http://foo.bar"]) }.to raise_error(ArgumentError) expect { NetHttpProvider.new(whitelist: ["foo/bar"]) }.to raise_error(ArgumentError) end end end end roadie-3.1.1/spec/lib/roadie/url_rewriter_spec.rb0000644000175000017500000000535312650430563022232 0ustar terceiroterceirorequire 'spec_helper' require 'shared_examples/url_rewriter' module Roadie describe UrlRewriter do let(:generator) { double("URL generator") } subject(:rewriter) { UrlRewriter.new(generator) } it_behaves_like "url rewriter" describe "transforming DOM trees" do def dom_document(html); Nokogiri::HTML.parse html; end it "rewrites all a[href]" do expect(generator).to receive(:generate_url).with("some/path").and_return "http://foo.com/" dom = dom_document <<-HTML Some path HTML expect { rewriter.transform_dom dom }.to change { dom.at_css("a")["href"] }.to "http://foo.com/" end it "rewrites relative img[src]" do expect(generator).to receive(:generate_url).with("some/path.jpg").and_return "http://foo.com/image.jpg" dom = dom_document <<-HTML HTML expect { rewriter.transform_dom dom }.to change { dom.at_css("img")["src"] }.to "http://foo.com/image.jpg" end it "rewrites url() directives inside style attributes" do expect(generator).to receive(:generate_url).with("some/path.jpg").and_return "http://foo.com/image.jpg" dom = dom_document <<-HTML

HTML expect { rewriter.transform_dom dom }.to change { dom.at_css("div")["style"] }.to 'background-image: url("http://foo.com/image.jpg");' end end describe "transforming css" do it "rewrites all url() directives" do expect(generator).to receive(:generate_url).with("some/path.jpg").and_return "http://foo.com/image.jpg" css = "body { background: top url(some/path.jpg) #eee; }" expect { rewriter.transform_css css }.to change { css }.to "body { background: top url(http://foo.com/image.jpg) #eee; }" end it "correctly identifies URLs with single quotes" do expect(generator).to receive(:generate_url).with("images/foo.png").and_return "x" rewriter.transform_css "url('images/foo.png')" end it "correctly identifies URLs with double quotes" do expect(generator).to receive(:generate_url).with("images/foo.png").and_return "x" rewriter.transform_css 'url("images/foo.png")' end it "correctly identifies URLs with parenthesis inside them" do expect(generator).to receive(:generate_url).with("images/map_(large_(extra)).png").and_return "x" rewriter.transform_css 'url(images/map_(large_(extra)).png)' end end end end roadie-3.1.1/spec/lib/roadie/cached_provider_spec.rb0000644000175000017500000000301312650430563022615 0ustar terceiroterceirorequire '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.1.1/spec/lib/roadie/asset_scanner_spec.rb0000644000175000017500000001737012650430563022337 0ustar terceiroterceiro# encoding: UTF-8 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 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.1.1/spec/lib/roadie/utils_spec.rb0000644000175000017500000000172012650430563020637 0ustar terceiroterceirorequire "spec_helper" module Roadie describe Utils, "path_is_absolute?" do RSpec::Matchers.define :be_absolute do match { |path| Utils.path_is_absolute?(path) } end it "detects absolute HTTP URLs" do expect("http://example.com").to be_absolute expect("https://example.com").to be_absolute expect("https://example.com/path?foo=bar").to be_absolute end it "detects absolute URLs without schemes" do expect("//example.com").to be_absolute expect("//").to be_absolute end it "detects relative URLs without hosts" do expect("path/to/me").not_to be_absolute expect("/path/to/me").not_to be_absolute expect("../../path").not_to be_absolute expect("/").not_to be_absolute end end describe Utils, "warn" do it "passes the message on to Kernel.warn" do expect(Kernel).to receive(:warn).with("Roadie: Hello from specs") Utils.warn "Hello from specs" end end end roadie-3.1.1/spec/lib/roadie/deduplicator_spec.rb0000644000175000017500000000106412650430563022157 0ustar terceiroterceirorequire "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.1.1/spec/lib/roadie/path_rewriter_provider_spec.rb0000644000175000017500000000236112650430563024272 0ustar terceiroterceirorequire '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.1.1/spec/lib/roadie/style_block_spec.rb0000644000175000017500000000211312650430563022006 0ustar terceiroterceiro# 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.1.1/spec/lib/roadie/stylesheet_spec.rb0000644000175000017500000000416112650430563021672 0ustar terceiroterceiro# 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 end end roadie-3.1.1/spec/lib/roadie/url_generator_spec.rb0000644000175000017500000001514512650430563022355 0ustar terceiroterceiro# encoding: UTF-8 require 'spec_helper' module Roadie describe UrlGenerator do it "is initialized with URL options" do expect(UrlGenerator.new(host: "foo.com").url_options).to eq({host: "foo.com"}) end it "raises an argument error if no URL options are specified" do expect { UrlGenerator.new(nil) }.to raise_error ArgumentError, /url options/i end it "raises an argument error if no host is specified" do expect { UrlGenerator.new(port: 3000) }.to raise_error ArgumentError, /host/i end it "raises an argument error if unknown option is passed" do expect { UrlGenerator.new(host: "localhost", secret: true) }.to raise_error ArgumentError, /secret/ end describe "generating URLs" do def url(path, options = {}) UrlGenerator.new(options).generate_url(path) end it "uses the given host" do expect(url("/hello.jpg", host: "goats.com")).to eq("http://goats.com/hello.jpg") end it "uses the given port" do expect(url("/", host: "example.com", port: 1337)).to eq("http://example.com:1337/") end it "uses the given scheme" do expect(url("/", host: "example.com", scheme: "https")).to eq("https://example.com/") end it "regards :protocol as an alias for scheme" do expect(url("/", host: "example.com", protocol: "https")).to eq("https://example.com/") end it "strips extra characters from the scheme" do expect(url("/", host: "example.com", scheme: "https://")).to eq("https://example.com/") end it "uses the given path as a prefix" do expect(url("/my_file", host: "example.com", path: "/my_app")).to eq( "http://example.com/my_app/my_file" ) end it "returns the original URL if it is absolute" do expect(url("http://foo.com/", host: "bar.com")).to eq("http://foo.com/") end it "returns the original URL if it is just an anchor" do expect(url("#top", host: "bar.com")).to eq("#top") end it "returns the base URL for blank paths" do expect(url("", host: "foo.com")).to eq("http://foo.com") expect(url(nil, host: "foo.com")).to eq("http://foo.com") end it "raises an error on invalid path" do expect { url("://", host: "example.com") }.to raise_error InvalidUrlPath, %r{://} end it "accepts base paths without a slash in the beginning" do expect(url("/bar", host: "example.com", path: "foo")).to eq("http://example.com/foo/bar") expect(url("/bar/", host: "example.com", path: "foo/")).to eq("http://example.com/foo/bar/") end it "accepts input paths without a slash in the beginning" do expect(url("bar", host: "example.com", path: "/foo")).to eq("http://example.com/foo/bar") expect(url("bar", host: "example.com", path: "/foo/")).to eq("http://example.com/foo/bar") end it "does not touch data: URIs" do # We've had failures with longer data URIs, but I have not been able to # pinpoint where the problem is. I suspect a specific version of Ruby. # This test might not actually catch the real issues since I couldn't # get it red for the reported cases. # It was solved by being more relaxed when determining if a URI is # absolute or not. data_uri = "data:image/png;dead/beef+/==" expect(url(data_uri, host: "example.com")).to eq(data_uri) end it "does not touch custom schemes" do expect(url("myapp://", host: "example.com")).to eq("myapp://") end it "does not care if absolute URLs have parse errors" do # Pipe character is invalid inside URLs, but that does not stop a whole # lot of templating/emailing systems for using them as template # markers. expect(url("https://foo.com/%|MARKETING_TOKEN|%", host: "example.com")).to eq("https://foo.com/%|MARKETING_TOKEN|%") end # A lot of email clients do not support schemeless URLs (it's a HTML5 # feature) so we should add a scheme to them. context "on schemeless urls" do # Checking for host matches would be too complex, and it's not too hard # to assume that schemeless URLs to assets comes from a shared # configuration with a web page which uses HTTP and HTTPS in different # cases. That also means that we'd like to match the assets URLs with # whatever we want to link to, most likely. it "adds given scheme, even when host does not match" do result = url("//assets.myapp.com/foo.jpg", host: "example.com", scheme: "https") expect(result).to eq("https://assets.myapp.com/foo.jpg") end it "adds standard http: scheme when no scheme given" do result = url("//assets.myapp.com/foo.jpg", host: "example.com") expect(result).to eq("http://assets.myapp.com/foo.jpg") end it "adds scheme to invalid URLs" do result = url("//foo.com/%|TOKEN|%", scheme: "ftp", host: "example.com") expect(result).to eq("ftp://foo.com/%|TOKEN|%") end end end # URLs in resources that are not based inside the root requires that we may # specify a "custom base" to properly handle relative paths. Here's an # example: # # # / # # document.html # # images/ # # bg.png # # stylesheets/ # # my_style.css # # # stylesheets/my_style.css # body { background-image: url(../images/bg.png); } # # In this example, the stylesheet refers to /images/bg.png by using a # relative path from /stylesheets/. In order to understand these cases, we # need to specify where the "base" is in relation to the root. describe "generating URLs with custom base" do it "resolves relative paths" do generator = UrlGenerator.new(host: "foo.com") expect(generator.generate_url("../images/bg.png", "/stylesheets")).to eq("http://foo.com/images/bg.png") expect(generator.generate_url("../images/bg.png", "email/stylesheets")).to eq("http://foo.com/email/images/bg.png") expect(generator.generate_url("images/bg.png", "email/")).to eq("http://foo.com/email/images/bg.png") end it "does not use the base when presented with a root-based path" do generator = UrlGenerator.new(host: "foo.com") expect(generator.generate_url("/images/bg.png", "/stylesheets")).to eq("http://foo.com/images/bg.png") expect(generator.generate_url("/images/bg.png", "email/stylesheets")).to eq("http://foo.com/images/bg.png") end end end end roadie-3.1.1/spec/lib/roadie/provider_list_spec.rb0000644000175000017500000001007512650430563022367 0ustar terceiroterceiro# encoding: UTF-8 require 'spec_helper' require 'roadie/rspec' module Roadie describe ProviderList do let(:test_provider) { TestProvider.new } subject(:provider) { ProviderList.new([test_provider]) } it_behaves_like "roadie asset provider", valid_name: "valid", invalid_name: "invalid" do let(:test_provider) { TestProvider.new "valid" => "" } end it "finds using all given providers" do first = TestProvider.new "foo.css" => "foo { color: green; }" second = TestProvider.new "bar.css" => "bar { color: green; }" provider = ProviderList.new [first, second] expect(provider.find_stylesheet("foo.css").to_s).to include "foo" expect(provider.find_stylesheet("bar.css").to_s).to include "bar" expect(provider.find_stylesheet("baz.css")).to be_nil end it "is enumerable" do expect(provider).to be_kind_of(Enumerable) expect(provider).to respond_to(:each) expect(provider.each.to_a).to eq([test_provider]) end it "has a size" do expect(provider.size).to eq(1) expect(provider).not_to be_empty end it "has a first and a last element" do providers = [double("1"), double("2"), double("3")] list = ProviderList.new(providers) expect(list.first).to eq(providers.first) expect(list.last).to eq(providers.last) end it "can have providers pushed and popped" do other = double "Some other provider" expect { provider.push other provider << other }.to change(provider, :size).by(2) expect { expect(provider.pop).to eq(other) }.to change(provider, :size).by(-1) end it "can have providers shifted and unshifted" do other = double "Some other provider" expect { provider.unshift other }.to change(provider, :size).by(1) expect { expect(provider.shift).to eq(other) }.to change(provider, :size).by(-1) end it "has a readable string represenation" do provider = double("Provider", to_s: "Some provider") sublist = ProviderList.new([provider, provider]) list = ProviderList.new([provider, sublist, provider]) expect(list.to_s).to eql( "ProviderList: [\n" + "\tSome provider,\n" + "\tProviderList: [\n" + "\t\tSome provider,\n" + "\t\tSome provider\n" + "\t],\n" + "\tSome provider\n" + "]" ) end it "raises a readable error message" do provider = double("Provider", to_s: "Some provider") allow(provider).to receive(:find_stylesheet!).and_raise( CssNotFound.new("style.css", "I tripped", provider) ) sublist = ProviderList.new([provider, provider]) list = ProviderList.new([provider, sublist, provider]) expect { list.find_stylesheet!("style.css") }.to raise_error { |error| expect(error.message).to eq( "Could not find stylesheet \"style.css\": All providers failed\n" + "Used providers:\n" + "\tSome provider: I tripped\n" + "\tSome provider: I tripped\n" + "\tSome provider: I tripped\n" + "\tSome provider: I tripped\n" ) } end describe "wrapping" do it "creates provider lists with the arguments" do expect(ProviderList.wrap(test_provider)).to be_instance_of(ProviderList) expect(ProviderList.wrap(test_provider, test_provider).size).to eq(2) end it "flattens arrays" do expect(ProviderList.wrap([test_provider, test_provider], test_provider).size).to eq(3) expect(ProviderList.wrap([test_provider, test_provider]).size).to eq(2) end it "combines with providers from other lists" do other_list = ProviderList.new([test_provider, test_provider]) expect(ProviderList.wrap(test_provider, other_list).size).to eq(3) end it "returns the passed list if only a single ProviderList is passed" do other_list = ProviderList.new([test_provider]) expect(ProviderList.wrap(other_list)).to eql other_list end end end end roadie-3.1.1/spec/lib/roadie/filesystem_provider_spec.rb0000644000175000017500000000521112650430563023574 0ustar terceiroterceiro# encoding: UTF-8 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-3.1.1/spec/lib/roadie/markup_improver_spec.rb0000644000175000017500000000547712650430563022736 0ustar terceiroterceiro# 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.1.1/spec/lib/roadie/null_provider_spec.rb0000644000175000017500000000115112650430563022361 0ustar terceiroterceiro# 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.1.1/spec/lib/roadie/style_attribute_builder_spec.rb0000644000175000017500000000257012650430563024434 0ustar terceiroterceirorequire 'spec_helper' module Roadie describe StyleAttributeBuilder do it "sorts the added properties" do builder = StyleAttributeBuilder.new builder << StyleProperty.new("color", "green", true, 1) builder << StyleProperty.new("font-size", "110%", false, 15) builder << StyleProperty.new("color", "red", false, 15) expect(builder.attribute_string).to eq "font-size:110%;color:red;color:green !important" end it "preserves the order of added attributes with the same specificity" do builder = StyleAttributeBuilder.new builder << StyleProperty.new("color", "pink", false, 50) builder << StyleProperty.new("color", "red", false, 50) builder << StyleProperty.new("color", "green", false, 50) # We need one different element to trigger the problem with Ruby's # unstable sort builder << StyleProperty.new("background", "white", false, 1) expect(builder.attribute_string).to eq "background:white;color:pink;color:red;color:green" end it "removes duplicate properties" do builder = StyleAttributeBuilder.new builder << StyleProperty.new("color", "pink", false, 10) builder << StyleProperty.new("color", "green", false, 20) builder << StyleProperty.new("color", "pink", false, 50) expect(builder.attribute_string).to eq "color:green;color:pink" end end end roadie-3.1.1/spec/lib/roadie/document_spec.rb0000644000175000017500000001215612650430563021322 0ustar terceiroterceiro# 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.1.1/spec/lib/roadie/selector_spec.rb0000644000175000017500000000343012650430563021317 0ustar terceiroterceiro# 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.1.1/spec/lib/roadie/css_not_found_spec.rb0000644000175000017500000000141512650430563022343 0ustar terceiroterceirorequire '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.1.1/spec/lib/roadie/style_property_spec.rb0000644000175000017500000000344212650430563022606 0ustar terceiroterceirorequire '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.1.1/spec/lib/roadie/inliner_spec.rb0000644000175000017500000001736612650430563021154 0ustar terceiroterceiro# 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