# HTML-Pipeline
HTML processing filters and utilities. This module is a small
framework for defining CSS-based content filters and applying them to user
provided content.
[Although this project was started at GitHub](https://github.com/blog/1311-html-pipeline-chainable-content-filters), they no longer use it. This gem must be considered standalone and independent from GitHub.
- [HTML-Pipeline](#html-pipeline)
- [Installation](#installation)
- [Usage](#usage)
- [More Examples](#more-examples)
- [Filters](#filters)
- [TextFilters](#textfilters)
- [ConvertFilter](#convertfilter)
- [Sanitization](#sanitization)
- [NodeFilters](#nodefilters)
- [Dependencies](#dependencies)
- [Documentation](#documentation)
- [Instrumenting](#instrumenting)
- [Third Party Extensions](#third-party-extensions)
- [FAQ](#faq)
- [1. Why doesn't my pipeline work when there's no root element in the document?](#1-why-doesnt-my-pipeline-work-when-theres-no-root-element-in-the-document)
- [2. How do I customize an allowlist for `SanitizationFilter`s?](#2-how-do-i-customize-an-allowlist-for-sanitizationfilters)
- [Contributors](#contributors)
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'html-pipeline'
```
And then execute:
```sh
$ bundle
```
Or install it by yourself as:
```sh
$ gem install html-pipeline
```
## Usage
This library provides a handful of chainable HTML filters to transform user
content into HTML markup. Each filter does some work, and then hands off the
results tothe next filter. A pipeline has several kinds of filters available to use:
- Multiple `TextFilter`s, which operate a UTF-8 string
- A `ConvertFilter` filter, which turns text into HTML (eg., Commonmark/Asciidoc -> HTML)
- A `SanitizationFilter`, which remove dangerous/unwanted HTML elements and attributes
- Multiple `NodeFilter`s, which operate on a UTF-8 HTML document
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:
```
Hey there, @gjtorikian
```
With the content, we also want to:
- change every instance of `Hey` to `Hello`
- strip undesired HTML
- linkify @mention
We can construct a pipeline to do all that like this:
```ruby
require 'html_pipeline'
class HelloJohnnyFilter < HTMLPipelineFilter
def call
text.gsub("Hey", "Hello")
end
end
pipeline = HTMLPipeline.new(
text_filters: [HelloJohnnyFilter.new]
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
# note: next line is not needed as sanitization occurs by default;
# see below for more info
sanitization_config: HTMLPipeline::SanitizationFilter::DEFAULT_CONFIG,
node_filters: [HTMLPipeline::NodeFilter::MentionFilter.new]
)
pipeline.call(user_supplied_text) # recommended: can call pipeline over and over
```
Filters can be custom ones you create (like `HelloJohnnyFilter`), and `HTMLPipeline` additionally provides several helpful ones (detailed below). If you only need a single filter, you can call one individually, too:
```ruby
filter = HTMLPipeline::ConvertFilter::MarkdownFilter.new
filter.call(text)
```
Filters combine into a sequential pipeline, and each filter hands its
output to the next filter's input. Text filters are
processed first, then the convert filter, sanitization filter, and finally, the node filters.
Some filters take optional `context` and/or `result` hash(es). These are
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 } } }
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
Different pipelines can be defined for different parts of an app. Here are a few
paraphrased snippets to get you started:
```ruby
# The context hash is how you pass options between different filters.
# See individual filter source for explanation of options.
context = {
asset_root: "http://your-domain.com/where/your/images/live/icons",
base_url: "http://your-domain.com"
}
# Pipeline used for user provided content on the web
MarkdownPipeline = HTMLPipeline.new (
text_filters: [HTMLPipeline::TextFilter::ImageFilter.new],
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
node_filters: [
HTMLPipeline::NodeFilter::HttpsFilter.new,HTMLPipeline::NodeFilter::MentionFilter.new,
], context: context)
# Pipelines aren't limited to the web. You can use them for email
# processing also.
HtmlEmailPipeline = HTMLPipeline.new(
text_filters: [
PlainTextInputFilter.new,
ImageFilter.new
], {})
```
## Filters
### TextFilters
`TextFilter`s must define a method named `call` which is called on the text. `@text`, `@config`, and `@result` are available to use, and any changes made to these ivars are passed on to the next filter.
- `ImageFilter` - converts image `url` into `` tag
- `PlainTextInputFilter` - html escape text and wrap the result in a `
This is great:
some_code(:first)
```
Sanitization can be disabled if and only if `nil` is explicitly passed as
the config:
```ruby
pipeline = HTMLPipeline.new \
text_filters: [
HTMLPipeline::TextFilter::ImageFilter.new,
],
convert_filter: HTMLPipeline::ConvertFilter::MarkdownFilter.new,
sanitization_config: nil
```
For more examples of customizing the sanitization process to include the tags you want, check out [the tests](test/sanitization_filter_test.rb) and [the FAQ](#faq).
### NodeFilters
`NodeFilters`s can operate either on HTML elements or text nodes using CSS selectors. Each `NodeFilter` must define a method named `selector` which provides an instance of `Selma::Selector`. If elements are being manipulated, `handle_element` must be defined, taking one argument, `element`; if text nodes are being manipulated, `handle_text_chunk` must be defined, taking one argument, `text_chunk`. `@config`, and `@result` are available to use, and any changes made to these ivars are passed on to the next filter.
`NodeFilter` also has an optional method, `after_initialize`, which is run after the filter initializes. This can be useful in setting up a fresh custom state for `result` to start from each time the pipeline is called.
Here's an example `NodeFilter` that adds a base url to images that are root relative:
```ruby
require 'uri'
class RootRelativeFilter < HTMLPipeline::NodeFilter
SELECTOR = Selma::Selector.new(match_element: "img")
def selector
SELECTOR
end
def handle_element(img)
next if img['src'].nil?
src = img['src'].strip
if src.start_with? '/'
img["src"] = URI.join(context[:base_url], src).to_s
end
end
end
```
For more information on how to write effective `NodeFilter`s, refer to the provided filters, and see the underlying lib, [Selma](https://www.github.com/gjtorikian/selma) for more information.
- `AbsoluteSourceFilter`: replace relative image urls with fully qualified versions
- `AssetProxyFilter`: replace image links with an encoded link to an asset server
- `EmojiFilter`: converts `: