npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pdeditor-basic

v0.2.2

Published

Basic visual math editor — lightweight formula input without advanced formatting or templates

Readme

pdeditor-basic

Lightweight Visual Math Editor — Basic Formula Input

npm version License: MIT

Widget React untuk membuat soal & jawaban matematika secara visual — tanpa template lanjutan atau formatting berlebihan. Cocok untuk CMS soal, bank pertanyaan, dan platform ujian.

Versi saat ini: 0.2.0


Daftar Isi

  1. Instalasi
  2. Quick Start
  3. Panduan Implementasi Lengkap
  4. Upload & Paste Gambar
  5. Math, LaTeX & KaTeX
  6. Props API
  7. Exports & Import Paths
  8. Troubleshooting
  9. Tech Stack

📦 Instalasi

Requirements

| Dependency | Versi | |------------|-------| | react | ^18.0.0 atau ^19.0.0 | | react-dom | ^18.0.0 atau ^19.0.0 |

Install

npm install pdeditor-basic

Package ini bundled — TipTap, MathLive, KaTeX, dan DOMPurify sudah termasuk. Anda hanya perlu meng-import CSS secara eksplisit (lihat di bawah).


🚀 Quick Start

Editor (tulis soal)

import { MathTextXEditor } from 'pdeditor-basic'
import 'pdeditor-basic/styles'

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

Viewer (tampilkan soal — read-only)

import { ContentViewer } from 'pdeditor-basic/viewer'
import 'pdeditor-basic/viewer/styles'

export default function SoalCard({ html }: { html: string }) {
  return <ContentViewer content={html} />
}

Penting: Selalu import CSS editor (pdeditor-basic/styles) dan viewer (pdeditor-basic/viewer/styles) terpisah. Viewer CSS sudah menyertakan KaTeX stylesheet.


🛠️ Panduan Implementasi Lengkap

1. Vite + React

// src/App.tsx
import { useState } from 'react'
import { MathTextXEditor } from 'pdeditor-basic'
import { ContentViewer } from 'pdeditor-basic/viewer'
import 'pdeditor-basic/styles'
import 'pdeditor-basic/viewer/styles'

export default function App() {
  const [html, setHtml] = useState('')

  return (
    <div>
      <MathTextXEditor
        content={html}
        onChange={setHtml}
        placeholder="Tulis soal..."
        minHeight="320px"
      />
      <h3>Preview</h3>
      <ContentViewer content={html} />
    </div>
  )
}

2. Next.js (App Router)

Editor dan MathLive harus client-only (tidak support SSR).

// app/soal/editor/page.tsx
'use client'

import dynamic from 'next/dynamic'
import 'pdeditor-basic/styles'

const MathTextXEditor = dynamic(
  () => import('pdeditor-basic').then((m) => m.MathTextXEditor),
  { ssr: false, loading: () => <p>Memuat editor...</p> }
)

export default function SoalEditorPage() {
  return (
    <MathTextXEditor
      placeholder="Tulis soal matematika..."
      onChange={(html) => console.log(html)}
    />
  )
}
// app/soal/[id]/page.tsx — Viewer bisa SSR (tanpa MathLive)
import { ContentViewer } from 'pdeditor-basic/viewer'
import 'pdeditor-basic/viewer/styles'

async function getSoal(id: string) {
  const res = await fetch(`https://api.example.com/soal/${id}`, { cache: 'no-store' })
  return res.json()
}

export default async function SoalPage({ params }: { params: { id: string } }) {
  const soal = await getSoal(params.id)
  return <ContentViewer content={soal.html} />
}

3. Simpan & Muat dari API

import { useState, useEffect } from 'react'
import { MathTextXEditor } from 'pdeditor-basic'
import 'pdeditor-basic/styles'

export function SoalForm({ soalId }: { soalId: string }) {
  const [html, setHtml] = useState('')
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch(`/api/soal/${soalId}`)
      .then((r) => r.json())
      .then((data) => setHtml(data.content))
      .finally(() => setLoading(false))
  }, [soalId])

  const handleSave = async () => {
    await fetch(`/api/soal/${soalId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: html }),
    })
  }

  if (loading) return <p>Memuat...</p>

  return (
    <div>
      <MathTextXEditor
        content={html}
        onChange={setHtml}
        placeholder="Tulis pertanyaan..."
        minHeight="280px"
      />
      <button type="button" onClick={handleSave}>Simpan</button>
    </div>
  )
}

onChange di-debounce 150 ms — HTML yang dikirim sudah siap disimpan ke database.

4. Multi-Instance (Soal + Pilihan Jawaban)

import { useState } from 'react'
import { MathTextXEditor } from 'pdeditor-basic'
import 'pdeditor-basic/styles'

type Option = { id: string; content: string }

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

  const updateOption = (id: string, content: string) => {
    setOptions((prev) => prev.map((o) => (o.id === id ? { ...o, content } : o)))
  }

  return (
    <div className="space-y-4">
      <section>
        <label>Pertanyaan</label>
        <MathTextXEditor
          content={question}
          onChange={setQuestion}
          placeholder="Tulis pertanyaan..."
          minHeight="160px"
        />
      </section>

      {options.map((opt) => (
        <section key={opt.id}>
          <label>Opsi {opt.id}</label>
          <MathTextXEditor
            content={opt.content}
            onChange={(html) => updateOption(opt.id, html)}
            placeholder={`Jawaban ${opt.id}...`}
            minHeight="72px"
          />
        </section>
      ))}
    </div>
  )
}

5. Mode Read-Only di Editor

<MathTextXEditor
  content={savedHtml}
  editable={false}
  minHeight="200px"
/>

6. Komponen Terpisah (Custom Layout)

import {
  MathTextXEditor,
  MainToolbar,
  MathToolbar,
  MathTypeDialog,
} from 'pdeditor-basic'
import 'pdeditor-basic/styles'

// MathTextXEditor sudah menyertakan toolbar bawaan.
// Export di atas berguna jika Anda membangun layout editor custom
// dengan TipTap instance sendiri via createExtensions().

🖼️ Upload & Paste Gambar

Editor mendukung: tombol Image, drag-drop, paste screenshot, dan paste HTML (Word / Google Docs / website).

Upload ke Server

import { MathTextXEditor } from 'pdeditor-basic'
import 'pdeditor-basic/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 gagal')
    const data = await res.json()
    return data.url // contoh: "https://cdn.example.com/soal/img-123.jpg"
  }

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

Base64 Fallback (tanpa server)

Jika onImageUpload tidak disediakan, paste/drop gambar otomatis dikonversi ke data URL base64.

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 cocok untuk demo/prototipe. Untuk produksi, gunakan URL CDN agar HTML tidak membengkak.

Re-upload Gambar dari Paste HTML

Saat paste dari Google Docs / website, gambar sering memakai URL eksternal. Gunakan onBeforePasteHTML:

<MathTextXEditor
  onImageUpload={handleImageUpload}
  onBeforePasteHTML={async (html) => {
    const imgRegex = /<img[^>]+src="([^"]+)"[^>]*>/gi
    let result = html
    let match

    while ((match = imgRegex.exec(html)) !== null) {
      const src = match[1]
      if (src.startsWith('data:')) continue

      try {
        const blob = await (await fetch(src)).blob()
        const ext = blob.type.split('/')[1] || 'png'
        const file = new File([blob], `paste.${ext}`, { type: blob.type })
        const newUrl = await handleImageUpload(file)
        result = result.replace(src, newUrl)
      } catch {
        console.warn('Skip image:', src)
      }
    }

    return result
  }}
/>

Interaksi Gambar di Editor

| Aksi | Cara | |------|------| | Select | Klik gambar | | Resize | Tarik titik biru di sudut/sisi | | Geser posisi | Klik & tahan area tengah gambar, lalu drag | | Paste | Ctrl+V screenshot atau gambar dari clipboard |

Posisi disimpan sebagai offsetX / offsetY pada tag <figure>.

Format HTML Gambar (disimpan ke DB)

<figure
  class="mtx-image-figure mtx-image--center"
  data-type="image"
  data-offset-x="48"
  data-offset-y="-12"
  style="width: 420px; transform: translate3d(48px, -12px, 0)"
>
  <img
    src="https://cdn.example.com/gambar.png"
    alt=""
    style="display: block; max-width: 100%; height: auto"
  />
</figure>

ContentViewer membaca data-offset-x / data-offset-y dan menampilkan posisi yang sama dengan editor.


🧮 Math, LaTeX & KaTeX

Alur Data Math

┌─────────────┐    getHTML / onChange     ┌──────────────────┐
│   Editor    │ ────────────────────────► │  Database / API  │
│  (MathLive) │   HTML + data-latex       │  (simpan string) │
└─────────────┘                           └────────┬─────────┘
                                                   │
                                                   ▼
                                          ┌──────────────────┐
                                          │  ContentViewer   │
                                          │  (KaTeX render)  │
                                          └──────────────────┘

| Tahap | Teknologi | Keterangan | |-------|-----------|------------| | Input | MathLive | User mengetik rumus secara visual di editor | | Penyimpanan | HTML + data-latex | LaTeX disimpan sebagai atribut, bukan sebagai gambar | | Tampilan | KaTeX | ContentViewer merender LaTeX menjadi HTML matematika |

User tidak wajib tahu LaTeX saat mengetik — MathLive menangani input visual. LaTeX tetap tersimpan sehingga bisa di-render ulang dengan KaTeX.

Cara Insert Math di Editor

| Cara | Keterangan | |------|------------| | Ctrl+M | Insert inline math langsung | | Toolbar Math Formula | Insert inline math | | Math Toolbar (baris kedua) | Sisipkan simbol: pecahan, akar, pangkat, dll. | | Klik field math → Edit | Buka MathTypeDialog untuk edit rumus |

Format HTML Math (yang disimpan)

Inline math (sejajar teks):

<p>
  Nilai
  <span
    class="mtx-math-inline"
    data-type="math-inline"
    data-latex="x^2 + y^2 = r^2"
    data-display="inline"
  ></span>
  adalah konstanta.
</p>

Block math (baris terpisah, centered):

<div
  class="mtx-math-block"
  data-type="math-block"
  data-latex="\int_0^1 x^2 \, dx = \frac{1}{3}"
  data-display="block"
></div>

Elemen math disimpan sebagai tag kosong dengan atribut data-latex. Isi visual dihasilkan oleh KaTeX saat dibaca di viewer.

Implementasi Viewer (KaTeX) — Direkomendasikan

import { ContentViewer } from 'pdeditor-basic/viewer'
import 'pdeditor-basic/viewer/styles'

function HalamanUjian({ soalHtml }: { soalHtml: string }) {
  return (
    <article className="soal-card">
      <ContentViewer content={soalHtml} />
    </article>
  )
}

ContentViewer otomatis:

  1. Sanitasi HTML dengan DOMPurify (anti-XSS)
  2. Mencari .mtx-math-inline, .mtx-math-block, [data-type="math-inline"], [data-type="math-block"]
  3. Membaca data-latex dan merender dengan KaTeX (throwOnError: false)
  4. Mendukung mhchem untuk rumus kimia (mis. \ce{H2O})

Implementasi KaTeX Manual (tanpa ContentViewer)

Jika Anda punya HTML sendiri dan hanya perlu merender math:

import { useEffect, useRef } from 'react'
import katex from 'katex'
import 'katex/dist/katex.min.css'
import 'katex/contrib/mhchem' // opsional: rumus kimia

function renderMathInContainer(container: HTMLElement) {
  const nodes = container.querySelectorAll(
    '.mtx-math-inline, .mtx-math-block, [data-type="math-inline"], [data-type="math-block"]'
  )

  nodes.forEach((el) => {
    if (el.querySelector('.katex')) return // sudah di-render

    const latex = el.getAttribute('data-latex') || el.getAttribute('latex')
    if (!latex) return

    const isBlock = el.classList.contains('mtx-math-block')
      || el.getAttribute('data-display') === 'block'

    katex.render(latex, el as HTMLElement, {
      throwOnError: false,
      displayMode: isBlock,
      output: 'htmlAndMathml',
    })
  })
}

export function CustomViewer({ html }: { html: string }) {
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (ref.current) renderMathInContainer(ref.current)
  }, [html])

  return (
    <div
      ref={ref}
      className="soal-html"
      dangerouslySetInnerHTML={{ __html: html }}
    />
  )
}

Untuk produksi, tetap gunakan DOMPurify sebelum dangerouslySetInnerHTML. ContentViewer sudah melakukan ini.

Render LaTeX Murni (string → HTML)

Jika Anda punya string LaTeX terpisah (bukan dari editor):

import katex from 'katex'
import 'katex/dist/katex.min.css'

// Inline
const inlineHtml = katex.renderToString('E = mc^2', {
  throwOnError: false,
  displayMode: false,
})

// Block / display
const blockHtml = katex.renderToString('\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}', {
  throwOnError: false,
  displayMode: true,
})

// Di JSX
<div dangerouslySetInnerHTML={{ __html: blockHtml }} />

Contoh LaTeX yang Didukung

| Jenis | LaTeX (data-latex) | |-------|----------------------| | Pecahan | \frac{a}{b} | | Akar | \sqrt{x}, \sqrt[3]{x} | | Pangkat / subskrip | x^2, a_{n} | | Integral | \int_0^1 f(x)\,dx | | Sigma | \sum_{k=1}^{n} k | | Matriks | \begin{pmatrix} a & b \\ c & d \end{pmatrix} | | Kimia (mhchem) | \ce{H2SO4} | | Greek | \alpha, \beta, \pi |

Migrasi Konten Lama

Dari CKEditor / MathType (WIRIS):

<!-- Lama -->
<img class="Wirisformula" data-mathml="..." />

<!-- Baru (setelah edit di pdeditor-basic) -->
<span class="mtx-math-inline" data-latex="x^2" data-display="inline"></span>

Gunakan sanitizeCKEditorHTML() saat memuat HTML lama ke editor:

import { MathTextXEditor, sanitizeCKEditorHTML } from 'pdeditor-basic'

<MathTextXEditor content={sanitizeCKEditorHTML(legacyHtml)} onChange={...} />

Dari raw LaTeX di database:

Jika database menyimpan LaTeX string saja, bungkus sebelum ditampilkan:

function wrapLatex(latex: string, mode: 'inline' | 'block' = 'inline') {
  const cls = mode === 'block' ? 'mtx-math-block' : 'mtx-math-inline'
  const type = mode === 'block' ? 'math-block' : 'math-inline'
  const tag = mode === 'block' ? 'div' : 'span'
  return `<${tag} class="${cls}" data-type="${type}" data-latex="${latex.replace(/"/g, '&quot;')}" data-display="${mode}"></${tag}>`
}

// Contoh
const html = `<p>Hasil: ${wrapLatex('\\frac{1}{2}', 'inline')}</p>`
return <ContentViewer content={html} />

Utility Serializer

import { getHTML, sanitizeCKEditorHTML, toCompatibleHTML } from 'pdeditor-basic'

// Ambil HTML bersih dari instance TipTap (jika extend editor sendiri)
const html = getHTML(editor)

// Bersihkan HTML dari CKEditor sebelum dimuat
const safe = sanitizeCKEditorHTML(rawHtml)

// Konversi ke format kompatibel (fallback text untuk math)
const compat = toCompatibleHTML(html)

✨ Fitur

Rich Text

  • Bold, Italic, Underline, Strikethrough
  • Heading 1–4, Paragraph
  • Bullet / Ordered list, Indent / Outdent
  • Link, Table, Code block (syntax highlight)
  • Task list, Blockquote, Horizontal rule

Visual Math

  • Inline & block math (MathLive)
  • Math toolbar: Basic, Relation, Set, Calc, Structure
  • Equation Editor dialog (MathTypeDialog)

Image

  • Insert, paste, drag-drop
  • Resize (8 handles) & drag-to-position
  • Editor ↔ Viewer posisi konsisten

Keamanan

  • DOMPurify pada paste & viewer
  • HTML math disimpan sebagai data-latex, bukan <script>

⌨️ Keyboard Shortcuts

| Shortcut | Aksi | |----------|------| | Ctrl+B / Ctrl+I / Ctrl+U | Bold / Italic / Underline | | Ctrl+Z / Ctrl+Y | Undo / Redo | | Ctrl+M | Insert inline math | | Shift+Ctrl+V | Paste as plain text |


📋 Props API

MathTextXEditor

| Prop | Type | Default | Deskripsi | |------|------|---------|-----------| | content | string | '' | HTML awal | | onChange | (html: string) => void | — | Callback saat konten berubah (debounce 150ms) | | onSave | (html: string) => void | — | Callback saat Ctrl+S | | placeholder | string | 'Tulis soal di sini...' | Placeholder editor kosong | | editable | boolean | true | Mode read-only jika false | | minHeight | string | '200px' | Tinggi minimum area edit | | maxHeight | string | — | Tinggi maksimum (scroll) | | autoFocus | boolean | false | Fokus otomatis saat mount | | className | string | — | Class tambahan pada wrapper | | onImageUpload | (file: File) => Promise<string> | — | Upload gambar, return URL | | onBeforePasteHTML | (html: string) => Promise<string> | — | Transform HTML sebelum paste |

ContentViewer

| Prop | Type | Deskripsi | |------|------|-----------| | content | string | HTML dari database (wajib) | | className | string | Class tambahan pada wrapper |


📦 Exports & Import Paths

Main package

import {
  MathTextXEditor,
  ContentViewer,
  MainToolbar,
  MathToolbar,
  MathTypeDialog,
  MathInlineNode,
  MathBlockNode,
  createExtensions,
  getHTML,
  sanitizeCKEditorHTML,
  toCompatibleHTML,
} from 'pdeditor-basic'

import type {
  MathTextXEditorProps,
  ContentViewerProps,
  MathTypeDialogProps,
} from 'pdeditor-basic'

import 'pdeditor-basic/styles'

Viewer (subpath — lebih ringan)

import { ContentViewer } from 'pdeditor-basic/viewer'
import type { ContentViewerProps } from 'pdeditor-basic/viewer'
import 'pdeditor-basic/viewer/styles'

Tabel import path

| Import | File | |--------|------| | pdeditor-basic | dist/pdeditor-basic.js (ESM) / dist/pdeditor-basic.umd.cjs (UMD) | | pdeditor-basic/styles | dist/assets/pdeditor-basic.css | | pdeditor-basic/viewer | dist/viewer.js | | pdeditor-basic/viewer/styles | dist/viewer-styles.js (+ KaTeX CSS) |

UMD (script tag / legacy bundler)

<link rel="stylesheet" href="/node_modules/pdeditor-basic/dist/assets/pdeditor-basic.css" />
<script src="/node_modules/pdeditor-basic/dist/pdeditor-basic.umd.cjs"></script>
<script>
  const { MathTextXEditor } = PDEditorBasic
</script>

⚠️ Troubleshooting

| Masalah | Solusi | |---------|--------| | Can't resolve 'pdeditor-basic' | Jalankan npm install pdeditor-basic | | window is not defined (Next.js) | Gunakan dynamic(..., { ssr: false }) untuk editor | | Editor area kosong / tanpa placeholder | Pastikan import 'pdeditor-basic/styles' | | Math tidak tampil di viewer | Import pdeditor-basic/viewer/styles (sudah include KaTeX CSS) | | Rumus kimia tidak render | KaTeX mhchem sudah di-load di ContentViewer; untuk manual KaTeX import katex/contrib/mhchem | | MathLive font error | Set sebelum mount editor: (window as any).MATHLIVE_FONTS_PATH = '/fonts' dan salin font MathLive ke public/fonts | | Gambar tidak bisa di-drag | Klik area tengah gambar (bukan titik resize biru) | | Posisi gambar beda di viewer | Pastikan HTML punya data-offset-x / data-offset-y pada <figure class="mtx-image-figure"> | | HTML math hilang setelah sanitize | Pastikan allowlist DOMPurify menyertakan data-latex, data-display, data-type |

MathLive fonts (opsional)

// main.tsx — sebelum render editor
if (typeof window !== 'undefined') {
  (window as Window & { MATHLIVE_FONTS_PATH?: string }).MATHLIVE_FONTS_PATH = '/fonts'
}

Salin folder fonts dari node_modules/mathlive/dist/fonts ke public/fonts proyek Anda.


🛠️ Tech Stack

| Layer | Library | |-------|---------| | UI | React 18+ | | Editor | TipTap / ProseMirror | | Math input | MathLive | | Math render | KaTeX (+ mhchem) | | XSS | DOMPurify | | Build | Vite (ESM + UMD) |


📄 License

MIT


🔗 Links