Md is a markup parser allowing fully customized syntax definition and understanding the wide range of markdown out of the box.

It is stream-aware, extendable, flexible, blazingly fast, with callbacks and more.

Main Focus

This library is not yet another markdown parser, rather it’s a highly configurable and extendable parser for any custom markdown-like markup. It has been created mostly to allow custom markdown syntax, like ^foo^ for superscript, or ⇓bar⇓ for subscript. It also supports custom parsers for anything that cannot be handled with generic parsers, inspired by markdown (something more complex than standard markdown provides.)

The library provides callbacks for all the default syntax handlers, as well as for custom handlers, allowing the on-fly modification of what’s currently being processed.

Md parses the incoming stream once and keeps the state, producing an AST of the input document. It has an ability to recover from errors collecting them.

It currently does not support (and I frankly doubt it ever will) lists with embedded quotes, and other contrived syntax. If one needs to perfectly parse the common markdown, Md is probably not the correct choice.

But if one wants to easily extend syntax almost without limits, Md might be good.

Security

Md renders markup written by authors, which makes two things worth knowing:

  • Inline attributes are filtered. {{class: "x"}} attaches attributes to the preceding element, so it is the one place a document could inject HTML. Under the default :safe policy only a known-good set of attributes is kept (plus data-*/aria-*), URI attributes refuse executable schemes such as javascript:, and on* event handlers are refused under every policy. See the :attribute_allowlist setting in stuff/handler-parameters.md.
  • Attribute names never grow the atom table. Names are resolved against a fixed lookup table, so a hostile document cannot exhaust the VM's atom table.

When the parser gets stuck

The clause set is generated from the syntax map, so it is closed: every input the parser can reach is covered by some declaration. If an input ever falls through to the catch-all clause, that is a gap in the syntax definition (or a bug in the engine), not merely an odd document.

Rather than a bare FunctionClauseError, Md logs the full context at :error level and raises Md.Parser.Error carrying the same information:

Md: no `do_parse/2` clause matches this input.

  parser:       MyApp.Parser
  continuation: "<aside>hello"
  bytes left:   12
  mode:         [linefeed: 0]
  path:         [{:p, nil, []}]
  bag:          %{indent: [], stock: [], deferred: []}

  hint:         mode {:linefeed, 0} is open; the next bytes are "<aside>hello"

The mode is the most useful field: it names the construct whose clause is missing. The exception exposes continuation, mode, path, bag, parser and hint as struct fields, so a test or a crash reporter can assert on them instead of parsing a message.

Markup Handling

There are several different syntax patterns recognizable by Md. Those are:

  • custom — the custom parser implementing Md.Parser behavious would be called
  • attributes — inplace attributes for all tags "{{foo:'bar'}}" → "<… foo='bar' …>"
  • substitute — simple substitution, like "<" → "&lt;"
  • escape — characters to be treated as is, not as a part of syntax
  • comment — characters to be treated as a comment, discarded in the output
  • flush — somewhat breaking a paragraph flow, like triple-dash
  • magnet — the markup for a single work following the patters, like #tag
  • block — the whole block of input treated distinguished, like triple-backtick
  • shift — the same as block, but the opening marker should precede each line and "\n" is treated as the closing marker
  • pair — the opening marker followed by closing marker, and a subsequent pair of opening and closing, like ![name](#anchor); the second element might be an internal shortcut to the deferred disclosure
  • disclosure — the disclosure of elements previously declared as pair with deferred parameter provided
  • paragraph — a header, blockquote, or such, followed by a paragraph flow break
  • list — a list, like - one\n- two
  • tag — allowed tags (e. g. <sup>2</sup>)
  • brace — a most common markdown feature, like text decoration or such (e. g. **bold**)

Syntax description

The syntax must be configured at compile time (because parse/2 handlers are generated in compile time.) It is a map, having settings key

settings: %{
  outer: :p,
  span: :span,
  empty_tags: ~w|img hr br|a
}

and key ⇒ list_of_tuples key-values, providing a text markup representation and its handling rules. Here is the excerpt from the default parser for braces

  brace: %{
    "*" => %{tag: :b},
    "_" => %{tag: :i},
    "**" => %{tag: :strong, attributes: %{class: "nota-bene"}},
    "__" => %{tag: :em},
    "~" => %{tag: :s},
    "~~" => %{tag: :del},
    "`" => %{tag: :code, mode: :raw, attributes: %{class: "code-inline"}}
  }

A block: entry HTML-entity-escapes (', ", &, <, >) its captured content by default, same as ``` fenced code; set escape: false on a specific entry to carry its content through verbatim instead (useful for a custom block whose content isn't meant to become HTML).

For more examples of what properties are allowed for each kind of handlers, see the sources (ATM.)

Predefined parsers

Md comes with a generic predefined parser Md.Parser.Default, which includes all the markup currently supported by Md.

Custom parser definition would be usually based on Md.Parser.Syntax.Void syntax as shown below

defmodule MyParser do
  use Md.Parser

  alias Md.Parser.Syntax.Void

  @default_syntax Map.put(Void.syntax(), :settings, Void.settings())
  @syntax @default_syntax |> Map.merge(%{
    comment: [{"<!--", %{closing: "-->"}}],
    paragraph: [
      {"##", %{tag: :h2}},
      {"###", %{tag: :h3}},
      {">", %{tag: :blockquote}}
    ],
    list:
      [
        {"- ", %{tag: :li, outer: :ul}},
        {"+ ", %{tag: :li, outer: :ol}}
      ]
    brace: [
      {"*", %{tag: :b}},
      {"_", %{tag: :i}},
      {"~", %{tag: :s}},
      {"`", %{tag: :code, mode: :raw, attributes: %{class: "code-inline"}}}
    ]
  })
end

@syntax module attribute must be declared, or DSL used as shown below (declarations), or an argument in a call to use Md.Parser. The separate declarations will be collected and merged.

defmodule MyDSLParser do
  @my_syntax %{brace: [{"***", %{tag: "u"}}]}
  
  use Md.Parser, syntax: @my_syntax
  import Md.Parser.DSL

  comment "<!--", %{closing: "-->"}
  ...
end

Instead of @syntax module attribute, one might use

  • a parameter to use Md.Parser as use Md.Parser, syntax: map()
  • a DSL like paragraph {"#", %{tag: :h1}}.

Summary

Functions

Parses input into a Md.Parser.State, whose :ast field holds the document tree.

Helper function to supply a custom parser in call to Md.parse/2 as

Functions

generate(input)

@spec generate(binary() | Md.Listener.state()) :: binary() | {binary(), any()}

See Md.Parser.generate/2.

generate(input, options)

@spec generate(binary() | Md.Listener.state(), keyword()) ::
  binary() | {binary(), any()}

See Md.Parser.generate/2.

generate(input, parser, options)

@spec generate(binary() | Md.Listener.state(), module(), keyword()) ::
  binary() | {binary(), any()}

See Md.Parser.generate/3.

parse(input, state \\ %State{})

@spec parse(binary(), Md.Listener.state()) :: Md.Listener.state()

Parses input into a Md.Parser.State, whose :ast field holds the document tree.

The parser is selected by the :type field of state, so a custom syntax is supplied with Md.with/1:

Md.parse("some _text_", Md.with(MyParser))

Note that this is a convenience wrapper, not an implementation of the Md.Parser behaviour: the behaviour's c:parse/2 callback returns {remainder, state}, while this returns the state alone (the remainder is always empty once a document has been consumed). Implement the behaviour on a parser module, and call this to use it.

Examples

iex> Md.parse("   foo")
%Md.Parser.State{ast: [{:p, nil, ["foo"]}], mode: [:finished]}

iex> Regex.replace(~r/\s+/, Md.generate("*bold*"), "")
"<p><b>bold</b></p>"

iex> Md.generate("It’s all *bold* and _italic_!", Md.Parser.Default, format: :none)
"<p>It’s all <b>bold</b> and <i>italic</i>!</p>"

with(parser)

@spec with(module()) :: Md.Parser.State.t()

Helper function to supply a custom parser in call to Md.parse/2 as

Md.parse("some _text_ with *markup*", Md.with(Md.Parser.Default))