English | 简体中文
A modern formula editor workspace with legacy KityFormula compatibility, modular runtime loading, shared rendering protocols, and rich-text editor integrations.
FormulaX is a modern formula editor project. The current implementation keeps a compatibility runtime adapted from KityFormula / kf-editor, while progressively separating core model logic, renderer contracts, read-only rendering, and host-editor adapters into clearer packages.
It is not an official KityFormula project. KityFormula-related code is treated as a legacy compatibility backend under @formulaxjs/kity-runtime.
- Formula editing backed by a legacy KityFormula-compatible runtime
- Shared renderer protocol for future engine swapping
- Kity-based read-only SVG rendering through a dedicated renderer package
- Modular package structure with lazy-loaded runtime chunks
- Optional idle or hover preloading to reduce the first editor-open delay
- PNG/JPG export support loaded on demand
- Published adapter packages copy supporting CSS, images, and fonts into
distfor bundler-friendly npm consumption - Rich-text editor integrations for Tiptap, TinyMCE, and CKEditor 5
Some packages are experimental and not yet published to npm.
| Package | Description |
|---|---|
@formulaxjs/core |
Core data model, LaTeX parsing, and shared pure logic |
@formulaxjs/renderer |
Shared renderer contracts, formula markup helpers, base styles, cache helpers, and SVG utilities |
@formulaxjs/renderer-kity |
Kity-based read-only renderer that turns LaTeX into inline SVG markup |
@formulaxjs/renderer-image |
SVG-to-PNG upload helpers for image-based formula persistence |
@formulaxjs/editor |
Modal-oriented FormulaX editor UI helpers built on top of the runtime |
@formulaxjs/kity-runtime |
Legacy KityFormula compatibility runtime, embedded assets, and low-level editor factory |
@formulaxjs/tiptap |
Tiptap integration adapter |
@formulaxjs/tinymce |
TinyMCE integration adapter |
@formulaxjs/ckeditor5 |
CKEditor 5 integration adapter |
- Demo hub: https://vndmea.github.io/formulaX/
- Playground: https://vndmea.github.io/formulaX/playground/
- CKEditor 5 demo: https://vndmea.github.io/formulaX/ckeditor5/
- Tiptap demo: https://vndmea.github.io/formulaX/tiptap/
- TinyMCE demo: https://vndmea.github.io/formulaX/tinymce/
- Vue 3 + TinyMCE demo: https://vndmea.github.io/formulaX/vue/
- React + Tiptap v3 demo: https://vndmea.github.io/formulaX/react/
- Svelte + CKEditor 5 demo: https://vndmea.github.io/formulaX/svelte/
FormulaX now separates shared rendering concerns from the Kity-specific read-only renderer and from the modal editing UI:
FormulaX workspace
├── @formulaxjs/core (document model, LaTeX parser/serializer)
├── @formulaxjs/renderer (renderer protocol, markup, styles, svg helpers)
├── @formulaxjs/renderer-kity (Kity-based LaTeX -> inline SVG renderer)
├── @formulaxjs/renderer-image (SVG -> PNG upload helpers for persisted image output)
├── @formulaxjs/editor (modal UI and embedded editor orchestration)
├── @formulaxjs/kity-runtime (legacy compatibility runtime and embedded assets)
│ ├── KityFormula runtime (lazy-loaded chunk)
│ ├── Parser runtime (lazy-loaded chunk)
│ ├── Font maps, sprite position maps, and static assets
│ └── canvg export runtime (lazy-loaded, only when exporting PNG/JPG)
├── @formulaxjs/tiptap (Tiptap adapter)
├── @formulaxjs/tinymce (TinyMCE adapter)
└── @formulaxjs/ckeditor5 (CKEditor 5 adapter)This architecture allows:
- Reusing one renderer contract across adapters
- Keeping Kity-specific rendering isolated behind
@formulaxjs/renderer-kity - Keeping modal editing behavior isolated from read-only rendering
- Preparing for future engines such as
renderer-katexwithout reworking adapters
The current editing runtime is still based on a legacy compatibility layer adapted from Baidu FEX Team's KityFormula / kf-editor ecosystem.
FormulaX keeps this code in a dedicated runtime package (@formulaxjs/kity-runtime) and treats it as a compatibility backend rather than the long-term public architecture.
This approach:
- Preserves existing editing and rendering behavior
- Modernizes packaging and lazy loading
- Allows host adapters to preload the runtime before the first modal open
- Keeps host adapters decoupled from direct Kity runtime usage
- Prepares for future renderer implementations
- Node.js 22+
corepackpnpm9+
corepack enable
corepack prepare pnpm@9.12.3 --activate
pnpm installRun the standalone playground:
pnpm devRun editor integration demos:
pnpm dev:ckeditor5
pnpm dev:react
pnpm dev:svelte
pnpm dev:tiptap
pnpm dev:tinymce
pnpm dev:upload
pnpm dev:vuepnpm buildAPIs are experimental and may change before the first stable npm release.
Pass locale: 'zh_CN' when you need localized modal UI and legacy runtime labels. The default locale is en_US.
The examples below intentionally show more optional fields than a minimal setup so you can see the current configuration surface in one place.
import { createKityFormulaRenderer } from '@formulaxjs/renderer-kity';
const renderer = createKityFormulaRenderer({
fontSize: 40, // default font size used by the Kity-backed renderer
height: 320, // optional runtime workspace height when rendering
assetCacheKey: 'formulax-cdn-v1', // optional cache namespace when asset URLs change
assets: {
// optional partial overrides when fonts / toolbar sprites / CSS live on your CDN
styles: {
editor: '/static/formulax/editor.css',
},
},
});
const { html } = await renderer.renderLatex('\\frac{a}{b}', {
displayMode: false, // inline by default; set true for block-style rendering
fontSize: 40, // per-render override
className: 'formulax-inline', // custom wrapper class for rendered output
throwOnError: false, // return fallback markup instead of throwing
cache: true, // enable renderer-level caching
});import {
ensureFormulaXModalStyles,
mountFormulaXEditor,
} from '@formulaxjs/editor';
ensureFormulaXModalStyles(document); // injects modal + base formula styles once
const mounted = mountFormulaXEditor(document.querySelector('#host') as HTMLElement, {
initialLatex: '\\sqrt{x}', // optional initial formula
height: 320, // modal editor body height
autofocus: true, // focus editor after mount
locale: 'zh_CN', // optional, defaults to en_US
assets: {
// optional partial asset override passed through to the Kity runtime
styles: {
editor: '/static/formulax/editor.css',
},
},
render: {
fontsize: 40, // font size used when generating preview HTML
},
});
const latex = await mounted.getLatex();
const state = await mounted.getState();
const renderHtml = await mounted.getRenderHtml();
mounted.destroy();import { FormulaXEditor } from '@formulaxjs/kity-runtime';
const editor = new FormulaXEditor({
el: '#app', // HTMLElement or selector
initialLatex: '\\int_0^1 x^2 dx', // optional starting content
height: 320, // runtime workspace height
autofocus: true, // focus after mount
assets: {
// optional partial asset override
styles: {
editor: '/static/formulax/editor.css',
},
},
render: {
fontsize: 40, // preview / export font size
},
});
await editor.execCommand('render', '\\frac{a}{b}'); // replace current formula content
await editor.focus();
await editor.destroy();FormulaX adapters default to runtime SVG output. When you need persisted PNG images instead, switch to output: 'image' and provide image.upload.
For local verification inside this workspace:
pnpm dev:upload
pnpm dev:tinymce
pnpm dev:tiptap
pnpm dev:ckeditor5Default local upload endpoint:
http://localhost:3109/api/formula-image/uploadImportant:
- GitHub Pages demos cannot reach your own
http://localhost:3109upload server. - Image mode is mainly intended for local development verification or applications with their own reachable upload API.
- Even in image mode, adapters still preserve source LaTeX metadata for later editing.
import { parseLatex, serializeLatex } from '@formulaxjs/core';
const doc = parseLatex('\\frac{a}{\\sqrt{b}}');
const latex = serializeLatex(doc);import StarterKit from '@tiptap/starter-kit';
import { Editor } from '@tiptap/core';
import { createFormulaXNode } from '@formulaxjs/tiptap';
import { createKityFormulaRenderer } from '@formulaxjs/renderer-kity';
const formulaNode = createFormulaXNode(undefined, {
name: 'formulaX', // custom node name when avoiding schema collisions
formulaClassName: 'formulax-math', // wrapper class written to DOM
formulaAttributeName: 'data-formulax-latex', // where LaTeX is stored in the DOM
cursorStyle: 'pointer', // cursor for inline formula widgets
initialLatex: '\\placeholder ', // default content for newly inserted formulas
preload: 'idle', // 'idle' | 'hover' | false
renderer: createKityFormulaRenderer({
fontSize: 40,
}), // optional custom renderer
modal: {
title: 'FormulaX Editor',
insertText: 'Insert',
updateText: 'Update',
cancelText: 'Cancel',
closeOnBackdrop: true,
},
editor: {
height: 320,
autofocus: true,
locale: 'zh_CN',
assets: {},
render: {
fontsize: 40,
},
},
});
const editor = new Editor({
element: document.querySelector('#editor'),
extensions: [StarterKit, formulaNode],
content: '<p>Click the FormulaX toolbar button to insert a formula.</p>',
});
editor.commands.openFormulaX();Tiptap image mode:
const formulaNode = createFormulaXNode(undefined, {
output: 'image',
image: {
upload: async ({ blob, filename, latex }) => {
const formData = new FormData();
formData.append('file', blob, filename);
formData.append('latex', latex);
const response = await fetch('http://localhost:3109/api/formula-image/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Formula image upload failed: ${response.status}`);
}
const payload = await response.json() as { url?: string; location?: string };
return {
url: payload.url ?? payload.location ?? '',
};
},
},
});import tinymce from 'tinymce';
import 'tinymce/icons/default';
import 'tinymce/models/dom';
import 'tinymce/themes/silver';
import 'tinymce/skins/ui/oxide/skin';
import 'tinymce/skins/content/default/content';
import {
createTinyMceFormulaMarkup,
registerFormulaXTinyMcePlugin,
} from '@formulaxjs/tinymce';
registerFormulaXTinyMcePlugin(tinymce, {
pluginName: 'formulax', // TinyMCE plugin id used in the plugins list
buttonName: 'formulax', // toolbar button id used in the toolbar string
menuItemName: 'formulax', // menu item id when adding the command to menus
toolbarText: 'FormulaX',
tooltip: 'Insert or edit formula',
cursorStyle: 'pointer',
formulaClassName: 'formulax-math',
formulaAttributeName: 'data-formulax-latex',
initialLatex: '\\sqrt{x}',
preload: 'idle', // 'idle' | 'hover' | false
modal: {
title: 'FormulaX Editor',
insertText: 'Insert',
updateText: 'Update',
cancelText: 'Cancel',
width: '1100px',
height: 'auto',
closeOnBackdrop: true,
},
editor: {
height: 320,
autofocus: true,
locale: 'zh_CN',
assets: {},
render: {
fontsize: 40,
},
},
});
await tinymce.init({
target: document.querySelector('#tiny-host'),
plugins: 'formulax', // must match pluginName unless you customize both sides
toolbar: 'undo redo | formulax',
menubar: false,
license_key: 'gpl',
});
const html = createTinyMceFormulaMarkup('\\sqrt{x}'); // optional content-level helperTinyMCE image mode:
registerFormulaXTinyMcePlugin(tinymce, {
output: 'image',
image: {
upload: async ({ blob, filename, latex }) => {
const formData = new FormData();
formData.append('file', blob, filename);
formData.append('latex', latex);
const response = await fetch('http://localhost:3109/api/formula-image/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Formula image upload failed: ${response.status}`);
}
const payload = await response.json() as { url?: string; location?: string };
return {
url: payload.url ?? payload.location ?? '',
};
},
},
});import { ClassicEditor, Essentials, Paragraph } from 'ckeditor5';
import { FormulaX } from '@formulaxjs/ckeditor5';
await ClassicEditor.create(document.querySelector('#editor')!, {
licenseKey: 'GPL',
plugins: [Essentials, Paragraph, FormulaX],
toolbar: ['formulaX'], // must include buttonName when customized
formulaX: {
name: 'formulaX', // custom model name when avoiding schema collisions
buttonName: 'formulaX',
toolbarText: 'FormulaX',
tooltip: 'Insert or edit formula',
cursorStyle: 'pointer',
formulaClassName: 'formulax-math',
formulaAttributeName: 'data-formulax-latex',
preload: 'idle', // 'idle' | 'hover' | false
modal: {
title: 'FormulaX Editor',
insertText: 'Insert',
updateText: 'Update',
cancelText: 'Cancel',
closeOnBackdrop: true,
},
editor: {
height: 320,
autofocus: true,
locale: 'zh_CN',
assets: {},
render: {
fontsize: 40,
},
},
},
});CKEditor 5 image mode:
await ClassicEditor.create(document.querySelector('#editor')!, {
licenseKey: 'GPL',
plugins: [Essentials, Paragraph, FormulaX],
toolbar: ['formulaX'],
formulaX: {
output: 'image',
image: {
upload: async ({ blob, filename, latex }) => {
const formData = new FormData();
formData.append('file', blob, filename);
formData.append('latex', latex);
const response = await fetch('http://localhost:3109/api/formula-image/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Formula image upload failed: ${response.status}`);
}
const payload = await response.json() as { url?: string; location?: string };
return {
url: payload.url ?? payload.location ?? '',
};
},
},
},
} as any);apps/vue-demoshows Vue 3 + TinyMCE v7 using the published@formulaxjs/tinymcepackage directlyapps/react-demoshows React + Tiptap v3 using@formulaxjs/tiptapapps/svelte-demoshows Svelte + CKEditor 5 using@formulaxjs/ckeditor5
pnpm dev- Start the standalone FormulaX playgroundpnpm dev:ckeditor5- Start the CKEditor 5 demopnpm dev:react- Start the React + Tiptap v3 demopnpm dev:svelte- Start the Svelte + CKEditor 5 demopnpm dev:tiptap- Start the Tiptap demopnpm dev:tinymce- Start the TinyMCE demopnpm dev:upload- Start the local upload server used by image-mode demospnpm dev:vue- Start the Vue 3 + TinyMCE v7 demopnpm build- Build all packages and demo appspnpm build:packages- Build workspace packages onlypnpm build:pages- Build GitHub Pages demo hubpnpm changeset- Create a changeset for package release notes and version intentpnpm changeset:version- Apply pending changesets and update package versions/changelogspnpm changeset:publish- Publish versioned packages to npmpnpm release- Build packages and publish via Changesetspnpm lint- Run ESLintpnpm typecheck- Run TypeScript type checkingpnpm test- Run Vitest unit testspnpm test:browser- Run Playwright browser tests
FormulaX contains code adapted from Baidu FEX Team's KityFormula / kf-editor ecosystem.
KityFormula-related code and assets retain their original copyright and license notices.