Extensions

This guide explains how to enable and use Markly's Markdown extensions.

Choosing a Markdown Dialect

Markly parses standard CommonMark by default. Extensions change which syntax is recognized, so applications should enable them explicitly and consistently. This is especially important when several components parse or render the same document.

Pass extension names as symbols using the extensions: keyword:

EXTENSIONS = %i[table tasklist strikethrough autolink].freeze

html = Markly.render_html(markdown, extensions: EXTENSIONS)

When parsing and rendering separately, use the same configuration at both boundaries:

document = Markly.parse(markdown, extensions: EXTENSIONS)
html = document.to_html(extensions: EXTENSIONS)

Extension names must be symbols. A string such as "table" raises TypeError, and an unknown symbol raises ArgumentError.

GitHub Flavored Markdown Extensions

Markly includes the five syntax extensions defined by GitHub Flavored Markdown. None are enabled by default.

Tables

The :table extension recognizes a paragraph followed by a delimiter row as a table. Colons in the delimiter row specify column alignment:

markdown = <<~MARKDOWN
	| Package | Status |
	| :--- | ---: |
	| Markly | Ready |
MARKDOWN

document = Markly.parse(markdown, extensions: [:table])
table = document.first_child

table.type
# => :table

table.table_alignments
# => [:left, :right]

The table AST contains :table, :table_header, :table_row, and :table_cell nodes. By default, HTML rendering uses align attributes for aligned cells. Pass Markly::TABLE_PREFER_STYLE_ATTRIBUTES when rendering to use style="text-align: ..." instead:

document.to_html(
	flags: Markly::TABLE_PREFER_STYLE_ATTRIBUTES,
	extensions: [:table],
)

Task Lists

The :tasklist extension recognizes checked and unchecked list items:

- [x] Parse Markdown
- [ ] Render HTML

Task-list state is available on the list-item node and can be changed before rendering:

document = Markly.parse("- [x] Parse Markdown", extensions: [:tasklist])
item = document.first_child.first_child

item.tasklist_item_checked?
# => true

item.tasklist_item_checked = false

Strikethrough

The :strikethrough extension renders strikethrough text using <del>:

Markly.render_html("~~obsolete~~", extensions: [:strikethrough])
# => "<p><del>obsolete</del></p>\n"

Pass Markly::STRIKETHROUGH_DOUBLE_TILDE while parsing to accept only spans surrounded by exactly two tildes. This is useful when compatibility requires single or longer runs of tildes to remain literal:

Markly.render_html(
	"~one~ ~~two~~ ~~~three~~~",
	flags: Markly::STRIKETHROUGH_DOUBLE_TILDE,
	extensions: [:strikethrough],
)
# => "<p>~one~ <del>two</del> ~~~three~~~</p>\n"

Tag Filtering

The :tagfilter extension escapes the raw HTML tags prohibited by the GitHub Flavored Markdown specification. It is typically combined with Markly::UNSAFE, which otherwise permits raw HTML:

Markly.render_html(
	"<script>alert('no')</script><strong>yes</strong>",
	flags: Markly::UNSAFE,
	extensions: [:tagfilter],
)

Tag filtering is not a general-purpose HTML sanitizer. It filters the specific tag names required by the GFM specification, while other raw HTML remains available when Markly::UNSAFE is enabled. Sanitize untrusted HTML separately when the application requires a stricter policy.

Markly Syntax Features

Markly also provides syntax features that are not named GFM extensions. Enable these with parser flags rather than adding names to extensions:.

Front Matter

Markly::FRONT_MATTER recognizes a --- delimited block only at the beginning of a document:

markdown = <<~MARKDOWN
	--- yaml
	title: Extensions
	---
	# Document
MARKDOWN

document = Markly.parse(markdown, flags: Markly::FRONT_MATTER)
front_matter = document.first_child

front_matter.type
# => :front_matter

front_matter.string_content
# => "title: Extensions\n"

front_matter.code_info
# => "yaml"

Front matter is omitted from HTML and plain-text output. Markly exposes its raw contents but does not interpret YAML, TOML, or any other format. Treat the contents as untrusted input and parse them according to the application's own policy.

The closing delimiter must be an exact --- line. If it is missing, the rest of the document belongs to the front-matter node.

Inline Code Information

Markly::INLINE_CODE_INFO recognizes a language prefix immediately before an inline code span:

document = Markly.parse(
	"ruby:`Object.new`",
	flags: Markly::INLINE_CODE_INFO,
)
code = document.first_child.first_child

code.code_info
# => "ruby"

code.code_language
# => "ruby"

document.to_html
# => "<p><code class=\"language-ruby\">Object.new</code></p>\n"

Without the flag, the prefix remains ordinary text. Inline code information is a single language token; richer code-block information belongs on a fenced code block instead.

Code Block Metadata

Node#code_info is the general information-string accessor for fenced code blocks and front matter. Node#code_language returns the first token of that information string.

For a fenced code block, Node#fence returns a Markly::Node::Fence containing the fence character, length, and indentation:

block = Markly.parse("  ~~~~ ruby\n  Object.new\n  ~~~~").first_child

block.code_info
# => "ruby"

block.fence
# => #<struct Markly::Node::Fence character="~", length=4, indent=2>

Indented code blocks and other node types return nil from Node#fence.