The Problem

If you write scientific or chemistry-related content in Markdown, you've probably wished for something like this:

  • KaTeX lets you write $E = mc^2$ and it magically becomes a beautiful equation.
  • But if you write CCCC (a SMILES string for butane), it just stays as plain text.

There was no equivalent "auto-render" experience for chemical structures — until now.

auto-render-mol.js is a lightweight, zero-dependency JavaScript library that scans your HTML for SMILES/Molfile code blocks and inline codes, then replaces them with publication-quality 2D structure diagrams using RDKit.js (WebAssembly).

KaTeX auto-render-mol
renderMathInElement(document.body) renderMolInElement(document.body)
Scans for $...$ delimiters Scans for code.language-mol and mol: prefix
Replaces text nodes with <span class="katex"> Replaces code blocks with <div class="mol-container">
Synchronous (pure JS) Asynchronous (WASM loading)

One function call. That's it.

Quick Start

<!-- 1. Load RDKit WASM -->
<script src="https://unpkg.com/@rdkit/rdkit/dist/RDKit_minimal.js"></script>

<!-- 2. Load auto-render-mol, render on load -->
<script src="auto-render-mol.js"
        onload="renderMolInElement(document.body)"></script>

Markdown Syntax

Block-Level Molecules (Fenced Code Blocks)

Use the standard Markdown fenced code block with mol as the language identifier. Any Markdown parser (marked.js, markdown-it, Markdig, etc.) will convert this to <code class="language-mol">, which auto-render-mol picks up automatically.

Default size (300×200):

```mol
CC(=O)Oc1ccccc1C(=O)O
```

Custom size — append {width, height} inside the block:

```mol
CC(=O)Oc1ccccc1C(=O)O
{500, 400}
```

Width only (height falls back to default):

```mol
c1ccccc1
{600}
```

Molfile

```mol
NoName
  -INDIGO-09162620492D

 40 43  0  0  0  0  0  0  0  0999 V2000
    5.0068   -3.6090    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.5068   -4.4751    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.0068   -5.3411    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    5.5068   -6.2071    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.0068   -7.0731    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.0068   -7.0731    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.5068   -6.2071    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    2.5068   -6.2071    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    2.0068   -7.0731    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.0068   -7.0731    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.0068   -7.0731    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    2.5068   -7.9392    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.5068   -7.9392    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    6.5068   -6.2071    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.0068   -7.0731    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.5068   -7.9392    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    8.0068   -7.0731    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    8.5068   -7.9392    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    9.5068   -7.9392    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   10.0068   -8.8052    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   11.0068   -8.8052    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   11.5068   -7.9392    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   12.5068   -7.9392    0.0000 S   0  0  0  0  0  0  0  0  0  0  0  0
   12.5068   -6.9392    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   12.5068   -8.9392    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
   13.5068   -7.9392    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
   11.0068   -7.0731    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   10.0068   -7.0731    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.0068   -5.3411    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.0068   -5.3411    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    6.5068   -4.4751    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.0068   -3.6090    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.0068   -3.6090    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.5068   -2.7430    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.0068   -1.8770    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.0068   -1.8770    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.5068   -1.0110    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.5068   -1.0110    0.0000 F   0  0  0  0  0  0  0  0  0  0  0  0
    7.0068   -0.1449    0.0000 F   0  0  0  0  0  0  0  0  0  0  0  0
    6.5068   -2.7430    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  0  0  0  0
  2  3  4  0  0  0  0
  3  4  4  0  0  0  0
  4  5  1  0  0  0  0
  5  6  1  0  0  0  0
  6  7  4  0  0  0  0
  7  8  4  0  0  0  0
  8  9  4  0  0  0  0
  9 10  1  0  0  0  0
 10 11  3  0  0  0  0
  9 12  4  0  0  0  0
 12 13  4  0  0  0  0
 13  6  4  0  0  0  0
  4 14  4  0  0  0  0
 14 15  1  0  0  0  0
 15 16  2  0  0  0  0
 15 17  1  0  0  0  0
 17 18  1  0  0  0  0
 18 19  1  0  0  0  0
 19 20  4  0  0  0  0
 20 21  4  0  0  0  0
 21 22  4  0  0  0  0
 22 23  1  0  0  0  0
 23 24  1  0  0  0  0
 23 25  2  0  0  0  0
 23 26  2  0  0  0  0
 22 27  4  0  0  0  0
 27 28  4  0  0  0  0
 28 19  4  0  0  0  0
 14 29  4  0  0  0  0
 29 30  2  0  0  0  0
 29 31  4  0  0  0  0
 31  2  4  0  0  0  0
 31 32  1  0  0  0  0
 32 33  4  0  0  0  0
 33 34  4  0  0  0  0
 34 35  4  0  0  0  0
 35 36  4  0  0  0  0
 36 37  1  0  0  0  0
 37 38  1  0  0  0  0
 37 39  1  0  0  0  0
 36 40  4  0  0  0  0
 40 32  4  0  0  0  0
M  STY  1   1 DAT
M  SLB  1   1   1
M  SAL   1  1   3
M  SDT   1 MRV_IMPLICIT_H                                                       
M  SDD   1     0.0000    0.0000    DA    ALL  1       1  
M  SED   1 IMPL_H1
M  END
{300, 200}
```

The size annotation can appear before or after the SMILES string — the parser handles both.

Inline Molecules

For inline molecules within a paragraph, use a backtick code span with the mol: prefix:

Ethanol `mol:CCO` is the most common alcohol.

Benzene `mol:c1ccccc1` is the simplest aromatic hydrocarbon.

Inline with custom size:

Aspirin `mol:CC(=O)Oc1ccccc1C(=O)O {80, 60}` is an analgesic.

Syntax Summary

Context Markdown Syntax Default Size
Block (large diagram) mol\nSMILES\n 300 × 200
Block with size mol\nSMILES\n{W, H}\n W × H
Inline (small diagram) mol:SMILES 80 × 60
Inline with size mol:SMILES {W, H} W × H

How It Works

The rendering pipeline follows 5 steps:

Markdown source
    │  (marked.js / markdown-it / Markdig / any parser)
    ▼
HTML with <pre><code class="language-mol">SMILES</code></pre>
    │  and <code>mol:SMILES</code>
    │
    ▼  renderMolInElement(rootElement)
    │
    ├─ 1. Wait for RDKit WASM to initialize (singleton)
    ├─ 2. querySelectorAll("code.language-mol") — block molecules
    ├─ 3. parseMolBlock() — extract SMILES + optional {width, height}
    ├─ 4. RDKit.get_mol(smiles).get_svg(w, h) — generate SVG
    ├─ 5. Replace <pre><code> with <div class="mol-container">
    │
    └─ 6. querySelectorAll("code") where text starts with "mol:"
       └─ Same flow → replace <code> with <span class="mol-inline">

Key implementation details:

  • WASM Singleton: RDKit is loaded only once. Subsequent calls to getRDKit() return the cached instance.
  • Memory Safety: Every mol object is explicitly freed with mol.delete() to prevent WASM memory leaks.
  • Error Handling: Invalid SMILES strings are caught and displayed as error messages instead of crashing the page.
  • DOM-safe: Uses element.replaceWith() — no innerHTML mutations on parent elements, so existing event listeners are preserved.

Full Source Code

/**
 * auto-render-mol.js
 * A KaTeX-style auto-renderer for chemical structures powered by RDKit.js
 *
 * Usage:
 *   <script src="RDKit_minimal.js"></script>
 *   <script src="auto-render-mol.js"
 *           onload="renderMolInElement(document.body)"></script>
 */
(function (global) {

    // ========================================
    // 1. RDKit WASM Singleton
    // ========================================
    let _rdkitModule = null;
    let _rdkitPromise = null;

    function getRDKit() {
        if (_rdkitModule) return Promise.resolve(_rdkitModule);
        if (_rdkitPromise) return _rdkitPromise;

        if (typeof global.initRDKitModule !== "function") {
            return Promise.reject(
                new Error(
                    "[auto-render-mol] initRDKitModule not found. " +
                    "Make sure RDKit_minimal.js is loaded before this script."
                )
            );
        }

        _rdkitPromise = global.initRDKitModule().then(function (mod) {
            _rdkitModule = mod;
            console.log("✅ [auto-render-mol] RDKit WASM ready");
            return mod;
        }).catch(function (err) {
            _rdkitPromise = null; // allow retry on failure
            throw err;
        });

        return _rdkitPromise;
    }

    // ========================================
    // 2. Content Parser
    // ========================================
    /**
     * Extract SMILES and optional {width, height} from raw text.
     *
     * Supported formats:
     *   CCCC                → smiles="CCCC", size=default
     *   CCCC\n{300, 400}    → smiles="CCCC", size=300x400
     *   {300, 400}\nCCCC    → smiles="CCCC", size=300x400
     *   CCCC\n{500}         → smiles="CCCC", size=500xdefaultH
     *   CCCC {300, 400}     → smiles="CCCC", size=300x400
     *
     * @param {string} raw   Raw text from the code element
     * @param {number} defW  Default width
     * @param {number} defH  Default height
     * @returns {{ smiles: string, width: number, height: number }}
     */
    function parseMolBlock(raw, defW, defH) {
        var width = defW;
        var height = defH;

        // Match {width} or {width, height} with optional spaces
        var sizeRegex = /\{\s*(\d+)\s*(?:,\s*(\d+)\s*)?\}/;
        var match = raw.match(sizeRegex);

        if (match) {
            width = parseInt(match[1], 10) || width;
            if (match[2]) {
                height = parseInt(match[2], 10) || height;
            }
            // Strip the size marker; whatever remains is the SMILES
            raw = raw.replace(sizeRegex, "");
        }

        return {
            smiles: raw.trim(),
            width: width,
            height: height,
        };
    }

    // ========================================
    // 3. Inline Molecule Renderer
    // ========================================
    /**
     * Render inline molecules written as `mol:SMILES` (single backtick code spans).
     */
    function renderInlineMol(rootElement, RDKit, config) {
        var inlineCodes = rootElement.querySelectorAll("code");
        var prefix = "mol:";
        var count = 0;

        for (var i = 0; i < inlineCodes.length; i++) {
            var codeEl = inlineCodes[i];

            // Skip block-level codes (child of <pre>)
            if (codeEl.parentElement && codeEl.parentElement.tagName === "PRE") {
                continue;
            }

            var text = codeEl.textContent.trim();
            if (!text.startsWith(prefix)) continue;

            // Strip "mol:" prefix, then parse the body
            var body = text.slice(prefix.length).trim();
            var parsed = parseMolBlock(
                body,
                config.inlineWidth || 80,
                config.inlineHeight || 60
            );
            if (!parsed.smiles) continue;

            try {
                var mol = RDKit.get_mol(parsed.smiles);
                if (!mol) throw new Error("Invalid SMILES");

                var svg = mol.get_svg(parsed.width, parsed.height);
                mol.delete(); // free WASM memory

                // Replace <code> with <span> to preserve inline layout
                var span = document.createElement("span");
                span.className = "mol-inline";
                span.innerHTML = svg;
                span.setAttribute("data-smiles", parsed.smiles);
                span.title = "SMILES: " + parsed.smiles;

                codeEl.replaceWith(span);
                count++;
            } catch (err) {
                var errSpan = document.createElement("span");
                errSpan.className = "mol-error";
                errSpan.textContent = "❌" + parsed.smiles;
                codeEl.replaceWith(errSpan);
            }
        }

        if (count > 0) {
            console.log("🧬 [auto-render-mol] Rendered " + count + " inline molecule(s)");
        }
    }

    // ========================================
    // 4. Block Molecule Renderer
    // ========================================
    /**
     * Render block molecules from <code class="language-mol"> elements.
     */
    function renderBlockMol(rootElement, RDKit, config) {
        var molBlocks = rootElement.querySelectorAll(config.selector);
        if (molBlocks.length === 0) return;

        console.log(
            "🔬 [auto-render-mol] Found " + molBlocks.length + " block molecule(s)"
        );

        for (var i = 0; i < molBlocks.length; i++) {
            var codeEl = molBlocks[i];
            var rawText = codeEl.textContent;
            if (!rawText.trim()) continue;

            var parsed = parseMolBlock(rawText, config.width, config.height);
            if (!parsed.smiles) continue;

            // Choose the element to replace: parent <pre> or the <code> itself
            var targetEl =
                config.removePre &&
                codeEl.parentElement &&
                codeEl.parentElement.tagName === "PRE"
                    ? codeEl.parentElement
                    : codeEl;

            try {
                var mol = RDKit.get_mol(parsed.smiles);
                if (!mol) throw new Error("Invalid SMILES");

                var svg = mol.get_svg(parsed.width, parsed.height);
                mol.delete(); // free WASM memory

                var container = document.createElement("div");
                container.className = "mol-container";
                container.innerHTML = svg;
                container.setAttribute("data-smiles", parsed.smiles);
                container.title = "SMILES: " + parsed.smiles;

                targetEl.replaceWith(container);
            } catch (err) {
                var errorDiv = document.createElement("div");
                errorDiv.className = "mol-error";
                errorDiv.textContent = "❌ " + parsed.smiles + " → " + err.message;
                targetEl.replaceWith(errorDiv);
            }
        }
    }

    // ========================================
    // 5. Main Entry Point
    // ========================================
    /**
     * Scan rootElement for both block and inline mol references,
     * and replace them with rendered SVG structures.
     *
     * @param {HTMLElement} rootElement  Root node to scan
     * @param {Object}      options      Configuration
     * @param {string}      options.selector      CSS selector for block molecules
     *                                            (default: "code.language-mol")
     * @param {number}      options.width         Block SVG width  (default: 300)
     * @param {number}      options.height        Block SVG height (default: 200)
     * @param {number}      options.inlineWidth   Inline SVG width  (default: 80)
     * @param {number}      options.inlineHeight  Inline SVG height (default: 60)
     * @param {boolean}     options.removePre     Remove parent <pre> when replacing
     *                                            (default: true)
     */
    async function renderMolInElement(rootElement, options) {
        if (!rootElement) {
            console.warn("[auto-render-mol] rootElement is null, skipping");
            return;
        }

        var config = Object.assign(
            {
                selector: "code.language-mol",
                width: 300,
                height: 200,
                inlineWidth: 80,
                inlineHeight: 60,
                removePre: true,
            },
            options || {}
        );

        // Wait for RDKit WASM to be ready
        var RDKit;
        try {
            RDKit = await getRDKit();
        } catch (err) {
            console.error(err);
            return;
        }

        // ✅ Always run BOTH renderers, regardless of whether either finds matches
        renderBlockMol(rootElement, RDKit, config);
        renderInlineMol(rootElement, RDKit, config);
    }

    // ========================================
    // 6. Expose to global (mirrors KaTeX's renderMathInElement)
    // ========================================
    global.renderMolInElement = renderMolInElement;

})(typeof window !== "undefined" ? window : this);

Configuration Options

Option Type Default Description
selector string "code.language-mol" CSS selector for block-level mol elements
width number 300 Default SVG width for block molecules
height number 200 Default SVG height for block molecules
inlineWidth number 80 Default SVG width for inline molecules
inlineHeight number 60 Default SVG height for inline molecules
removePre boolean true Whether to remove the parent <pre> when replacing

Example with custom options:

<script>
    renderMolInElement(document.body, {
        width: 400,
        height: 300,
        inlineWidth: 100,
        inlineHeight: 80,
    });
</script>
/* Block molecule container */
.mol-container {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    padding: 8px;
    margin: 4px 0;
    background: #ffffff;
    border: 1px solid #e0e0e0;
    border-radius: 6px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.mol-container svg {
    display: block;
    max-width: 100%;
}

/* Inline molecule */
.mol-inline {
    display: inline-block;
    vertical-align: middle;
    margin: 0 2px;
    padding: 2px 4px;
    background: #fafafa;
    border: 1px solid #eee;
    border-radius: 3px;
    line-height: 0;
}
.mol-inline svg {
    display: inline-block;
    vertical-align: middle;
}

/* Error state */
.mol-error {
    color: #c00;
    font-family: monospace;
    background: #fff0f0;
    padding: 6px 10px;
    border-radius: 4px;
}

Complete Test Page

The following HTML tests renderMolInElement across every common DOM context — tables, blockquotes, nested blockquotes, lists, mixed nesting, inline molecules, and negative cases.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>auto-render-mol Compatibility Test</title>

    <style>
        body {
            font-family: -apple-system, "Segoe UI", sans-serif;
            max-width: 1000px;
            margin: 40px auto;
            padding: 0 20px;
            line-height: 1.6;
            color: #333;
        }
        h1 { border-bottom: 2px solid #333; padding-bottom: 8px; }
        h2 { color: #0066cc; margin-top: 40px; }

        table {
            width: 100%;
            border-collapse: collapse;
            margin: 16px 0;
        }
        table th, table td {
            border: 1px solid #ccc;
            padding: 12px;
            text-align: left;
            vertical-align: middle;
        }
        table th { background: #f0f4f8; }

        blockquote {
            border-left: 4px solid #0066cc;
            background: #f7faff;
            padding: 12px 20px;
            margin: 16px 0;
            border-radius: 0 6px 6px 0;
        }

        .mol-container {
            display: inline-flex;
            justify-content: center;
            align-items: center;
            padding: 8px;
            margin: 4px 0;
            background: #ffffff;
            border: 1px solid #e0e0e0;
            border-radius: 6px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.05);
        }
        .mol-container svg { display: block; max-width: 100%; }

        .mol-inline {
            display: inline-block;
            vertical-align: middle;
            margin: 0 2px;
            padding: 2px 4px;
            background: #fafafa;
            border: 1px solid #eee;
            border-radius: 3px;
            line-height: 0;
        }
        .mol-inline svg { display: inline-block; vertical-align: middle; }

        .mol-error {
            color: #c00;
            font-family: monospace;
            background: #fff0f0;
            padding: 6px 10px;
            border-radius: 4px;
        }

        pre {
            background: #f5f5f5;
            padding: 10px;
            border-radius: 4px;
            overflow-x: auto;
        }
        code.language-mol { color: #0066cc; }
    </style>
</head>
<body>

    <h1>🧪 auto-render-mol Compatibility Test</h1>
    <p>Testing <code>renderMolInElement</code> across various DOM contexts.</p>

    <!-- ===== Test 1: Basic top-level block ===== -->
    <h2>1. Basic Block (Control Group)</h2>
    <pre><code class="language-mol">CCCC
{300, 200}</code></pre>

    <!-- ===== Test 2: Inside a table ===== -->
    <h2>2. Inside a Table</h2>
    <table>
        <thead>
            <tr>
                <th>Compound</th>
                <th>SMILES</th>
                <th>Structure</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>Butane</td>
                <td><code>CCCC</code></td>
                <td><pre><code class="language-mol">CCCC
{200, 150}</code></pre></td>
            </tr>
            <tr>
                <td>Benzene</td>
                <td><code>c1ccccc1</code></td>
                <td><pre><code class="language-mol">c1ccccc1
{200, 150}</code></pre></td>
            </tr>
            <tr>
                <td>Aspirin</td>
                <td><code>CC(=O)Oc1ccccc1C(=O)O</code></td>
                <td><pre><code class="language-mol">CC(=O)Oc1ccccc1C(=O)O
{300, 200}</code></pre></td>
            </tr>
            <tr>
                <td>Caffeine</td>
                <td><code>CN1C=NC2=C1C(=O)N(C(=O)N2C)C</code></td>
                <td><pre><code class="language-mol">CN1C=NC2=C1C(=O)N(C(=O)N2C)C
{300, 200}</code></pre></td>
            </tr>
            <tr>
                <td>Invalid (error test)</td>
                <td><code>XYZ_INVALID</code></td>
                <td><pre><code class="language-mol">XYZ_INVALID
{200, 150}</code></pre></td>
            </tr>
        </tbody>
    </table>

    <!-- ===== Test 3: Inside a blockquote ===== -->
    <h2>3. Inside a Blockquote</h2>
    <blockquote>
        <p><strong>Quote:</strong> Butane is one of the simplest alkanes.</p>
        <pre><code class="language-mol">CCCC
{300, 200}</code></pre>
        <p>— Chemistry Textbook</p>
    </blockquote>

    <blockquote>
        <p>Another quote featuring <em>benzene</em>:</p>
        <pre><code class="language-mol">c1ccccc1</code></pre>
    </blockquote>

    <!-- ===== Test 4: Nested blockquotes ===== -->
    <h2>4. Nested Blockquotes</h2>
    <blockquote>
        <p>Level 1</p>
        <blockquote>
            <p>Level 2 — Ethanol:</p>
            <pre><code class="language-mol">CCO
{250, 180}</code></pre>
            <blockquote>
                <p>Level 3 — Aspirin:</p>
                <pre><code class="language-mol">CC(=O)Oc1ccccc1C(=O)O
{300, 200}</code></pre>
            </blockquote>
        </blockquote>
    </blockquote>

    <!-- ===== Test 5: Table + Blockquote mixed ===== -->
    <h2>5. Table + Blockquote Mixed Nesting</h2>
    <table>
        <thead>
            <tr><th>Category</th><th>Description & Structure</th></tr>
        </thead>
        <tbody>
            <tr>
                <td>Alcohols</td>
                <td>
                    <blockquote>
                        <p>Ethanol is the most common alcohol:</p>
                        <pre><code class="language-mol">CCO
{200, 150}</code></pre>
                    </blockquote>
                </td>
            </tr>
            <tr>
                <td>Aromatics</td>
                <td>
                    <blockquote>
                        <p>Benzene is the simplest aromatic:</p>
                        <pre><code class="language-mol">c1ccccc1
{200, 150}</code></pre>
                    </blockquote>
                </td>
            </tr>
        </tbody>
    </table>

    <!-- ===== Test 6: Inside lists ===== -->
    <h2>6. Inside Lists</h2>
    <ul>
        <li>
            <p>Methane:</p>
            <pre><code class="language-mol">C
{150, 120}</code></pre>
        </li>
        <li>
            <p>Ethanol:</p>
            <pre><code class="language-mol">CCO
{200, 150}</code></pre>
        </li>
    </ul>
    <ol>
        <li>
            <p>Step 1 — Benzene:</p>
            <pre><code class="language-mol">c1ccccc1
{200, 150}</code></pre>
        </li>
        <li>
            <p>Step 2 — Aspirin:</p>
            <pre><code class="language-mol">CC(=O)Oc1ccccc1C(=O)O
{300, 200}</code></pre>
        </li>
    </ol>

    <!-- ===== Test 7: Inline molecules ===== -->
    <h2>7. Inline Molecules</h2>
    <p>Ethanol <code>mol:CCO</code> is a common solvent, and benzene <code>mol:c1ccccc1</code> is the simplest aromatic.</p>
    <p>Inline with custom size: aspirin <code>mol:CC(=O)Oc1ccccc1C(=O)O {100, 80}</code> is an analgesic.</p>
    <p>A complex molecule inline: <code>mol:Cn1c(=O)n2n(c1=O)[C@@H]1CC3=C(C(=O)C=C(Br)C3=O)[C@@H](c3ccc(CO)o3)C1=CC2 {300, 200}</code></p>

    <!-- ===== Test 8: Negative cases (should NOT render) ===== -->
    <h2>8. Negative Cases (Should NOT Render)</h2>
    <p>These should remain as-is:</p>

    <pre><code class="language-javascript">console.log("CCCC"); // This is JS, not mol</code></pre>

    <pre><code>CCCC (no language-mol class — should be ignored)</code></pre>

    <p>Regular inline code: <code>let x = 42;</code> — untouched.</p>
    <p>Inline code without mol prefix: <code>CCCC</code> — untouched.</p>

    <!-- ===== Scripts ===== -->
    <script src="https://unpkg.com/@rdkit/rdkit/dist/RDKit_minimal.js"></script>
    <script src="./auto-render-mol.js"
            onload="renderMolInElement(document.body)"></script>

</body>
</html>

Complete Test Markdown

The HTML test page above verifies DOM-level behavior. But in a real workflow, your content starts as Markdown, gets converted to HTML by a parser (marked.js, markdown-it, Markdig…), and only then does renderMolInElement take over.

The Markdown document below is the source-level equivalent of the HTML test page. Drop it into your Markdown pipeline to verify that molecules survive the full Markdown → HTML → SVG journey.

# 🧪 auto-render-mol Compatibility Test

Testing `renderMolInElement` across various DOM contexts.

## 1. Basic Block (Control Group)

```mol
CCCC
{300, 200}
```

## 2. Inside a Table

| Compound              | SMILES                          | Structure                                       |
|-----------------------|---------------------------------|-------------------------------------------------|
| Butane                | `CCCC`                          | `mol:CCCC {200, 150}`                           |
| Benzene               | `c1ccccc1`                      | `mol:c1ccccc1 {200, 150}`                       |
| Aspirin               | `CC(=O)Oc1ccccc1C(=O)O`         | `mol:CC(=O)Oc1ccccc1C(=O)O {300, 200}`          |
| Caffeine              | `CN1C=NC2=C1C(=O)N(C(=O)N2C)C`  | `mol:CN1C=NC2=C1C(=O)N(C(=O)N2C)C {300, 200}`   |
| Invalid (error test)  | `XYZ_INVALID`                   | `mol:XYZ_INVALID {200, 150}`                    |

## 3. Inside a Blockquote

> **Quote:** Butane is one of the simplest alkanes.
>
> ```mol
> CCCC
> {300, 200}
> ```
>
> — Chemistry Textbook

> Another quote featuring *benzene*:
>
> ```mol
> c1ccccc1
> ```

## 4. Nested Blockquotes

> Level 1
>
> > Level 2 — Ethanol:
> >
> > ```mol
> > CCO
> > {250, 180}
> > ```
> >
> > > Level 3 — Aspirin:
> > >
> > > ```mol
> > > CC(=O)Oc1ccccc1C(=O)O
> > > {300, 200}
> > > ```

## 5. Table + Blockquote Mixed Nesting

| Category   | Description & Structure                                     |
|------------|-------------------------------------------------------------|
| Alcohols   | Ethanol is the most common alcohol: `mol:CCO {200, 150}`    |
| Aromatics  | Benzene is the simplest aromatic: `mol:c1ccccc1 {200, 150}` |

## 6. Inside Lists

- Methane:

  ```mol
  C
  {150, 120}
  ```

- Ethanol:

  ```mol
  CCO
  {200, 150}
  ```

1. Step 1 — Benzene:

   ```mol
   c1ccccc1
   {200, 150}
   ```

2. Step 2 — Aspirin:

   ```mol
   CC(=O)Oc1ccccc1C(=O)O
   {300, 200}
   ```

## 7. Inline Molecules

Ethanol `mol:CCO` is a common solvent, and benzene `mol:c1ccccc1` is the simplest aromatic.

Inline with custom size: aspirin `mol:CC(=O)Oc1ccccc1C(=O)O {100, 80}` is an analgesic.

A complex molecule inline: `mol:Cn1c(=O)n2n(c1=O)[C@@H]1CC3=C(C(=O)C=C(Br)C3=O)[C@@H](c3ccc(CO)o3)C1=CC2 {300, 200}`

## 8. Negative Cases (Should NOT Render)

These should remain as-is:

```javascript
console.log("CCCC"); // This is JS, not mol
```

```
CCCC (no language-mol class — should be ignored)
```

Regular inline code: `let x = 42;` — untouched.

Inline code without mol prefix: `CCCC` — untouched.

Markdown Authoring Notes

A few structural constraints are worth knowing when writing chemistry content in Markdown:

1. Tables cannot contain fenced code blocks.

This is a limitation of CommonMark and GFM, not of auto-render-mol. A table cell is parsed as inline content only — it cannot hold block-level elements like ```mol. This is exactly why inline molecules exist:

❌ Does not work ✅ Works
```mol inside a \| cell `mol:CCCC {200,150}` inside a cell

Since inline molecules accept an explicit {width, height}, you can still get full-size diagrams inside tables:

| Aspirin | `mol:CC(=O)Oc1ccccc1C(=O)O {300, 200}` |

2. Blockquotes require a > on every line of the code block.

> ```mol
> CCCC
> {300, 200}
> ```

3. List items require consistent indentation.

The code block must be indented to align with the list item's text content — 2 spaces for - bullets, 3 spaces for 1. numbers:

- Methane:

  ```mol
  C
  {150, 120}
  ```

1. Benzene:

   ```mol
   c1ccccc1
   {200, 150}
   ```

Omitting the > on the fence lines breaks the blockquote into separate blocks.

4. Escaping molecules in documentation.

If you need to show the ```mol syntax without rendering it (like in this blog post), wrap it in a fence with more backticks:


````markdown
```mol
CCCC
```
````markdown

5. Complex SMILES with special characters are safe.

Stereochemistry markers (@, @@), charges (+, -), brackets ([, ]), and ring closures are all preserved verbatim inside code blocks and code spans — Markdown does not apply emphasis or link parsing inside backticks:

`mol:Cn1c(=O)n2n(c1=O)[C@@H]1CC3=C(C(=O)C=C(Br)C3=O)[C@@H](c3ccc(CO)o3)C1=CC2`

mol:Cn1c(=O)n2n(c1=O)[C@@H]1CC3=C(C(=O)C=C(Br)C3=O)[C@@H](c3ccc(CO)o3)C1=CC2{200,200}

Rendering the Test Markdown

<div id="output"></div>

<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://unpkg.com/@rdkit/rdkit/dist/RDKit_minimal.js"></script>
<script src="./auto-render-mol.js"></script>

<script>
    fetch("./test.md")
        .then(res => res.text())
        .then(md => {
            // 1. Markdown → HTML
            document.getElementById("output").innerHTML = marked.parse(md);

            // 2. HTML → SVG molecules
            return renderMolInElement(document.getElementById("output"));
        })
        .then(() => console.log("✅ All molecules rendered"));
</script>

Open the browser console and you should see:

✅ [auto-render-mol] RDKit WASM ready
🔬 [auto-render-mol] Found 11 block molecule(s)
✅ All molecules rendered

Test Coverage Matrix

# Test Context Expected
1 Basic block Top-level <pre><code> ✅ Renders
2 Table cell <td> containing code block ✅ Renders
3 Blockquote <blockquote> containing code block ✅ Renders
4 Nested blockquotes 3 levels deep ✅ Renders
5 Table + Blockquote <td> → <blockquote> → code block ✅ Renders
6 Lists <ul> / <ol> containing code block ✅ Renders
7 Inline molecules <code>mol:SMILES</code> in paragraph ✅ Renders
8a Wrong language language-javascript ❌ Ignored
8b No class Plain <code> without class ❌ Ignored
8c No mol: prefix Inline <code> without mol: ❌ Ignored
2e Invalid SMILES XYZ_INVALID ⚠️ Error message

All positive cases work because querySelectorAll() recursively searches all descendant nodes regardless of nesting depth.

Compatibility

  • Markdown Parsers: Works with any parser that follows the CommonMark spec for fenced code blocks — marked.js, markdown-it, Markdig (.NET), remark, etc.
  • Browsers: Any browser that supports WebAssembly (Chrome 57+, Firefox 52+, Safari 11+, Edge 16+).
  • Frameworks: Call renderMolInElement(container) in Vue's nextTick, React's useEffect, or after any dynamic DOM update.

License

MIT