RedQuilt

A modern Markdown document processor in pure Ruby, with an arena-style AST. Passes the full CommonMark spec test suite, and generally faster than kramdown.

Installation

Add this line to Gemfile:

gem "red_quilt"

Quick Start

Parsing and rendering

require "red_quilt"

# Parse Markdown to a document
doc = RedQuilt.parse("# Hello\n\nThis is **bold**.")
html = doc.to_html
# => "<h1>Hello</h1>\n<p>This is <strong>bold</strong>.</p>\n"

# Or render directly (without building AST)
html = RedQuilt.render_html("# Hello\n\n**bold**")

HTML is safe by default

RedQuilt.render_html("Hi <em>tag</em>")
# => "<p>Hi &lt;em&gt;tag&lt;/em&gt;</p>\n"

RedQuilt.render_html("Hi <em>tag</em>", allow_html: true)
# => "<p>Hi <em>tag</em></p>\n"

API Reference

Document

doc = RedQuilt.parse("# Title\n\nBody")

doc.root              # Root node (NodeRef)
doc.walk              # Traverse all nodes (block: { |node| ... } or Enumerator)
doc.to_html           # Render as HTML
doc.to_ast            # Export complete AST as Hash
doc.to_json           # Export as MDAST-compatible JSON
doc.to_mdast          # Export as MDAST Hash
doc.source_map        # Line/column lookup (lazy memoized)
doc.allow_html?       # Check HTML pass-through setting

NodeRef (AST node wrapper)

node = doc.root.children.first

# Traversal
node.type             # :heading, :paragraph, :link, etc. (Symbol)
node.children         # Array[NodeRef]
node.walk             # Enumerator[NodeRef] or { |node| ... } block
node.find_all(:link)  # Array[NodeRef] with matching type
node.text             # String (concatenated child text)

# Position information (byte offset)
node.source_span      # SourceSpan with start_byte, end_byte

# Position information (line/column)
node.source_location  # { start_line, start_column, end_line, end_column }
                      # line: 1-indexed, column: 0-indexed (character-based)

# AST export
node.to_h             # Export subtree as Hash[Symbol, untyped]

SourceSpan

span = node.source_span
span.start_byte       # Integer (0-indexed byte offset)
span.end_byte         # Integer (exclusive)
span.length           # Computed: end_byte - start_byte

Supported Syntax

Block elements

  • Paragraphs: Plain text blocks
  • Headings: ATX headings (# Title)
  • Thematic breaks: ---, ***, ___
  • Code blocks: Indented and fenced (with info string)
  • Block quotes: > quote text
  • Lists: Ordered (1.) and unordered (-, *, +)
  • List items: Nested blocks, tight/loose detection
  • Tables: GFM syntax with header/body rows
  • Raw HTML blocks: 7 types (script, comment, etc.)
  • Link reference definitions: [foo]: /url "title"

Inline elements

  • Text: Plain strings
  • Emphasis/Strong: *em*, **strong**, _em_, __strong__
  • Code spans: `code`
  • Links: [text](/url), [text](/url "title"), reference links
  • Images: ![alt](/url), ![alt](/url "title"), reference images
  • Soft/Hard line breaks: Implicit (soft) and explicit \ or two spaces
  • Raw HTML inline: <a href="#">link</a>
  • Autolinks: <http://example.com>, <user@example.com>
  • Character references: &amp;, &#x27;, etc.

CommonMark Compatibility

RedQuilt achieves 100% compliance with the CommonMark v0.31.2 specification.

Command-line Tool

RedQuilt ships with a redquilt CLI for converting Markdown files to HTML or inspecting the AST.

Basic usage

# Convert Markdown file to HTML
redquilt input.md > output.html

# Convert from stdin
echo "# Hello" | redquilt

# Output as AST (for debugging)
redquilt --format ast input.md

# Output as MDAST-compatible JSON (for external tools)
redquilt --format json input.md

# Standalone HTML document with title
redquilt --standalone --title "My Document" input.md

# Enable GFM extended autolinks
redquilt --extended-autolinks input.md

Options

--format FORMAT          Output format: html (default), ast, json
--allow-html             Pass raw HTML through to the output
--extended-autolinks     Linkify bare URLs and email addresses (GFM)
--[no-]standalone        Wrap HTML in full document (default: on)
--auto-title             Use the first heading's text as <title>
--title TITLE            Explicit <title> text
--lang LANG              html lang attribute (default: "en")
--css URL                Add a stylesheet link
--diagnostics            Print diagnostics to stderr
--diagnostics-only       Print diagnostics only (suppress output)
-h, --help               Show help
-v, --version            Show version

Exit code is 0 on success, 1 if errors are detected.

Safe-by-Default HTML Rendering

Security model

RedQuilt prioritizes security by default:

# Default: All HTML is escaped, dangerous URLs blocked
RedQuilt.render_html("<script>alert('xss')</script>")
# => "<p>&lt;script&gt;alert('xss')&lt;/script&gt;</p>"

RedQuilt.render_html("[click](javascript:alert(1))")
# => "<p><a href=\"\">click</a></p>"

Allowed URL schemes

In link/image destinations, only these schemes are permitted:

  • Absolute: http://, https://, ftp://, tel:, ssh://
  • Relative: /path, #anchor, path/to/file
  • Special: mailto: (autolinks only)

All other schemes (javascript:, data:, vbscript:, etc.) are blocked by replacing the URL with an empty string.

Opting into HTML pass-through

# Allow raw HTML (use with trusted input only)
RedQuilt.render_html(user_markdown, allow_html: true)

# This passes HTML blocks and inline tags through unchanged

Usage Examples

Extract all headings

doc = RedQuilt.parse(source)
headings = doc.root.find_all(:heading)

headings.each do |node|
  level = node.to_h[:attributes][:level]
  text = node.text
  puts "#{'#' * level} #{text}"
end

Walk the AST with line numbers

doc = RedQuilt.parse(source)

doc.root.walk do |node|
  loc = node.source_location
  if loc
    puts "#{node.type} at line #{loc[:start_line]}"
  end
end

Export and transform

doc = RedQuilt.parse("# Title\n\nBody with [link](/url)")
ast = doc.to_ast

# Print AST structure (for debugging)
pp ast

# Process nodes
doc.root.find_all(:link).each do |link|
  attrs = link.to_h[:attributes]
  puts "Link: #{link.text}#{attrs[:destination]}"
end

Development

Running tests

bundle exec rake spec

Runs 70+ CommonMark compatibility and feature tests.

Benchmark

ruby spec/bench_inline.rb
ruby spec/bench_block.rb

Profiles parse performance on various Markdown patterns.

Performance (v0.6.1, Ruby 4.0.5)

Comparison against kramdown on arm64-darwin (Apple Silicon), measured with spec/bench_vs_kramdown.rb (benchmark-ips):

Fixture Size RedQuilt (i/s) kramdown (i/s) RedQuilt vs kramdown
short_paragraph 49 B 26,531 5,416 4.90x faster
long_paragraph 1.4 KB 981 926 within error
nested_emphasis 1.4 KB 999 689 1.45x faster
many_links 2.0 KB 1,131 794 1.43x faster
mixed_markup 1.8 KB 1,028 729 1.41x faster
deep_nesting 800 B 827 349 2.37x faster
cmark_spec 205 KB 39.1 30.2 1.30x faster

Reproduce locally:

bundle exec ruby spec/bench_vs_kramdown.rb

License

MIT