📚 CBTEditor Documentation

CBTEditor is a free, open-source (MIT) WYSIWYG editor with visual math and chemistry editing. Built on TipTap (ProseMirror) + MathLive. No framework required — drop a <script> tag into any HTML page, PHP template, or CMS.

Features

💡 What's in the box? cbt-editor.min.js (~1.1 MB), cbt-editor.min.css (~6 KB), and a fonts/ directory for MathLive's KaTeX math fonts (~2 MB). That's it.

Quick Start

Option A: Plain <script> tag (no build tools)

Copy three things into your project and add 5 lines of HTML:

<link rel="stylesheet" href="cbt-editor.min.css">
<script src="cbt-editor.min.js"></script>
<div id="editor"></div>
<script>
  CBTEditor.setMathLiveFontsDir('./fonts');
  const editor = new CBTEditor({ target: document.getElementById('editor') });
</script>

Works in plain HTML, PHP, WordPress, any CMS. Zero build step.

Option B: npm (if you use a bundler)

npm install cbt-editor
import CBTEditor from 'cbt-editor';
const editor = new CBTEditor({ target: document.getElementById('editor') });

Installation

Option A: Plain <script> tag — No build tools

This is the primary path. Copy these into your project:

your-project/
├── cbt-editor.min.js    ← the editor bundle
├── cbt-editor.min.css   ← styles
└── fonts/               ← MathLive math fonts (required for rendering)

Then in any HTML or PHP page:

<link rel="stylesheet" href="cbt-editor.min.css">
<script src="cbt-editor.min.js"></script>
<div id="editor"></div>
<script>
  CBTEditor.setMathLiveFontsDir('./fonts');
  new CBTEditor({ target: document.getElementById('editor') });
</script>

Using in PHP / WordPress

<link rel="stylesheet"
      href="<?php echo get_template_directory_uri(); ?>/cbt-editor/cbt-editor.min.css">
<script src="<?php echo get_template_directory_uri(); ?>/cbt-editor/cbt-editor.min.js"></script>

<div id="my-editor"></div>
<script>
CBTEditor.setMathLiveFontsDir(
  '<?php echo get_template_directory_uri(); ?>/cbt-editor/fonts'
);
new CBTEditor({ target: document.getElementById('my-editor') });
</script>

Option B: npm + bundler

npm install cbt-editor
import CBTEditor from 'cbt-editor';
import 'cbt-editor/dist/cbt-editor.min.css';

const editor = new CBTEditor({
  target: document.getElementById('editor')
});

Note: even with npm, you still need the fonts/ directory accessible at a URL. Copy it from node_modules/cbt-editor/dist/fonts/ to your public folder.

Usage & API Reference

Creating an Editor

const editor = new CBTEditor({
  target: document.getElementById('editor'),  // REQUIRED
  initialContent: myDocumentJSON,              // optional
  onSave: (json) => { /* handle save */ },     // optional
  readOnly: false                              // optional
});

Options

OptionTypeDefaultDescription
targetHTMLElementrequiredThe DOM element to mount the editor into
initialContentObjectnullA TipTap-compatible JSON document to load on init
onSaveFunctionnullCalled with document JSON when user clicks Save
readOnlybooleanfalseWhether the editor is read-only

Methods

editor.getJSON() — Returns the full document as a TipTap-compatible JSON object.

const doc = editor.getJSON();

editor.getHTML() — Returns the document as an HTML string.

const html = editor.getHTML();

editor.loadJSON(json) — Loads a document from JSON (replaces current content).

editor.loadJSON(savedDocument);

editor.setReadOnly(bool) — Toggles read-only mode.

editor.setReadOnly(true);  // disable editing
editor.setReadOnly(false); // enable editing

editor.destroy() — Destroys the editor and cleans up the DOM.

editor.destroy();

editor.on(event, callback) — Register an event listener (supports 'save').

editor.on('save', (json) => {
  fetch('/api/documents', { method: 'POST', body: JSON.stringify(json) });
});

Static Method

CBTEditor.setMathLiveFontsDir(dir) — Sets the directory where MathLive looks for its KaTeX font files. Call once before creating any editor.

CBTEditor.setMathLiveFontsDir('./dist/fonts');

Export & Import

editor.exportToHTML()

Returns the document as an HTML string, suitable for PDF generation or web display.

const html = editor.exportToHTML();

editor.exportMathAsLatex()

Extracts LaTeX strings from all MathBlocks in the document. Returns an array of { latex: string } objects.

const mathBlocks = editor.exportMathAsLatex();
// [{ latex: "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" }]
mathBlocks.forEach(m => console.log(m.latex));

editor.exportChemAsJSON()

Extracts the chemistry formula tree from all ChemBlocks. Returns an array of parsed formula objects.

const chemBlocks = editor.exportChemAsJSON();
// [{ terms: [{ coefficient: 2, groups: [...] }, ...] }]
chemBlocks.forEach(c => console.log(c.terms));

editor.importFromJSON(json)

Imports a document from saved JSON. Validates the document has a type field. Returns true on success.

const ok = editor.importFromJSON(savedDocument);
if (!ok) console.error('Invalid document format');
⚠️ Critical guarantee: importFromJSON() (and passing initialContent) reconstructs the full ProseMirror document tree. Math blocks and chemistry blocks reappear as fully editable interactive blocks — never flattened to static text or images. As long as the JSON includes the correct "type": "mathBlock" / "type": "chemBlock" node types with their attributes intact, the blocks will be click-to-edit.

Edit / Update Pattern (full lifecycle)

This is the most common pattern: load a saved document, edit it, save back:

// 1. Fetch saved document from your backend
const response = await fetch('/api/documents/123');
const savedDoc = await response.json();

// 2. Load it into the editor (math & chem blocks are EDITABLE)
const editor = new CBTEditor({
  target: document.getElementById('editor'),
  initialContent: savedDoc
});

// 3. User edits... then saves
editor.on('save', async (updatedJson) => {
  await fetch('/api/documents/123', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ document: updatedJson })
  });
  console.log('✅ Saved!');
});

Round-Trip Test

// Confidence check: save, clear, reload, verify blocks still editable
const saved = editor.getJSON();
editor.loadJSON({ type: 'doc', content: [] });        // clear
editor.importFromJSON(saved);                          // reload

const math = editor.exportMathAsLatex();
const chem = editor.exportChemAsJSON();
console.assert(math.length > 0, 'Math blocks survived');
console.assert(chem.length > 0, 'Chem blocks survived');
console.log('✅ Round-trip passed');

Architecture

CBTEditor is built in three independent layers, following a shell + plugins pattern:

CBTEditor (public API)
│
├── Shell: TipTap (ProseMirror)          ← document model, toolbar, formatting
│     MIT license
│
├── Custom Node: MathBlock               ← visual math editing
│     wraps MathLive (<math-field>)       ← MIT license
│     stores content as LaTeX string
│
└── Custom Node: ChemBlock               ← visual chemistry formula editing
      custom tree-based model
      stores formula tree as JSON string

1. Shell (TipTap)

TipTap (built on ProseMirror) handles the hard parts: document model, state management, selection/cursor, keyboard handling, undo/redo history, JSON/HTML serialization, table editing, and an extension system. We use @tiptap/core directly — not @tiptap/react — to keep the package framework-agnostic. The toolbar is built with plain DOM elements.

2. MathBlock

A custom TipTap Node that uses the NodeView API to render a MathLive <math-field> web component at the node's position in the document. LaTeX is stored as a node attribute (attrs.latex). Clicking the display toggles the math-field into edit mode. MathLive provides visual fraction/superscript/subscript editing, tap-to-build virtual keyboard, and LaTeX/MathJSON import/export.

3. ChemBlock

A custom TipTap Node with its own visual editor built in plain HTML/CSS. The formula is stored as a JSON tree in attrs.formula:

{
  "terms": [
    { "coefficient": 2, "groups": [
        { "symbol": "H", "subscript": "2", "superscript": "" }
    ]},
    { "operator": "+" },
    { "coefficient": 1, "groups": [
        { "symbol": "O", "subscript": "2", "superscript": "" }
    ]},
    { "operator": "→" },
    { "coefficient": 2, "groups": [
        { "symbol": "H", "subscript": "2", "superscript": "" },
        { "symbol": "O", "subscript": "1", "superscript": "" }
    ]}
  ]
}

Extending with Custom Nodes

Follow this pattern to add a new block type (e.g. GraphBlock, CodeBlock, DiagramBlock):

Step 1: Create the Node file

Create src/nodes/GraphBlock.js:

import { Node } from '@tiptap/core';

const GraphBlock = Node.create({
  name: 'graphBlock',        // Must be unique
  group: 'block',
  atom: true,                // Atomic unit (no nested text editing)
  inline: false,
  draggable: true,

  addAttributes() {
    return {
      data: {
        default: '{}',
        parseHTML: (el) => el.getAttribute('data-graph') || '{}',
        renderHTML: (attrs) => ({ 'data-graph': attrs.data }),
      },
    };
  },

  parseHTML() {
    return [{ tag: 'div[data-graph-block]' }];
  },

  renderHTML({ node }) {
    return ['div', {
      'data-graph-block': '',
      'data-graph': node.attrs.data,
      class: 'cbt-graph-block',
    }, ['span', {}, '📊 Graph']];
  },

  addNodeView() {
    return ({ node, getPos, editor }) => {
      const wrapper = document.createElement('div');
      wrapper.className = 'cbt-graph-block';
      wrapper.setAttribute('data-graph-block', '');
      wrapper.contentEditable = 'false';  // ← IMPORTANT

      // Build your custom UI here...
      const canvas = document.createElement('canvas');
      canvas.width = 400; canvas.height = 200;
      wrapper.appendChild(canvas);

      return {
        dom: wrapper,
        update(updatedNode) {
          if (updatedNode.attrs.data !== node.attrs.data) {
            // Re-render
          }
          return true;
        },
        ignoreMutation(mutation) {
          return wrapper.contains(mutation.target);
        },
      };
    };
  },
});

Step 2: Register in CBTEditor.js

import GraphBlock from './nodes/GraphBlock.js';

// Inside _createTipTapEditor():
extensions: [
  StarterKit.configure({ heading: { levels: [1, 2, 3] } }),
  Table.configure({ resizable: true }),
  TableRow, TableCell, TableHeader,
  MathBlock,
  ChemBlock,
  GraphBlock,  // ← add here
],

Step 3: Add Toolbar Button

In _toolbarHTML(), add your button with data-action="insertGraphBlock", then handle it in _bindToolbarEvents():

case 'insertGraphBlock':
  this._editor.chain().focus().insertContent({
    type: 'graphBlock', attrs: { data: '{}' }
  }).run();
  break;

Step 4: Test Round-Tripping

editor.getJSON();   // → should include your graphBlock
editor.importFromJSON(savedJson);  // → should reconstruct it as editable

The key rules:

Updating CBTEditor

Versioning

CBTEditor follows Semantic Versioning (MAJOR.MINOR.PATCH):

⚠️ Most important step before releasing: Test that old saved documents still load correctly. Users have documents built with previous versions. After ANY change to how math or chemistry content is stored, verify old documents open correctly.

Release Checklist

  1. Test backward compatibility — load an old document, save it, reload it. Verify math/chem blocks are still editable.
  2. Check the public API — all documented methods still work
  3. Test both distribution paths — UMD (script tag) and npm (ESM import)
  4. Build and check sizesnpm run build
  5. Update CHANGELOG.md
  6. Bump version in package.json
  7. Run bash build-release.sh
  8. Upload public-release/ to your hosting
  9. Tag the release in git: git tag v1.2.0 && git push --tags

Backward Compatibility Test Script

const oldDoc = { /* JSON from a previous version */ };
editor.importFromJSON(oldDoc);

const mathBlocks = editor.exportMathAsLatex();
console.assert(mathBlocks.length > 0, 'Math blocks should survive');

const chemBlocks = editor.exportChemAsJSON();
console.assert(chemBlocks.length > 0, 'Chem blocks should survive');

const saved = editor.getJSON();
const reloaded = editor.importFromJSON(saved);
console.assert(reloaded, 'Round-trip should succeed');

Contributing

Getting Started

git clone <repo-url>
cd cbt-editor
npm install
npm run build
open test/index.html    # verify it works

Development Workflow

  1. Make changes in src/
  2. Rebuild: npm run build
  3. Test by opening test/index.html in a browser
  4. Check browser console for errors

Coding Style

Before Submitting

License

By contributing, you agree that your contributions will be licensed under the MIT License.