Skip to content

Commit

Permalink
Merge pull request #403 from gjtorikian/pass-context-along-fully
Browse files Browse the repository at this point in the history
Pass context along to every part of the pipeline
  • Loading branch information
gjtorikian committed Apr 30, 2024
2 parents ec6ff5b + 22e96b9 commit d4a5269
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 17 deletions.
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# HTML-Pipeline

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

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

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

- change every instance of `$NAME` to "`Johnny"
```
Hey there, @gjtorikian
```

With the content, we also want to:

- change every instance of `Hey` to `Hello`
- strip undesired HTML
- linkify @mention

Expand All @@ -73,7 +76,7 @@ require 'html_pipeline'

class HelloJohnnyFilter < HTMLPipelineFilter
def call
text.gsub("$NAME", "Johnny")
text.gsub("Hey", "Hello")
end
end

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

```ruby
context = { markdown: { extensions: { footnotes: false } } }
context = { markdown: { extensions: { footnotes: false } } }
filter = HTMLPipeline::ConvertFilter::MarkdownFilter.new(context: context)
filter.call("Hi **world**!")
```

Alternatively, you can construct a pipeline, and pass in a context during the call:

```ruby
pipeline = HTMLPipeline.new(
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new]
)
pipeline.call(user_supplied_text, context: { markdown: { extensions: { footnotes: false } } })
```

Please refer to the documentation for each filter to understand what configuration options are available.

### More Examples
Expand Down
7 changes: 4 additions & 3 deletions lib/html_pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def call(text, context: {}, result: {})
instrument("call_text_filters.html_pipeline", payload) do
result[:output] =
@text_filters.inject(text) do |doc, filter|
perform_filter(filter, doc, context: context, result: result)
perform_filter(filter, doc, context: (filter.context || {}).merge(context), result: result)
end
end
end
Expand All @@ -171,12 +171,13 @@ def call(text, context: {}, result: {})
text
else
instrument("call_convert_filter.html_pipeline", payload) do
html = @convert_filter.call(text)
html = @convert_filter.call(text, context: (@convert_filter.context || {}).merge(context))
end
end

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

instrument("html_pipeline.sanitization", payload) do
instrument("sanitization.html_pipeline", payload) do
result[:output] = Selma::Rewriter.new(sanitizer: @sanitization_config, handlers: @node_filters).rewrite(html)
end

Expand Down
6 changes: 3 additions & 3 deletions lib/html_pipeline/convert_filter/markdown_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
HTMLPipeline.require_dependency("commonmarker", "MarkdownFilter")

class HTMLPipeline
class ConvertFilter
class ConvertFilter < Filter
# HTML Filter that converts Markdown text into HTML.
#
# Context options:
Expand All @@ -16,8 +16,8 @@ def initialize(context: {}, result: {})
end

# Convert Commonmark to HTML using the best available implementation.
def call(text)
options = @context.fetch(:markdown, {})
def call(text, context: @context)
options = context.fetch(:markdown, {})
plugins = options.fetch(:plugins, {})
Commonmarker.to_html(text, options: options, plugins: plugins).rstrip!
end
Expand Down
2 changes: 1 addition & 1 deletion lib/html_pipeline/filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def initialize(context: {}, result: {})
# Public: Returns a simple Hash used to pass extra information into filters
# and also to allow filters to make extracted information available to the
# caller.
attr_reader :context
attr_accessor :context

# Public: Returns a Hash used to allow filters to pass back information
# to callers of the various Pipelines. This can be used for
Expand Down
2 changes: 2 additions & 0 deletions lib/html_pipeline/node_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

class HTMLPipeline
class NodeFilter < Filter
attr_accessor :context

def initialize(context: {}, result: {})
super(context: context, result: {})
send(:after_initialize) if respond_to?(:after_initialize)
Expand Down
2 changes: 1 addition & 1 deletion lib/html_pipeline/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

class HTMLPipeline
VERSION = "3.1.1"
VERSION = "3.2.0"
end
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_anchors_and_list_are_added_properly
def test_custom_anchor_html_added_properly
orig = %(# Ice cube)
expected = %(<h1><a href="#ice-cube" aria-hidden="true" id="ice-cube" class="anchor">#</a>Ice cube</h1>)
pipeline = HTMLPipeline.new(convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter, node_filters: [
pipeline = HTMLPipeline.new(convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new, node_filters: [
TocFilter.new(context: { anchor_html: "#" }),
])
result = pipeline.call(orig)
Expand Down
78 changes: 78 additions & 0 deletions test/html_pipeline_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,82 @@ def test_kitchen_sink

assert_equal("<p>!&gt;eeuqram/eeuqram&lt; ees ot evoL .yllib@ ,ereht <strong>yeH</strong></p>", result)
end

def test_context_is_carried_over_in_call
text = "yeH! I _think_ <marquee>@gjtorikian is ~great~</marquee>!"

pipeline = HTMLPipeline.new(
text_filters: [YehBolderFilter.new],
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new],
)
result = pipeline.call(text)[:output]

# note:
# - yeH is bolded
# - strikethroughs are rendered
# - mentions are not linked
assert_equal("<p><strong>yeH</strong>! I <em>think</em> <a href=\"/gjtorikian\">@gjtorikian</a> is <del>great</del>!</p>", result)

context = {
bolded: false,
markdown: { extension: { strikethrough: false } },
base_url: "http://your-domain.com",
}
result_with_context = pipeline.call(text, context: context)[:output]

# note:
# - yeH is not bolded
# - strikethroughs are not rendered
# - mentions are linked
assert_equal("<p>yeH! I <em>think</em> <a href=\"http://your-domain.com/gjtorikian\">@gjtorikian</a> is ~great~!</p>", result_with_context)
end

def test_text_filter_instance_context_is_carried_over_in_call
text = "yeH! I _think_ <marquee>@gjtorikian is ~great~</marquee>!"

pipeline = HTMLPipeline.new(
text_filters: [YehBolderFilter.new(context: { bolded: false })],
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new],
)

result = pipeline.call(text)[:output]

# note:
# - yeH is not bolded due to previous context
assert_equal("<p>yeH! I <em>think</em> <a href=\"/gjtorikian\">@gjtorikian</a> is <del>great</del>!</p>", result)
end

def test_convert_filter_instance_context_is_carried_over_in_call
text = "yeH! I _think_ <marquee>@gjtorikian is ~great~</marquee>!"

pipeline = HTMLPipeline.new(
text_filters: [YehBolderFilter.new],
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new(context: { extension: { strikethrough: false } }),
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new],
)

result = pipeline.call(text)[:output]

# note:
# - strikethroughs are not rendered due to previous context
assert_equal("<p><strong>yeH</strong>! I <em>think</em> <a href=\"/gjtorikian\">@gjtorikian</a> is <del>great</del>!</p>", result)
end

def test_node_filter_instance_context_is_carried_over_in_call
text = "yeH! I _think_ <marquee>@gjtorikian is ~great~</marquee>!"

pipeline = HTMLPipeline.new(
text_filters: [YehBolderFilter.new],
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new(context: { base_url: "http://your-domain.com" })],
)

result = pipeline.call(text)[:output]

# note:
# - mentions are linked
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)
end
end
26 changes: 26 additions & 0 deletions test/sanitization_filter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -263,5 +263,31 @@ def test_sanitization_pipeline_can_be_removed

assert_equal(result[:output].to_s, expected.chomp)
end

def test_sanitization_pipeline_does_not_need_node_filters
config = {
elements: ["p", "pre", "code"],
}

pipeline = HTMLPipeline.new(
convert_filter:
HTMLPipeline::ConvertFilter::MarkdownFilter.new,
sanitization_config: config,
)

result = pipeline.call(<<~CODE)
This is *great*, @birdcar:
some_code(:first)
CODE

expected = <<~HTML
<p>This is great, @birdcar:</p>
<pre><code>some_code(:first)
</code></pre>
HTML

assert_equal(result[:output].to_s, expected.chomp)
end
end
end
2 changes: 1 addition & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ def call(input, context: {}, result: {})
# bolds any instance of the word yeH
class YehBolderFilter < HTMLPipeline::TextFilter
def call(input, context: {}, result: {})
input.gsub("yeH", "**yeH**")
input.gsub("yeH", "**yeH**") unless context[:bolded] == false
end
end

0 comments on commit d4a5269

Please sign in to comment.