Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/react-mathtype/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# React MathType example

This example demonstrates a standalone WIRIS MathType integration for a
Lexical React editor. It keeps MathType as an example dependency and stores
formulas in a custom Lexical node instead of allowing MathType to mutate the
Lexical contenteditable directly.

The bridge uses `@wiris/mathtype-generic` to open the MathType/ChemType UI,
converts the generated MathML image into `MathTypeNode` state inside
`editor.update`, and reopens MathType when a formula node is double-clicked.

MathType stores formulas as MathML. The rendered formula image is kept in node
state for the demo, while DOM export emits the same `img.Wirisformula` shape
that MathType's parser expects.

## Running

```bash
pnpm install
pnpm run dev
pnpm run build
pnpm run typecheck
```

MathType may require a WIRIS license or self-hosted services for production
use. See the MathType generic integration documentation for service
configuration details.
13 changes: 13 additions & 0 deletions examples/react-mathtype/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link href="data:," rel="icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lexical MathType Example</title>
</head>
<body>
<div id="root"></div>
<script src="/src/main.tsx" type="module"></script>
</body>
</html>
33 changes: 33 additions & 0 deletions examples/react-mathtype/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@lexical/react-mathtype-example",
"private": true,
"version": "0.45.0",
"type": "module",
"scripts": {
"dev": "vite",
"monorepo:dev": "vite -c vite.config.monorepo.ts",
"build": "tsc && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@lexical/react": "0.45.0",
"@wiris/mathtype-generic": "^8.15.2",
"lexical": "0.45.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.2",
"cross-env": "^7.0.3",
"typescript": "^5.9.2",
"vite": "^7.3.2"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.52.0",
"@rollup/rollup-darwin-arm64": "4.52.0",
"@rollup/rollup-win32-x64-msvc": "4.52.0"
}
}
75 changes: 75 additions & 0 deletions examples/react-mathtype/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {
$createParagraphNode,
$createTextNode,
$getRoot,
ParagraphNode,
TextNode,
} from 'lexical';

import {MathTypeProvider} from './MathTypeContext';
import {MathTypeNode} from './MathTypeNode';
import {MathTypePlugin} from './MathTypePlugin';

const placeholder = 'Write a math note...';

const theme = {
paragraph: 'editor-paragraph',
};

const editorConfig = {
editorState: () => {
$getRoot().append(
$createParagraphNode().append(
$createTextNode('Use the MathType toolbar to insert a formula. '),
),
);
},
namespace: 'MathType Example',
nodes: [ParagraphNode, TextNode, MathTypeNode],
onError(error: Error) {
throw error;
},
theme,
};

export default function App() {
return (
<LexicalComposer initialConfig={editorConfig}>
<MathTypeProvider>
<div className="editor-shell">
<MathTypePlugin />
<div className="editor-inner">
<RichTextPlugin
contentEditable={
<ContentEditable
className="editor-input"
aria-placeholder={placeholder}
placeholder={
<div className="editor-placeholder">{placeholder}</div>
}
/>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<AutoFocusPlugin />
</div>
</div>
</MathTypeProvider>
</LexicalComposer>
);
}
78 changes: 78 additions & 0 deletions examples/react-mathtype/src/MathTypeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type {MathTypeFormula} from './MathTypeData';
import type {MathTypeIntegrationInstance} from './MathTypeGlobals';
import type {NodeKey} from 'lexical';
import type {JSX, MutableRefObject, PropsWithChildren} from 'react';

import {createContext, useCallback, useContext, useMemo, useRef} from 'react';

import {createImageFromFormula} from './MathTypeData';

type MathTypeContextValue = {
editFormula: (nodeKey: NodeKey, formula: MathTypeFormula) => boolean;
integrationRef: MutableRefObject<MathTypeIntegrationInstance | null>;
pendingNodeKeyRef: MutableRefObject<NodeKey | null>;
};

const MathTypeContext = createContext<MathTypeContextValue | null>(null);

export function MathTypeProvider({children}: PropsWithChildren): JSX.Element {
const integrationRef = useRef<MathTypeIntegrationInstance | null>(null);
const pendingNodeKeyRef = useRef<NodeKey | null>(null);

const editFormula = useCallback(
(nodeKey: NodeKey, formula: MathTypeFormula): boolean => {
const integration = integrationRef.current;
if (integration === null) {
return false;
}
pendingNodeKeyRef.current = nodeKey;
const temporalImage = createImageFromFormula(formula);
integration.core.editionProperties.temporalImage = temporalImage;
integration.core.editionProperties.dbclick = true;
integration.core.editionProperties.isNewElement = false;

const customEditors = integration.core.getCustomEditors();
customEditors.disable();
if (formula.customEditor !== null) {
customEditors.enable(formula.customEditor);
}

integration.openExistingFormulaEditor();
return true;
},
[],
);

const value = useMemo(
() => ({
editFormula,
integrationRef,
pendingNodeKeyRef,
}),
[editFormula],
);

return (
<MathTypeContext.Provider value={value}>
{children}
</MathTypeContext.Provider>
);
}

export function useMathTypeContext(): MathTypeContextValue {
const context = useContext(MathTypeContext);
if (context === null) {
throw new Error(
'MathType components must be rendered inside MathTypeProvider.',
);
}
return context;
}
117 changes: 117 additions & 0 deletions examples/react-mathtype/src/MathTypeData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

export const WIRIS_FORMULA_CLASS = 'Wirisformula';
export const WIRIS_MATHML_ATTRIBUTE = 'data-mathml';
export const WIRIS_CUSTOM_EDITOR_ATTRIBUTE = 'data-custom-editor';

export type MathTypeFormula = {
altText: string;
customEditor: null | string;
height: null | number;
mathML: string;
src: string;
width: null | number;
};

export function parseOptionalNumber(value: string | null): null | number {
if (value === null || value === '') {
return null;
}
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}

export function encodeMathML(mathML: string): string {
const wirisPlugin = window.WirisPlugin;
const wirisMathML =
wirisPlugin === undefined ? undefined : wirisPlugin.MathML;
if (wirisMathML) {
return wirisMathML.safeXmlEncode(mathML);
}
return mathML
.split('&')
.join('\u00a7')
.split('<')
.join('\u00ab')
.split('>')
.join('\u00bb')
.split('"')
.join('\u00a8')
.split("'")
.join('`');
}

export function decodeMathML(encodedMathML: string): string {
const wirisPlugin = window.WirisPlugin;
const wirisMathML =
wirisPlugin === undefined ? undefined : wirisPlugin.MathML;
if (wirisMathML) {
return wirisMathML.safeXmlDecode(encodedMathML);
}
return encodedMathML
.split('&laquo;')
.join('<')
.split('&raquo;')
.join('>')
.split('&uml;')
.join('"')
.split('&quot;')
.join('"')
.split('\u00ab')
.join('<')
.split('\u00bb')
.join('>')
.split('\u00a8')
.join('"')
.split('\u00a7')
.join('&')
.split('`')
.join("'");
}

export function createFormulaFromImage(
image: HTMLImageElement,
fallbackMathML?: string,
): MathTypeFormula {
const encodedMathML = image.getAttribute(WIRIS_MATHML_ATTRIBUTE);
const mathML =
fallbackMathML ??
(encodedMathML === null ? '' : decodeMathML(encodedMathML));
return {
altText: image.getAttribute('alt') ?? '',
customEditor: image.getAttribute(WIRIS_CUSTOM_EDITOR_ATTRIBUTE),
height: parseOptionalNumber(image.getAttribute('height')),
mathML,
src: image.getAttribute('src') ?? '',
width: parseOptionalNumber(image.getAttribute('width')),
};
}

export function createImageFromFormula(
formula: MathTypeFormula,
): HTMLImageElement {
const image = document.createElement('img');
image.align = 'middle';
image.className = WIRIS_FORMULA_CLASS;
image.src = formula.src;
image.alt = formula.altText;
image.setAttribute('role', 'math');
image.setAttribute(WIRIS_MATHML_ATTRIBUTE, encodeMathML(formula.mathML));
if (formula.customEditor !== null) {
image.setAttribute(WIRIS_CUSTOM_EDITOR_ATTRIBUTE, formula.customEditor);
}
if (formula.width !== null) {
image.setAttribute('width', String(formula.width));
}
if (formula.height !== null) {
image.setAttribute('height', String(formula.height));
}
image.style.maxWidth = 'none';
return image;
}
Loading