lightmetal 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0
4
+
5
+ - Added six optional CSS skins: Bootstrap, Material Classic, Material Outline,
6
+ Frost, Patina, and Sterling. Each supports system, light, and dark modes.
7
+ - Build plain and minified skins alongside the unchanged default stylesheet.
8
+ - Added a local skin gallery and browser/package checks for skin appearance,
9
+ keyboard focus, Source mode, tables, contrast, and narrow layouts.
10
+
11
+ ## 0.3.0
12
+
13
+ - Added configurable formatting toolbar and custom actions.
14
+ - Added HTML source mode and content access methods.
15
+ - Added image insertion by URL, file picker, paste/drop, base64, and upload hook.
16
+ - Added table insertion and contextual row/column controls.
17
+ - Added system, light, and dark themes.
18
+ - Added import/paste sanitization and normal-mode HTML-drop rejection.
19
+ - Added link/image URL validation and support for base64 JPEG, PNG, GIF, and
20
+ WebP images in sanitized imports.
21
+ - Added `rawMode` to bypass HTML sanitization while retaining insertion URL
22
+ and file checks.
23
+ - Added `unrestrictedMode` to bypass HTML, URL, file-type, and file-size checks.
24
+ - Applied `images.enabled` to programmatic image insertion and `allowBase64`
25
+ to upload results and sanitized imports.
26
+ - Fixed missing-MIME file conversion in standalone and ES module builds.
27
+ - Kept paragraph and CSS formatting settings independent between editor instances.
28
+ - Prevented deferred formatting from changing other editors or running after destroy.
29
+ - Stopped late upload results from scheduling UI updates or image loads after destroy.
30
+ - Validated toolbar actions before registering editor listeners.
31
+ - Fixed content loss from leading closing tags during HTML import.
32
+ - Added per-instance listener cleanup through `destroy()`.
33
+ - Included standalone and ES module bundles with separate CSS.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gunnar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,416 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="ligHTMetaL.png" alt="ligHTMetaL logo" width="320">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ # ligHTMetaL
6
+
7
+ A small HTML editor. Vanilla JavaScript and CSS. Zero runtime dependencies.
8
+
9
+ - Format text, headings, lists, quotes, and links.
10
+ - Insert images from URLs, files, clipboard, or drag and drop.
11
+ - Create tables and edit rows and columns.
12
+ - Edit HTML source, customize the toolbar, and choose light or dark themes.
13
+ - Keep the browser's native undo and redo.
14
+
15
+ **Your content belongs to you.** Data sovereignty is a permanent design
16
+ constraint. No accounts, telemetry, analytics, vendor cloud, or automatic
17
+ document uploads. We will not add tracking or send your documents to a vendor
18
+ cloud. The editor runs in your browser. Your application chooses where content
19
+ is saved and whether anything is uploaded.
20
+
21
+ Browsers still load image URLs you provide, and an upload hook you configure
22
+ can send files to your server. Self-host the assets and use local/base64 images
23
+ for offline use. Unfiltered HTML can also load resources or execute code; see
24
+ [HTML and safety](#html-and-safety).
25
+
26
+ - [Use without a build](#use-without-a-build)
27
+ - [Use with npm](#use-with-npm)
28
+ - [Content and API](#content-and-api)
29
+ - [Toolbar](#toolbar)
30
+ - [Images](#images)
31
+ - [Tables](#tables)
32
+ - [Themes and styling](#themes-and-styling)
33
+ - [HTML and safety](#html-and-safety)
34
+ - [Development and tests](#development-and-tests)
35
+
36
+ ## Use without a build
37
+
38
+ Copy `dist/lightmetal.js` and `dist/lightmetal.css` from this repository beside
39
+ your HTML file. No package manager, framework, or server is required:
40
+
41
+ ```html
42
+ <link rel="stylesheet" href="lightmetal.css">
43
+ <script src="lightmetal.js"></script>
44
+
45
+ <div id="editor" class="lightmetal"></div>
46
+
47
+ <script>
48
+ const editor = lightmetal.init({
49
+ element: document.getElementById("editor"),
50
+ onChange: (html) => console.log(html)
51
+ });
52
+
53
+ editor.setHTML("<p>Hello, ligHTMetaL!</p>");
54
+ </script>
55
+ ```
56
+
57
+ Use `lightmetal.min.js` and `lightmetal.min.css` for smaller downloads.
58
+ For native browser modules, copy `lightmetal.esm.js` too and replace the scripts
59
+ above with:
60
+
61
+ ```html
62
+ <script type="module">
63
+ import { init } from "./lightmetal.esm.js";
64
+ const editor = init({ element: document.getElementById("editor") });
65
+ </script>
66
+ ```
67
+
68
+ Keep the stylesheet and host div. Run module examples over HTTP(S); the
69
+ standalone example also works over `file://`.
70
+
71
+ ## Use with npm
72
+
73
+ ```bash
74
+ npm install lightmetal --ignore-scripts
75
+ ```
76
+
77
+ The package ships ready-built JS and CSS with no runtime dependencies or
78
+ install hooks. With a bundler:
79
+
80
+ ```js
81
+ import { init } from "lightmetal/dist/lightmetal.esm.js";
82
+ import "lightmetal/dist/lightmetal.css";
83
+
84
+ const editor = init({
85
+ element: document.getElementById("editor"),
86
+ onChange: (html) => console.log(html)
87
+ });
88
+ ```
89
+
90
+ Add `<div id="editor" class="lightmetal"></div>` to your page. The editor needs
91
+ a browser DOM; do not initialize it in Node.js or during server-side rendering.
92
+
93
+ ## Content and API
94
+
95
+ The examples below use the standalone `lightmetal` global. With ES modules,
96
+ import `init` and `exec` instead.
97
+
98
+ ```js
99
+ editor.setHTML("<h2>A fresh start</h2><p>Write something here.</p>");
100
+ const html = editor.getHTML();
101
+ const text = editor.getText();
102
+ // Save or export these values using your application's own code.
103
+ ```
104
+
105
+ `init(options)` returns:
106
+
107
+ | Member | Purpose |
108
+ | --- | --- |
109
+ | `element` | Host DOM element. |
110
+ | `content` | Contenteditable div. |
111
+ | `source` | HTML source textarea. |
112
+ | `getHTML()` | Current HTML in either visual or source mode. |
113
+ | `setHTML(html)` | Replace content in either mode; sanitize by default and fire `onChange`. |
114
+ | `getText()` | Plain text from the current HTML in either mode. |
115
+ | `insertImage(urlOrFile)` | Insert a URL, data URI, or File/Blob; resolve to a boolean. |
116
+ | `insertTable(rows = 3, columns = 3)` | Insert a table; return a boolean. |
117
+ | `destroy()` | Remove editor-owned UI and listeners; preserve the host and other host content. |
118
+
119
+ Call `editor.destroy()` when removing an editor. Image and table insertion
120
+ refuse Source mode and destroyed instances. Multiple editors keep their own
121
+ configuration and listeners.
122
+
123
+ ### Options
124
+
125
+ | Option | Default | Purpose |
126
+ | --- | --- | --- |
127
+ | `element` | Required | Host element; add `class="lightmetal"` for the built-in styles. |
128
+ | `onChange(html)` | None | Receive content updates. Persistence is up to your application. |
129
+ | `actions` | All built-ins | Ordered toolbar action names or objects. |
130
+ | `defaultParagraphSeparator` | `"div"` | Use `"div"` or `"p"` for new paragraphs. |
131
+ | `styleWithCSS` | `false` | Ask native formatting commands to use CSS styles. |
132
+ | `classes` | Built-in names | Override generated class names; supply matching CSS. |
133
+ | `images` | See below | Image insertion settings. |
134
+ | `tables` | See below | Picker size and insertion limits. |
135
+ | `rawMode` | `false` | Bypass HTML import/paste filtering; insertion checks remain. |
136
+ | `unrestrictedMode` | `false` | Also bypass URL, file-type, and file-size checks. Trusted content only. |
137
+
138
+ ## Toolbar
139
+
140
+ Choose actions and their order:
141
+
142
+ ```js
143
+ const editor = lightmetal.init({
144
+ element: document.getElementById("editor"),
145
+ actions: ["bold", "italic", "heading2", "ulist", "link", "image", "table", "source"]
146
+ });
147
+ ```
148
+
149
+ The built-ins are `bold`, `italic`, `underline`, `strikethrough`, `heading1`,
150
+ `heading2`, `paragraph`, `quote`, `olist`, `ulist`, `code`, `line`, `link`,
151
+ `table`, `image`, and `source`. Omit `actions` to show all; use `[]` for no
152
+ main toolbar buttons.
153
+
154
+ Mix names with objects to customize a built-in or add an action:
155
+
156
+ ```js
157
+ actions: [
158
+ "bold",
159
+ { name: "heading2", title: "Section heading" },
160
+ {
161
+ icon: "H₃",
162
+ title: "Heading 3",
163
+ result: () => lightmetal.exec("formatBlock", "<h3>")
164
+ },
165
+ "source"
166
+ ]
167
+ ```
168
+
169
+ An optional `state()` function controls the selected appearance. Custom
170
+ `result()` callbacks replace built-in behavior; image/table actions with a
171
+ null or undefined result use their built-in workflow. Invalid actions throw
172
+ `TypeError` before listeners or editor UI are installed. Icons accept HTML:
173
+ only use trusted configuration.
174
+
175
+ `exec(command, value = null)` wraps native `document.execCommand`. Use it for
176
+ custom editing commands to keep native undo/redo.
177
+
178
+ Click **Source** to edit HTML and click again to return to visual mode. Changes
179
+ in both modes reach `onChange`. Source edits are not sanitized. Text remains
180
+ exact in the textarea, but browsers may normalize HTML when returning to visual
181
+ mode. Omit `"source"` from `actions` to leave out this button.
182
+
183
+ ## Images
184
+
185
+ The **Image** button prompts for a URL. Files can also be pasted, dropped, or
186
+ inserted from code. Files stay in the document as base64 unless you supply an
187
+ upload hook:
188
+
189
+ ```js
190
+ await editor.insertImage("/images/photo.png");
191
+ const file = document.querySelector("input[type=file]").files[0];
192
+ if (file) await editor.insertImage(file);
193
+ ```
194
+
195
+ Insertion resolves to `true` on success or `false` on failure; rejected images
196
+ show a toolbar note. JPEG, PNG, GIF, and WebP are supported, with a default
197
+ 2 MiB file limit. Missing MIME types fall back to the filename extension.
198
+ These checks do not authenticate file contents.
199
+
200
+ Pass these settings under `images`:
201
+
202
+ | Option | Default | Purpose |
203
+ | --- | --- | --- |
204
+ | `enabled` | `true` | Enable image APIs and file insertion. |
205
+ | `allowURL` | `true` | Show a URL prompt; `false` opens a file picker instead. |
206
+ | `allowBase64` | `true` | Allow base64 insertion and base64 sources in sanitized imports. |
207
+ | `dragDrop` | `true` | Handle dropped files. |
208
+ | `paste` | `true` | Handle clipboard image files. |
209
+ | `maxSize` | `2 * 1024 * 1024` | Maximum file size in bytes. |
210
+ | `onInsert` | `null` | Async file hook returning a URL or data URI. |
211
+
212
+ `allowURL` only changes the toolbar; programmatic URLs and upload results still
213
+ work. These switches do not filter raw/unrestricted HTML or Source edits.
214
+ Base64 images have uneven HTML email support; handle CID attachments in your
215
+ application.
216
+
217
+ ### Upload to your own server
218
+
219
+ Uploads are opt-in application code, not an editor service. The same hook
220
+ handles picked, dropped, pasted, and programmatically inserted files:
221
+
222
+ ```js
223
+ const editor = lightmetal.init({
224
+ element: document.getElementById("editor"),
225
+ images: {
226
+ allowBase64: false,
227
+ onInsert: async (file) => {
228
+ const body = new FormData();
229
+ body.append("image", file);
230
+ const response = await fetch("/upload", { method: "POST", body });
231
+ if (!response.ok) throw new Error("Upload failed");
232
+ return (await response.json()).url;
233
+ }
234
+ }
235
+ });
236
+ ```
237
+
238
+ Your application must provide `/upload` and return `{ "url": "..." }`. URL
239
+ checks also apply to hook results; returning base64 requires `allowBase64: true`.
240
+ Pending uploads do not insert in Source mode or after destruction.
241
+
242
+ ## Tables
243
+
244
+ Click **Insert table**, choose a grid size, or enter **Columns** and **Rows**.
245
+ The display shows columns × rows; manual fields default to 3, clamped to your
246
+ limits. Arrow keys move through the grid; Enter/Space inserts. Escape or an
247
+ outside click closes the picker.
248
+
249
+ ```js
250
+ editor.insertTable(); // 3 rows, 3 columns
251
+ editor.insertTable(3, 4); // 3 rows, 4 columns
252
+ ```
253
+
254
+ Insertion uses the current selection, or the end if the selection is outside
255
+ the editor. The picker saves the position before it opens. Focus moves into the
256
+ first cell; a blank paragraph follows the table. Nested insertion is refused.
257
+
258
+ Pass these positive-integer settings under `tables`:
259
+
260
+ ```js
261
+ tables: {
262
+ gridColumns: 10,
263
+ gridRows: 8,
264
+ maxColumns: 100,
265
+ maxRows: 1000,
266
+ maxCells: 10000
267
+ }
268
+ ```
269
+
270
+ These are the defaults. Grid/manual defaults clamp to your limits. Insertion
271
+ limits do not restrict imported tables. Contextual growth has separate fixed
272
+ caps: 1,000 rows, 100 columns, and 10,000 logical cells (rows × columns).
273
+
274
+ Click a cell to insert or delete rows/columns, or delete the table. Row edits
275
+ stay within the current section; column edits span sections. Tab/Enter work
276
+ on the controls. Added cells are blank and preserve header type/scope without
277
+ duplicating IDs. References to deleted headers are removed. Removing the last
278
+ row or column removes the table. Insertion and edits use native undo/redo.
279
+
280
+ Sanitized imports preserve captions, sections, column groups, spans, `scope`,
281
+ `headers`, and cell IDs. Single-cell, full-width section headings support row
282
+ edits; column edits from ordinary cells adjust heading spans. Other merged
283
+ cells, rowspans, uneven rows, and nested tables disable structural edits.
284
+ Column groups disable only column edits. Whole-table deletion remains available.
285
+ Use Source for unsupported structures; cell merging and resizing are not built in.
286
+
287
+ Table presentation comes from editor CSS, not styles added to new table HTML.
288
+ Supply table styles wherever your application publishes the exported content.
289
+
290
+ ## Themes and styling
291
+
292
+ The editor follows the system theme. Pin it with `lightmetal-dark` or
293
+ `lightmetal-light` on the host; remove both to follow the system again:
294
+
295
+ ```html
296
+ <div id="editor" class="lightmetal lightmetal-dark"></div>
297
+ ```
298
+
299
+ Styles are scoped under `.lightmetal`. Override CSS variables and editor size:
300
+
301
+ ```css
302
+ #editor { --lightmetal-accent: #c05020; }
303
+ #editor .lightmetal-content,
304
+ #editor .lightmetal-sources { height: 240px; }
305
+ ```
306
+
307
+ Toolbar icons use `currentColor` to follow the theme.
308
+
309
+ ### Optional skins
310
+
311
+ Preview all six skins alongside the default by opening `examples/skins.html`
312
+ from the checkout; it also works over `file://`. With the demo server running,
313
+ visit `http://localhost:5000/skins.html`.
314
+
315
+ | Skin | Host class | Appearance |
316
+ | --- | --- | --- |
317
+ | Bootstrap | `lightmetal-bootstrap` | Familiar form controls and blue accents. |
318
+ | Material Classic | `lightmetal-material-classic` | Filled toolbar, round buttons, and an accented baseline. |
319
+ | Material Outline | `lightmetal-material-outline` | Outlined frame and active controls. |
320
+ | Frost | `lightmetal-frost` | Spacious layout, cool colors, and lighter icon strokes. |
321
+ | Patina | `lightmetal-patina` | Crisp toolbar chrome with blue accents. |
322
+ | Sterling | `lightmetal-sterling` | Compact, neutral toolbar chrome. |
323
+
324
+ Load the default CSS first, then the skin. Add one skin class to the host:
325
+
326
+ ```html
327
+ <link rel="stylesheet" href="dist/lightmetal.css">
328
+ <link rel="stylesheet" href="dist/skins/frost.min.css">
329
+ <div id="editor" class="lightmetal lightmetal-frost"></div>
330
+ ```
331
+
332
+ Initialize the editor as usual. Both plain and minified skin files are built
333
+ from `src/skins/` into `dist/skins/`. Each skin supports the same system,
334
+ `lightmetal-light`, and `lightmetal-dark` modes as the default. Different skins
335
+ can coexist on one page. Skins add no JavaScript, frameworks, fonts, or external
336
+ requests; they use the editor's existing icons.
337
+
338
+ All skins are original ligHTMetaL CSS. Bootstrap and Material describe the
339
+ design inspiration, not dependencies or affiliations. The default appearance stays unchanged if you load no skin.
340
+
341
+ ## HTML and safety
342
+
343
+ **Validate and sanitize HTML in your application before rendering or publishing
344
+ it, including on the server.** The editor's small import filter is not a safety
345
+ guarantee and does not filter live typing or Source edits.
346
+
347
+ | Mode | HTML paste and `setHTML()` | HTML drops | Link/image insertion and files |
348
+ | --- | --- | --- | --- |
349
+ | Normal (default) | DOMParser allowlist filter | Rejected | URL, type, and size checks |
350
+ | `rawMode: true` | Unfiltered | Native browser behavior | Checks remain |
351
+ | `unrestrictedMode: true` | Unfiltered | Native browser behavior | URL, type, and size checks bypassed |
352
+
353
+ Normal imports remove scripts/styles with their contents, strip handlers,
354
+ styles, and classes, and unwrap unknown elements while cleaning their children.
355
+ HTML drops are rejected regardless of image settings; use paste for filtered
356
+ rich-text import. Plain-text paste uses native editing.
357
+
358
+ In normal/raw mode, link insertion allows HTTP(S), relative URLs resolving to
359
+ HTTP(S), and `mailto:`. Image insertion allows HTTP(S), relative HTTP(S), and
360
+ syntactically valid base64 JPEG/PNG/GIF/WebP data URIs when enabled. Normal
361
+ imports use the same URL policy. SVG data URIs and other schemes are rejected.
362
+ Relative URLs under `file://` do not resolve to HTTP(S).
363
+
364
+ Remote images can reveal your IP and make tracking requests; the editor does
365
+ not enforce an origin allowlist. Local/base64 images need no remote image host.
366
+
367
+ Use raw mode only when your application handles the imported HTML's safety.
368
+ **Unrestricted mode and unfiltered HTML can execute code in your application.**
369
+ Unrestricted mode includes raw mode's bypass; there is no need to enable both.
370
+ Explicit image switches, attribute escaping, table limits, and insertion guards
371
+ still apply to their own workflows. Browsers may normalize or refuse markup;
372
+ unrestricted does not mean every script will execute. Keep untrusted input out
373
+ of these bypass paths, including Source mode.
374
+
375
+ ## Browser support
376
+
377
+ Tested in Chromium and Firefox. Safari is not covered yet. The editor deliberately
378
+ uses deprecated `document.execCommand` to preserve native editing and undo/redo.
379
+ Browser-generated HTML can differ.
380
+
381
+ ## Development and tests
382
+
383
+ The browser library has no dependencies. Building uses Node.js/npm, Bash, and
384
+ esbuild; the optional demo server uses Python 3.14+, uv, and Flask:
385
+
386
+ ```bash
387
+ npm ci
388
+ uv sync --frozen
389
+ npm run build
390
+ uv run python examples/server.py
391
+ ```
392
+
393
+ Open `http://localhost:5000/`, or open `examples/index.html` directly with the
394
+ tracked bundles. The demo has default, custom-hook, and raw editors plus a
395
+ shared theme toggle. Its custom hook returns a local data URI; it uploads nothing.
396
+ Python/Flask are not needed to use the editor in your application.
397
+
398
+ Tests use a global Playwright installation and its Chromium/Firefox browsers:
399
+
400
+ ```bash
401
+ npm install --global playwright
402
+ playwright install chromium firefox
403
+ npm test
404
+ npm run test:package
405
+ ```
406
+
407
+ `npm test` rebuilds bundles, runs browser/security checks in both browsers, and
408
+ checks the standalone demo over `file://` in Chromium. After building,
409
+ `npm run test:security` runs just security regressions. `npm run test:package`
410
+ packs and installs the package with scripts disabled, then checks standalone,
411
+ minified, ESM, and bundled use. It publishes nothing. Keep rebuilt `dist/` files
412
+ with source changes.
413
+
414
+ ## License
415
+
416
+ MIT. See [LICENSE](LICENSE). Inspired by [Pell](https://github.com/jaredreich/pell).