npm package: @fengrui-tec/vue-math-editor
Description: A Vue 3 component for rich text editing with math formula support using MathLive
Author: feng
Latest version: 1.0.0

# Vue Math Editor

> A Vue 3 component for rich text editing with math formula support using MathLive

[![npm version](https://badge.fury.io/js/%40feng%2Fvue-math-editor.svg)](https://www.npmjs.com/package/@fengrui-tec/vue-math-editor)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- 📝 Rich text editing (bold, italic, underline, strikethrough)
- 🔢 Visual math formula editor using MathLive with symbol panels
- 🎨 WYSIWYG formula editing experience
- ⌨️ Keyboard shortcuts for common operations
- ↩️ Undo/redo support with configurable history size
- 🔒 Built-in XSS protection with DOMPurify sanitization
- 📏 Optional content length limit
- 🎯 Full TypeScript support
- 🔧 Fully customizable via slots and props
- 📦 Lightweight and tree-shakeable
- 🖱️ Click on formulas to edit them

## Installation

```bash
npm install @fengrui-tec/vue-math-editor
# or
yarn add @fengrui-tec/vue-math-editor
# or
pnpm add @fengrui-tec/vue-math-editor
```

## Basic Usage

### Global Registration

```typescript
import { createApp } from 'vue'
import VueMathEditor from '@fengrui-tec/vue-math-editor'
import '@fengrui-tec/vue-math-editor/style.css'

const app = createApp(App)
app.use(VueMathEditor)
```

`style.css` includes the MathLive font assets required for correct radical,
fraction, and superscript/subscript rendering. If formulas look different after
integration, check for missing `KaTeX_*.woff2` requests in the browser network panel.

### Component Usage

```vue
<template>
  <MathEditor
    v-model="content"
    placeholder="在此输入文字和公式..."
    :show-format-toolbar="true"
    @update:modelValue="handleUpdate"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { MathEditor } from '@fengrui-tec/vue-math-editor'
import '@fengrui-tec/vue-math-editor/style.css'

const content = ref('<p>欢迎使用数学编辑器!</p>')

const handleUpdate = (value: string) => {
  console.log('Content updated:', value)
}
</script>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `modelValue` | `string` | `''` | v-model bound content (HTML) |
| `placeholder` | `string` | `'在此输入文字和公式...'` | Placeholder text |
| `showFormatToolbar` | `boolean` | `true` | Show format toolbar |
| `showClearButton` | `boolean` | `true` | Show clear button |
| `showFontSize` | `boolean` | `true` | Show font size selector |
| `showAlignment` | `boolean` | `true` | Show alignment buttons |
| `showLists` | `boolean` | `true` | Show list buttons |
| `showFormulaCount` | `boolean` | `true` | Show formula count in status bar |
| `showHelp` | `boolean` | `true` | Show help panel |
| `maxHistorySize` | `number` | `50` | Maximum history entries |
| `maxLength` | `number` | `undefined` | Maximum content length (characters) |

## Events

| Event | Payload | Description |
|-------|---------|-------------|
| `update:modelValue` | `string` | Emitted when content changes |
| `max-length-exceeded` | - | Emitted when content exceeds maxLength |

## Slots

```vue
<MathEditor v-model="content">
  <!-- Custom toolbar -->
  <template #toolbar>
    <CustomToolbar @insert="handleInsert" />
  </template>

  <!-- Custom format toolbar -->
  <template #format-toolbar>
    <CustomFormatToolbar @format="handleFormat" />
  </template>

  <!-- Custom status bar -->
  <template #status-bar="{ formulaCount, lastSaved }">
    <CustomStatusBar :count="formulaCount" :saved="lastSaved" />
  </template>

  <!-- Custom modal -->
  <template #modal>
    <CustomFormulaModal v-model="showModal" @confirm="handleConfirm" />
  </template>

  <!-- Custom help panel -->
  <template #help>
    <CustomHelpPanel />
  </template>
</MathEditor>
```

## Keyboard Shortcuts

| Shortcut | Action |
|----------|--------|
| `Ctrl` + `B` | Bold |
| `Ctrl` + `I` | Italic |
| `Ctrl` + `U` | Underline |
| `Ctrl` + `=` | Insert formula |
| `Ctrl` + `Z` | Undo |
| `Ctrl` + `Y` | Redo |

## Exposed Methods

The `MathEditor` component exposes the following methods that you can call via template ref:

| Method | Parameters | Description |
|--------|------------|-------------|
| `insertFormula()` | - | Open formula modal to insert a new formula |
| `clearAll()` | - | Clear all editor content (with confirmation) |
| `getContent()` | - | Get current HTML content |
| `setContent(content)` | `content: string` | Set editor content |

### Example using exposed methods:

```vue
<template>
  <MathEditor
    ref="editorRef"
    v-model="content"
  />
  <button @click="insertNewFormula">Insert Formula</button>
  <button @click="clearEditor">Clear All</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { MathEditor } from '@fengrui-tec/vue-math-editor'
import '@fengrui-tec/vue-math-editor/style.css'

const content = ref('')
const editorRef = ref<InstanceType<typeof MathEditor> | null>(null)

const insertNewFormula = () => {
  editorRef.value?.insertFormula()
}

const clearEditor = () => {
  editorRef.value?.clearAll()
}
</script>
```

```typescript
import { useHistory, useFormula, useContent, useKeyboard } from '@fengrui-tec/vue-math-editor'

// History management
const { save, undo, redo, canUndo, canRedo } = useHistory({
  maxSize: 50,
  editorRef,
  loadContent: (content) => { /* ... */ }
})

// Formula operations
const { createFormulaNode, insertAtCursor } = useFormula({
  editorRef,
  isEmpty: computed(() => /* ... */)
})

// Content management
const { getHTMLContent, loadContent, updateValue } = useContent({
  editorRef,
  emit: (event, value) => { /* ... */ }
})

// Keyboard shortcuts
const { handleKeydown, getShortcutHelp } = useKeyboard({
  formatText: (cmd, val) => { /* ... */ },
  undo: () => { /* ... */ },
  redo: () => { /* ... */ }
})
```

## Individual Components

### FormulaModal

The `FormulaModal` component provides a full-featured formula editing interface with symbol panels and LaTeX preview.

**Props:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `modelValue` | `boolean` | `false` | Controls modal visibility (v-model) |
| `latex` | `string` | `''` | Initial LaTeX formula |
| `title` | `string` | `'编辑公式'` | Modal title |
| `confirmText` | `string` | `'确定'` | Confirm button text |
| `cancelText` | `string` | `'取消'` | Cancel button text |

**Events:**

| Event | Payload | Description |
|-------|---------|-------------|
| `update:modelValue` | `boolean` | Emitted when modal opens/closes |
| `update:latex` | `string` | Emitted when LaTeX changes |
| `confirm` | `latex: string` | Emitted when user confirms |
| `cancel` | - | Emitted when user cancels |

**Features:**
- 📋 Symbol panels: Basic, Greek, Operations, Calculus, Sets, Arrows
- ↶️ Undo/Redo support for formula editing
- 🗑️ Clear button
- 📋 Copy LaTeX code button
- 🎨 Real-time LaTeX preview

**Example:**

```vue
<template>
  <FormulaModal
    v-model="showModal"
    v-model:latex="currentFormula"
    title="Custom Title"
    confirm-text="Add"
    cancel-text="Close"
    @confirm="handleConfirm"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { FormulaModal } from '@fengrui-tec/vue-math-editor'

const showModal = ref(false)
const currentFormula = ref('')

const handleConfirm = (latex: string) => {
  console.log('Formula confirmed:', latex)
  showModal.value = false
}
</script>
```

### Other Components

You can also import other individual components:

```vue
<script setup lang="ts">
import { FormulaToolbar, FormatToolbar, StatusBar } from '@fengrui-tec/vue-math-editor'
</script>

<template>
  <FormulaToolbar
    :show-clear="true"
    @insert="handleInsert"
    @undo="undo"
    @redo="redo"
    @clear="clearAll"
  />
  <FormatToolbar
    :show-font-size="true"
    :show-alignment="true"
    :show-lists="true"
    @format="handleFormat"
  />
  <StatusBar
    :formula-count="formulaCount"
    :last-saved="lastSaved"
    :show-formula-count="true"
  />
</template>
```

## TypeScript Support

This package is written in TypeScript and includes full type definitions:

```typescript
import type {
  MathEditorProps,
  FormulaNode,
  HistoryReturn,
  UseFormulaReturn
} from '@fengrui-tec/vue-math-editor'
```

## Usage Examples

### Example 1: Basic Editor with Custom Toolbar

```vue
<template>
  <MathEditor
    v-model="content"
    placeholder="Start typing..."
    :show-format-toolbar="true"
    :show-clear-button="true"
    @update:modelValue="handleUpdate"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { MathEditor } from '@fengrui-tec/vue-math-editor'
import '@fengrui-tec/vue-math-editor/style.css'

const content = ref('<p>Welcome to the editor!</p>')

const handleUpdate = (value: string) => {
  console.log('Content updated:', value)
  // Save to backend, etc.
}
</script>
```

### Example 2: Editor with Content Length Limit

```vue
<template>
  <MathEditor
    v-model="content"
    :max-length="5000"
    @max-length-exceeded="handleMaxLengthExceeded"
  />
  <p v-if="showWarning" class="warning">
    Content exceeds maximum length (5000 characters)!
  </p>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { MathEditor } from '@fengrui-tec/vue-math-editor'
import '@fengrui-tec/vue-math-editor/style.css'

const content = ref('')
const showWarning = ref(false)

const handleMaxLengthExceeded = () => {
  showWarning.value = true
  setTimeout(() => { showWarning.value = false }, 3000)
}
</script>
```

### Example 3: Custom Slots

```vue
<template>
  <MathEditor v-model="content">
    <!-- Custom toolbar without clear button -->
    <template #toolbar="{ insert, undo, redo }">
      <div class="my-toolbar">
        <button @click="insert">Insert Formula</button>
        <button @click="undo">Undo</button>
        <button @click="redo">Redo</button>
      </div>
    </template>

    <!-- Custom status bar -->
    <template #status-bar="{ formulaCount, lastSaved }">
      <div class="my-status">
        Formulas: {{ formulaCount }} | Last saved: {{ lastSaved }}
      </div>
    </template>
  </MathEditor>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { MathEditor } from '@fengrui-tec/vue-math-editor'
import '@fengrui-tec/vue-math-editor/style.css'

const content = ref('')
</script>
```

### Example 4: Using Individual Components

```vue
<template>
  <div class="my-editor">
    <FormulaToolbar
      :show-clear="false"
      @insert="handleInsert"
      @undo="undo"
      @redo="redo"
    />
    <MathEditor
      ref="editorRef"
      v-model="content"
      placeholder="Type here..."
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { FormulaToolbar, MathEditor } from '@fengrui-tec/vue-math-editor'
import '@fengrui-tec/vue-math-editor/style.css'

const content = ref('')

const handleInsert = () => {
  // Custom insert logic
}

const undo = () => {
  // Custom undo logic
}

const redo = () => {
  // Custom redo logic
}
</script>
```

## Security Features

This editor includes built-in security features to protect against XSS attacks:

### Content Sanitization

All content is automatically sanitized using [DOMPurify](https://github.com/cure53/DOMPurify) before being saved or retrieved.

**Allowed HTML Tags:**
- Text: `P`, `STRONG`, `EM`, `U`, `S`, `STRIKE`
- Structure: `UL`, `OL`, `LI`, `DIV`, `BLOCKQUOTE`, `BR`
- Formula: `MATH-FIELD`, `SPAN`

**Allowed Attributes:**
- `class`, `id`, `style`
- `data-latex` (for formula storage)
- `contenteditable`, `readonly`, `value`

### Protection Features

- ✅ **Automatic Sanitization**: All `getHTMLContent()` and `loadContent()` operations pass through DOMPurify
- ✅ **Safe Loading**: When loading content from external sources, it's sanitized before rendering
- ✅ **Error Recovery**: Graceful fallback for corrupted or invalid content
- ✅ **Math Field Initialization**: Safe re-initialization of math-field elements after content loads
- ✅ **Length Limiting**: Optional `maxLength` prop to prevent excessive content (counts text characters, not HTML)

### Example with Security Options

```vue
<template>
  <MathEditor
    v-model="content"
    :max-length="10000"
    @max-length-exceeded="handleMaxLength"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { MathEditor } from '@fengrui-tec/vue-math-editor'

const content = ref('')

const handleMaxLength = () => {
  alert('Content exceeds maximum length!')
}
</script>
```

## Browser Support

- Chrome/Edge 90+
- Firefox 88+
- Safari 14+

## License

MIT

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.