/dev/posts/

Use HTML pipeline in Middleman

Published:

Updated:

How to use html-pipeline in middleman.

Why

The idea is to be able add postprocessing steps after the markdown processing. The same idea can be used to wrap a default tilt template with any kind of postprocessing operations.

How

We need those gems (Gemfile):

gem 'html-pipeline'
gem 'github-linguist'
gem 'github-markdown'
# Ships with non-free emojis but you can use free ones:
gem 'gemoji'

Define a tilt template based on a given html-pipeline (lib/mytemplate.rb) generating HTML from markdown:

require 'tilt/template'

class MarkdownHtmlFilterTemplate < Tilt::Template
  self.default_mime_type = "text/html"

  def self.engine_initialized?
    defined? ::Pygments and defined? ::Html::Pipeline and defined? ::Linguist
  end

  def initialize_engine
    require 'html/pipeline'
    require 'linguist'
    require "pygments"
  end

  def prepare
    @engine = HTML::Pipeline.new [
      HTML::Pipeline::MarkdownFilter,
      HTML::Pipeline::EmojiFilter,
      HTML::Pipeline::SyntaxHighlightFilter,
      HTML::Pipeline::TableOfContentsFilter
    ], :asset_root => "/", :gfm => false
  end

  def evaluate(scope, locals, &block)
    @output ||= @engine.call(data)[:output].to_s
  end

end

Use it in middleman (config.rb) for processing markdown files:

require 'lib/mytemplates'
# We need to omit the Template suffix:
set :markdown_engine, :MarkdownHtmlFilter

Alternative: rack filter

Another solution is to use rack filter:

module Rack
  class MyEmojiFilter

    def initialize(app, options = {})
      @app = app
      @options = options
    end

    def call(env)
      status, headers, response = @app.call(env)
      if headers["Content-Type"] and headers["Content-Type"].include? "text/html"
        html = ""
        response.each { |chunk| html << chunk }
        html = process(html)
        headers['Content-Type'] = "text/html; charset=utf-8"
        headers['Content-Length'] = "".respond_to?(:bytesize) ? html.bytesize.to_s : html.size.to_s
        [status, headers, [html]]
      else
        [status, headers, response]
      end
    end

    def process(html)
      html.gsub(/:([a-zA-Z0-9_]{1,}):/) do |match|
        emoji = $1
        "<img class='emoji' src='/img/emoji/#{emoji}.png' alt=':#{emoji}:' />"
      end
    end

  end
end

The difference is that this processes all the files and the whole content of them.

Alternative: extending the basic template by composition

class MarkdownTemplate < Tilt::Template
  self.default_mime_type = "text/html"
  def self.engine_initialized?
    defined? ::Tilt::KramdownTemplate
  end
  def initialize_engine
    require 'tilt/markdown'
  end
  def prepare
    @template = Tilt::KramdownTemplate.new(@file, @line, @options, &@reader)
  end
  def replace_emoji
    text.gsub(/:([a-zA-Z0-9_]{1,}):/) do |match|
      "<img class='emoji' src='/img/emoji/#{$1}.png' alt=':#{$1}:' />"
    end
  end
  def evaluate(scope, locals, &block)
   @output ||= replace_emoji(@template.render(scope, locals, &block))
  end
end

Alternative: extending the processor

This monkey-patches the Kramdown parser in order to recognise emojis:

require 'kramdown/parser/kramdown.rb'

module Kramdown
  module Parser
    class Kramdown
      alias_method :old_emoji_initialize, :initialize
      def initialize(source, options)
        old_emoji_initialize source, options
        @span_parsers.unshift(:emoji)
      end
      def parse_emoji
        start_line_number = @src.current_line_number
        @src.pos += @src.matched_size
        emoji = @src[1]
        el = Element.new(:img, nil, nil, :location => start_line_number)
        add_link(el, "/img/emoji/#{emoji}.png", nil, ":#{emoji}:")
      end
      EMOJI_MATCH = /:([0-9a-zA-Z_]{1,}):/
      define_parser(:emoji, EMOJI_MATCH)
    end
  end
end