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 +33 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/dist/lightmetal.css +334 -0
- package/dist/lightmetal.esm.js +1099 -0
- package/dist/lightmetal.js +1121 -0
- package/dist/lightmetal.min.css +1 -0
- package/dist/lightmetal.min.js +1 -0
- package/dist/skins/bootstrap.css +68 -0
- package/dist/skins/bootstrap.min.css +1 -0
- package/dist/skins/frost.css +76 -0
- package/dist/skins/frost.min.css +1 -0
- package/dist/skins/material-classic.css +74 -0
- package/dist/skins/material-classic.min.css +1 -0
- package/dist/skins/material-outline.css +73 -0
- package/dist/skins/material-outline.min.css +1 -0
- package/dist/skins/patina.css +67 -0
- package/dist/skins/patina.min.css +1 -0
- package/dist/skins/sterling.css +74 -0
- package/dist/skins/sterling.min.css +1 -0
- package/ligHTMetaL.png +0 -0
- package/package.json +28 -4
- package/src/lightmetal.css +334 -0
- package/src/lightmetal.js +1302 -0
- package/src/skins/bootstrap.css +68 -0
- package/src/skins/frost.css +76 -0
- package/src/skins/material-classic.css +74 -0
- package/src/skins/material-outline.css +73 -0
- package/src/skins/patina.css +67 -0
- package/src/skins/sterling.css +74 -0
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
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="ligHTMetaL.png" alt="ligHTMetaL logo" width="320">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
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).
|