Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filters Manage Dependencies #80

Merged
merged 10 commits into from
Oct 23, 2013
21 changes: 18 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
source 'https://rubygems.org'
source "https://rubygems.org"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor pet peeve, but it's nice to keep pull requests focused on the changes described. These formatting edits make it trickier to review PRs. Don't worry about it for this PR, but just a heads up 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry and sure thing!


# Specify your gem's dependencies in html-pipeline.gemspec
gemspec

group :development do
gem 'bundler'
gem 'rake'
gem "bundler"
gem "rake"
end

group :test do
gem "rinku", "~> 1.7", :require => false
gem "gemoji", "~> 1.0", :require => false
gem "RedCloth", "~> 4.2.9", :require => false
gem "escape_utils", "~> 0.3", :require => false
gem "github-linguist", "~> 2.6.2", :require => false
gem "github-markdown", "~> 0.5", :require => false

if RUBY_VERSION < "1.9.2"
gem "sanitize", ">= 2", "< 2.0.4", :require => false
else
gem "sanitize", "~> 2.0", :require => false
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move these into their own platform blocks. Search for :platforms in the Gemfile documentation for details.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried platforms two ways:

  platforms :ruby_18 do
    gem "sanitize", ">= 2", "< 2.0.4", :require => false
  end

  platforms :ruby_19, :ruby_20 do
    gem "sanitize", "~> 2.0", :require => false
  end
  gem "sanitize", ">= 2", "< 2.0.4", :require => false, :platforms => :ruby_18
  gem "sanitize", "~> 2.0",          :require => false, :platforms => [:ruby_19, :ruby_20]

Bundler doesn't like either way.

❯ bundle install
You cannot specify the same gem twice with different version requirements. 
You specified: sanitize (< 2.0.4, >= 2) and sanitize (~> 2.0)

I think we should stick with the RUBY_VERSION conditional. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference: rubygems/bundler#751

end
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,29 @@ filter.call
* `TextileFilter` - convert textile to html
* `TableOfContentsFilter` - anchor headings with name attributes and generate Table of Contents html unordered list linking headings

## Syntax highlighting
## Dependencies

Filter gem dependencies are not bundled; you must bundle the filter's gem
dependencies. The below list details filters with dependencies. For example,
`SyntaxHighlightFilter` uses [github-linguist](https://github.com/github/linguist)
to detect and highlight languages. It isn't included as a dependency by default
because it's a large dependency and
[a hassle to build on heroku](https://github.com/jch/html-pipeline/issues/33).
To use the filter, add the following to your Gemfile:
to detect and highlight languages. To use the `SyntaxHighlightFilter`,
add the following to your Gemfile:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, to use the SyntaxHighlightFilter, add the following to your Gemfile:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice edit.


```ruby
gem 'github-linguist'
```

* `AutolinkFilter` - `rinku`
* `EmailReplyFilter` - `escape_utils`
* `EmojiFilter` - `gemoji`
* `MarkdownFilter` - `github-markdown`
* `PlainTextInputFilter` - `escape_utils`
* `SanitizationFilter` - `sanitize`
* `SyntaxHighlightFilter` - `github-linguist`
* `TextileFilter` - `RedCloth`

_Note:_ See [Gemfile](https://github.com/jch/html-pipeline/blob/master/Gemfile) `:test` block for version requirements.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub markdown supports relative links now. So you can just do /Gemfile.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.


## Examples

We define different pipelines for different parts of our app. Here are a few
Expand Down
18 changes: 11 additions & 7 deletions html-pipeline.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ Gem::Specification.new do |gem|
gem.test_files = gem.files.grep(%r{^test})
gem.require_paths = ["lib"]

gem.add_dependency "gemoji", "~> 1.0"
gem.add_dependency "nokogiri", RUBY_VERSION < "1.9.2" ? [">= 1.4", "< 1.6"] : "~> 1.4"
gem.add_dependency "github-markdown", "~> 0.5"
gem.add_dependency "sanitize", RUBY_VERSION < "1.9.2" ? [">= 2", "< 2.0.4"] : "~> 2.0"
gem.add_dependency "rinku", "~> 1.7"
gem.add_dependency "escape_utils", "~> 0.3"
gem.add_dependency "nokogiri", RUBY_VERSION < "1.9.2" ? [">= 1.4", "< 1.6"] : "~> 1.4"

gem.add_development_dependency "activesupport", RUBY_VERSION < "1.9.3" ? [">= 2", "< 4"] : ">= 2"
gem.add_development_dependency "github-linguist", "~> 2.6.2"

gem.post_install_message = <<msg
----------------------------------------------
Thank you for installing html-pipeline!
Filter gem dependencies are no longer bundled.
You must bundle Filter gem dependencies.
See html-pipeline README.md for more details.
https://github.com/jch/html-pipeline
----------------------------------------------
msg
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice touch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, since we're not 1.x yet, I'd remove the Filter gem dependencies are no longer bundled.. I'd also update the hyperlink url to https://github.com/jch/html-pipeline#dependencies

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I always forget this is available.

end
1 change: 0 additions & 1 deletion lib/html/pipeline.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require "nokogiri"
require "active_support/xml_mini/nokogiri" # convert Documents to hashes
require "escape_utils"

module HTML
# GitHub HTML processing filters and utilities. This module includes a small
Expand Down
7 changes: 6 additions & 1 deletion lib/html/pipeline/autolink_filter.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
require 'rinku'
begin
require "rinku"
rescue LoadError => e
missing = HTML::Pipeline::Filter::MissingDependencyException
raise missing, missing::MESSAGE % "rinku", e.backtrace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's overkill to have a custom exception class here because we want to exit as early as possible. The stack trace useful because it's not a programming error, it's a installation error. I'd prefer having the error string directly and calling exit with a non-zero exit code:

begin
  require 'rinku'
rescue LoadError => e
  $stderr.puts "Missing dependency 'rinku' for AutolinkFilter. See README.md for details"
  exit 1
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make this simpler:

begin
  require 'rinku'
rescue LoadError => e
  abort "Missing dependency 'rinku' for AutolinkFilter. See README.md for details"
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ learn me something new today.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jch Agreed. Glad we're going this route; it's straightforward.

@rsanheim Thanks for the abort tip!

end

module HTML
class Pipeline
Expand Down
9 changes: 8 additions & 1 deletion lib/html/pipeline/email_reply_filter.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
begin
require "escape_utils"
rescue LoadError => e
missing = HTML::Pipeline::Filter::MissingDependencyException
raise missing, missing::MESSAGE % "escape_utils", e.backtrace
end

module HTML
class Pipeline
# HTML Filter that converts email reply text into an HTML DocumentFragment.
Expand Down Expand Up @@ -53,4 +60,4 @@ def call
end
end
end
end
end
9 changes: 7 additions & 2 deletions lib/html/pipeline/emoji_filter.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
require 'emoji'
begin
require "gemoji"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gemoji.rb is just a stub for emoji.rb. Changing this back to require "emoji" will eliminate a require. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave it alone in case the gem ever decides to load more things in gemoji.rb.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

rescue LoadError => e
missing = HTML::Pipeline::Filter::MissingDependencyException
raise missing, missing::MESSAGE % "gemoji", e.backtrace
end

module HTML
class Pipeline
Expand Down Expand Up @@ -51,4 +56,4 @@ def asset_root
end
end
end
end
end
17 changes: 17 additions & 0 deletions lib/html/pipeline/filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ class Pipeline
# Each filter may define additional options and output values. See the class
# docs for more info.
class Filter
# Public: Custom Exception raised when a Filter dependency is not installed.
#
# Examples
#
# begin
# require "rinku"
# rescue LoadError => e
# missing = HTML::Pipeline::Filter::MissingDependencyException
# raise missing, missing::MESSAGE % "rinku", e.backtrace
# end
class MissingDependencyException < StandardError
# Public: Format String for MissingDependencyException message.
MESSAGE = "Missing html-pipeline dependency: " +
"Please add `%s` to your Gemfile; " +
"see html-pipeline Gemfile for version."
end

class InvalidDocumentException < StandardError; end

def initialize(doc, context = nil, result = nil)
Expand Down
9 changes: 7 additions & 2 deletions lib/html/pipeline/markdown_filter.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
require 'github/markdown'
begin
require "github/markdown"
rescue LoadError => e
missing = HTML::Pipeline::Filter::MissingDependencyException
raise missing, missing::MESSAGE % "github-markdown", e.backtrace
end

module HTML
class Pipeline
Expand Down Expand Up @@ -26,4 +31,4 @@ def call
end
end
end
end
end
9 changes: 8 additions & 1 deletion lib/html/pipeline/plain_text_input_filter.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
begin
require "escape_utils"
rescue LoadError => e
missing = HTML::Pipeline::Filter::MissingDependencyException
raise missing, missing::MESSAGE % "escape_utils", e.backtrace
end

module HTML
class Pipeline
# Simple filter for plain text input. HTML escapes the text input and wraps it
Expand All @@ -8,4 +15,4 @@ def call
end
end
end
end
end
7 changes: 6 additions & 1 deletion lib/html/pipeline/sanitization_filter.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
require 'sanitize'
begin
require "sanitize"
rescue LoadError => e
missing = HTML::Pipeline::Filter::MissingDependencyException
raise missing, missing::MESSAGE % "sanitize", e.backtrace
end

module HTML
class Pipeline
Expand Down
9 changes: 5 additions & 4 deletions lib/html/pipeline/syntax_highlight_filter.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
begin
require 'linguist'
rescue LoadError
raise LoadError, "You need to install 'github-linguist' before using the SyntaxHighlightFilter. See README.md for details"
require "linguist"
rescue LoadError => e
missing = HTML::Pipeline::Filter::MissingDependencyException
raise missing, missing::MESSAGE % "github-linguist", e.backtrace
end

module HTML
Expand Down Expand Up @@ -30,4 +31,4 @@ def highlight_with_timeout_handling(lexer, text)
end
end
end
end
end
9 changes: 8 additions & 1 deletion lib/html/pipeline/textile_filter.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
begin
require "redcloth"
rescue LoadError => e
missing = HTML::Pipeline::Filter::MissingDependencyException
raise missing, missing::MESSAGE % "RedCloth", e.backtrace
end

module HTML
class Pipeline
# HTML Filter that converts Textile text into HTML and converts into a
Expand All @@ -18,4 +25,4 @@ def call
end
end
end
end
end
92 changes: 92 additions & 0 deletions test/helpers/testing_dependency.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Public: Methods useful for testing Filter dependencies. All methods are class
# methods and should be called on the TestingDependency class.
#
# Examples
#
# TestingDependency.temporarily_remove_dependency_by gem_name do
# exception = assert_raise HTML::Pipeline::Filter::MissingDependencyException do
# load TestingDependency.filter_path_from filter_name
# end
# end
class TestingDependency
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting class, but feels heavy handed. Seeing how we're really just testing rescuing a load error, I'd rather skip these tests entirely rather than adding this large testing dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understandable. I wanted to somehow stub require and force raise a LoadError, but it wasn't that easy. I'll double check.

It was a fun exercise. 💦

# Public: Use to safely test a Filter's gem dependency error handling.
# For a certain gem dependency, remove the gem's loaded paths and features.
# Once these are removed, yield to a block which can assert a specific
# exception. Once the block is finished, add back the gem's paths and
# features to the load path and loaded features, so other tests can assert
# Filter functionality.
#
# gem_name - The String of the gem's name.
# block - Required block which asserts gem dependency error handling.
#
# Examples
#
# TestingDependency.temporarily_remove_dependency_by gem_name do
# exception = assert_raise HTML::Pipeline::Filter::MissingDependencyException do
# load TestingDependency.filter_path_from filter_name
# end
# end
#
# Returns nothing.
def self.temporarily_remove_dependency_by(gem_name, &block)
paths = gem_load_paths_from gem_name
features = gem_loaded_features_from gem_name

$LOAD_PATH.delete_if { |path| paths.include? path }
$LOADED_FEATURES.delete_if { |feature| features.include? feature }

yield

$LOAD_PATH.unshift(*paths)
$LOADED_FEATURES.unshift(*features)
end

# Public: Find a Filter's load path.
#
# gem_name - The String of the gem's name.
#
# Examples
#
# filter_path_from("autolink_filter")
# # => "/Users/simeon/Projects/html-pipeline/test/helpers/../../lib/html/pipeline/autolink_filter.rb"
#
# Returns String of load path.
def self.filter_path_from(filter_name)
File.join(File.dirname(__FILE__), "..", "..", "lib", "html", "pipeline", "#{filter_name}.rb")
end

private
# Internal: Find a gem's load paths.
#
# gem_name - The String of the gem's name.
#
# Examples
#
# gem_load_paths_from("rinku")
# # => ["/Users/simeon/.rbenv/versions/1.9.3-p429/lib/ruby/gems/1.9.1/gems/rinku-1.7.3/lib"]
#
# Returns Array of load paths.
def self.gem_load_paths_from(gem_name)
$LOAD_PATH.select{ |path| /#{gem_name}/i =~ path }
end

# Internal: Find a gem's loaded features.
#
# gem_name - The String of the gem's name.
#
# Examples
#
# gem_loaded_features_from("rinku")
# # => ["/Users/simeon/.rbenv/versions/1.9.3-p429/lib/ruby/gems/1.9.1/gems/rinku-1.7.3/lib/rinku.bundle",
# "/Users/simeon/.rbenv/versions/1.9.3-p429/lib/ruby/gems/1.9.1/gems/rinku-1.7.3/lib/rinku.rb"]
#
# Returns Array of loaded features.
def self.gem_loaded_features_from(gem_name)
# gem github-markdown has a feature "github/markdown.rb".
# Replace gem name dashes and underscores with regexp
# range to match all features.
gem_name_regexp = gem_name.split(/[-_]/).join("[\/_-]")

$LOADED_FEATURES.select{ |feature| /#{gem_name_regexp}/i =~ feature }
end
end
4 changes: 4 additions & 0 deletions test/html/pipeline/autolink_filter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
AutolinkFilter = HTML::Pipeline::AutolinkFilter

class HTML::Pipeline::AutolinkFilterTest < Test::Unit::TestCase
def test_dependency_management
assert_dependency_management_error "autolink_filter", "rinku"
end

def test_uses_rinku_for_autolinking
# just try to parse a complicated piece of HTML
# that Rails auto_link cannot handle
Expand Down
7 changes: 7 additions & 0 deletions test/html/pipeline/email_reply_filter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "test_helper"

class HTML::Pipeline::EmailReplyFilterTest < Test::Unit::TestCase
def test_dependency_management
assert_dependency_management_error "email_reply_filter", "escape_utils"
end
end
14 changes: 9 additions & 5 deletions test/html/pipeline/emoji_filter_test.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
require 'test_helper'
require "test_helper"

class HTML::Pipeline::EmojiFilterTest < Test::Unit::TestCase
EmojiFilter = HTML::Pipeline::EmojiFilter


def test_dependency_management
assert_dependency_management_error "emoji_filter", "gemoji"
end

def test_emojify
filter = EmojiFilter.new("<p>:shipit:</p>", {:asset_root => 'https://foo.com'})
filter = EmojiFilter.new("<p>:shipit:</p>", {:asset_root => "https://foo.com"})
doc = filter.call
assert_match "https://foo.com/emoji/shipit.png", doc.search('img').attr('src').value
assert_match "https://foo.com/emoji/shipit.png", doc.search("img").attr("src").value
end

def test_required_context_validation
Expand All @@ -15,4 +19,4 @@ def test_required_context_validation
}
assert_match /:asset_root/, exception.message
end
end
end
Loading