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
 * KaTeX-style auto-renderer for chemical structures using 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;
        });

        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=300×400
     *   {300, 400}\nCCCC    → smiles="CCCC", size=300×400
     *   CCCC\n{500}         → smiles="CCCC", size=500×defaultH
     *   CCCC {300, 400}     → smiles="CCCC", size=300×400
     */
    function parseMolBlock(raw, defW, defH) {
        var width = defW;
        var height = defH;

        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;
            }
            raw = raw.replace(sizeRegex, "");
        }

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

    // ========================================
    // 3. Inline Molecule Renderer
    // ========================================
    async function renderInlineMol(rootElement, RDKit, config) {
        var inlineCodes = rootElement.querySelectorAll("code");
        var prefix = "mol:";

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

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

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

            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();

                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);
            } catch (err) {
                var errSpan = document.createElement("span");
                errSpan.className = "mol-error";
                errSpan.textContent = "❌" + parsed.smiles;
                codeEl.replaceWith(errSpan);
            }
        }
    }

    // ========================================
    // 4. Core Render Function
    // ========================================
    /**
     * Scan rootElement for mol code blocks and inline codes,
     * replace them with rendered SVG structures.
     *
     * @param {HTMLElement} rootElement  Root node to scan
     * @param {Object}      options      Configuration
     * @param {string}      options.selector    CSS selector (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> tag (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 || {}
        );

        var RDKit;
        try {
            RDKit = await getRDKit();
        } catch (err) {
            console.error(err);
            return;
        }

        // --- Block-level rendering ---
        var molBlocks = rootElement.querySelectorAll(config.selector);

        if (molBlocks.length > 0) {
            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;

                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();

                    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);
                }
            }
        }

        // --- Inline rendering ---
        await renderInlineMol(rootElement, RDKit, config);
    }

    // ========================================
    // 5. Export to global scope
    // ========================================
    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.

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