html-pipeline-no-charlock 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +19 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +9 -0
- data/LICENSE +22 -0
- data/README.md +221 -0
- data/Rakefile +13 -0
- data/html-pipeline-no-charlock.gemspec +25 -0
- data/html-pipeline.gemspec +26 -0
- data/lib/html/pipeline.rb +130 -0
- data/lib/html/pipeline/@mention_filter.rb +118 -0
- data/lib/html/pipeline/autolink_filter.rb +22 -0
- data/lib/html/pipeline/body_content.rb +42 -0
- data/lib/html/pipeline/camo_filter.rb +70 -0
- data/lib/html/pipeline/email_reply_filter.rb +56 -0
- data/lib/html/pipeline/emoji_filter.rb +54 -0
- data/lib/html/pipeline/filter.rb +178 -0
- data/lib/html/pipeline/https_filter.rb +13 -0
- data/lib/html/pipeline/image_max_width_filter.rb +37 -0
- data/lib/html/pipeline/markdown_filter.rb +29 -0
- data/lib/html/pipeline/plain_text_input_filter.rb +11 -0
- data/lib/html/pipeline/sanitization_filter.rb +105 -0
- data/lib/html/pipeline/syntax_highlight_filter.rb +29 -0
- data/lib/html/pipeline/text_filter.rb +14 -0
- data/lib/html/pipeline/textile_filter.rb +21 -0
- data/lib/html/pipeline/toc_filter.rb +28 -0
- data/lib/html/pipeline/version.rb +5 -0
- data/test/html/pipeline/autolink_filter_test.rb +22 -0
- data/test/html/pipeline/camo_filter_test.rb +47 -0
- data/test/html/pipeline/emoji_filter_test.rb +18 -0
- data/test/html/pipeline/image_max_width_filter_test.rb +50 -0
- data/test/html/pipeline/markdown_filter_test.rb +101 -0
- data/test/html/pipeline/mention_filter_test.rb +158 -0
- data/test/html/pipeline/plain_text_input_filter_test.rb +22 -0
- data/test/html/pipeline/sanitization_filter_test.rb +47 -0
- data/test/html/pipeline/toc_filter_test.rb +47 -0
- data/test/test_helper.rb +38 -0
- metadata +214 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module HTML
|
4
|
+
class Pipeline
|
5
|
+
# HTML filter that replaces @user mentions with links. Mentions within <pre>,
|
6
|
+
# <code>, and <a> elements are ignored. Mentions that reference users that do
|
7
|
+
# not exist are ignored.
|
8
|
+
#
|
9
|
+
# Context options:
|
10
|
+
# :base_url - Used to construct links to user profile pages for each
|
11
|
+
# mention.
|
12
|
+
# :info_url - Used to link to "more info" when someone mentions @mention
|
13
|
+
# or @mentioned.
|
14
|
+
#
|
15
|
+
class MentionFilter < Filter
|
16
|
+
# Public: Find user @mentions in text. See
|
17
|
+
# MentionFilter#mention_link_filter.
|
18
|
+
#
|
19
|
+
# MentionFilter.mentioned_logins_in(text) do |match, login, is_mentioned|
|
20
|
+
# "<a href=...>#{login}</a>"
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# text - String text to search.
|
24
|
+
#
|
25
|
+
# Yields the String match, the String login name, and a Boolean determining
|
26
|
+
# if the match = "@mention[ed]". The yield's return replaces the match in
|
27
|
+
# the original text.
|
28
|
+
#
|
29
|
+
# Returns a String replaced with the return of the block.
|
30
|
+
def self.mentioned_logins_in(text)
|
31
|
+
text.gsub MentionPattern do |match|
|
32
|
+
login = $1
|
33
|
+
yield match, login, MentionLogins.include?(login.downcase)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Pattern used to extract @mentions from text.
|
38
|
+
MentionPattern = /
|
39
|
+
(?:^|\W) # beginning of string or non-word char
|
40
|
+
@((?>[a-z0-9][a-z0-9-]*)) # @username
|
41
|
+
(?!\/) # without a trailing slash
|
42
|
+
(?=
|
43
|
+
\.+[ \t\W]| # dots followed by space or non-word character
|
44
|
+
\.+$| # dots at end of line
|
45
|
+
[^0-9a-zA-Z_.]| # non-word character except dot
|
46
|
+
$ # end of line
|
47
|
+
)
|
48
|
+
/ix
|
49
|
+
|
50
|
+
# List of username logins that, when mentioned, link to the blog post
|
51
|
+
# about @mentions instead of triggering a real mention.
|
52
|
+
MentionLogins = %w(
|
53
|
+
mention
|
54
|
+
mentions
|
55
|
+
mentioned
|
56
|
+
mentioning
|
57
|
+
)
|
58
|
+
|
59
|
+
# Don't look for mentions in text nodes that are children of these elements
|
60
|
+
IGNORE_PARENTS = %w(pre code a).to_set
|
61
|
+
|
62
|
+
def call
|
63
|
+
doc.search('text()').each do |node|
|
64
|
+
content = node.to_html
|
65
|
+
next if !content.include?('@')
|
66
|
+
next if has_ancestor?(node, IGNORE_PARENTS)
|
67
|
+
html = mention_link_filter(content, base_url, info_url)
|
68
|
+
next if html == content
|
69
|
+
node.replace(html)
|
70
|
+
end
|
71
|
+
doc
|
72
|
+
end
|
73
|
+
|
74
|
+
# The URL to provide when someone @mentions a "mention" name, such as
|
75
|
+
# @mention or @mentioned, that will give them more info on mentions.
|
76
|
+
def info_url
|
77
|
+
context[:info_url] || nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# Replace user @mentions in text with links to the mentioned user's
|
81
|
+
# profile page.
|
82
|
+
#
|
83
|
+
# text - String text to replace @mention usernames in.
|
84
|
+
# base_url - The base URL used to construct user profile URLs.
|
85
|
+
# info_url - The "more info" URL used to link to more info on @mentions.
|
86
|
+
# If nil we don't link @mention or @mentioned.
|
87
|
+
#
|
88
|
+
# Returns a string with @mentions replaced with links. All links have a
|
89
|
+
# 'user-mention' class name attached for styling.
|
90
|
+
def mention_link_filter(text, base_url='/', info_url=nil)
|
91
|
+
self.class.mentioned_logins_in(text) do |match, login, is_mentioned|
|
92
|
+
link =
|
93
|
+
if is_mentioned
|
94
|
+
link_to_mention_info(login, info_url)
|
95
|
+
else
|
96
|
+
link_to_mentioned_user(login)
|
97
|
+
end
|
98
|
+
|
99
|
+
link ? match.sub("@#{login}", link) : match
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def link_to_mention_info(text, info_url=nil)
|
104
|
+
return "@#{text}" if info_url.nil?
|
105
|
+
"<a href='#{info_url}' class='user-mention'>" +
|
106
|
+
"@#{text}" +
|
107
|
+
"</a>"
|
108
|
+
end
|
109
|
+
|
110
|
+
def link_to_mentioned_user(login)
|
111
|
+
url = File.join(base_url, login)
|
112
|
+
"<a href='#{url}' class='user-mention'>" +
|
113
|
+
"@#{login}" +
|
114
|
+
"</a>"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rinku'
|
2
|
+
|
3
|
+
module HTML
|
4
|
+
class Pipeline
|
5
|
+
# HTML Filter for auto_linking urls in HTML.
|
6
|
+
#
|
7
|
+
# Context options:
|
8
|
+
# :autolink - boolean whether to autolink urls
|
9
|
+
# :flags - additional Rinku flags. See https://github.com/vmg/rinku
|
10
|
+
#
|
11
|
+
# This filter does not write additional information to the context.
|
12
|
+
class AutolinkFilter < Filter
|
13
|
+
def call
|
14
|
+
return html if context[:autolink] == false
|
15
|
+
flags = 0
|
16
|
+
flags |= context[:flags] if context[:flags]
|
17
|
+
|
18
|
+
Rinku.auto_link(html, :urls, nil, %w[a script kbd pre code], flags)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,42 @@
|
|
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
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module HTML
|
4
|
+
class Pipeline
|
5
|
+
# HTML Filter for replacing http image URLs with camo versions. See:
|
6
|
+
#
|
7
|
+
# https://github.com/atmos/camo
|
8
|
+
#
|
9
|
+
# All images provided in user content should be run through this
|
10
|
+
# filter so that http image sources do not cause mixed-content warnings
|
11
|
+
# in browser clients.
|
12
|
+
#
|
13
|
+
# Context options:
|
14
|
+
# :asset_proxy (required) - Base URL for constructed asset proxy URLs.
|
15
|
+
# :asset_proxy_secret_key (required) - The shared secret used to encode URLs.
|
16
|
+
#
|
17
|
+
# This filter does not write additional information to the context.
|
18
|
+
class CamoFilter < Filter
|
19
|
+
# Hijacks images in the markup provided, replacing them with URLs that
|
20
|
+
# go through the github asset proxy.
|
21
|
+
def call
|
22
|
+
doc.search("img").each do |element|
|
23
|
+
next if element['src'].nil?
|
24
|
+
src = element['src'].strip
|
25
|
+
src = src.sub(%r!^http://github.com!, 'https://github.com')
|
26
|
+
next if context[:disable_asset_proxy]
|
27
|
+
|
28
|
+
if src =~ /^http:/ || src =~ /^https:\/\/img.skitch.com\//
|
29
|
+
element['src'] = asset_proxy_url(src)
|
30
|
+
else
|
31
|
+
element['src'] = src
|
32
|
+
end
|
33
|
+
end
|
34
|
+
doc
|
35
|
+
end
|
36
|
+
|
37
|
+
# Implementation of validate hook.
|
38
|
+
# Errors should raise exceptions or use an existing validator.
|
39
|
+
def validate
|
40
|
+
needs :asset_proxy, :asset_proxy_secret_key
|
41
|
+
end
|
42
|
+
|
43
|
+
# The camouflaged URL for a given image URL.
|
44
|
+
def asset_proxy_url(url)
|
45
|
+
"#{asset_proxy_host}/#{asset_url_hash(url)}/#{hexencode(url)}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Private: calculate the HMAC digest for a image source URL.
|
49
|
+
def asset_url_hash(url)
|
50
|
+
digest = OpenSSL::Digest::Digest.new('sha1')
|
51
|
+
OpenSSL::HMAC.hexdigest(digest, asset_proxy_secret_key, url)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Private: the hostname to use for generated asset proxied URLs.
|
55
|
+
def asset_proxy_host
|
56
|
+
context[:asset_proxy]
|
57
|
+
end
|
58
|
+
|
59
|
+
def asset_proxy_secret_key
|
60
|
+
context[:asset_proxy_secret_key]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Private: helper to hexencode a string. Each byte ends up encoded into
|
64
|
+
# two characters, zero padded value in the range [0-9a-f].
|
65
|
+
def hexencode(str)
|
66
|
+
str.to_enum(:each_byte).map { |byte| "%02x" % byte }.join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module HTML
|
2
|
+
class Pipeline
|
3
|
+
# HTML Filter that converts email reply text into an HTML DocumentFragment.
|
4
|
+
# It must be used as the first filter in a pipeline.
|
5
|
+
#
|
6
|
+
# Context options:
|
7
|
+
# None
|
8
|
+
#
|
9
|
+
# This filter does not write any additional information to the context hash.
|
10
|
+
class EmailReplyFilter < TextFilter
|
11
|
+
include EscapeUtils
|
12
|
+
|
13
|
+
EMAIL_HIDDEN_HEADER = %(<span class="email-hidden-toggle"><a href="#">…</a></span><div class="email-hidden-reply" style="display:none">).freeze
|
14
|
+
EMAIL_QUOTED_HEADER = %(<div class="email-quoted-reply">).freeze
|
15
|
+
EMAIL_SIGNATURE_HEADER = %(<div class="email-signature-reply">).freeze
|
16
|
+
EMAIL_FRAGMENT_HEADER = %(<div class="email-fragment">).freeze
|
17
|
+
EMAIL_HEADER_END = "</div>".freeze
|
18
|
+
|
19
|
+
# Scans an email body to determine which bits are quoted and which should
|
20
|
+
# be hidden. EmailReplyParser is used to split the comment into an Array
|
21
|
+
# of quoted or unquoted Blocks. Now, we loop through them and attempt to
|
22
|
+
# add <div> tags around them so we can hide the hidden blocks, and style
|
23
|
+
# the quoted blocks differently. Since multiple blocks may be hidden, be
|
24
|
+
# sure to keep the "email-hidden-reply" <div>s around "email-quoted-reply"
|
25
|
+
# <div> tags. Call this on each comment of a visible thread in the order
|
26
|
+
# that they are displayed. Note: all comments are processed so we can
|
27
|
+
# maintain a Set of SHAs of paragraphs. Only plaintext comments skip the
|
28
|
+
# markdown step.
|
29
|
+
#
|
30
|
+
# Returns the email comment HTML as a String
|
31
|
+
def call
|
32
|
+
found_hidden = nil
|
33
|
+
paragraphs = EmailReplyParser.read(text.dup).fragments.map do |fragment|
|
34
|
+
pieces = [escape_html(fragment.to_s.strip).gsub(/^\s*(>|>)/, '')]
|
35
|
+
if fragment.quoted?
|
36
|
+
pieces.unshift EMAIL_QUOTED_HEADER
|
37
|
+
pieces << EMAIL_HEADER_END
|
38
|
+
elsif fragment.signature?
|
39
|
+
pieces.unshift EMAIL_SIGNATURE_HEADER
|
40
|
+
pieces << EMAIL_HEADER_END
|
41
|
+
else
|
42
|
+
pieces.unshift EMAIL_FRAGMENT_HEADER
|
43
|
+
pieces << EMAIL_HEADER_END
|
44
|
+
end
|
45
|
+
if fragment.hidden? && !found_hidden
|
46
|
+
found_hidden = true
|
47
|
+
pieces.unshift EMAIL_HIDDEN_HEADER
|
48
|
+
end
|
49
|
+
pieces.join
|
50
|
+
end
|
51
|
+
paragraphs << EMAIL_HEADER_END if found_hidden
|
52
|
+
paragraphs.join("\n")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'emoji'
|
2
|
+
|
3
|
+
module HTML
|
4
|
+
class Pipeline
|
5
|
+
# HTML filter that replaces :emoji: with images.
|
6
|
+
#
|
7
|
+
# Context:
|
8
|
+
# :asset_root (required) - base url to link to emoji sprite
|
9
|
+
class EmojiFilter < Filter
|
10
|
+
# Build a regexp that matches all valid :emoji: names.
|
11
|
+
EmojiPattern = /:(#{Emoji.names.map { |name| Regexp.escape(name) }.join('|')}):/
|
12
|
+
|
13
|
+
def call
|
14
|
+
doc.search('text()').each do |node|
|
15
|
+
content = node.to_html
|
16
|
+
next if !content.include?(':')
|
17
|
+
next if has_ancestor?(node, %w(pre code))
|
18
|
+
html = emoji_image_filter(content)
|
19
|
+
next if html == content
|
20
|
+
node.replace(html)
|
21
|
+
end
|
22
|
+
doc
|
23
|
+
end
|
24
|
+
|
25
|
+
# Implementation of validate hook.
|
26
|
+
# Errors should raise exceptions or use an existing validator.
|
27
|
+
def validate
|
28
|
+
needs :asset_root
|
29
|
+
end
|
30
|
+
|
31
|
+
# Replace :emoji: with corresponding images.
|
32
|
+
#
|
33
|
+
# text - String text to replace :emoji: in.
|
34
|
+
#
|
35
|
+
# Returns a String with :emoji: replaced with images.
|
36
|
+
def emoji_image_filter(text)
|
37
|
+
return text unless text.include?(':')
|
38
|
+
|
39
|
+
text.gsub EmojiPattern do |match|
|
40
|
+
name = $1
|
41
|
+
"<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{File.join(asset_root, "emoji", "#{name}.png")}' height='20' width='20' align='absmiddle' />"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# The base url to link emoji sprites
|
46
|
+
#
|
47
|
+
# Raises ArgumentError if context option has not been provided.
|
48
|
+
# Returns the context's asset_root.
|
49
|
+
def asset_root
|
50
|
+
context[:asset_root]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module HTML
|
2
|
+
class Pipeline
|
3
|
+
# Base class for user content HTML filters. Each filter takes an
|
4
|
+
# HTML string or Nokogiri::HTML::DocumentFragment, performs
|
5
|
+
# modifications and/or writes information to the result hash. Filters must
|
6
|
+
# return a DocumentFragment (typically the same instance provided to the call
|
7
|
+
# method) or a String with HTML markup.
|
8
|
+
#
|
9
|
+
# Example filter that replaces all images with trollface:
|
10
|
+
#
|
11
|
+
# class FuuuFilter < HTML::Pipeline::Filter
|
12
|
+
# def call
|
13
|
+
# doc.search('img').each do |img|
|
14
|
+
# img['src'] = "http://paradoxdgn.com/junk/avatars/trollface.jpg"
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# The context Hash passes options to filters and should not be changed in
|
20
|
+
# place. A Result Hash allows filters to make extracted information
|
21
|
+
# available to the caller and is mutable.
|
22
|
+
#
|
23
|
+
# Common context options:
|
24
|
+
# :base_url - The site's base URL
|
25
|
+
# :repository - A Repository providing context for the HTML being processed
|
26
|
+
#
|
27
|
+
# Each filter may define additional options and output values. See the class
|
28
|
+
# docs for more info.
|
29
|
+
class Filter
|
30
|
+
class InvalidDocumentException < StandardError; end
|
31
|
+
|
32
|
+
def initialize(doc, context = nil, result = nil)
|
33
|
+
if doc.kind_of?(String)
|
34
|
+
@html = doc.to_str
|
35
|
+
@doc = nil
|
36
|
+
else
|
37
|
+
@doc = doc
|
38
|
+
@html = nil
|
39
|
+
end
|
40
|
+
@context = context || {}
|
41
|
+
@result = result || {}
|
42
|
+
validate
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: Returns a simple Hash used to pass extra information into filters
|
46
|
+
# and also to allow filters to make extracted information available to the
|
47
|
+
# caller.
|
48
|
+
attr_reader :context
|
49
|
+
|
50
|
+
# Public: Returns a Hash used to allow filters to pass back information
|
51
|
+
# to callers of the various Pipelines. This can be used for
|
52
|
+
# #mentioned_users, for example.
|
53
|
+
attr_reader :result
|
54
|
+
|
55
|
+
# The Nokogiri::HTML::DocumentFragment to be manipulated. If the filter was
|
56
|
+
# provided a String, parse into a DocumentFragment the first time this
|
57
|
+
# method is called.
|
58
|
+
def doc
|
59
|
+
@doc ||= parse_html(html)
|
60
|
+
end
|
61
|
+
|
62
|
+
# The String representation of the document. If a DocumentFragment was
|
63
|
+
# provided to the Filter, it is serialized into a String when this method is
|
64
|
+
# called.
|
65
|
+
def html
|
66
|
+
raise InvalidDocumentException if @html.nil? && @doc.nil?
|
67
|
+
@html || doc.to_html
|
68
|
+
end
|
69
|
+
|
70
|
+
# The main filter entry point. The doc attribute is guaranteed to be a
|
71
|
+
# Nokogiri::HTML::DocumentFragment when invoked. Subclasses should modify
|
72
|
+
# this document in place or extract information and add it to the context
|
73
|
+
# hash.
|
74
|
+
def call
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
# Make sure the context has everything we need. Noop: Subclasses can override.
|
79
|
+
def validate
|
80
|
+
end
|
81
|
+
|
82
|
+
# The Repository object provided in the context hash, or nil when no
|
83
|
+
# :repository was specified.
|
84
|
+
#
|
85
|
+
# It's assumed that the repository context has already been checked
|
86
|
+
# for permissions
|
87
|
+
def repository
|
88
|
+
context[:repository]
|
89
|
+
end
|
90
|
+
|
91
|
+
# The User object provided in the context hash, or nil when no user
|
92
|
+
# was specified
|
93
|
+
def current_user
|
94
|
+
context[:current_user]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return whether the filter can access a given repo while
|
98
|
+
# applying a filter
|
99
|
+
#
|
100
|
+
# A repo can only be accessed if its pullable by the user who
|
101
|
+
# submitted the content of this filter, or if it's the same as
|
102
|
+
# the repository context in which the filter runs
|
103
|
+
def can_access_repo?(repo)
|
104
|
+
return false if repo.nil?
|
105
|
+
return true if repo == repository
|
106
|
+
repo.pullable_by?(current_user)
|
107
|
+
end
|
108
|
+
|
109
|
+
# The site's base URL provided in the context hash, or '/' when no
|
110
|
+
# base URL was specified.
|
111
|
+
def base_url
|
112
|
+
context[:base_url] || '/'
|
113
|
+
end
|
114
|
+
|
115
|
+
# Ensure the passed argument is a DocumentFragment. When a string is
|
116
|
+
# provided, it is parsed and returned; otherwise, the DocumentFragment is
|
117
|
+
# returned unmodified.
|
118
|
+
def parse_html(html)
|
119
|
+
HTML::Pipeline.parse(html)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Helper method for filter subclasses used to determine if any of a node's
|
123
|
+
# ancestors have one of the tag names specified.
|
124
|
+
#
|
125
|
+
# node - The Node object to check.
|
126
|
+
# tags - An array of tag name strings to check. These should be downcase.
|
127
|
+
#
|
128
|
+
# Returns true when the node has a matching ancestor.
|
129
|
+
def has_ancestor?(node, tags)
|
130
|
+
while node = node.parent
|
131
|
+
if tags.include?(node.name.downcase)
|
132
|
+
break true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Perform a filter on doc with the given context.
|
138
|
+
#
|
139
|
+
# Returns a HTML::Pipeline::DocumentFragment or a String containing HTML
|
140
|
+
# markup.
|
141
|
+
def self.call(doc, context = nil, result = nil)
|
142
|
+
new(doc, context, result).call
|
143
|
+
end
|
144
|
+
|
145
|
+
# Like call but guarantees that a DocumentFragment is returned, even when
|
146
|
+
# the last filter returns a String.
|
147
|
+
def self.to_document(input, context = nil)
|
148
|
+
html = call(input, context)
|
149
|
+
HTML::Pipeline::parse(html)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Like call but guarantees that a string of HTML markup is returned.
|
153
|
+
def self.to_html(input, context = nil)
|
154
|
+
output = call(input, context)
|
155
|
+
if output.respond_to?(:to_html)
|
156
|
+
output.to_html
|
157
|
+
else
|
158
|
+
output.to_s
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Validator for required context. This will check that anything passed in
|
163
|
+
# contexts exists in @contexts
|
164
|
+
#
|
165
|
+
# If any errors are found an ArgumentError will be raised with a
|
166
|
+
# message listing all the missing contexts and the filters that
|
167
|
+
# require them.
|
168
|
+
def needs(*keys)
|
169
|
+
missing = keys.reject { |key| context.include? key }
|
170
|
+
|
171
|
+
if missing.any?
|
172
|
+
raise ArgumentError,
|
173
|
+
"Missing context keys for #{self.class.name}: #{missing.map(&:inspect).join ', '}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|