Beyond SMILES Captioning: A Critical Survey of Optical Chemical Structure Recognition Paradigms
Abstract
Optical Chemical Structure Recognition (OCSR) — the task of converting molecular structure images into machine-readable representations — stands at a pivotal inflection point. Four distinct paradigms now compete for dominance: rule-based classical pipelines, end-to-end SMILES sequence generation, graph-first structured prediction, and Vision-Language Model (VLM)-driven approaches. This article critically surveys the strengths and limitations of each paradigm, reviews key systems published from 2020 through 2026, and argues that the future of production-grade OCSR lies in a hybrid graph-first architecture augmented by VLM reasoning — rather than in the "image captioning" metaphor that has dominated the field.
mol:CC(C)[C@@H](C#N)NC(=O)O{150, 100} ▶▶▶▶ CC(C)[C@@H](C#N)NC(=O)O
1. Introduction
Optical Chemical Structure Recognition plays a pivotal role in modern chemical informatics, enabling the automated conversion of chemical structure images from scientific literature, patents, and educational materials into machine-readable molecular representations. This capability is essential for large-scale chemical data mining, drug discovery pipelines, and Large Language Model (LLM) applications in related domains.The scale of the challenge is staggering. In recent decades, chemistry publications and patents have increased rapidly, and a significant portion of key information is embedded in molecular structure figures, complicating large-scale literature searches and limiting the application of large language models in fields such as biology, chemistry, and pharmaceuticals.
Against this backdrop, a critical architectural debate has emerged: should OCSR models directly generate SMILES strings via autoregressive decoding, or should they reconstruct molecular graphs — atoms, bonds, and coordinates — as an intermediate representation? And now, with the rise of powerful VLMs, a third question arises: can general-purpose visual reasoning models subsume the task entirely?
This article systematically addresses all three questions. We trace the four generations of OCSR methodology, analyze their fundamental trade-offs, and propose a principled architecture for industrial-grade deployment.
2. Four Generations of OCSR
2.1 Generation 1: Rule-Based Classical Systems
OCSR tool development can be traced as far back as 1992, with the Kekule system by McDaniel & Balmuth. Classical systems such as OSRA (Filippov & Nicklaus, 2009), MolVec, and Imago rely on hand-crafted image processing pipelines: binarization, skeletonization, line segment detection, OCR text recognition, and graph assembly. These systems perform robustly on clean vector graphics but degrade on scanned documents, hand-drawn structures, and images containing abbreviations.
2.2 Generation 2: End-to-End SMILES Generation
The second generation, pioneered by DECIMER, reframed OCSR as an image captioning problem. A visual encoder (CNN or ViT) encodes the molecular image, and an autoregressive Transformer decoder generates a SMILES, DeepSMILES, or SELFIES string token by token. Models in this family — including SwinOCSR, Img2Mol, Image2SMILES, and ABC-Net — share the same fundamental architecture: a convolutional or transformer-based image encoder paired with a sequence decoder that outputs a linear molecular representation.
This paradigm is elegantly simple, but suffers from deep structural problems, analyzed in Section 3.
2.3 Generation 3: Graph-First Structured Prediction
The third generation reframes OCSR as a structured prediction problem: detect atoms and their coordinates, detect bonds and their types, and assemble a molecular graph that can be directly ingested by cheminformatics toolkits. MolScribe, MolGrapher, and MolNexTR are the canonical representatives of this line.
2.4 Generation 4: Vision-Language Models (VLMs)
While recent vision-language models (VLMs) have shown promise, their image-captioning approach often struggles with complex molecular structures and inconsistent annotations. New systems such as **GTR-VL**, **MolSight**, and **MolParser** introduce architectural innovations — graph traversal reasoning, reinforcement learning, and curriculum learning — to overcome these limitations. This generation is explored in depth in Section 5.3. The Fundamental Flaws of End-to-End SMILES Generation
3.1 Hallucination and Lack of Visual Grounding
When a Transformer decoder generates a SMILES string, each token is produced from learned statistical priors rather than precise visual evidence. If a six-membered ring in the input image is partially occluded or blurry, the model may "hallucinate" a benzene ring simply because aromatic systems are overrepresented in training corpora. There is no mechanism to trace a generated atom back to a specific pixel region in the source image, rendering the output fundamentally unexplainable.
3.2 Stereochemistry Failures
Existing OCSR systems face significant challenges in accurately recognizing stereochemical information due to the subtle visual cues that distinguish stereoisomers, such as wedge and dash bonds, ring conformations, and spatial arrangements. Autoregressive models are prone to correctly recovering the molecular connectivity graph while misassigning stereochemical configurations at chiral centers — a failure that is chemically catastrophic, as enantiomers can have radically different biological activities.3.3 Syntax and Chemical Validity
SMILES is a context-sensitive grammar. Mismatched parentheses, incorrect ring-closure digits, or invalid branch nesting produce strings that cannot be parsed by RDKit or other cheminformatics toolkits. While SELFIES guarantees syntactic validity, it does not resolve the deeper issue of chemical plausibility — a SELFIES string may be syntactically valid yet correspond to a physically impossible molecule.
3.4 Incompatibility with Human-in-the-Loop Workflows
In industrial settings — patent mining, electronic lab notebooks, regulatory submissions — OCSR outputs inevitably require human correction. When the output is a flat SMILES string, there is no spatial correspondence between the string and the original image. A chemist cannot click on a misidentified atom in the image to correct it; they must mentally decode the SMILES, locate the error, and manually edit the string — a process that is both tedious and error-prone.
3.5 Loss of Layout Information
The original 2D layout of the molecule — critical for visual comparison in patent documents and for understanding stereochemistry — is entirely discarded when the output is a linear SMILES string. Wedge bonds, hash bonds, and the spatial arrangement of substituents are compressed into @/@@ and //\ notations that models find notoriously difficult to predict correctly.
4. The Graph-First Paradigm: Atoms, Bonds, and Coordinates
4.1 Core Advantages
Seamless cheminformatics integration. Once a molecular graph with 2D coordinates is constructed in memory (as an RDKit Mol object or an MDL V2000/V3000 MolBlock), conversion to any desired format — SMILES, InChI, SDF, SVG — is trivial. This decouples the recognition problem from the representation problem.
Native support for interactive correction. Because every atom and bond carries its original (x, y) coordinate in the source image, the recognition result can be overlaid directly onto the input. A chemist can click on a misidentified atom, delete a spurious bond, or add a missing functional group through an intuitive graphical interface.
Explainability and debuggability. When recognition fails, the graph-based approach makes it straightforward to diagnose where the failure occurred: Was an atom misclassified by the detection module? Was a bond missed by the detection head? Was the ring-closure algorithm confused by a crowded region? This granular error attribution is impossible with end-to-end SMILES generation.
Layout preservation. Retaining 2D coordinates ensures that the spatial arrangement of the molecule — substituent orientation, macrocycle shape, relative positioning of fused ring systems — is faithfully preserved from the original document.
4.2 Key Systems
MolScribe (MIT, J. Chem. Inf. Model., 2023)
MolScribe is an image-to-graph generation model that explicitly predicts atoms and bonds along with their geometric layout, using a Swin Transformer encoder paired with a Transformer decoder. The model jointly predicts atom types, bond types, and (x, y) coordinates, producing a structured output that is assembled into a molecular graph.
MolGrapher (IBM/EPFL, ICCV 2023)
MolGrapher (Morin et al., 2023) was presented at ICCV, proposing a graph-based approach to the visual recognition of chemical structures. The system builds a hypergraph over detected keypoints and classifies nodes using a Graph Neural Network, recovering full atom and bond identities. This detection-centric design enables MolGrapher to handle larger molecules more gracefully than captioning-based methods.MolNexTR (J. Cheminformatics, 2024)
Chen et al. introduced MolNexTR, a generalized deep learning model for molecular image recognition, published in the *Journal of Cheminformatics* in 2024. MolNexTR combines a ConvNeXt backbone with a Vision Transformer encoder and a Transformer decoder to simultaneously output both a SMILES string and an atom-coordinate-annotated MolFile — enabling direct visualization aligned to the original structure orientation.MarkushGrapher (IBM/ETH, CVPR 2025)
Morin et al. followed up with MarkushGrapher, a system for joint visual and textual recognition of Markush structures, presented at CVPR 2025. This extension directly addresses the challenge of variable R-group notation and combinatorial structure enumeration that appears ubiquitously in patent literature.4.3 Persistent Challenges
The graph-first approach is not without difficulties:
- Cascading errors: Multi-stage pipelines are susceptible to error propagation — a single missed atom detection can break an entire ring system.
- Superatom abbreviations: Handling
COOH,OTf,Boc,Ph, and similar abbreviations requires a robust OCR module coupled with an extensive chemical dictionary for expansion. - Implicit hydrogen completion and valence balancing: Post-processing rules are required to avoid RDKit sanitization failures.
- Fixed-resolution encoders: Even graph-first models based on ViT encoders (e.g., MolScribe) are still constrained by quadratic self-attention complexity at high resolutions.
5. The Resolution Curse: Why Transformers Fail on Large Molecules
5.1 Quadratic Complexity and the Patch Bottleneck
The self-attention mechanism in Vision Transformers scales as \(O(N^2)\) with respect to the number of patches \(N\). For a 384×384 input with 16×16 patches, the sequence length is a manageable 576. However, a 1024×1024 image — necessary to resolve fine details of a large natural product or macrocycle — yields a sequence length of 4,096, causing memory consumption to explode quadratically.
5.2 Information Destruction via Resizing
Most current OCSR models enforce a fixed input resolution (typically 384×384 or 448×448). When a complex molecule with 80+ heavy atoms — such as paclitaxel, rapamycin, or vancomycin — is resized to fit this constraint:
- Individual bonds shrink to 1–2 pixels, making double and triple bonds indistinguishable;
- Text labels (e.g.,
OMe,NHBoc) blur into illegible blobs; - Dense polycyclic systems (steroids, porphyrins, fullerenes) collapse into unresolvable clusters.
5.3 The Benchmark Illusion
Standard benchmarks (USPTO, UOB, JPO, CLEF) are biased toward small, drug-like molecules. The presence of numerous Markush structures in real-world documents, along with variations in molecular image quality, drawing styles, and noise, significantly limits the performance of existing OCSR methods when they are deployed beyond their training distribution.
A 2024 comparative study evaluating eight open-source OCSR tools found F1 scores ranging from 34% to 93%, with no single tool consistently outperforming others across all categories of chemical structure images — a stark reminder that benchmark performance and real-world performance remain poorly correlated.
6. Generation 4: Vision-Language Models Enter the Arena
6.1 MolParser (DP Technology / AI for Science Institute, ICCV 2025)
MolParser is a novel end-to-end OCSR method that efficiently and accurately recognizes chemical structures from real-world documents, including difficult Markush structures. Trained using a curriculum learning approach, MolParser significantly outperforms classical and learning-based methods across most scenarios, with potential for broader downstream applications. The model uses an extended SMILES encoding rule to represent Markush structures, connection points, and abstract ring systems that standard SMILES cannot express.6.2 GTR-VL / GTR-CoT (Shanghai AI Lab et al., arXiv 2025–2026)
GTR-VL features two key innovations: (1) the *Graph Traversal as Visual Chain of Thought* mechanism that emulates human reasoning by incrementally parsing molecular graphs through sequential atom-bond predictions, and (2) the data-centric *Faithfully Recognize What You've Seen* principle, which aligns abbreviated structures in images with their expanded annotations.Concretely, instead of predicting atoms and bonds in isolation or jumping straight to a SMILES string, GTR-VL performs a depth-first traversal of the molecular graph, interleaving atom and bond predictions in a single sequence to mimic human reasoning. This step-by-step approach provides mutual constraints — bonds are predicted based on previously identified atoms — which significantly reduces the search space and improves structural consistency.
For hand-drawn OCSR tasks, where datasets lack graph annotations and only provide final SMILES, reinforcement learning using the GRPO method is applied, with reward mechanisms including format reward, graph reward, and SMILES reward. To support model development, the authors constructed GTR-CoT-1.3M, a large-scale instruction-tuning dataset with meticulously corrected annotations, and introduced MolRec-Bench, the first benchmark designed for fine-grained evaluation of graph-parsing accuracy in OCSR. In scenarios involving molecular images with functional group abbreviations, GTR-Mol-VLM outperforms the second-best baseline by approximately 14 percentage points, in both SMILES-based and graph-based metrics.6.3 MolSight (HUST, AAAI 2026)
MolSight is a comprehensive learning framework for OCSR, designed to bridge the gap between computer vision and chemical informatics. It introduces a three-stage training paradigm:- In the first stage, MolSight supports pretraining using large quantities of images annotated only with SMILES, with the objective of enhancing the perception capabilities of the image encoder for molecular images.
- In the second stage, by introducing the additional chemical bond head and coordinate head, the performance of the model is improved.
- Reinforcement learning has shown tremendous potential in improving models' ability to solve complex reasoning tasks; MolSight innovatively introduces RL algorithms into OCSR tasks, utilizing images of stereoisomers that are commonly confused by the model for RL optimization, thereby further enhancing the model's understanding of chemical semantics.
7. Benchmark Evolution: From Curated to Wild
The OCSR community is increasingly aware that standard benchmarks do not reflect the difficulty of real-world documents. Several new evaluation frameworks have been introduced:
| Benchmark | Year | Key Property |
|---|---|---|
| USPTO / UOB / JPO / CLEF | 2009–2020 | Clean, small-molecule biased; standard evaluation |
| MolRec-Bench | 2025 | Addresses the gap in existing OCSR evaluation sets by providing means to assess molecular graph structure parsing results |
| GTR-CoT-1.3M | 2025 | Large-scale instruction-tuning dataset with corrected abbreviation annotations |
| Stereo-200K | 2025 | 200K stereoisomeric molecules for chiral recognition training |
| MolParser-7M | 2024 | Large-scale pretraining corpus covering diverse image styles and patent sources |
A recurring finding is that models achieving >90% exact match on USPTO can fail catastrophically on images drawn from total synthesis publications — underscoring the importance of distributional diversity in both training data and evaluation.
8. Current Systems: A Comparative Overview
| System | Institution | Venue | Paradigm | Key Innovation |
|---|---|---|---|---|
| DECIMER 2.x | Steinbeck Lab | J. Cheminform. 2021–2024 | EfficientNet + Transformer → SMILES | Pioneer deep-learning OCSR; continuous iteration |
| MolScribe | MIT (Coley & Barzilay) | J. Chem. Inf. Model. 2023 | Swin + Transformer → graph | Explicit atom coordinates + bonds; strong small-molecule SOTA |
| MolGrapher | IBM / EPFL | ICCV 2023 | CNN keypoints + GNN | Hypergraph + node classification; robust on large molecules |
| MolNexTR | Chen et al. | J. Cheminform. 2024 | ConvNeXt + ViT + Transformer | Dual output: SMILES + MolFile with coordinates |
| MarkushGrapher | IBM / ETH | CVPR 2025 | Detection + GNN | Joint visual + textual recognition of Markush structures |
| MolParser | DP Technology | ICCV 2025 | Image captioning + curriculum learning | Markush support; extended SMILES; trained on MolParser-7M |
| GTR-VL | Shanghai AI Lab et al. | arXiv 2025–2026 | VLM + graph traversal CoT + GRPO | Depth-first atom-bond prediction; abbreviation alignment |
| MolSight | HUST | AAAI 2026 | EfficientViT + 3-stage RL training | First RL-OCSR; GRPO reward; Stereo-200K dataset |
| OSRA | NIH (Filippov) | J. Chem. Inf. Model. 2009 | Classical CV / rule-based | No deep learning; stable on clean vector graphics |
9. A Principled Architecture for Production OCSR
Based on the preceding analysis, we recommend the following hybrid architecture for an industrial-grade OCSR system:
Input Image (arbitrary resolution — no forced resizing)
│
▼
┌──────────────────────────────────────────────────────────┐
│ High-Resolution Backbone │
│ (fully convolutional or linear attention) │ ← ConvNeXt / HRNet / EfficientViT
│ O(N) or O(N log N) complexity │
└─────────────────────────┬────────────────────────────────┘
│
┌───────────────┼───────────────┬───────────────┐
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Atom Det. │ │ Bond Det. │ │Stereo Head │ │ OCR Head │
│ (bbox+cls) │ │ (skeleton) │ │(wedge/hash) │ │ (abbrev.) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
└───────────────┴───────────────┴───────────────┘
│
▼
┌───────────────────────────────┐
│ Geometric + Graph Assembly │ ← Ring perception,
│ (spatial matching, topology) │ valence check,
│ │ superatom expansion
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ RDKit Validation & Export │ ← SanitizeMol →
│ (MolBlock, SDF, InChI, SVG) │ SMILES / MolBlock / SVG
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ (Optional) VLM Second Pass │ ← Low-confidence re-check,
│ │ hand-drawn structures,
│ │ Markush / abbreviation
│ │ resolution
└───────────────────────────────┘
Key design principles:
- Never resize the input. Use fully convolutional backbones or linear-attention encoders that accept arbitrary resolutions.
- Predict coordinates, not sequences. Every atom and bond must be grounded in pixel space to enable overlay, debugging, and interactive correction.
- Decouple recognition from representation. Delegate format conversion and chemical validation to established cheminformatics toolkits (RDKit, OpenBabel).
- Design for human-in-the-loop correction. Overlay predictions on the original image and provide interactive editing tools — essential for achieving the >99% accuracy required in production.
- Use ensemble / multi-model strategies. Cross-validating outputs from multiple systems (e.g., a rule-based tool + a graph-first model) and flagging disagreements for human review is a practical path to production robustness.
- Address stereochemistry as a first-class problem. Train on stereochemically diverse datasets (e.g., Stereo-200K) and include dedicated stereo-prediction heads or RL post-training stages.
10. Conclusion
The prevailing practice of treating OCSR as an image-to-text translation problem — directly generating SMILES strings via Transformer decoders — represents a misapplication of NLP paradigms to a fundamentally geometric and topological task. Molecular structure diagrams are not natural language; they are precise spatial graphs governed by strict chemical rules.
While recent vision-language models have shown potential in this task, their image-captioning approach often struggles with complex molecular structures and inconsistent annotations.The correct paradigm for production OCSR is a graph-first reconstruction approach augmented by VLM-based reasoning: detect atoms and bonds at high resolution, recover their spatial coordinates, assemble a molecular graph, and delegate format conversion and chemical validation to established cheminformatics toolkits. For challenging cases — Markush structures, hand-drawn images, heavily abbreviated compounds — a VLM second pass using graph traversal chain-of-thought reasoning provides powerful complementary capability.
This hybrid approach offers:
- Superior explainability — every atom and bond is spatially grounded;
- Seamless human-in-the-loop correction — predictions overlay directly onto the source image;
- Native support for arbitrary image resolutions — no forced downsampling of complex molecules;
- Representational flexibility — any desired output format via RDKit;
- Stereochemical fidelity — with dedicated stereo heads and RL-based post-training.
While it demands more engineering effort than end-to-end SMILES generation, it is the only path toward the accuracy, robustness, and interpretability required for industrial-scale chemical digitization.
The future of OCSR lies not in teaching machines to "caption" molecules, but in teaching them to "see" and "reconstruct" molecular graphs — atom by atom, bond by bond — and to reason about what they observe.
References
- McDaniel, J.R. & Balmuth, J.R. Kekule: OCR-optical chemical (structure) recognition. J. Chem. Inf. Comput. Sci. (1992).
- Filippov, I.V. & Nicklaus, M.C. Optical Structure Recognition Software to Recover Chemical Information: OSRA, an Open Source Solution. J. Chem. Inf. Model. (2009).
- Rajan, K. et al. DECIMER: Towards Deep Learning for Chemical Image Recognition. J. Cheminform. (2020).
- Rajan, K. et al. DECIMER 1.0: Deep Learning for Chemical Image Recognition using Transformers. J. Cheminform. (2021).
- Rajan, K. et al. Advancements in hand-drawn chemical structure recognition through an enhanced DECIMER architecture. J. Cheminform. (2024).
- Qian, Y. et al. MolScribe: Robust Molecular Structure Recognition with Image-to-Graph Generation. J. Chem. Inf. Model. (2023).
- Morin, L. et al. MolGrapher: Graph-based Visual Recognition of Chemical Structures. ICCV (2023).
- Chen, Y. et al. MolNexTR: A Generalized Deep Learning Model for Molecular Image Recognition. J. Cheminform. (2024).
- Morin, L. et al. MarkushGrapher: Joint Visual and Textual Recognition of Markush Structures. CVPR (2025).
- Fang, X. et al. MolParser: End-to-end Visual Recognition of Molecule Structures in the Wild. ICCV (2025).
- Wang, J. et al. GTR-CoT: Graph Traversal as Visual Chain of Thought for Molecular Structure Recognition. arXiv:2506.07553 (2025).
- Zhang, W. et al. MolSight: Optical Chemical Structure Recognition with SMILES Pretraining, Multi-Granularity Learning and Reinforcement Learning. AAAI (2026).
- Fan, V. et al. OpenChemIE: An Information Extraction Toolkit for Chemistry Literature. J. Chem. Inf. Model. (2024).
- Rajan, K. et al. MARCUS: Molecular Annotation and Recognition for Curating Unravelled Structures. Digital Discovery (2025).
- Krasnov, A. et al. Comparing Software Tools for Optical Chemical Structure Recognition. Digital Discovery (2024).
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
* 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>
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.
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'snextTick, React'suseEffect, or after any dynamic DOM update.
License
MIT