MDEx Documentation

repository·main·Indexed 19 days ago

https://github.com/leandrocp/mdex

A fast and extensible CommonMark-compliant Markdown parser for Elixir. MDEx supports multiple output formats including HTML, Phoenix HEEx, JSON, XML, Quill Delta, and Slack mrkdwn. It features integration with Lumis and Syntect for syntax highlighting, GitHub Flavored Markdown (GFM) support via mdex_gfm, and a specialized ~MD sigil for compile-time and runtime conversion.

Tokens
16.9K
Snippets
78
Records
87
Agent score
65%

What's inside MDEx

  1. Handle HTML safety in MDEx

    main

    MDEx provides four mechanisms to manage how raw HTML in input is handled during rendering: omitting (default), escaping, sanitizing, and unsafe rendering.

    • Omitting (Default): Raw HTML is removed and replaced with a comment <!-- raw HTML omitted -->.
    • Escape: Raw HTML is rendered but all tags are escaped (e.g., < becomes &lt;).
    • Sanitize: Raw HTML is rendered but cleaned using a set of safety rules. Note: You must set render: [unsafe: true] to enable sanitization.
    • Unsafe: Raw HTML is rendered exactly as provided without any modifications.
    MDEx employs 4 mechanisms to handle safety: omitting, escaping, sanitizing, and unsafe rendering.
  2. How plugins work in MDEx

    main

    Plugins are reusable modules used to extend MDEx's functionality. They work by registering custom options, appending processing steps to the document pipeline, and transforming the document tree. A plugin is defined as any module that implements an attach/2 function. This function receives a MDEx.Document and an options map/list, and returns a modified document.

    defmodule MyPlugin do
      alias MDEx.Document
    
      def attach(document, options \ []) do
        document
        |> Document.register_options([:my_option])
        |> Document.put_options(options)
        |> Document.append_steps(my_step: &my_step/1)
      end
    
      defp my_step(document) do
        # Transform the document
        document
      end
    end
  3. Pass variables to Markdown using Assigns

    main

    You can inject variables from your Phoenix assigns into Markdown using the {@var} syntax. For backward compatibility, the standard EEx <%= @var %> syntax is also supported.

    ~MD"""
    Welcome back, **{@user.name}**!
    
    You have {@notification_count} unread notifications.
    """HEEX
  4. Manipulate the Markdown AST with `MDEx.Document`

    main

    When you need to inspect, transform, or programmatically build Markdown, use the MDEx.Document API. You can start with a one-off AST using MDEx.parse_document!/2 or build a reusable pipeline using MDEx.new/1.

    Common operations include:

    • MDEx.Document.update_nodes(doc, node_type, transform_fn): Replaces or updates nodes of a specific type.
    • MDEx.Document.put_options/2: Sets extension options.
    • MDEx.Document.put_plugins/2: Attaches plugins.
    • MDEx.Document.put_markdown/2: Appends content to a document (useful for streaming).
    doc = 
      markdown
      |> MDEx.parse_document!()
      |> MDEx.Document.update_nodes(MDEx.Text, fn node ->
        %{node | literal: String.upcase(node.literal)}
      end)
    
    MDEx.to_html!(doc)
  5. How streaming mode works in MDEx

    main

    Streaming mode is designed for rendering Markdown from LLM responses that arrive in small chunks. Instead of producing broken HTML (like unclosed <strong> tags or half-open code fences), MDEx uses a fragment parser that maintains state between renders.

    When a chunk contains incomplete syntax, the parser temporarily closes the syntax to produce valid HTML. When the subsequent chunk provides the closing marker, the parser re-renders the content with the correct, completed syntax.

    The workflow involves three steps:

    1. Initialize a document with MDEx.new(streaming: true).
    2. Append chunks using MDEx.Document.put_markdown/2 (this only updates the buffer).
    3. Render the current state using MDEx.to_html!/1 (or other MDEx.to_* functions), which flushes the buffer and applies fragment completion.
    # 1. Create streaming document
    doc = MDEx.new(streaming: true)
    
    # 2. Append a chunk with unclosed syntax
    doc = MDEx.Document.put_markdown(doc, "**Fol")
    MDEx.to_html!(doc)
    #=> "<p><strong>Fol</strong></p>"
    
    # 3. Append the closing chunk
    doc = MDEx.Document.put_markdown(doc, "low**")
    MDEx.to_html!(doc)
    #=> "<p><strong>Follow</strong></p>"
  6. Stream or chunk Markdown content

    main

    For handling streaming data (like LLM outputs), initialize a document with streaming: true. You can then accumulate chunks using MDEx.Document.put_markdown/2 or by using Enum.into/2 with a list of chunks. Calling a to_* function (like to_html!/1) will flush the buffer and render the current state.

    # Using Enum.into to accumulate chunks
    doc = Enum.into(["# Hel", "lo\n\n", "**world**"], MDEx.new(streaming: true))
    html = MDEx.to_html!(doc)
    
    # Manual appending
    doc = MDEx.new(streaming: true)
    doc = MDEx.Document.put_markdown(doc, "**Hel")
    html = MDEx.to_html!(doc)
  7. Use the ~MD[...]HEEX sigil for compile-time Markdown

    main

    The ~MD[...]HEEX sigil is the preferred approach for LiveView templates because it parses Markdown at compile-time for optimal performance. Use this when your Markdown content is static within your code but needs to interact with Phoenix components, LiveView bindings, or assigns.

    def render(assigns) do
      ~MD"""
      # Welcome, {@username}!
    
      <.link href={@profile_url}>View Profile</.link>
      """HEEX
    end
  8. Compile MDEx native dependencies manually

    main

    If you are on an unsupported Linux system or require a custom build, you can compile the native dependency manually.

    Prerequisites:

    1. Install Rust.
    2. Install a C compiler or build packages (e.g., build-essential on Ubuntu).

    Steps: Set the MDEX_NATIVE_BUILD environment variable to 1 before running mix deps.get and mix compile.

    export MDEX_NATIVE_BUILD=1
    mix deps.get
    mix compile
  9. Install MDEx native dependencies via precompiled binaries

    main

    MDEx uses the :mdex_native dependency for its Rust-backed Markdown parser, HTML sanitizer, and syntax highlighter. In most cases, you do not need to install Rust because precompiled binaries are available for the following targets:

    • aarch64-apple-darwin
    • aarch64-unknown-linux-gnu
    • aarch64-unknown-linux-musl
    • arm-unknown-linux-gnueabihf
    • riscv64gc-unknown-linux-gnu
    • x86_64-apple-darwin
    • x86_64-pc-windows-gnu
    • x86_64-pc-windows-msvc
    • x86_64-unknown-freebsd
    • x86_64-unknown-linux-gnu
    • x86_64-unknown-linux-musl

    Linux Compatibility Note: Linux binaries are compiled using Ubuntu 22 on libc 2.35. You must use at least Ubuntu 22, Debian Bookworm, or a system with a compatible libc version. If you are on an older Linux system, you must compile manually.

  10. Configure Lumis for Code Block Decorators

    main

    To use code block decorators, you must integrate the lumis library and configure mdex_native and your render options.

    1. Add lumis to your Elixir dependencies.
    2. Configure :mdex_native to use :lumis as the syntax highlighter before compiling dependencies.
    3. Enable github_pre_lang and full_info_string in your render options.
    4. Enable the :lumis engine in your syntax_highlight configuration.
    # 1. Add to deps
    {:lumis, "~> 0.1"}
    
    # 2. Configure mdex_native (before compiling deps)
    config :mdex_native, syntax_highlighter: :lumis
    
    # 3. & 4. Render configuration
    render: [
      github_pre_lang: true,
      full_info_string: true
    ]
    
    syntax_highlight: [engine: :lumis, opts: [formatter: :html_inline]]