├── lib └── html │ ├── pipeline │ ├── version.rb │ ├── text_filter.rb │ ├── plain_text_input_filter.rb │ ├── image_filter.rb │ ├── https_filter.rb │ ├── textile_filter.rb │ ├── autolink_filter.rb │ ├── markdown_filter.rb │ ├── image_max_width_filter.rb │ ├── syntax_highlight_filter.rb │ ├── body_content.rb │ ├── absolute_source_filter.rb │ ├── toc_filter.rb │ ├── email_reply_filter.rb │ ├── camo_filter.rb │ ├── emoji_filter.rb │ ├── @mention_filter.rb │ ├── filter.rb │ └── sanitization_filter.rb │ └── pipeline.rb ├── script ├── package ├── release └── changelog ├── .gitignore ├── Rakefile ├── .travis.yml ├── test ├── helpers │ └── mocked_instrumentation_service.rb ├── test_helper.rb └── html │ ├── pipeline │ ├── syntax_highlight_filter_test.rb │ ├── plain_text_input_filter_test.rb │ ├── image_filter_test.rb │ ├── autolink_filter_test.rb │ ├── https_filter_test.rb │ ├── absolute_source_filter_test.rb │ ├── image_max_width_filter_test.rb │ ├── emoji_filter_test.rb │ ├── camo_filter_test.rb │ ├── markdown_filter_test.rb │ ├── toc_filter_test.rb │ ├── sanitization_filter_test.rb │ └── mention_filter_test.rb │ └── pipeline_test.rb ├── Gemfile ├── LICENSE ├── html-pipeline.gemspec ├── CONTRIBUTING.md ├── bin └── html-pipeline ├── CHANGELOG.md └── README.md /lib/html/pipeline/version.rb: -------------------------------------------------------------------------------- 1 | module HTML 2 | class Pipeline 3 | VERSION = "2.2.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /script/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Usage: script/gem 3 | # Updates the gemspec and builds a new gem in the pkg directory. 4 | 5 | mkdir -p pkg 6 | gem build *.gemspec 7 | mv *.gem pkg 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | exec/* 19 | vendor/gems -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require 'rake/testtask' 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | t.verbose = true 9 | end 10 | 11 | task :default => :test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | 4 | addons: 5 | apt: 6 | sources: 7 | - libicu-dev 8 | 9 | script: "bundle exec rake" 10 | 11 | rvm: 12 | - 2.0 13 | - 2.1 14 | - 2.2 15 | - ruby-head 16 | 17 | matrix: 18 | fast_finish: true 19 | allow_failures: 20 | - rvm: ruby-head 21 | -------------------------------------------------------------------------------- /lib/html/pipeline/text_filter.rb: -------------------------------------------------------------------------------- 1 | module HTML 2 | class Pipeline 3 | class TextFilter < Filter 4 | attr_reader :text 5 | 6 | def initialize(text, context = nil, result = nil) 7 | raise TypeError, "text cannot be HTML" if text.is_a?(DocumentFragment) 8 | # Ensure that this is always a string 9 | @text = text.respond_to?(:to_str) ? text.to_str : text.to_s 10 | super nil, context, result 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Usage: script/release 3 | # Build the package, tag a commit, push it to origin, and then release the 4 | # package publicly. 5 | 6 | set -e 7 | 8 | version="$(script/package | grep Version: | awk '{print $2}')" 9 | [ -n "$version" ] || exit 1 10 | 11 | echo $version 12 | git commit --allow-empty -a -m "Release $version" 13 | git tag "v$version" 14 | git push origin 15 | git push origin "v$version" 16 | gem push pkg/*-${version}.gem 17 | -------------------------------------------------------------------------------- /test/helpers/mocked_instrumentation_service.rb: -------------------------------------------------------------------------------- 1 | class MockedInstrumentationService 2 | attr_reader :events 3 | def initialize(event = nil, events = []) 4 | @events = events 5 | subscribe event 6 | end 7 | def instrument(event, payload = nil) 8 | payload ||= {} 9 | res = yield payload 10 | events << [event, payload, res] if @subscribe == event 11 | res 12 | end 13 | def subscribe(event) 14 | @subscribe = event 15 | @events 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/html/pipeline/plain_text_input_filter.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "escape_utils" 3 | rescue LoadError => _ 4 | abort "Missing dependency 'escape_utils' for PlainTextInputFilter. See README.md for details." 5 | end 6 | 7 | module HTML 8 | class Pipeline 9 | # Simple filter for plain text input. HTML escapes the text input and wraps it 10 | # in a div. 11 | class PlainTextInputFilter < TextFilter 12 | def call 13 | "
#{EscapeUtils.escape_html(@text, false)}
" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/html/pipeline/image_filter.rb: -------------------------------------------------------------------------------- 1 | module HTML 2 | class Pipeline 3 | # HTML Filter that converts image's url into tag. 4 | # For example, it will convert 5 | # http://example.com/test.jpg 6 | # into 7 | # . 8 | 9 | class ImageFilter < TextFilter 10 | def call 11 | @text.gsub(/(https|http)?:\/\/.+\.(jpg|jpeg|bmp|gif|png)(\?\S+)?/i) do |match| 12 | %|| 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'html/pipeline' 3 | require 'minitest/autorun' 4 | 5 | require 'active_support/core_ext/string' 6 | 7 | module TestHelpers 8 | # Asserts that two html fragments are equivalent. Attribute order 9 | # will be ignored. 10 | def assert_equal_html(expected, actual) 11 | assert_equal Nokogiri::HTML::DocumentFragment.parse(expected).to_hash, 12 | Nokogiri::HTML::DocumentFragment.parse(actual).to_hash 13 | end 14 | end 15 | 16 | Minitest::Test.send(:include, TestHelpers) 17 | -------------------------------------------------------------------------------- /test/html/pipeline/syntax_highlight_filter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | SyntaxHighlightFilter = HTML::Pipeline::SyntaxHighlightFilter 4 | 5 | class HTML::Pipeline::SyntaxHighlightFilterTest < Minitest::Test 6 | def test_highlight_default 7 | filter = SyntaxHighlightFilter.new \ 8 | "
hello
", :highlight => "coffeescript" 9 | 10 | doc = filter.call 11 | assert !doc.css(".highlight-coffeescript").empty? 12 | end 13 | 14 | def test_highlight_default_will_not_override 15 | filter = SyntaxHighlightFilter.new \ 16 | "
hello
", :highlight => "coffeescript" 17 | 18 | doc = filter.call 19 | assert doc.css(".highlight-coffeescript").empty? 20 | assert !doc.css(".highlight-c").empty? 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/html/pipeline/plain_text_input_filter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HTML::Pipeline::PlainTextInputFilterTest < Minitest::Test 4 | PlainTextInputFilter = HTML::Pipeline::PlainTextInputFilter 5 | 6 | def test_fails_when_given_a_documentfragment 7 | body = "

heyo

" 8 | doc = Nokogiri::HTML::DocumentFragment.parse(body) 9 | assert_raises(TypeError) { PlainTextInputFilter.call(doc, {}) } 10 | end 11 | 12 | def test_wraps_input_in_a_div_element 13 | doc = PlainTextInputFilter.call("howdy pahtner", {}) 14 | assert_equal "
howdy pahtner
", doc.to_s 15 | end 16 | 17 | def test_html_escapes_plain_text_input 18 | doc = PlainTextInputFilter.call("See: ", {}) 19 | assert_equal "
See: <http://example.org>
", 20 | doc.to_s 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/html/pipeline/https_filter.rb: -------------------------------------------------------------------------------- 1 | module HTML 2 | class Pipeline 3 | # HTML Filter for replacing http references to :http_url with https versions. 4 | # Subdomain references are not rewritten. 5 | # 6 | # Context options: 7 | # :http_url - The HTTP url to force HTTPS. Falls back to :base_url 8 | class HttpsFilter < Filter 9 | def call 10 | doc.css(%Q(a[href^="#{http_url}"])).each do |element| 11 | element['href'] = element['href'].sub(/^http:/,'https:') 12 | end 13 | doc 14 | end 15 | 16 | # HTTP url to replace. Falls back to :base_url 17 | def http_url 18 | context[:http_url] || context[:base_url] 19 | end 20 | 21 | # Raise error if :http_url undefined 22 | def validate 23 | needs :http_url unless http_url 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/html/pipeline/textile_filter.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "redcloth" 3 | rescue LoadError => _ 4 | abort "Missing dependency 'RedCloth' for TextileFilter. See README.md for details." 5 | end 6 | 7 | module HTML 8 | class Pipeline 9 | # HTML Filter that converts Textile text into HTML and converts into a 10 | # DocumentFragment. This is different from most filters in that it can take a 11 | # non-HTML as input. It must be used as the first filter in a pipeline. 12 | # 13 | # Context options: 14 | # :autolink => false Disable autolinking URLs 15 | # 16 | # This filter does not write any additional information to the context hash. 17 | # 18 | # NOTE This filter is provided for really old comments only. It probably 19 | # shouldn't be used for anything new. 20 | class TextileFilter < TextFilter 21 | # Convert Textile to HTML and convert into a DocumentFragment. 22 | def call 23 | RedCloth.new(@text).to_html 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in html-pipeline.gemspec 4 | gemspec 5 | 6 | group :development do 7 | gem "bundler" 8 | gem "rake" 9 | end 10 | 11 | group :test do 12 | gem "minitest", "~> 5.3" 13 | gem "rinku", "~> 1.7", :require => false 14 | gem "gemoji", "~> 2.0", :require => false 15 | gem "RedCloth", "~> 4.2.9", :require => false 16 | gem "github-markdown", "~> 0.5", :require => false 17 | gem "email_reply_parser", "~> 0.5", :require => false 18 | gem "sanitize", "~> 2.0", :require => false 19 | 20 | if RUBY_VERSION < "2.1.0" 21 | gem "escape_utils", "~> 0.3", :require => false 22 | gem "github-linguist", "~> 2.6.2", :require => false 23 | else 24 | gem "escape_utils", "~> 1.0", :require => false 25 | gem "github-linguist", "~> 2.10", :require => false 26 | end 27 | 28 | if RUBY_VERSION < "1.9.3" 29 | gem "activesupport", ">= 2", "< 4" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/html/pipeline/autolink_filter.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "rinku" 3 | rescue LoadError => _ 4 | abort "Missing dependency 'rinku' for AutolinkFilter. See README.md for details." 5 | end 6 | 7 | module HTML 8 | class Pipeline 9 | # HTML Filter for auto_linking urls in HTML. 10 | # 11 | # Context options: 12 | # :autolink - boolean whether to autolink urls 13 | # :link_attr - HTML attributes for the link that will be generated 14 | # :skip_tags - HTML tags inside which autolinking will be skipped. 15 | # See Rinku.skip_tags 16 | # :flags - additional Rinku flags. See https://github.com/vmg/rinku 17 | # 18 | # This filter does not write additional information to the context. 19 | class AutolinkFilter < Filter 20 | def call 21 | return html if context[:autolink] == false 22 | 23 | skip_tags = context[:skip_tags] 24 | flags = 0 25 | flags |= context[:flags] if context[:flags] 26 | 27 | Rinku.auto_link(html, :urls, context[:link_attr], skip_tags, flags) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 GitHub Inc. and Jerry Cheung 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/html/pipeline/image_filter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | ImageFilter = HTML::Pipeline::ImageFilter 4 | 5 | class HTML::Pipeline::ImageFilterTest < Minitest::Test 6 | def filter(html) 7 | ImageFilter.to_html(html) 8 | end 9 | 10 | def test_jpg 11 | assert_equal %(), 12 | filter(%(http://example.com/test.jpg)) 13 | end 14 | 15 | def test_jpeg 16 | assert_equal %(), 17 | filter(%(http://example.com/test.jpeg)) 18 | end 19 | 20 | def test_bmp 21 | assert_equal %(), 22 | filter(%(http://example.com/test.bmp)) 23 | end 24 | 25 | def test_gif 26 | assert_equal %(), 27 | filter(%(http://example.com/test.gif)) 28 | end 29 | 30 | def test_png 31 | assert_equal %(), 32 | filter(%(http://example.com/test.png)) 33 | end 34 | 35 | def test_https_url 36 | assert_equal %(), 37 | filter(%(https://example.com/test.png)) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /html-pipeline.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path("../lib/html/pipeline/version", __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = "html-pipeline" 6 | gem.version = HTML::Pipeline::VERSION 7 | gem.license = "MIT" 8 | gem.authors = ["Ryan Tomayko", "Jerry Cheung"] 9 | gem.email = ["ryan@github.com", "jerry@github.com"] 10 | gem.description = %q{GitHub HTML processing filters and utilities} 11 | gem.summary = %q{Helpers for processing content through a chain of filters} 12 | gem.homepage = "https://github.com/jch/html-pipeline" 13 | 14 | gem.files = `git ls-files`.split $/ 15 | gem.test_files = gem.files.grep(%r{^test}) 16 | gem.require_paths = ["lib"] 17 | 18 | gem.add_dependency "nokogiri", ">= 1.4" 19 | gem.add_dependency "activesupport", [">= 2", "< 5"] 20 | 21 | gem.post_install_message = < _ 4 | abort "Missing dependency 'github-markdown' for MarkdownFilter. See README.md for details." 5 | end 6 | 7 | module HTML 8 | class Pipeline 9 | # HTML Filter that converts Markdown text into HTML and converts into a 10 | # DocumentFragment. This is different from most filters in that it can take a 11 | # non-HTML as input. It must be used as the first filter in a pipeline. 12 | # 13 | # Context options: 14 | # :gfm => false Disable GFM line-end processing 15 | # 16 | # This filter does not write any additional information to the context hash. 17 | class MarkdownFilter < TextFilter 18 | def initialize(text, context = nil, result = nil) 19 | super text, context, result 20 | @text = @text.gsub "\r", '' 21 | end 22 | 23 | # Convert Markdown to HTML using the best available implementation 24 | # and convert into a DocumentFragment. 25 | def call 26 | mode = (context[:gfm] != false) ? :gfm : :markdown 27 | html = GitHub::Markdown.to_html(@text, mode) 28 | html.rstrip! 29 | html 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/html/pipeline/image_max_width_filter.rb: -------------------------------------------------------------------------------- 1 | module HTML 2 | class Pipeline 3 | # This filter rewrites image tags with a max-width inline style and also wraps 4 | # the image in an tag that causes the full size image to be opened in a 5 | # new tab. 6 | # 7 | # The max-width inline styles are especially useful in HTML email which 8 | # don't use a global stylesheets. 9 | class ImageMaxWidthFilter < Filter 10 | def call 11 | doc.search('img').each do |element| 12 | # Skip if there's already a style attribute. Not sure how this 13 | # would happen but we can reconsider it in the future. 14 | next if element['style'] 15 | 16 | # Bail out if src doesn't look like a valid http url. trying to avoid weird 17 | # js injection via javascript: urls. 18 | next if element['src'].to_s.strip =~ /\Ajavascript/i 19 | 20 | element['style'] = "max-width:100%;" 21 | 22 | if !has_ancestor?(element, %w(a)) 23 | link_image element 24 | end 25 | end 26 | 27 | doc 28 | end 29 | 30 | def link_image(element) 31 | link = doc.document.create_element('a', :href => element['src'], :target => '_blank') 32 | link.add_child(element.dup) 33 | element.replace(link) 34 | end 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /lib/html/pipeline/syntax_highlight_filter.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "linguist" 3 | rescue LoadError => _ 4 | abort "Missing dependency 'github-linguist' for SyntaxHighlightFilter. See README.md for details." 5 | end 6 | 7 | module HTML 8 | class Pipeline 9 | # HTML Filter that syntax highlights code blocks wrapped 10 | # in
.
11 |     class SyntaxHighlightFilter < Filter
12 |       def call
13 |         doc.search('pre').each do |node|
14 |           default = context[:highlight] && context[:highlight].to_s
15 |           next unless lang = node['lang'] || default
16 |           next unless lexer = lexer_for(lang)
17 |           text = node.inner_text
18 | 
19 |           html = highlight_with_timeout_handling(lexer, text)
20 |           next if html.nil?
21 | 
22 |           if (node = node.replace(html).first)
23 |             klass = node["class"]
24 |             klass = [klass, "highlight-#{lang}"].compact.join " "
25 | 
26 |             node["class"] = klass
27 |           end
28 |         end
29 |         doc
30 |       end
31 | 
32 |       def highlight_with_timeout_handling(lexer, text)
33 |         lexer.highlight(text)
34 |       rescue Timeout::Error => boom
35 |         nil
36 |       end
37 | 
38 |       def lexer_for(lang)
39 |         (Linguist::Language[lang] && Linguist::Language[lang].lexer) || Pygments::Lexer[lang]
40 |       end
41 |     end
42 |   end
43 | end
44 | 


--------------------------------------------------------------------------------
/lib/html/pipeline/body_content.rb:
--------------------------------------------------------------------------------
 1 | module HTML
 2 |   class Pipeline
 3 |     # Public: Runs a String of content through an HTML processing pipeline,
 4 |     # providing easy access to a generated DocumentFragment.
 5 |     class BodyContent
 6 |       attr_reader :result
 7 | 
 8 |       # Public: Initialize a BodyContent.
 9 |       #
10 |       # body     - A String body.
11 |       # context  - A Hash of context options for the filters.
12 |       # pipeline - A HTML::Pipeline object with one or more Filters.
13 |       def initialize(body, context, pipeline)
14 |         @body = body
15 |         @context = context
16 |         @pipeline = pipeline
17 |       end
18 | 
19 |       # Public: Gets the memoized result of the body content as it passed through
20 |       # the Pipeline.
21 |       #
22 |       # Returns a Hash, or something similar as defined by @pipeline.result_class.
23 |       def result
24 |         @result ||= @pipeline.call @body, @context
25 |       end
26 | 
27 |       # Public: Gets the updated body from the Pipeline result.
28 |       #
29 |       # Returns a String or DocumentFragment.
30 |       def output
31 |         @output ||= result[:output]
32 |       end
33 | 
34 |       # Public: Parses the output into a DocumentFragment.
35 |       #
36 |       # Returns a DocumentFragment.
37 |       def document
38 |         @document ||= HTML::Pipeline.parse output
39 |       end
40 |     end
41 |   end
42 | end
43 | 


--------------------------------------------------------------------------------
/test/html/pipeline/autolink_filter_test.rb:
--------------------------------------------------------------------------------
 1 | require "test_helper"
 2 | 
 3 | AutolinkFilter = HTML::Pipeline::AutolinkFilter
 4 | 
 5 | class HTML::Pipeline::AutolinkFilterTest < Minitest::Test
 6 |   def test_uses_rinku_for_autolinking
 7 |     # just try to parse a complicated piece of HTML
 8 |     # that Rails auto_link cannot handle
 9 |     assert_equal '

"http://www.github.com"

', 10 | AutolinkFilter.to_html('

"http://www.github.com"

') 11 | end 12 | 13 | def test_autolink_option 14 | assert_equal '

"http://www.github.com"

', 15 | AutolinkFilter.to_html('

"http://www.github.com"

', :autolink => false) 16 | end 17 | 18 | def test_autolink_link_attr 19 | assert_equal '

"http://www.github.com"

', 20 | AutolinkFilter.to_html('

"http://www.github.com"

', :link_attr => 'target="_blank"') 21 | end 22 | 23 | def test_autolink_flags 24 | assert_equal '

"http://github"

', 25 | AutolinkFilter.to_html('

"http://github"

', :flags => Rinku::AUTOLINK_SHORT_DOMAINS) 26 | end 27 | 28 | def test_autolink_skip_tags 29 | assert_equal '"http://github.com"', 30 | AutolinkFilter.to_html('"http://github.com"') 31 | 32 | assert_equal '"http://github.com"', 33 | AutolinkFilter.to_html('"http://github.com"', :skip_tags => %w(kbd script)) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /script/changelog: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage: script/changelog [-r ] [-b ] [-h ] 3 | # 4 | # repo: base string of GitHub repository url. e.g. "user_or_org/repository". Defaults to git remote url. 5 | # base: git ref to compare from. e.g. "v1.3.1". Defaults to latest git tag. 6 | # head: git ref to compare to. Defaults to "HEAD". 7 | # 8 | # Generate a changelog preview from pull requests merged between `base` and 9 | # `head`. 10 | # 11 | # https://github.com/jch/release-scripts/blob/master/changelog 12 | set -e 13 | 14 | [ $# -eq 0 ] && set -- --help 15 | while [[ $# > 1 ]] 16 | do 17 | key="$1" 18 | case $key in 19 | -r|--repo) 20 | repo="$2" 21 | shift 22 | ;; 23 | -b|--base) 24 | base="$2" 25 | shift 26 | ;; 27 | -h|--head) 28 | head="$2" 29 | shift 30 | ;; 31 | *) 32 | ;; 33 | esac 34 | shift 35 | done 36 | 37 | repo="${repo:-$(git remote -v | grep push | awk '{print $2}' | cut -d'/' -f4- | sed 's/\.git//')}" 38 | base="${base:-$(git tag -l | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -n 1)}" 39 | head="${head:-HEAD}" 40 | api_url="https://api.github.com" 41 | 42 | # get merged PR's. Better way is to query the API for these, but this is easier 43 | for pr in $(git log --oneline $base..$head | grep "Merge pull request" | awk '{gsub("#",""); print $5}') 44 | do 45 | # frustrated with trying to pull out the right values, fell back to ruby 46 | curl -s "$api_url/repos/$repo/pulls/$pr" | ruby -rjson -e 'pr=JSON.parse(STDIN.read); puts "* #{pr[%q(title)]} [##{pr[%q(number)]}](#{pr[%q(html_url)]})"' 47 | done 48 | -------------------------------------------------------------------------------- /lib/html/pipeline/absolute_source_filter.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module HTML 4 | class Pipeline 5 | 6 | class AbsoluteSourceFilter < Filter 7 | # HTML Filter for replacing relative and root relative image URLs with 8 | # fully qualified URLs 9 | # 10 | # This is useful if an image is root relative but should really be going 11 | # through a cdn, or if the content for the page assumes the host is known 12 | # i.e. scraped webpages and some RSS feeds. 13 | # 14 | # Context options: 15 | # :image_base_url - Base URL for image host for root relative src. 16 | # :image_subpage_url - For relative src. 17 | # 18 | # This filter does not write additional information to the context. 19 | # This filter would need to be run before CamoFilter. 20 | def call 21 | doc.search("img").each do |element| 22 | next if element['src'].nil? || element['src'].empty? 23 | src = element['src'].strip 24 | unless src.start_with? 'http' 25 | if src.start_with? '/' 26 | base = image_base_url 27 | else 28 | base = image_subpage_url 29 | end 30 | element["src"] = URI.join(base, src).to_s 31 | end 32 | end 33 | doc 34 | end 35 | 36 | # Private: the base url you want to use 37 | def image_base_url 38 | context[:image_base_url] or raise "Missing context :image_base_url for #{self.class.name}" 39 | end 40 | 41 | # Private: the relative url you want to use 42 | def image_subpage_url 43 | context[:image_subpage_url] or raise "Missing context :image_subpage_url for #{self.class.name}" 44 | end 45 | 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for using and improving `HTML::Pipeline`! 4 | 5 | - [Submitting a New Issue](#submitting-a-new-issue) 6 | - [Sending a Pull Request](#sending-a-pull-request) 7 | 8 | ## Submitting a New Issue 9 | 10 | If there's an idea you'd like to propose, or a design change, feel free to file a new issue. 11 | 12 | If you have an implementation question or believe you've found a bug, please provide as many details as possible: 13 | 14 | - Input document 15 | - Output HTML document 16 | - the exact `HTML::Pipeline` code you are using 17 | - output of the following from your project 18 | 19 | ``` 20 | ruby -v 21 | bundle exec nokogiri -v 22 | ``` 23 | 24 | ## Sending a Pull Request 25 | 26 | [Pull requests][pr] are always welcome! 27 | 28 | Check out [the project's issues list][issues] for ideas on what could be improved. 29 | 30 | Before sending, please add tests and ensure the test suite passes. 31 | 32 | ### Running the Tests 33 | 34 | To run the full suite: 35 | 36 | `bundle exec rake` 37 | 38 | To run a specific test file: 39 | 40 | `bundle exec ruby -Itest test/html/pipeline_test.rb` 41 | 42 | To run a specific test: 43 | 44 | `bundle exec ruby -Itest test/html/pipeline/markdown_filter_test.rb -n test_disabling_gfm` 45 | 46 | To run the full suite with all [supported rubies][travisyaml] in bash: 47 | 48 | ```bash 49 | rubies=(ree-1.8.7-2011.03 1.9.2-p290 1.9.3-p429 2.0.0-p247) 50 | for r in ${rubies[*]} 51 | do 52 | rbenv local $r # switch to your version manager of choice 53 | bundle install 54 | bundle exec rake 55 | done 56 | ``` 57 | 58 | [issues]: https://github.com/jch/html-pipeline/issues 59 | [pr]: https://help.github.com/articles/using-pull-requests 60 | [travisyaml]: https://github.com/jch/html-pipeline/blob/master/.travis.yml 61 | -------------------------------------------------------------------------------- /test/html/pipeline/https_filter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | HttpsFilter = HTML::Pipeline::HttpsFilter 4 | 5 | class HTML::Pipeline::AutolinkFilterTest < Minitest::Test 6 | def filter(html) 7 | HttpsFilter.to_html(html, @options) 8 | end 9 | 10 | def setup 11 | @options = {:base_url => "http://github.com"} 12 | end 13 | 14 | def test_http 15 | assert_equal %(github.com), 16 | filter(%(github.com)) 17 | end 18 | 19 | def test_https 20 | assert_equal %(github.com), 21 | filter(%(github.com)) 22 | end 23 | 24 | def test_subdomain 25 | assert_equal %(github.com), 26 | filter(%(github.com)) 27 | end 28 | 29 | def test_other 30 | assert_equal %(github.io), 31 | filter(%(github.io)) 32 | end 33 | 34 | def test_uses_http_url_over_base_url 35 | @options = {:http_url => "http://github.com", :base_url => "https://github.com"} 36 | 37 | assert_equal %(github.com), 38 | filter(%(github.com)) 39 | end 40 | 41 | def test_only_http_url 42 | @options = {:http_url => "http://github.com"} 43 | 44 | assert_equal %(github.com), 45 | filter(%(github.com)) 46 | end 47 | 48 | def test_validates_http_url 49 | @options.clear 50 | exception = assert_raises(ArgumentError) { filter("") } 51 | assert_match "HTML::Pipeline::HttpsFilter: :http_url", exception.message 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/html/pipeline/absolute_source_filter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HTML::Pipeline::AbsoluteSourceFilterTest < Minitest::Test 4 | AbsoluteSourceFilter = HTML::Pipeline::AbsoluteSourceFilter 5 | 6 | def setup 7 | @image_base_url = 'http://assets.example.com' 8 | @image_subpage_url = 'http://blog.example.com/a/post' 9 | @options = { 10 | :image_base_url => @image_base_url, 11 | :image_subpage_url => @image_subpage_url 12 | } 13 | end 14 | 15 | def test_rewrites_root_urls 16 | orig = %(

) 17 | assert_equal "

", 18 | AbsoluteSourceFilter.call(orig, @options).to_s 19 | end 20 | 21 | def test_rewrites_relative_urls 22 | orig = %(

) 23 | assert_equal "

", 24 | AbsoluteSourceFilter.call(orig, @options).to_s 25 | end 26 | 27 | def test_does_not_rewrite_absolute_urls 28 | orig = %(

) 29 | result = AbsoluteSourceFilter.call(orig, @options).to_s 30 | refute_match /@image_base_url/, result 31 | refute_match /@image_subpage_url/, result 32 | end 33 | 34 | def test_fails_when_context_is_missing 35 | assert_raises RuntimeError do 36 | AbsoluteSourceFilter.call("", {}) 37 | end 38 | assert_raises RuntimeError do 39 | AbsoluteSourceFilter.call("", {}) 40 | end 41 | end 42 | 43 | def test_tells_you_where_context_is_required 44 | exception = assert_raises(RuntimeError) { 45 | AbsoluteSourceFilter.call("", {}) 46 | } 47 | assert_match 'HTML::Pipeline::AbsoluteSourceFilter', exception.message 48 | 49 | exception = assert_raises(RuntimeError) { 50 | AbsoluteSourceFilter.call("", {}) 51 | } 52 | assert_match 'HTML::Pipeline::AbsoluteSourceFilter', exception.message 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /lib/html/pipeline/toc_filter.rb: -------------------------------------------------------------------------------- 1 | module HTML 2 | class Pipeline 3 | # HTML filter that adds an 'id' attribute to all headers 4 | # in a document, so they can be accessed from a table of contents. 5 | # 6 | # Generates the Table of Contents, with links to each header. 7 | # 8 | # Examples 9 | # 10 | # TocPipeline = 11 | # HTML::Pipeline.new [ 12 | # HTML::Pipeline::TableOfContentsFilter 13 | # ] 14 | # # => # 15 | # orig = %(

Ice cube

is not for the pop chart

) 16 | # # => "

Ice cube

is not for the pop chart

" 17 | # result = {} 18 | # # => {} 19 | # TocPipeline.call(orig, {}, result) 20 | # # => {:toc=> ...} 21 | # result[:toc] 22 | # # => "