Skip to content

Commit d4a5269

Browse files
authored
Merge pull request #403 from gjtorikian/pass-context-along-fully
Pass context along to every part of the pipeline
2 parents ec6ff5b + 22e96b9 commit d4a5269

File tree

10 files changed

+137
-17
lines changed

10 files changed

+137
-17
lines changed

README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
# HTML-Pipeline
22

3-
> **Note**
4-
> This README refers to the behavior in the new 3.0.0.pre gem.
5-
63
HTML processing filters and utilities. This module is a small
74
framework for defining CSS-based content filters and applying them to user
85
provided content.
@@ -60,9 +57,15 @@ results tothe next filter. A pipeline has several kinds of filters available to
6057

6158
You can assemble each sequence into a single pipeline, or choose to call each filter individually.
6259

63-
As an example, suppose we want to transform Commonmark source text into Markdown HTML. With the content, we also want to:
60+
As an example, suppose we want to transform Commonmark source text into Markdown HTML:
6461

65-
- change every instance of `$NAME` to "`Johnny"
62+
```
63+
Hey there, @gjtorikian
64+
```
65+
66+
With the content, we also want to:
67+
68+
- change every instance of `Hey` to `Hello`
6669
- strip undesired HTML
6770
- linkify @mention
6871

@@ -73,7 +76,7 @@ require 'html_pipeline'
7376

7477
class HelloJohnnyFilter < HTMLPipelineFilter
7578
def call
76-
text.gsub("$NAME", "Johnny")
79+
text.gsub("Hey", "Hello")
7780
end
7881
end
7982

@@ -104,11 +107,21 @@ used to pass around arguments and metadata between filters in a pipeline. For
104107
example, if you want to disable footnotes in the `MarkdownFilter`, you can pass an option in the context hash:
105108

106109
```ruby
107-
context = { markdown: { extensions: { footnotes: false } } }
110+
context = { markdown: { extensions: { footnotes: false } } }
108111
filter = HTMLPipeline::ConvertFilter::MarkdownFilter.new(context: context)
109112
filter.call("Hi **world**!")
110113
```
111114

115+
Alternatively, you can construct a pipeline, and pass in a context during the call:
116+
117+
```ruby
118+
pipeline = HTMLPipeline.new(
119+
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
120+
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new]
121+
)
122+
pipeline.call(user_supplied_text, context: { markdown: { extensions: { footnotes: false } } })
123+
```
124+
112125
Please refer to the documentation for each filter to understand what configuration options are available.
113126

114127
### More Examples

lib/html_pipeline.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def call(text, context: {}, result: {})
160160
instrument("call_text_filters.html_pipeline", payload) do
161161
result[:output] =
162162
@text_filters.inject(text) do |doc, filter|
163-
perform_filter(filter, doc, context: context, result: result)
163+
perform_filter(filter, doc, context: (filter.context || {}).merge(context), result: result)
164164
end
165165
end
166166
end
@@ -171,12 +171,13 @@ def call(text, context: {}, result: {})
171171
text
172172
else
173173
instrument("call_convert_filter.html_pipeline", payload) do
174-
html = @convert_filter.call(text)
174+
html = @convert_filter.call(text, context: (@convert_filter.context || {}).merge(context))
175175
end
176176
end
177177

178178
unless @node_filters.empty?
179179
instrument("call_node_filters.html_pipeline", payload) do
180+
@node_filters.each { |filter| filter.context = (filter.context || {}).merge(context) }
180181
result[:output] = Selma::Rewriter.new(sanitizer: @sanitization_config, handlers: @node_filters).rewrite(html)
181182
html = result[:output]
182183
payload = default_payload({
@@ -187,7 +188,7 @@ def call(text, context: {}, result: {})
187188
end
188189
end
189190

190-
instrument("html_pipeline.sanitization", payload) do
191+
instrument("sanitization.html_pipeline", payload) do
191192
result[:output] = Selma::Rewriter.new(sanitizer: @sanitization_config, handlers: @node_filters).rewrite(html)
192193
end
193194

lib/html_pipeline/convert_filter/markdown_filter.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
HTMLPipeline.require_dependency("commonmarker", "MarkdownFilter")
44

55
class HTMLPipeline
6-
class ConvertFilter
6+
class ConvertFilter < Filter
77
# HTML Filter that converts Markdown text into HTML.
88
#
99
# Context options:
@@ -16,8 +16,8 @@ def initialize(context: {}, result: {})
1616
end
1717

1818
# Convert Commonmark to HTML using the best available implementation.
19-
def call(text)
20-
options = @context.fetch(:markdown, {})
19+
def call(text, context: @context)
20+
options = context.fetch(:markdown, {})
2121
plugins = options.fetch(:plugins, {})
2222
Commonmarker.to_html(text, options: options, plugins: plugins).rstrip!
2323
end

lib/html_pipeline/filter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def initialize(context: {}, result: {})
2727
# Public: Returns a simple Hash used to pass extra information into filters
2828
# and also to allow filters to make extracted information available to the
2929
# caller.
30-
attr_reader :context
30+
attr_accessor :context
3131

3232
# Public: Returns a Hash used to allow filters to pass back information
3333
# to callers of the various Pipelines. This can be used for

lib/html_pipeline/node_filter.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
class HTMLPipeline
66
class NodeFilter < Filter
7+
attr_accessor :context
8+
79
def initialize(context: {}, result: {})
810
super(context: context, result: {})
911
send(:after_initialize) if respond_to?(:after_initialize)

lib/html_pipeline/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
class HTMLPipeline
4-
VERSION = "3.1.1"
4+
VERSION = "3.2.0"
55
end

test/html_pipeline/node_filter/table_of_contents_filter_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def test_anchors_and_list_are_added_properly
2828
def test_custom_anchor_html_added_properly
2929
orig = %(# Ice cube)
3030
expected = %(<h1><a href="#ice-cube" aria-hidden="true" id="ice-cube" class="anchor">#</a>Ice cube</h1>)
31-
pipeline = HTMLPipeline.new(convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter, node_filters: [
31+
pipeline = HTMLPipeline.new(convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new, node_filters: [
3232
TocFilter.new(context: { anchor_html: "#" }),
3333
])
3434
result = pipeline.call(orig)

test/html_pipeline_test.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,82 @@ def test_kitchen_sink
116116

117117
assert_equal("<p>!&gt;eeuqram/eeuqram&lt; ees ot evoL .yllib@ ,ereht <strong>yeH</strong></p>", result)
118118
end
119+
120+
def test_context_is_carried_over_in_call
121+
text = "yeH! I _think_ <marquee>@gjtorikian is ~great~</marquee>!"
122+
123+
pipeline = HTMLPipeline.new(
124+
text_filters: [YehBolderFilter.new],
125+
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
126+
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new],
127+
)
128+
result = pipeline.call(text)[:output]
129+
130+
# note:
131+
# - yeH is bolded
132+
# - strikethroughs are rendered
133+
# - mentions are not linked
134+
assert_equal("<p><strong>yeH</strong>! I <em>think</em> <a href=\"/gjtorikian\">@gjtorikian</a> is <del>great</del>!</p>", result)
135+
136+
context = {
137+
bolded: false,
138+
markdown: { extension: { strikethrough: false } },
139+
base_url: "http://your-domain.com",
140+
}
141+
result_with_context = pipeline.call(text, context: context)[:output]
142+
143+
# note:
144+
# - yeH is not bolded
145+
# - strikethroughs are not rendered
146+
# - mentions are linked
147+
assert_equal("<p>yeH! I <em>think</em> <a href=\"http://your-domain.com/gjtorikian\">@gjtorikian</a> is ~great~!</p>", result_with_context)
148+
end
149+
150+
def test_text_filter_instance_context_is_carried_over_in_call
151+
text = "yeH! I _think_ <marquee>@gjtorikian is ~great~</marquee>!"
152+
153+
pipeline = HTMLPipeline.new(
154+
text_filters: [YehBolderFilter.new(context: { bolded: false })],
155+
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
156+
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new],
157+
)
158+
159+
result = pipeline.call(text)[:output]
160+
161+
# note:
162+
# - yeH is not bolded due to previous context
163+
assert_equal("<p>yeH! I <em>think</em> <a href=\"/gjtorikian\">@gjtorikian</a> is <del>great</del>!</p>", result)
164+
end
165+
166+
def test_convert_filter_instance_context_is_carried_over_in_call
167+
text = "yeH! I _think_ <marquee>@gjtorikian is ~great~</marquee>!"
168+
169+
pipeline = HTMLPipeline.new(
170+
text_filters: [YehBolderFilter.new],
171+
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new(context: { extension: { strikethrough: false } }),
172+
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new],
173+
)
174+
175+
result = pipeline.call(text)[:output]
176+
177+
# note:
178+
# - strikethroughs are not rendered due to previous context
179+
assert_equal("<p><strong>yeH</strong>! I <em>think</em> <a href=\"/gjtorikian\">@gjtorikian</a> is <del>great</del>!</p>", result)
180+
end
181+
182+
def test_node_filter_instance_context_is_carried_over_in_call
183+
text = "yeH! I _think_ <marquee>@gjtorikian is ~great~</marquee>!"
184+
185+
pipeline = HTMLPipeline.new(
186+
text_filters: [YehBolderFilter.new],
187+
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
188+
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new(context: { base_url: "http://your-domain.com" })],
189+
)
190+
191+
result = pipeline.call(text)[:output]
192+
193+
# note:
194+
# - mentions are linked
195+
assert_equal("<p><strong>yeH</strong>! I <em>think</em> <a href=\"http://your-domain.com/gjtorikian\">@gjtorikian</a> is <del>great</del>!</p>", result)
196+
end
119197
end

test/sanitization_filter_test.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,5 +263,31 @@ def test_sanitization_pipeline_can_be_removed
263263

264264
assert_equal(result[:output].to_s, expected.chomp)
265265
end
266+
267+
def test_sanitization_pipeline_does_not_need_node_filters
268+
config = {
269+
elements: ["p", "pre", "code"],
270+
}
271+
272+
pipeline = HTMLPipeline.new(
273+
convert_filter:
274+
HTMLPipeline::ConvertFilter::MarkdownFilter.new,
275+
sanitization_config: config,
276+
)
277+
278+
result = pipeline.call(<<~CODE)
279+
This is *great*, @birdcar:
280+
281+
some_code(:first)
282+
CODE
283+
284+
expected = <<~HTML
285+
<p>This is great, @birdcar:</p>
286+
<pre><code>some_code(:first)
287+
</code></pre>
288+
HTML
289+
290+
assert_equal(result[:output].to_s, expected.chomp)
291+
end
266292
end
267293
end

test/test_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ def call(input, context: {}, result: {})
2525
# bolds any instance of the word yeH
2626
class YehBolderFilter < HTMLPipeline::TextFilter
2727
def call(input, context: {}, result: {})
28-
input.gsub("yeH", "**yeH**")
28+
input.gsub("yeH", "**yeH**") unless context[:bolded] == false
2929
end
3030
end

0 commit comments

Comments
 (0)