KaTeX-Style Auto-Renderer for Chemical Structures — auto-render-mol.js
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
molobject is explicitly freed withmol.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()— noinnerHTMLmutations 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>
Recommended CSS
/* 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'snextTick, React'suseEffect, or after any dynamic DOM update.
License
MIT