Leo He

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:
  1. 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.
  2. In the second stage, by introducing the additional chemical bond head and coordinate head, the performance of the model is improved.
  3. 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.
MolSight is the first OCSR system to integrate Reinforcement Learning, utilizing Group Relative Policy Optimization (GRPO) to directly optimize chemical validity, and introduces Stereo-200K — a new annotated dataset of 200,000 challenging stereoisomeric molecules specifically curated to address confusion in 3D chiral structures. Extensive experiments demonstrate that MolSight achieves state-of-the-art results in accuracy, similarity, and robustness, outperforming both classical and learning-based baselines.

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:

  1. Never resize the input. Use fully convolutional backbones or linear-attention encoders that accept arbitrary resolutions.
  2. Predict coordinates, not sequences. Every atom and bond must be grounded in pixel space to enable overlay, debugging, and interactive correction.
  3. Decouple recognition from representation. Delegate format conversion and chemical validation to established cheminformatics toolkits (RDKit, OpenBabel).
  4. 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.
  5. 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.
  6. 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

  1. McDaniel, J.R. & Balmuth, J.R. Kekule: OCR-optical chemical (structure) recognition. J. Chem. Inf. Comput. Sci. (1992).
  2. Filippov, I.V. & Nicklaus, M.C. Optical Structure Recognition Software to Recover Chemical Information: OSRA, an Open Source Solution. J. Chem. Inf. Model. (2009).
  3. Rajan, K. et al. DECIMER: Towards Deep Learning for Chemical Image Recognition. J. Cheminform. (2020).
  4. Rajan, K. et al. DECIMER 1.0: Deep Learning for Chemical Image Recognition using Transformers. J. Cheminform. (2021).
  5. Rajan, K. et al. Advancements in hand-drawn chemical structure recognition through an enhanced DECIMER architecture. J. Cheminform. (2024).
  6. Qian, Y. et al. MolScribe: Robust Molecular Structure Recognition with Image-to-Graph Generation. J. Chem. Inf. Model. (2023).
  7. Morin, L. et al. MolGrapher: Graph-based Visual Recognition of Chemical Structures. ICCV (2023).
  8. Chen, Y. et al. MolNexTR: A Generalized Deep Learning Model for Molecular Image Recognition. J. Cheminform. (2024).
  9. Morin, L. et al. MarkushGrapher: Joint Visual and Textual Recognition of Markush Structures. CVPR (2025).
  10. Fang, X. et al. MolParser: End-to-end Visual Recognition of Molecule Structures in the Wild. ICCV (2025).
  11. Wang, J. et al. GTR-CoT: Graph Traversal as Visual Chain of Thought for Molecular Structure Recognition. arXiv:2506.07553 (2025).
  12. Zhang, W. et al. MolSight: Optical Chemical Structure Recognition with SMILES Pretraining, Multi-Granularity Learning and Reinforcement Learning. AAAI (2026).
  13. Fan, V. et al. OpenChemIE: An Information Extraction Toolkit for Chemistry Literature. J. Chem. Inf. Model. (2024).
  14. Rajan, K. et al. MARCUS: Molecular Annotation and Recognition for Curating Unravelled Structures. Digital Discovery (2025).
  15. 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 mol object is explicitly freed with mol.delete() to prevent WASM memory leaks.
  • Error Handling: Invalid SMILES strings are caught and displayed as error messages instead of crashing the page.
  • DOM-safe: Uses element.replaceWith() — no innerHTML mutations on parent elements, so existing event listeners are preserved.

Full Source Code

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

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

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

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

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

        return _rdkitPromise;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Configuration Options

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

Example with custom options:

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

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

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

Complete Test Page

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</body>
</html>

Complete Test Markdown

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

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

# 🧪 auto-render-mol Compatibility Test

Testing `renderMolInElement` across various DOM contexts.

## 1. Basic Block (Control Group)

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

## 2. Inside a Table

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

## 3. Inside a Blockquote

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

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

## 4. Nested Blockquotes

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

## 5. Table + Blockquote Mixed Nesting

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

## 6. Inside Lists

- Methane:

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

- Ethanol:

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

1. Step 1 — Benzene:

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

2. Step 2 — Aspirin:

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

## 7. Inline Molecules

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

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

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

## 8. Negative Cases (Should NOT Render)

These should remain as-is:

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

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

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

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

Markdown Authoring Notes

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

1. Tables cannot contain fenced code blocks.

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

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

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

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

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

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

3. List items require consistent indentation.

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

- Methane:

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

1. Benzene:

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

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

4. Escaping molecules in documentation.

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


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

5. Complex SMILES with special characters are safe.

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

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

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

Rendering the Test Markdown

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

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

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

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

Open the browser console and you should see:

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

Test Coverage Matrix

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

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

Compatibility

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

License

MIT