npm package: @htmleditor/core
Description: Lightweight inline HTML editor — WYSIWYG + code view. Zero runtime dependencies.
Author: Andres Pacheco
Latest version: 0.2.0

# HTMLEditor

> Editor WYSIWYG + código HTML inline. Sin dependencias en runtime. MIT License.

[![npm](https://img.shields.io/npm/v/@htmleditor/core)](https://www.npmjs.com/package/@htmleditor/core)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Bundle size](https://img.shields.io/badge/gzip-~20kb-informational)](dist/)

## Características

- **3 modos de edición** — Visual (WYSIWYG), Código HTML directo y Preview
- **Toolbar completamente personalizable** — elige solo los botones que necesitas
- **Sin dependencias en runtime** — vanilla TypeScript/CSS compilado
- **Distribución universal** — UMD (`<script>`), ESM (`import`), TypeScript types
- **Sanitizador HTML integrado** — allowlist de tags/atributos, bloqueo de XSS y URLs peligrosas
- **Bloques de código** — 14 lenguajes, 8 temas de color, badge de lenguaje, botón de copia
- **Tablas dinámicas** — selector visual, agregar/eliminar filas y columnas, toggle de bordes
- **Adjuntos en base64** — PDF, XLS, PPT, DOCX sin necesidad de servidor de archivos
- **Selector de emojis** — categorías, navegación por teclado
- **Familias tipográficas** — selector integrado con fuentes Google
- **Theming** via variables CSS — sin modificar el source

---

## Instalación

```bash
npm install @htmleditor/core
# o
pnpm add @htmleditor/core
```

---

## Quickstart

### Via `<script>` (UMD)

```html
<link rel="stylesheet" href="dist/htmleditor.css" />
<script src="dist/htmleditor.js"></script>

<div id="editor"></div>

<script>
  // El bundle UMD expone window.HTMLEditor como namespace del módulo
  const { HTMLEditor } = window.HTMLEditor

  const editor = new HTMLEditor('#editor', {
    content: '<p>Hola mundo</p>',
    onChange: (html) => console.log(html),
    onSave:   (html) => fetch('/api/save', { method: 'POST', body: html }),
  })
</script>
```

### Via npm / ESM

```js
import { HTMLEditor } from '@htmleditor/core'
import '@htmleditor/core/dist/htmleditor.css'

const editor = new HTMLEditor('#editor', {
  content:  '<p>Hola mundo</p>',
  toolbar:  ['bold', 'italic', '|', 'h1', 'h2', '|', 'link', 'image'],
  onChange: (html) => console.log(html),
})
```

---

## API

### Constructor

```ts
new HTMLEditor(target: string | HTMLElement, options?: HTMLEditorOptions)
```

### Opciones

| Opción | Tipo | Default | Descripción |
|---|---|---|---|
| `content` | `string` | `''` | HTML inicial del editor |
| `mode` | `'visual'\|'code'\|'preview'` | `'visual'` | Modo inicial |
| `toolbar` | `ToolbarItem[]` | full toolbar | Botones a mostrar. Usa `'|'` como separador |
| `placeholder` | `string` | `'Empieza a escribir...'` | Texto placeholder |
| `onChange` | `(html: string) => void` | — | Se llama en cada cambio con el HTML actual |
| `onSave` | `(html: string) => void` | — | Se llama con Ctrl+S / Cmd+S |
| `onReady` | `(editor: HTMLEditor) => void` | — | Se llama al terminar de inicializar |

### Métodos de instancia

```ts
editor.getHTML(): string               // HTML actual del editor
editor.setHTML(html: string): void     // Reemplaza el contenido (sanitizado)
editor.setMode(mode: EditorMode): void // 'visual' | 'code' | 'preview'
editor.getMode(): EditorMode           // Modo activo
editor.focus(): void                   // Foco en el área editable
editor.destroy(): void                 // Desmonta el editor y libera listeners

// Eventos programáticos
editor.on('change', (html) => { ... })
editor.on('save',   (html) => { ... })
editor.off('change', handler)
```

### Toolbar items disponibles

| Item | Descripción |
|---|---|
| `'bold'` | Negrita |
| `'italic'` | Cursiva |
| `'underline'` | Subrayado |
| `'strikethrough'` | Tachado |
| `'h1'` `'h2'` `'h3'` | Encabezados |
| `'paragraph'` | Párrafo normal |
| `'ul'` | Lista sin orden |
| `'ol'` | Lista ordenada |
| `'indent'` | Aumentar sangría |
| `'outdent'` | Reducir sangría |
| `'blockquote'` | Cita |
| `'link'` | Insertar enlace |
| `'image'` | Insertar imagen (desde archivo) |
| `'code'` | Código inline |
| `'codeblock'` | Bloque de código con lenguaje y color |
| `'table'` | Insertar tabla |
| `'hr'` | Línea horizontal |
| `'fontfamily'` | Selector de familia tipográfica |
| `'emoji'` | Selector de emojis |
| `'attachment'` | Adjuntar archivo (PDF, XLS, PPT, DOCX…) |
| `'|'` | Separador visual |

**Toolbar por defecto:**
```js
['h1', 'h2', 'h3', 'paragraph', '|',
 'bold', 'italic', 'underline', 'strikethrough', '|',
 'ul', 'ol', 'indent', 'outdent', 'blockquote', '|',
 'link', 'image', 'table', 'hr', '|',
 'code', 'codeblock', '|',
 'fontfamily', '|',
 'emoji', 'attachment']
```

---

## Theming

Sobreescribe las variables CSS en el selector del contenedor:

```css
/* Tema violeta */
#mi-editor {
  --he-primary:       #7c3aed;
  --he-primary-hover: #6d28d9;
  --he-primary-light: #f5f3ff;
  --he-bg-toolbar:    #faf5ff;
}

/* Tema oscuro */
#mi-editor {
  --he-primary:       #60a5fa;
  --he-primary-hover: #93c5fd;
  --he-primary-light: rgba(96, 165, 250, .12);
  --he-bg:            #1e293b;
  --he-bg-toolbar:    #0f172a;
  --he-border:        #334155;
  --he-text:          #f1f5f9;
  --he-text-muted:    #94a3b8;
}
```

### Variables disponibles

| Variable | Default | Descripción |
|---|---|---|
| `--he-primary` | `#2563eb` | Color de acento principal |
| `--he-primary-hover` | `#1d4ed8` | Hover del color primario |
| `--he-primary-light` | `#eff6ff` | Fondo claro de acento |
| `--he-bg` | `#ffffff` | Fondo del área de edición |
| `--he-bg-toolbar` | `#f8fafc` | Fondo de la toolbar |
| `--he-border` | `#e2e8f0` | Color de bordes |
| `--he-text` | `#0f172a` | Color del texto |
| `--he-text-muted` | `#64748b` | Texto secundario / íconos inactivos |

---

## Integración con frameworks

### Angular

```typescript
// angular.json → projects.architect.build.options.styles
"node_modules/@htmleditor/core/dist/htmleditor.css"

// editor.component.ts
import { Component, AfterViewInit, OnDestroy, Output, EventEmitter } from '@angular/core'
import { HTMLEditor } from '@htmleditor/core'

@Component({
  selector: 'app-editor',
  template: `<div id="he-host"></div>`,
})
export class EditorComponent implements AfterViewInit, OnDestroy {
  @Output() contentChange = new EventEmitter<string>()
  private editor!: HTMLEditor

  ngAfterViewInit() {
    this.editor = new HTMLEditor('#he-host', {
      toolbar: ['bold', 'italic', '|', 'h1', 'h2', '|', 'link', 'image'],
      onChange: (html) => this.contentChange.emit(html),
    })
  }

  ngOnDestroy()             { this.editor.destroy() }
  getValue(): string        { return this.editor.getHTML() }
  setValue(html: string)    { this.editor.setHTML(html) }
}
```

### Vue 3

```vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { HTMLEditor } from '@htmleditor/core'
import '@htmleditor/core/dist/htmleditor.css'

const html = ref('')
let editor: HTMLEditor

onMounted(() => {
  editor = new HTMLEditor('#editor', {
    onChange: (h) => { html.value = h },
  })
})

onUnmounted(() => editor?.destroy())
</script>

<template>
  <div id="editor"></div>
</template>
```

### Spring Boot + Thymeleaf

```html
<link rel="stylesheet" th:href="@{/vendor/htmleditor.css}"/>

<div id="content" th:utext="${post.content}"></div>

<script th:src="@{/vendor/htmleditor.js}"></script>
<script>
  const EditorClass = window.HTMLEditor.HTMLEditor

  const el          = document.getElementById('content')
  const initialHTML = el.innerHTML
  el.innerHTML      = ''   // limpiar antes de montar

  const editor = new EditorClass(el, { content: initialHTML })

  document.querySelector('form').addEventListener('submit', () => {
    const hidden = document.createElement('input')
    hidden.type  = 'hidden'
    hidden.name  = 'content'
    hidden.value = editor.getHTML()
    document.querySelector('form').appendChild(hidden)
  })
</script>
```

---

## Seguridad

El método `setHTML()` y el callback `content` del constructor pasan el HTML por un **sanitizador integrado** que:

- Mantiene solo un allowlist de tags seguros (`p`, `h1–h6`, `strong`, `em`, `a`, `img`, `ul`, `ol`, `pre`, `code`, `table`, etc.)
- Elimina completamente `<script>`, `<style>`, `<iframe>`, `<object>`, `<embed>`, `<noscript>`
- Valida `href` y `src` contra protocolos seguros (`https:`, `mailto:`, `/`, `data:image/*;base64,`)
- Bloquea `javascript:` y `expression()` en atributos `style`
- Fuerza `rel="noopener noreferrer"` en enlaces externos

> Para aplicaciones en producción se recomienda complementar con sanitización server-side (ej. [jsoup](https://jsoup.org/) en Java, [DOMPurify](https://github.com/cure53/DOMPurify) en Node).

---

## Desarrollo

```bash
git clone https://github.com/apessss001/htmleditor
cd htmleditor
npm install

npm run dev        # servidor de desarrollo con HMR en demo/
npm run build      # genera dist/ (UMD + ESM + types + CSS)
npm run test       # tests unitarios con Vitest
npm run typecheck  # verificación de tipos sin compilar
```

### Estructura del proyecto

```
src/
├── HTMLEditor.ts          # Clase principal y API pública
├── types.ts               # Tipos TypeScript exportados
├── styles/
│   └── editor.css         # Todos los estilos del editor
├── toolbar/
│   ├── Toolbar.ts         # Construcción y lógica de la toolbar
│   └── commands.ts        # execCommand wrappers + utilidades
├── modes/
│   ├── WysiwygMode.ts     # Modo visual (contenteditable)
│   ├── CodeMode.ts        # Modo código (textarea)
│   └── PreviewMode.ts     # Modo preview (iframe)
└── utils/
    ├── sanitize.ts        # Sanitizador HTML con allowlist
    ├── imageUpload.ts     # Carga y redimensión de imágenes
    ├── fileUpload.ts      # Carga de archivos adjuntos
    ├── emojiPicker.ts     # Picker de emojis
    ├── fontLoader.ts      # Carga de fuentes Google
    ├── tableDialog.ts     # Diálogo de inserción de tablas
    ├── codeBlockDialog.ts # Diálogo de bloque de código
    └── resizeDialog.ts    # Diálogo de redimensión de imágenes
```

---

## Licencia

[MIT](LICENSE) — Andres Pacheco