# erl-mathtextx-editor

**Visual Math Editor Component — Zero LaTeX Required**

[![npm version](https://badge.fury.io/js/erl-mathtextx-editor.svg)](https://www.npmjs.com/package/erl-mathtextx-editor)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Embeddable visual math editor widget untuk CMS dan platform edukasi. User tidak perlu tahu LaTeX — semua input matematika dilakukan secara visual.

---

## ✨ Fitur Utama

- 🎯 **Visual Math Input** — Insert math langsung inline tanpa dialog (Ctrl+M)
- 📝 **Rich Text Editor** — Bold, italic, tables, lists, links, images
- 🧮 **Equation Editor** — Dialog MathType-style dengan tab, grid, KaTeX preview (Edit existing math)
- 📋 **100+ Formula Templates** — Algebra, calculus, trigonometry, chemistry, matrix
- 🏆 **Mode Olimpiade** — Toolbar khusus untuk kompetisi matematika dengan simbol set, Greek letters, NT/Combo
- 🖼️ **Free-form Image Drag** — Drag gambar ke posisi bebas (pixel-perfect)
- 📄 **DOCX Import** — Import file .docx via mammoth.js (toolbar + drag-drop)
- 🗂️ **Google Docs Paste** — Paste dari Google Docs (equations + document) auto-cleaned
- 👁️ **Content Viewer** — Read-only renderer dengan KaTeX + DOMPurify
- 🎨 **Table Editor** — 6 templates, column resize, cell merge/split
- 🔒 **XSS Protection** — DOMPurify sanitization di paste + serializer
- 🛡️ **Error Boundary** — Anti white-screen crash protection

---

## 📦 Installation

```bash
npm install erl-mathtextx-editor
```

---

## 🚀 Quick Start

### 1. Basic Editor

```tsx
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'

function App() {
  return (
    <MathTextXEditor
      onChange={(html) => console.log(html)}
      placeholder="Tulis soal di sini..."
    />
  )
}
```

### 2. Editor dengan Save Handler

```tsx
import { useRef } from 'react'
import { MathTextXEditor, getHTML } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'

function QuestionForm() {
  const editorRef = useRef<HTMLDivElement>(null)

  const handleSave = () => {
    if (!editorRef.current) return
    const html = getHTML(editorRef.current)
    fetch('/api/questions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: html }),
    })
  }

  return (
    <div>
      <MathTextXEditor ref={editorRef} placeholder="Tulis pertanyaan..." onSave={handleSave} />
      <button onClick={handleSave}>Simpan</button>
    </div>
  )
}
```

### 3. Edit Existing Content

```tsx
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'

function EditQuestion({ existingHtml }: { existingHtml: string }) {
  return (
    <MathTextXEditor
      content={existingHtml}
      onChange={(html) => console.log('Updated:', html)}
    />
  )
}
```

### 4. Read-Only Mode (Viewer)

```tsx
import { ContentViewer } from 'erl-mathtextx-editor/viewer'
import 'erl-mathtextx-editor/viewer/styles'

function ExamQuestion({ questionHtml }: { questionHtml: string }) {
  return <ContentViewer content={questionHtml} />
}
```

### 5. Multi-Instance (Soal + Pilihan Jawaban)

```tsx
import { useState } from 'react'
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'

function MultipleChoiceForm() {
  const [question, setQuestion] = useState('')
  const [options, setOptions] = useState(
    ['A', 'B', 'C', 'D'].map((id) => ({ id, content: '' }))
  )

  return (
    <div>
      <label>Pertanyaan:</label>
      <MathTextXEditor
        content={question}
        onChange={setQuestion}
        placeholder="Tulis pertanyaan..."
        minHeight="150px"
      />
      {options.map((opt) => (
        <div key={opt.id}>
          <label>Opsi {opt.id}:</label>
          <MathTextXEditor
            content={opt.content}
            onChange={(html) => setOptions((prev) => prev.map((o) => o.id === opt.id ? { ...o, content: html } : o))}
            placeholder={`Jawaban ${opt.id}...`}
            minHeight="60px"
          />
        </div>
      ))}
    </div>
  )
}
```

### 6. Next.js (App Router)

```tsx
'use client'
import dynamic from 'next/dynamic'
import 'erl-mathtextx-editor/styles'

const MathTextXEditor = dynamic(
  () => import('erl-mathtextx-editor').then((mod) => mod.MathTextXEditor),
  { ssr: false }
)

export default function EditorPage() {
  return (
    <MathTextXEditor
      placeholder="Tulis soal matematika..."
      onChange={(html) => console.log(html)}
      minHeight="300px"
    />
  )
}
```

### 7. Vite + React

```tsx
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'

function App() {
  return (
    <MathTextXEditor
      placeholder="Tulis soal..."
      onChange={(html) => console.log(html)}
    />
  )
}
export default App
```

---

## 🎛️ Toolbar Modes

Editor menyediakan 3 preset toolbar yang bisa dipilih via prop `toolbarMode`:

### Basic
Toolbar standar dengan tombol format teks dasar, insert media, dan math formula.

### Advanced
Toolbar lengkap dengan font family, text color, alignment, table operations, superscript/subscript, chemistry formula, dan formatting lanjutan.

### Olimpiade
Toolbar minimal untuk kompetisi matematika — hanya tombol esensial:

| Grup | Tombol |
|---|---|
| **Undo/Redo** | Undo, Redo |
| **Format** | Bold, Italic, Underline |
| **Structure** | Paragraph, H1, H2 |
| **Lists** | Bullet List, Ordered List, Outdent, Indent |
| **Math** | Math Formula (inline), Block Math |
| **Actions** | Remove Format |

**Math Toolbar** di mode Olimpiade menampilkan section khusus: Basic, Relation, Set, Greek, Structure — tanpa section Calc yang kurang relevan untuk olimpiade.

### Custom Mode
Jika preset tidak sesuai, Anda bisa atur sendiri via `internalToolbarMode` state atau kontrol toolbar secara manual menggunakan komponen terpisah (`MainToolbar`, `MathToolbar`, `MathTypeDialog`).

---

## 🧰 Image Upload

Editor mendukung upload gambar dari: **Insert dialog**, **drag-drop**, **paste dari clipboard**, dan **DOCX import**. Semuanya melalui satu callback `onImageUpload`.

### Basic Upload

```tsx
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'

function EditorWithUpload() {
  const handleImageUpload = async (file: File): Promise<string> => {
    const formData = new FormData()
    formData.append('image', file)
    const res = await fetch('/api/upload', { method: 'POST', body: formData })
    if (!res.ok) throw new Error('Upload failed: ' + res.statusText)
    const data = await res.json()
    return data.url // Expected: { "url": "https://cdn.example.com/img.jpg" }
  }

  return (
    <MathTextXEditor
      placeholder="Tulis soal..."
      onImageUpload={handleImageUpload}
    />
  )
}
```

### Re-upload Gambar dari Paste (Google Docs / Website)

```tsx
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'

function EditorWithPasteReupload() {
  const handleImageUpload = async (file: File): Promise<string> => {
    const formData = new FormData()
    formData.append('image', file)
    const res = await fetch('/api/upload', { method: 'POST', body: formData })
    return (await res.json()).url
  }

  const handleBeforePasteHTML = async (html: string): Promise<string> => {
    // Download + re-upload external images from pasted HTML
    const imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/gi
    const replacements: Array<[string, string]> = []
    let match

    while ((match = imgRegex.exec(html)) !== null) {
      const src = match[1]
      if (src.startsWith('data:') || src.includes('cdn.example.com')) continue
      try {
        const blob = await (await fetch(src)).blob()
        const file = new File([blob], 'image.' + (blob.type.split('/')[1] || 'jpg'))
        replacements.push([src, await handleImageUpload(file)])
      } catch { console.warn('Skip image:', src) }
    }

    let result = html
    for (const [oldSrc, newSrc] of replacements) result = result.replaceAll(oldSrc, newSrc)
    return result
  }

  return (
    <MathTextXEditor
      placeholder="Tulis soal..."
      onImageUpload={handleImageUpload}
      onBeforePasteHTML={handleBeforePasteHTML}
    />
  )
}
```

### Base64 Fallback (Tanpa Server)

```tsx
const handleImageUpload = async (file: File): Promise<string> => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => resolve(reader.result as string)
    reader.onerror = reject
    reader.readAsDataURL(file)
  })
}
// ⚠️ Base64 hanya cocok untuk gambar < 100KB. Untuk produksi, gunakan upload ke server.
```

---

## 📋 Props API

### MathTextXEditor

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `content` | `string` | `''` | Initial HTML content |
| `onChange` | `(html: string) => void` | — | Callback on content change (debounced 150ms) |
| `onSave` | `(html: string) => void` | — | Callback on Ctrl+S |
| `onImageUpload` | `(file: File) => Promise<string>` | — | Custom image upload handler |
| `onBeforePasteHTML` | `(html: string) => Promise<string>` | — | Transform pasted HTML (re-upload images) |
| `placeholder` | `string` | `'Tulis soal...'` | Placeholder text |
| `minHeight` | `string` | `'200px'` | Minimum editor height |
| `maxHeight` | `string` | — | Maximum editor height |
| `autoFocus` | `boolean` | `false` | Auto-focus editor on mount |
| `toolbarMode` | `'basic' \| 'advanced' \| 'olimpiade'` | `'basic'` | Toolbar preset |
| `onInsertBlockMath` | `() => void` | — | Callback to insert a block-level math node directly |
| `editable` | `boolean` | `true` | Set false for read-only mode |
| `className` | `string` | — | Additional CSS class |

### ContentViewer

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `content` | `string` | — | HTML content to render (required) |
| `className` | `string` | — | Additional CSS class |

---

## 📦 Exports

### Main Package (`erl-mathtextx-editor`)

```tsx
import {
  MathTextXEditor,       // Main editor component
  ContentViewer,         // Read-only renderer
  MathTypeDialog,        // Standalone equation editor dialog
  TemplatePanel,         // Standalone formula template panel
  MainToolbar,           // Standalone text formatting toolbar
  MathToolbar,           // Standalone math symbols toolbar
  SymbolPalette,         // Standalone symbol picker
  WordCount,             // Status bar word/char counter
  LinkDialog,            // Standalone link insert/edit dialog
  ImageEditDialog,       // Standalone image edit dialog
  InsertTableDialog,     // Standalone table insert dialog
  TableMenu,             // Standalone table context menu
  CellPropertiesDialog,  // Standalone cell properties dialog
  TablePropertiesDialog, // Standalone table properties dialog
  TableTemplatesDialog,  // Standalone table template picker
  MathInlineNode,        // TipTap inline math extension
  MathBlockNode,         // TipTap block math extension
  getHTML,               // Serialize editor → HTML string
  getJSON,               // Serialize editor → JSON
  sanitizeCKEditorHTML,  // Clean CKEditor HTML for compatibility
  toCompatibleHTML,      // Convert to CKEditor-compatible format
  createExtensions,      // Create TipTap extensions programmatically
  mathTemplates,         // Template definitions
  getTemplatesByLevel,   // Filter templates by education level
  getTemplatesByCategory,// Filter templates by category
  getTemplateCategories, // Get all template categories
  countWords,            // Word count utility
  countCharacters,       // Character count utility
  getTemplateStyles,     // Table template CSS generator
} from 'erl-mathtextx-editor'

import 'erl-mathtextx-editor/styles'
```

### Viewer Only (`erl-mathtextx-editor/viewer`)

```tsx
import { ContentViewer } from 'erl-mathtextx-editor/viewer'
import 'erl-mathtextx-editor/viewer/styles'
```

---

## ⌨️ Keyboard Shortcuts

| Shortcut | Action |
|----------|--------|
| `Ctrl+B` | Bold |
| `Ctrl+I` | Italic |
| `Ctrl+U` | Underline |
| `Ctrl+K` | Insert/edit link |
| `Ctrl+M` | Insert inline math directly (without dialog) |
| `Ctrl+Shift+T` | Insert table |
| `Ctrl+S` | Save document |
| `Shift+Ctrl+V` | Paste as plain text |
| `Esc` (equation editor dialog) | Close dialog |

---

## ⚠️ Troubleshooting

| Error | Solution |
|---|---|
| `Can't resolve 'erl-mathtextx-editor'` | `npm install erl-mathtextx-editor` |
| `Can't resolve 'erl-mathtextx-editor/styles'` | Version ≥ 0.1.3. Alternatif: `import 'erl-mathtextx-editor/dist/assets/erl-mathtextx-editor.css'` |
| `MathTextXEditor is not a function` | Pastikan React ≥ 18 (`npm ls react`) |
| `window is not defined` (Next.js) | Gunakan `dynamic()` dengan `{ ssr: false }` |
| `Unexpected token 'export'` (CRA) | Webpack config: `resolve.mainFields: ['main', 'module']` |
| MathLive fonts error | Set `(window as any).MATHLIVE_FONTS_PATH = '/fonts'` + copy font ke `public/fonts/` |

---

## ✅ Verified Import Paths

| Import | Resolves to |
|---|---|
| `erl-mathtextx-editor` | `dist/erl-mathtextx-editor.js` |
| `erl-mathtextx-editor/styles` | `dist/assets/erl-mathtextx-editor.css` |
| `erl-mathtextx-editor/viewer` | `dist/viewer.js` |
| `erl-mathtextx-editor/viewer/styles` | `dist/viewer-styles.js` |

---

## 🛠️ Tech Stack

- **UI Framework:** React 18+
- **Editor Engine:** TipTap / ProseMirror
- **Math Input:** MathLive (WYSIWYG math)
- **Math Rendering:** KaTeX
- **DOCX Import:** mammoth.js
- **XSS Protection:** DOMPurify
- **Graph Plotting:** Function Plot
- **Syntax Highlight:** lowlight (100+ languages)
- **Build:** Vite (Library Mode)

---

## 📄 License

[MIT](https://github.com/erlangga/richtext-editor-research/blob/main/LICENSE) © Erlangga Team

---

## 🔗 Links

- **NPM:** [erl-mathtextx-editor](https://www.npmjs.com/package/erl-mathtextx-editor)
- **Source:** [GitHub Repository](https://github.com/erlangga/richtext-editor-research)
- **Issues:** [Report Bug](https://github.com/erlangga/richtext-editor-research/issues)
- **📘 Embed Tutorial:** [docs/EMBED_TUTORIAL.md](https://github.com/erlangga/richtext-editor-research/blob/main/docs/EMBED_TUTORIAL.md)
