Kramdown Components

Rewrite and capture sections of Markdown documents using HTML Custom Elements.

Useful for enhancing code blocks and formatted markup with more sophisticated controls while maintaining a clean separation of presentation and content. Extract content from custom elements.

How it Works

Register new components for your documents. Use the custom elements as block level wrappers in your text markup. Mix and match HTML and Markdown syntax. Generate nested DOM trees from a single top level element.

This library is only responsible for converting HTML elements at compile time. To actually render and control the element lifecycle when your document is viewed in a browser, you will need to attach the appropriate JavaScript.

Usage

To use the enhanced documents with components, create a CustomDocument instance. This supports the same interface as the base Kramdown::Document.

require "kramdown-components"

doc = Kramdown::CustomDocument("# Title")

puts doc.to_html

Registering Components

To define a custom element that can wrap child elements with the Markdown document, use the define_element class method to set the tag name.

Kramdown::CustomDocument.define_element("wrapped-block")

You can customise the behaviour of components by extending CustomElement and binding the class to the tag name.

class ImageGallery < Kramdown::CustomElement; end

Kramdown::CustomDocument.define_element("image-gallery", ImageGallery)

Rewriting the DOM

To rewrite DOM subtrees wrapped by the component, implement the parse_dom method of CustomElement and manipulate the internal root and children elements, with instances of Kramdown::Element to represent each node.

class ButtonList < Kramdown::CustomElement
  def parse_dom(root)
    uls = root.children.filter { |child_el| child_el.type == :ul }
    uls.each do |ul|
      ul.children.each do |li|
        button = Kramdown::Element.new(:html_element, "button")
        button.children = li.children
        li.children = [button]
      end
    end
  end
end

Kramdown::CustomDocument.define_element("button-list", ButtonList)

When this #parse_dom hook is applied, all child lists nested within the <button-list> component get rewritten to have the content of each list item wrapped in a <button>.

Source:

<button-list>

- One
- Two
- Three

</button-list>

Result:

<button-list id="bl-uftcpsbnzytd">
  <ul>
    <li><button>One</button></li>
    <li><button>Two</button></li>
    <li><button>Three</button></li>
  </ul>
</button-list>

Extracting Content from the DOM

It’s also possible to use #parse_dom to extract content from the document, with or without rewriting.

class Color
  def self.from_hex(value)
    r, g, b = value.match(/^#(..)(..)(..)$/).captures.map(&:hex)
    new(r, g, b)
  end

  attr_reader :r, :g, :b

  def initialize(r, g, b)
    @r = r
    @g = g
    @b = b
  end

  def to_css
    "rgb(#{r}, #{g}, #{b})"
  end
end

class ColorSwatch < Kramdown::CustomElement
  def parse_dom(root)
    ul = root.children.find { |child_el| child_el.type == :ul }
    @colors = ul.children.map do |li|
      li.children.first.children.first.value
    end
  end

  def to_a
    @colors.map { |hex| Color.from_hex(hex) }
  end
end

Kramdown::CustomDocument.define_element("color-swatch", ColorSwatch)

Source:

Using the [HoneyGB palette](https://lospec.com/palette-list/honeygb):

<color-swatch>

- #3e3a42
- #877286
- #f0b695
- #e9f5da

</color-swatch>

Result:

document = Kramdown::CustomDocument(source)

# Get a reference to the parsed `ColorSwatch` instance
color_swatch = document.custom_elements.first

# Use the custom `#to_a` method to get the first extracted color
color = color_swatch.to_a.first

# Render the color value as an RGB string
color.to_css