mi-element 0.7.0 → 0.8.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/README.md +2 -0
- package/dist/element.js +6 -13
- package/dist/html.js +74 -0
- package/dist/index.js +1 -1
- package/dist/utils.js +12 -0
- package/docs/render.md +357 -0
- package/package.json +7 -7
- package/src/element.js +4 -16
- package/src/html.js +208 -0
- package/src/index.js +1 -1
- package/src/utils.js +22 -0
- package/types/html.d.ts +50 -0
- package/types/index.d.ts +1 -1
- package/types/utils.d.ts +2 -0
- package/dist/escape.js +0 -20
- package/src/escape.js +0 -60
- package/types/escape.d.ts +0 -3
package/README.md
CHANGED
|
@@ -94,6 +94,7 @@ In `./example` you'll find a working sample of a Todo App. Check it out with
|
|
|
94
94
|
# Documentation
|
|
95
95
|
|
|
96
96
|
- [element][docs-element] mi-element's lifecycle
|
|
97
|
+
- [render][docs-render] mi-elements html template literal and render function
|
|
97
98
|
- [controller][docs-controller] adding controllers to mi-element to hook into the lifecycle
|
|
98
99
|
- [signal][docs-signal] Signals and effect for reactive behavior
|
|
99
100
|
- [store][docs-store] Manage shared state in an application
|
|
@@ -106,6 +107,7 @@ In `./example` you'll find a working sample of a Todo App. Check it out with
|
|
|
106
107
|
MIT licensed
|
|
107
108
|
|
|
108
109
|
[docs-element]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/element.md
|
|
110
|
+
[docs-render]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/render.md
|
|
109
111
|
[docs-controller]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/controller.md
|
|
110
112
|
[docs-context]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/context.md
|
|
111
113
|
[docs-signal]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/signal.md
|
package/dist/element.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { createSignal } from 'mi-signal';
|
|
2
|
+
|
|
1
3
|
import { kebabToCamelCase, camelToKebabCase } from './case.js';
|
|
2
4
|
|
|
3
5
|
import { addGlobalStyles } from './styling.js';
|
|
4
6
|
|
|
5
7
|
import { refsBySelector } from './refs.js';
|
|
6
8
|
|
|
7
|
-
import {
|
|
9
|
+
import { toNumber, toJson } from './utils.js';
|
|
8
10
|
|
|
9
11
|
const nameMap = {
|
|
10
12
|
class: 'className',
|
|
@@ -129,18 +131,9 @@ const define = (tagName, elementClass, options) => {
|
|
|
129
131
|
elementClass.styles && (elementClass.styles = styles || (usedCssPrefix === cssPrefix ? elementClass.styles : elementClass.styles.replaceAll(`--${usedCssPrefix}-`, cssPrefix))),
|
|
130
132
|
renderTemplate(elementClass), window.customElements.define(tagName, elementClass);
|
|
131
133
|
}, renderTemplate = element => {
|
|
132
|
-
if (element.template instanceof HTMLTemplateElement) return;
|
|
134
|
+
if (!element.template || element.template instanceof HTMLTemplateElement) return;
|
|
133
135
|
const el = document.createElement('template');
|
|
134
|
-
el.innerHTML = element.template, element.template = el;
|
|
135
|
-
},
|
|
136
|
-
try {
|
|
137
|
-
return JSON.parse(any);
|
|
138
|
-
} catch {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
}, convertType = (value, type) => type === Boolean ? null !== value : type === Number ? (any => {
|
|
142
|
-
const n = Number(any);
|
|
143
|
-
return isNaN(n) ? 0 : n;
|
|
144
|
-
})(value) : type === Array ? toJson(value) ?? value.split(',').map(v => v.trim()) : type === Object ? toJson(value) : value;
|
|
136
|
+
el.innerHTML = element.template || '', element.template = el;
|
|
137
|
+
}, convertType = (value, type) => type === Boolean ? null !== value : type === Number ? toNumber(value) : type === Array ? toJson(value) ?? value.split(',').map(v => v.trim()) : type === Object ? toJson(value) : value;
|
|
145
138
|
|
|
146
139
|
export { MiElement, convertType, define };
|
package/dist/html.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { toJson } from './utils.js';
|
|
2
|
+
|
|
3
|
+
const globalRenderCache = new class {
|
|
4
|
+
cnt=0;
|
|
5
|
+
map=new Map;
|
|
6
|
+
cache=new WeakMap;
|
|
7
|
+
_inc() {
|
|
8
|
+
return this.cnt = 268435455 & ++this.cnt, this.cnt;
|
|
9
|
+
}
|
|
10
|
+
clear() {
|
|
11
|
+
this.cnt = 0, this.map.clear();
|
|
12
|
+
}
|
|
13
|
+
set(value) {
|
|
14
|
+
const key = '__rc:' + this._inc().toString(36), ref = {};
|
|
15
|
+
return this.map.set(key, ref), this.cache.set(ref, value), key;
|
|
16
|
+
}
|
|
17
|
+
get(key) {
|
|
18
|
+
const ref = this.map.get(key);
|
|
19
|
+
return this.map.delete(key), this.cache.get(ref);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class UnsafeHtml extends String {}
|
|
24
|
+
|
|
25
|
+
const unsafeHtml = str => new UnsafeHtml(str), escMap = {
|
|
26
|
+
'&': '&',
|
|
27
|
+
'<': '<',
|
|
28
|
+
'>': '>',
|
|
29
|
+
'"': '"',
|
|
30
|
+
"'": '''
|
|
31
|
+
}, esc = string => string.replace(/[&<>"']/g, tag => escMap[tag]), escHtml = string => string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string)), escValue = any => {
|
|
32
|
+
if (any instanceof UnsafeHtml) return any;
|
|
33
|
+
if ([ 'object', 'function' ].includes(typeof any)) {
|
|
34
|
+
const key = globalRenderCache.set(any);
|
|
35
|
+
return unsafeHtml(key);
|
|
36
|
+
}
|
|
37
|
+
return unsafeHtml(esc('' + any));
|
|
38
|
+
}, html = (strings, ...values) => unsafeHtml(String.raw({
|
|
39
|
+
raw: strings
|
|
40
|
+
}, ...values.map(val => Array.isArray(val) ? val.map(escValue).join('') : escValue(val))));
|
|
41
|
+
|
|
42
|
+
function render(node, template, handlers = {}) {
|
|
43
|
+
const refs = {}, div = document.createElement('div');
|
|
44
|
+
div.innerHTML = template.toString();
|
|
45
|
+
for (let child of Array.from(div.children)) renderAttrs(child, handlers, refs),
|
|
46
|
+
node.appendChild(child);
|
|
47
|
+
return refs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderAttrs(node, handlers = {}, refs = {}) {
|
|
51
|
+
if (node.nodeType === Node.ELEMENT_NODE) for (let attr of node.attributes) {
|
|
52
|
+
const startsWith = attr.name[0], name = attr.name.slice(1);
|
|
53
|
+
let rm = 0;
|
|
54
|
+
if ('?' === startsWith) toJson(attr.value) ? node.setAttribute(name, '') : node.removeAttribute(name),
|
|
55
|
+
rm = 1; else if ('...' === attr.name) {
|
|
56
|
+
const obj = globalRenderCache.get(attr.value);
|
|
57
|
+
if (obj && 'object' == typeof obj) for (const [k, v] of Object.entries(obj)) node[k] = v;
|
|
58
|
+
rm = 1;
|
|
59
|
+
} else if ('.' === startsWith) node[name] = globalRenderCache.get(attr.value) ?? attr.value,
|
|
60
|
+
rm = 1; else if ('@' === startsWith) {
|
|
61
|
+
const handlerName = attr.value, fn = globalRenderCache.get(handlerName);
|
|
62
|
+
fn ? node.addEventListener(name, e => fn(e)) : 'function' == typeof handlers[handlerName] && node.addEventListener(name, e => handlers[handlerName](e)),
|
|
63
|
+
rm = 1;
|
|
64
|
+
} else 'ref' === attr.name && (refs[attr.value] = node, rm = 1);
|
|
65
|
+
rm && requestAnimationFrame(() => {
|
|
66
|
+
node.removeAttribute(attr.name);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (!node.children?.length || customElements.get(node.localName)) return refs;
|
|
70
|
+
for (let child of Array.from(node.children)) renderAttrs(child, handlers, refs);
|
|
71
|
+
return refs;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { escHtml, globalRenderCache, html, render, renderAttrs, unsafeHtml };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { ContextConsumer, ContextProvider, ContextRequestEvent } from './context
|
|
|
2
2
|
|
|
3
3
|
export { MiElement, convertType, define } from './element.js';
|
|
4
4
|
|
|
5
|
-
export { escHtml, html, unsafeHtml } from './
|
|
5
|
+
export { escHtml, html, render, renderAttrs, unsafeHtml } from './html.js';
|
|
6
6
|
|
|
7
7
|
export { refsBySelector } from './refs.js';
|
|
8
8
|
|
package/dist/utils.js
ADDED
package/docs/render.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
**Table of contents**
|
|
2
|
+
|
|
3
|
+
<!-- !toc (minlevel=2) -->
|
|
4
|
+
|
|
5
|
+
- [html Tagged Template Literal](#html-tagged-template-literal)
|
|
6
|
+
- [Basic Usage](#basic-usage)
|
|
7
|
+
- [Working with Arrays](#working-with-arrays)
|
|
8
|
+
- [Nested Templates](#nested-templates)
|
|
9
|
+
- [Unsafe HTML](#unsafe-html)
|
|
10
|
+
- [render() Function](#render-function)
|
|
11
|
+
- [Signature](#signature)
|
|
12
|
+
- [Basic Example](#basic-example)
|
|
13
|
+
- [Special Attributes](#special-attributes)
|
|
14
|
+
- [Boolean Attributes (`?attr`)](#boolean-attributes-attr)
|
|
15
|
+
- [Property Binding (`.prop`)](#property-binding-prop)
|
|
16
|
+
- [Event Listeners (`@event`)](#event-listeners-event)
|
|
17
|
+
- [Element References (`ref`)](#element-references-ref)
|
|
18
|
+
- [Spread Properties (`...`)](#spread-properties-)
|
|
19
|
+
- [Complete Example](#complete-example)
|
|
20
|
+
- [Security](#security)
|
|
21
|
+
- [Best Practices](#best-practices)
|
|
22
|
+
- [Custom Elements](#custom-elements)
|
|
23
|
+
|
|
24
|
+
<!-- toc! -->
|
|
25
|
+
|
|
26
|
+
# HTML Templating and Rendering
|
|
27
|
+
|
|
28
|
+
MiElement offers a lightweight templating system for properly escaping HTML to
|
|
29
|
+
prevent XSS attacks. The `html` tagged template literal combined with the
|
|
30
|
+
`render()` function provides a safe and convenient way to create dynamic HTML
|
|
31
|
+
with event handlers, property bindings, and element references.
|
|
32
|
+
|
|
33
|
+
## html Tagged Template Literal
|
|
34
|
+
|
|
35
|
+
The `html` function is a tagged template literal that automatically escapes all
|
|
36
|
+
interpolated values to prevent XSS attacks.
|
|
37
|
+
|
|
38
|
+
### Basic Usage
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { html } from '@mi-element/mi-element'
|
|
42
|
+
|
|
43
|
+
// Simple text escaping
|
|
44
|
+
const userInput = '<script>alert("xss")</script>'
|
|
45
|
+
const safe = html`<div>${userInput}</div>`
|
|
46
|
+
// Result: <div><script>alert("xss")</script></div>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Working with Arrays
|
|
50
|
+
|
|
51
|
+
Arrays are automatically joined and each item is escaped:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const items = ['Apple', 'Banana', 'Cherry']
|
|
55
|
+
const list = html`<ul>
|
|
56
|
+
${items.map((item) => html`<li>${item}</li>`)}
|
|
57
|
+
</ul>`
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Nested Templates
|
|
61
|
+
|
|
62
|
+
HTML templates can be nested safely without double-escaping:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const header = html`<h1>${'<Title>'}</h1>`
|
|
66
|
+
const page = html`
|
|
67
|
+
<div>
|
|
68
|
+
${header}
|
|
69
|
+
<p>${'<content>'}</p>
|
|
70
|
+
</div>
|
|
71
|
+
`
|
|
72
|
+
// The header content remains escaped, not double-escaped
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Unsafe HTML
|
|
76
|
+
|
|
77
|
+
When you need to render raw HTML (use with caution):
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { unsafeHtml } from '@mi-element/mi-element'
|
|
81
|
+
|
|
82
|
+
const trustedHtml = '<strong>Bold</strong>'
|
|
83
|
+
const template = html`<div>${unsafeHtml(trustedHtml)}</div>`
|
|
84
|
+
// Result: <div><strong>Bold</strong></div>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## render() Function
|
|
88
|
+
|
|
89
|
+
The `render()` function takes an HTML template and renders it into a DOM node,
|
|
90
|
+
with support for special attributes for event handling, property binding, and
|
|
91
|
+
element references.
|
|
92
|
+
|
|
93
|
+
### Signature
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
render(node, template, (handlers = {}))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- `node`: Target DOM element to render into
|
|
100
|
+
- `template`: HTML template string (typically from `html` tagged template)
|
|
101
|
+
- `handlers`: Optional object containing event handler functions or HTMLElement
|
|
102
|
+
for method lookup
|
|
103
|
+
- **Returns**: Object with collected element references
|
|
104
|
+
|
|
105
|
+
### Basic Example
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import { html, render } from '@mi-element/mi-element'
|
|
109
|
+
|
|
110
|
+
const container = document.querySelector('#app')
|
|
111
|
+
const template = html`
|
|
112
|
+
<div>
|
|
113
|
+
<h1>Hello World</h1>
|
|
114
|
+
<p>Welcome to MiElement</p>
|
|
115
|
+
</div>
|
|
116
|
+
`
|
|
117
|
+
|
|
118
|
+
render(container, template)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Special Attributes
|
|
122
|
+
|
|
123
|
+
Special attributes provide powerful features for dynamic behavior. All special
|
|
124
|
+
attributes are removed from the DOM after processing.
|
|
125
|
+
|
|
126
|
+
### Boolean Attributes (`?attr`)
|
|
127
|
+
|
|
128
|
+
Use the `?` prefix for boolean attributes:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
const isDisabled = true
|
|
132
|
+
const template = html`
|
|
133
|
+
<input ?disabled=${isDisabled} />
|
|
134
|
+
<button ?hidden=${false}>Click</button>
|
|
135
|
+
`
|
|
136
|
+
|
|
137
|
+
render(container, template)
|
|
138
|
+
// Result: <input disabled=""> <button>Click</button>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Property Binding (`.prop`)
|
|
142
|
+
|
|
143
|
+
Use the `.` prefix to set properties directly on DOM elements (not attributes):
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const inputValue = 'Hello'
|
|
147
|
+
const userData = { name: 'John', age: 30 }
|
|
148
|
+
|
|
149
|
+
const template = html`
|
|
150
|
+
<input .value=${inputValue} />
|
|
151
|
+
<my-component .data=${userData}></my-component>
|
|
152
|
+
`
|
|
153
|
+
|
|
154
|
+
render(container, template)
|
|
155
|
+
// The input.value property is set, but not visible as an attribute
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Note**: Use kebab-case for property names - they will be automatically
|
|
159
|
+
converted to camelCase:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
html`<div .some-property=${'value'}></div>`
|
|
163
|
+
// Sets element.someProperty = 'value'
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Event Listeners (`@event`)
|
|
167
|
+
|
|
168
|
+
Use the `@` prefix for event listeners:
|
|
169
|
+
|
|
170
|
+
#### Inline Functions
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
const template = html`
|
|
174
|
+
<button @click=${(e) => console.log('Clicked!', e)}>Click Me</button>
|
|
175
|
+
`
|
|
176
|
+
|
|
177
|
+
render(container, template)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Named Handlers
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
const handlers = {
|
|
184
|
+
handleClick(e) {
|
|
185
|
+
console.log('Button clicked:', e.target)
|
|
186
|
+
},
|
|
187
|
+
handleInput(e) {
|
|
188
|
+
console.log('Input value:', e.target.value)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const template = html`
|
|
193
|
+
<button @click="handleClick">Click</button>
|
|
194
|
+
<input @input="handleInput" />
|
|
195
|
+
`
|
|
196
|
+
|
|
197
|
+
render(container, template, handlers)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Using HTMLElement Methods
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
class MyComponent extends HTMLElement {
|
|
204
|
+
handleClick(e) {
|
|
205
|
+
console.log('Clicked in component')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
connectedCallback() {
|
|
209
|
+
const template = html` <button @click="handleClick">Click</button> `
|
|
210
|
+
render(this, template, this)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Element References (`ref`)
|
|
216
|
+
|
|
217
|
+
Collect references to rendered elements:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
const template = html`
|
|
221
|
+
<div>
|
|
222
|
+
<input ref="nameInput" type="text" />
|
|
223
|
+
<button ref="submitBtn">Submit</button>
|
|
224
|
+
</div>
|
|
225
|
+
`
|
|
226
|
+
|
|
227
|
+
const refs = render(container, template)
|
|
228
|
+
// refs.nameInput -> the input element
|
|
229
|
+
// refs.submitBtn -> the button element
|
|
230
|
+
|
|
231
|
+
refs.nameInput.focus()
|
|
232
|
+
refs.submitBtn.addEventListener('click', () => {
|
|
233
|
+
console.log(refs.nameInput.value)
|
|
234
|
+
})
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Spread Properties (`...`)
|
|
238
|
+
|
|
239
|
+
Spread multiple properties from an object:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
const inputProps = {
|
|
243
|
+
value: 'Default text',
|
|
244
|
+
disabled: true,
|
|
245
|
+
placeholder: 'Enter text'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const template = html`<input ...=${inputProps} />`
|
|
249
|
+
|
|
250
|
+
render(container, template)
|
|
251
|
+
// Sets all properties: value, disabled, and placeholder
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Complete Example
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
import { html, render, MiElement, define } from '@mi-element/mi-element'
|
|
258
|
+
import { MiElement } from '@mi-element/mi-element'
|
|
259
|
+
|
|
260
|
+
class TodoList extends MiElement {
|
|
261
|
+
todos = [
|
|
262
|
+
{ id: 1, text: 'Learn MiElement', done: false },
|
|
263
|
+
{ id: 2, text: 'Build something', done: false }
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
toggleTodo(id) {
|
|
267
|
+
const todo = this.todos.find((t) => t.id === id)
|
|
268
|
+
if (todo) todo.done = !todo.done
|
|
269
|
+
this.requestUpdate()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// first time render
|
|
273
|
+
render() {
|
|
274
|
+
const template = html`
|
|
275
|
+
<div>
|
|
276
|
+
<h2>My Todos</h2>
|
|
277
|
+
<ul ref="list"></ul>
|
|
278
|
+
</div>
|
|
279
|
+
`
|
|
280
|
+
this.refs = render(this.renderRoot, template)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// any updates
|
|
284
|
+
update() {
|
|
285
|
+
const template = this.todos.map(
|
|
286
|
+
(todo) => html`
|
|
287
|
+
<li>
|
|
288
|
+
<input
|
|
289
|
+
type="checkbox"
|
|
290
|
+
?checked=${todo.done}
|
|
291
|
+
@change=${() => this.toggleTodo(todo.id)}
|
|
292
|
+
/>
|
|
293
|
+
<span>${todo.text}</span>
|
|
294
|
+
</li>
|
|
295
|
+
`
|
|
296
|
+
)
|
|
297
|
+
// clear all children first
|
|
298
|
+
this.refs.list.innerHTML = ''
|
|
299
|
+
// then rerender
|
|
300
|
+
render(this.refs.list, template)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
define('todo-list', TodoList)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Security
|
|
308
|
+
|
|
309
|
+
The `html` tagged template automatically escapes all values to prevent XSS
|
|
310
|
+
attacks:
|
|
311
|
+
|
|
312
|
+
- Strings are HTML-escaped
|
|
313
|
+
- Objects and functions are stored in a temporary cache and referenced by key
|
|
314
|
+
- Only use `unsafeHtml()` when you have trusted HTML content
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
// Safe - user input is escaped
|
|
318
|
+
const userInput = '<script>alert("xss")</script>'
|
|
319
|
+
html`<div>${userInput}</div>`
|
|
320
|
+
// Result: <div><script>alert("xss")</script></div>;
|
|
321
|
+
|
|
322
|
+
// Unsafe - only use with trusted content
|
|
323
|
+
const trustedHtml = '<strong>Safe HTML</strong>'
|
|
324
|
+
html`<div>${unsafeHtml(trustedHtml)}</div>`
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Best Practices
|
|
328
|
+
|
|
329
|
+
1. **Always use `html` for dynamic content** to ensure proper escaping
|
|
330
|
+
2. **Use kebab-case** for all attribute and event names
|
|
331
|
+
3. **Collect refs** instead of using `querySelector` when possible
|
|
332
|
+
4. **Avoid `unsafeHtml`** unless absolutely necessary with trusted content
|
|
333
|
+
5. **Use property binding** (`.prop`) for complex data structures
|
|
334
|
+
6. **Leverage event delegation** for dynamic lists with many items
|
|
335
|
+
7. **Keep handlers object** or use class methods for better organization
|
|
336
|
+
|
|
337
|
+
## Custom Elements
|
|
338
|
+
|
|
339
|
+
The render system automatically skips processing children of custom elements,
|
|
340
|
+
allowing them to manage their own content:
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
class MyElement extends HTMLElement {
|
|
344
|
+
connectedCallback() {
|
|
345
|
+
// This element controls its own rendering
|
|
346
|
+
this.innerHTML = `<input ref="inside" />`
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
customElements.define('my-element', MyElement)
|
|
351
|
+
|
|
352
|
+
// The render function will process my-element's attributes
|
|
353
|
+
// but won't process its children
|
|
354
|
+
const template = html`
|
|
355
|
+
<my-element .data=${{ test: true }} ref="custom"></my-element>
|
|
356
|
+
`
|
|
357
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mi-element",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Build lightweight reactive micro web-components",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/commenthol/mi-element/tree/main/packages/mi-element#readme",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"types": "./types/context.d.ts",
|
|
41
41
|
"default": "./dist/context.js"
|
|
42
42
|
},
|
|
43
|
-
"./
|
|
44
|
-
"development": "./src/
|
|
45
|
-
"types": "./types/
|
|
46
|
-
"default": "./dist/
|
|
43
|
+
"./html": {
|
|
44
|
+
"development": "./src/html.js",
|
|
45
|
+
"types": "./types/html.d.ts",
|
|
46
|
+
"default": "./dist/html.js"
|
|
47
47
|
},
|
|
48
48
|
"./refs": {
|
|
49
49
|
"development": "./src/refs.js",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"types"
|
|
70
70
|
],
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"mi-signal": "0.
|
|
72
|
+
"mi-signal": "0.8.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@eslint/js": "^9.39.2",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"typescript": "^5.9.3",
|
|
91
91
|
"vite": "^7.3.0",
|
|
92
92
|
"vitest": "^4.0.16",
|
|
93
|
-
"mi-html": "0.
|
|
93
|
+
"mi-html": "0.8.0"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"all": "npm-run-all pretty lint test build types",
|
package/src/element.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { createSignal } from 'mi-signal'
|
|
1
2
|
import { kebabToCamelCase, camelToKebabCase } from './case.js'
|
|
2
3
|
import { addGlobalStyles } from './styling.js'
|
|
3
4
|
import { refsBySelector } from './refs.js'
|
|
4
|
-
import {
|
|
5
|
+
import { toNumber, toJson } from './utils.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Mapping of attribute names to property names
|
|
@@ -376,27 +377,14 @@ export const define = (tagName, elementClass, options) => {
|
|
|
376
377
|
* @param {typeof MiElement} element
|
|
377
378
|
*/
|
|
378
379
|
const renderTemplate = (element) => {
|
|
379
|
-
if (element.template instanceof HTMLTemplateElement) {
|
|
380
|
+
if (!element.template || element.template instanceof HTMLTemplateElement) {
|
|
380
381
|
return
|
|
381
382
|
}
|
|
382
383
|
const el = document.createElement('template')
|
|
383
|
-
el.innerHTML = element.template
|
|
384
|
+
el.innerHTML = element.template || ''
|
|
384
385
|
element.template = el
|
|
385
386
|
}
|
|
386
387
|
|
|
387
|
-
const toNumber = (any) => {
|
|
388
|
-
const n = Number(any)
|
|
389
|
-
return isNaN(n) ? 0 : n
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const toJson = (any) => {
|
|
393
|
-
try {
|
|
394
|
-
return JSON.parse(any)
|
|
395
|
-
} catch {
|
|
396
|
-
return
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
388
|
/**
|
|
401
389
|
* convert a attribute string value to typed value
|
|
402
390
|
* @param {string} value
|
package/src/html.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { toJson } from './utils.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A cache for rendering values to avoid keeping them in memory too long
|
|
5
|
+
*/
|
|
6
|
+
class RenderCache {
|
|
7
|
+
cnt = 0
|
|
8
|
+
map = new Map()
|
|
9
|
+
cache = new WeakMap()
|
|
10
|
+
|
|
11
|
+
_inc() {
|
|
12
|
+
this.cnt = ++this.cnt & 0xfffffff
|
|
13
|
+
return this.cnt
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
clear() {
|
|
17
|
+
this.cnt = 0
|
|
18
|
+
this.map.clear()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
set(value) {
|
|
22
|
+
const key = '__rc:' + this._inc().toString(36)
|
|
23
|
+
const ref = {}
|
|
24
|
+
this.map.set(key, ref)
|
|
25
|
+
this.cache.set(ref, value)
|
|
26
|
+
return key
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(key) {
|
|
30
|
+
const ref = this.map.get(key)
|
|
31
|
+
this.map.delete(key)
|
|
32
|
+
return this.cache.get(ref)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const globalRenderCache = new RenderCache()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A helper class to avoid double escaping of HTML strings
|
|
40
|
+
*/
|
|
41
|
+
class UnsafeHtml extends String {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* tag a string as html for not to be escaped
|
|
45
|
+
* @param {string} str
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
// @ts-expect-error
|
|
49
|
+
export const unsafeHtml = (str) => new UnsafeHtml(str)
|
|
50
|
+
|
|
51
|
+
const escMap = {
|
|
52
|
+
'&': '&',
|
|
53
|
+
'<': '<',
|
|
54
|
+
'>': '>',
|
|
55
|
+
'"': '"', // need the quotes for escaping attribute values
|
|
56
|
+
"'": '''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const esc = (string) => string.replace(/[&<>"']/g, (tag) => escMap[tag])
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* escape HTML and prevent double escaping of '&'
|
|
63
|
+
* @param {string} string - which requires escaping
|
|
64
|
+
* @returns {string} escaped string
|
|
65
|
+
* @example
|
|
66
|
+
* escapeHTML('<h1>"One" & 'Two' & Works</h1>')
|
|
67
|
+
* //> <h1>"One" & 'Two' & Works</h1>
|
|
68
|
+
*/
|
|
69
|
+
export const escHtml = (string) =>
|
|
70
|
+
// @ts-expect-error
|
|
71
|
+
string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string))
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* escape any value for HTML context; objects and functions are stored in the render cache
|
|
75
|
+
* @param {any} any
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
const escValue = (any) => {
|
|
79
|
+
if (any instanceof UnsafeHtml) {
|
|
80
|
+
// @ts-expect-error
|
|
81
|
+
return any
|
|
82
|
+
}
|
|
83
|
+
if (['object', 'function'].includes(typeof any)) {
|
|
84
|
+
const key = globalRenderCache.set(any)
|
|
85
|
+
return unsafeHtml(key)
|
|
86
|
+
}
|
|
87
|
+
return unsafeHtml(esc('' + any))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* template literal to HTML escape all values preventing XSS;
|
|
92
|
+
* arrays will be escaped and joined
|
|
93
|
+
* @param {TemplateStringsArray} strings
|
|
94
|
+
* @param {...any} values
|
|
95
|
+
* @returns {string}
|
|
96
|
+
* @example
|
|
97
|
+
* const list = html`<ul>${['<foo', 'bar>'].map(item => html`<li>${item}</li>`)}</ul>`
|
|
98
|
+
* // '<ul><li><foo</li><li>bar></li></ul>'
|
|
99
|
+
*/
|
|
100
|
+
export const html = (strings, ...values) =>
|
|
101
|
+
unsafeHtml(
|
|
102
|
+
String.raw(
|
|
103
|
+
{ raw: strings },
|
|
104
|
+
...values.map((val) =>
|
|
105
|
+
Array.isArray(val) ? val.map(escValue).join('') : escValue(val)
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* render HTML template into given node with support for special attributes
|
|
112
|
+
*
|
|
113
|
+
* @param {Element} node to append rendered content
|
|
114
|
+
* @param {string|UnsafeHtml} template HTML template string
|
|
115
|
+
* @param {Record<string, Function>|HTMLElement} [handlers={}] event handlers or HTMLElement for method lookup
|
|
116
|
+
* @returns {Record<string, Element>} references collected
|
|
117
|
+
*/
|
|
118
|
+
export function render(node, template, handlers = {}) {
|
|
119
|
+
const refs = {}
|
|
120
|
+
const div = document.createElement('div')
|
|
121
|
+
div.innerHTML = template.toString()
|
|
122
|
+
// don't understand why `for (let child of div.children)` does not work here
|
|
123
|
+
for (let child of Array.from(div.children)) {
|
|
124
|
+
// @ts-expect-error
|
|
125
|
+
renderAttrs(child, handlers, refs)
|
|
126
|
+
node.appendChild(child)
|
|
127
|
+
}
|
|
128
|
+
// @ts-expect-error
|
|
129
|
+
return refs
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Post-processing of rendered nodes to handle special attributes:
|
|
134
|
+
*
|
|
135
|
+
* - `?attr=${boolean}` -> boolean attribute
|
|
136
|
+
* - `.prop=${objectOrAnyValue}` -> property binding for objects or any value
|
|
137
|
+
* - `...=${object}` -> spread properties from object
|
|
138
|
+
* - `@event=${(e) => {}}` -> event listener with templated inline function
|
|
139
|
+
* - `@event="handlerName"` -> event listener using handler name from handlers object
|
|
140
|
+
* - `ref="refName"` -> element reference collected and returned
|
|
141
|
+
*
|
|
142
|
+
* NOTE: For all attributes and event names always use kebab-case. For properties it will be converted to camelCase.
|
|
143
|
+
* Attributes starting with `?`, `@`, or `.` are removed from DOM after processing
|
|
144
|
+
*
|
|
145
|
+
* @param {Element} node to append rendered content
|
|
146
|
+
* @param {Record<string, Function>|HTMLElement} [handlers={}] event handlers or HTMLElement for method lookup
|
|
147
|
+
* @param {Record<string, Element>} [refs={}] collected references
|
|
148
|
+
* @returns {Record<string, Element>} references collected
|
|
149
|
+
*/
|
|
150
|
+
export function renderAttrs(node, handlers = {}, refs = {}) {
|
|
151
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
152
|
+
for (let attr of node.attributes) {
|
|
153
|
+
const startsWith = attr.name[0]
|
|
154
|
+
const name = attr.name.slice(1)
|
|
155
|
+
let rm = 0
|
|
156
|
+
if (startsWith === '?') {
|
|
157
|
+
// boolean attributes
|
|
158
|
+
if (toJson(attr.value)) {
|
|
159
|
+
node.setAttribute(name, '')
|
|
160
|
+
} else {
|
|
161
|
+
node.removeAttribute(name)
|
|
162
|
+
}
|
|
163
|
+
rm = 1
|
|
164
|
+
} else if (attr.name === '...') {
|
|
165
|
+
// spread attribute
|
|
166
|
+
const obj = globalRenderCache.get(attr.value)
|
|
167
|
+
if (obj && typeof obj === 'object') {
|
|
168
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
169
|
+
node[k] = v
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
rm = 1
|
|
173
|
+
} else if (startsWith === '.') {
|
|
174
|
+
// property binding
|
|
175
|
+
node[name] = globalRenderCache.get(attr.value) ?? attr.value
|
|
176
|
+
rm = 1
|
|
177
|
+
} else if (startsWith === '@') {
|
|
178
|
+
// event listener
|
|
179
|
+
const handlerName = attr.value
|
|
180
|
+
const fn = globalRenderCache.get(handlerName)
|
|
181
|
+
if (fn) {
|
|
182
|
+
node.addEventListener(name, (e) => fn(e))
|
|
183
|
+
} else if (typeof handlers[handlerName] === 'function') {
|
|
184
|
+
node.addEventListener(name, (e) => handlers[handlerName](e))
|
|
185
|
+
}
|
|
186
|
+
rm = 1
|
|
187
|
+
} else if (attr.name === 'ref') {
|
|
188
|
+
// element reference - remove as well to prevent collection by other processors
|
|
189
|
+
const refName = attr.value
|
|
190
|
+
refs[refName] = node
|
|
191
|
+
rm = 1
|
|
192
|
+
}
|
|
193
|
+
if (rm) {
|
|
194
|
+
requestAnimationFrame(() => {
|
|
195
|
+
node.removeAttribute(attr.name)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// early abort if no children or custom element
|
|
201
|
+
if (!node.children?.length || customElements.get(node.localName)) {
|
|
202
|
+
return refs
|
|
203
|
+
}
|
|
204
|
+
for (let child of Array.from(node.children)) {
|
|
205
|
+
renderAttrs(child, handlers, refs)
|
|
206
|
+
}
|
|
207
|
+
return refs
|
|
208
|
+
}
|
package/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export {
|
|
|
10
10
|
* @typedef {import('./element.js').HostController} HostController
|
|
11
11
|
*/
|
|
12
12
|
export { MiElement, convertType, define } from './element.js'
|
|
13
|
-
export { unsafeHtml, html, escHtml } from './
|
|
13
|
+
export { unsafeHtml, html, escHtml, render, renderAttrs } from './html.js'
|
|
14
14
|
export { refsBySelector } from './refs.js'
|
|
15
15
|
/**
|
|
16
16
|
* @typedef {import('./store.js').Action} Action
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* convert to number safely
|
|
3
|
+
* @param {*} any
|
|
4
|
+
* @returns {number} number or 0 if NaN
|
|
5
|
+
*/
|
|
6
|
+
export const toNumber = (any) => {
|
|
7
|
+
const n = Number(any)
|
|
8
|
+
return isNaN(n) ? 0 : n
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* safely parse JSON
|
|
13
|
+
* @param {string} any
|
|
14
|
+
* @returns {any|undefined} parsed object or undefined on error
|
|
15
|
+
*/
|
|
16
|
+
export const toJson = (any) => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(any)
|
|
19
|
+
} catch {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
}
|
package/types/html.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render HTML template into given node with support for special attributes
|
|
3
|
+
*
|
|
4
|
+
* @param {Element} node to append rendered content
|
|
5
|
+
* @param {string|UnsafeHtml} template HTML template string
|
|
6
|
+
* @param {Record<string, Function>|HTMLElement} [handlers={}] event handlers or HTMLElement for method lookup
|
|
7
|
+
* @returns {Record<string, Element>} references collected
|
|
8
|
+
*/
|
|
9
|
+
export function render(node: Element, template: string | UnsafeHtml, handlers?: Record<string, Function> | HTMLElement): Record<string, Element>;
|
|
10
|
+
/**
|
|
11
|
+
* Post-processing of rendered nodes to handle special attributes:
|
|
12
|
+
*
|
|
13
|
+
* - `?attr=${boolean}` -> boolean attribute
|
|
14
|
+
* - `.prop=${objectOrAnyValue}` -> property binding for objects or any value
|
|
15
|
+
* - `...=${object}` -> spread properties from object
|
|
16
|
+
* - `@event=${(e) => {}}` -> event listener with templated inline function
|
|
17
|
+
* - `@event="handlerName"` -> event listener using handler name from handlers object
|
|
18
|
+
* - `ref="refName"` -> element reference collected and returned
|
|
19
|
+
*
|
|
20
|
+
* NOTE: For all attributes and event names always use kebab-case. For properties it will be converted to camelCase.
|
|
21
|
+
* Attributes starting with `?`, `@`, or `.` are removed from DOM after processing
|
|
22
|
+
*
|
|
23
|
+
* @param {Element} node to append rendered content
|
|
24
|
+
* @param {Record<string, Function>|HTMLElement} [handlers={}] event handlers or HTMLElement for method lookup
|
|
25
|
+
* @param {Record<string, Element>} [refs={}] collected references
|
|
26
|
+
* @returns {Record<string, Element>} references collected
|
|
27
|
+
*/
|
|
28
|
+
export function renderAttrs(node: Element, handlers?: Record<string, Function> | HTMLElement, refs?: Record<string, Element>): Record<string, Element>;
|
|
29
|
+
export const globalRenderCache: RenderCache;
|
|
30
|
+
export function unsafeHtml(str: string): string;
|
|
31
|
+
export function escHtml(string: string): string;
|
|
32
|
+
export function html(strings: TemplateStringsArray, ...values: any[]): string;
|
|
33
|
+
/**
|
|
34
|
+
* A helper class to avoid double escaping of HTML strings
|
|
35
|
+
*/
|
|
36
|
+
declare class UnsafeHtml extends String {
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A cache for rendering values to avoid keeping them in memory too long
|
|
40
|
+
*/
|
|
41
|
+
declare class RenderCache {
|
|
42
|
+
cnt: number;
|
|
43
|
+
map: Map<any, any>;
|
|
44
|
+
cache: WeakMap<WeakKey, any>;
|
|
45
|
+
_inc(): number;
|
|
46
|
+
clear(): void;
|
|
47
|
+
set(value: any): string;
|
|
48
|
+
get(key: any): any;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
package/types/index.d.ts
CHANGED
|
@@ -6,5 +6,5 @@ export type HostController = import("./element.js").HostController;
|
|
|
6
6
|
export type Action = import("./store.js").Action;
|
|
7
7
|
export { ContextConsumer, ContextProvider, ContextRequestEvent } from "./context.js";
|
|
8
8
|
export { MiElement, convertType, define } from "./element.js";
|
|
9
|
-
export { unsafeHtml, html, escHtml } from "./
|
|
9
|
+
export { unsafeHtml, html, escHtml, render, renderAttrs } from "./html.js";
|
|
10
10
|
export { classNames, styleMap, addGlobalStyles, css } from "./styling.js";
|
package/types/utils.d.ts
ADDED
package/dist/escape.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
class UnsafeHtml extends String {}
|
|
2
|
-
|
|
3
|
-
const unsafeHtml = str => new UnsafeHtml(str), escMap = {
|
|
4
|
-
'&': '&',
|
|
5
|
-
'<': '<',
|
|
6
|
-
'>': '>'
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
let esc = string => string.replace(/&/g, '&').replace(/[&<>]/g, tag => escMap[tag]);
|
|
10
|
-
|
|
11
|
-
'undefined' != typeof document && (esc = string => {
|
|
12
|
-
const div = document.createElement('div');
|
|
13
|
-
return div.textContent = string, div.innerHTML;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const escHtml = string => string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string)), html = (strings, ...values) => unsafeHtml(String.raw({
|
|
17
|
-
raw: strings
|
|
18
|
-
}, ...values.map(val => Array.isArray(val) ? val.map(escHtml).join('') : escHtml(val))));
|
|
19
|
-
|
|
20
|
-
export { escHtml, html, unsafeHtml };
|
package/src/escape.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
class UnsafeHtml extends String {}
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* tag a string as html for not to be escaped
|
|
5
|
-
* @param {string} str
|
|
6
|
-
* @returns {string}
|
|
7
|
-
*/
|
|
8
|
-
// @ts-expect-error
|
|
9
|
-
export const unsafeHtml = (str) => new UnsafeHtml(str)
|
|
10
|
-
|
|
11
|
-
const escMap = {
|
|
12
|
-
'&': '&',
|
|
13
|
-
'<': '<',
|
|
14
|
-
'>': '>'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let esc = (string) => {
|
|
18
|
-
return string.replace(/&/g, '&').replace(/[&<>]/g, (tag) => escMap[tag])
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (typeof document !== 'undefined') {
|
|
22
|
-
// in browser environment, use DOM to escape
|
|
23
|
-
esc = (string) => {
|
|
24
|
-
const div = document.createElement('div')
|
|
25
|
-
div.textContent = string
|
|
26
|
-
return div.innerHTML
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* escape HTML and prevent double escaping of '&'
|
|
32
|
-
* @param {string} string - which requires escaping
|
|
33
|
-
* @returns {string} escaped string
|
|
34
|
-
* @example
|
|
35
|
-
* escapeHTML('<h1>"One" & 'Two' & Works</h1>')
|
|
36
|
-
* //> <h1>"One" & 'Two' & Works</h1>
|
|
37
|
-
*/
|
|
38
|
-
export const escHtml = (string) =>
|
|
39
|
-
// @ts-expect-error
|
|
40
|
-
string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string))
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* template literal to HTML escape all values preventing XSS;
|
|
44
|
-
* arrays will be escaped and joined
|
|
45
|
-
* @param {TemplateStringsArray} strings
|
|
46
|
-
* @param {...any} values
|
|
47
|
-
* @returns {string}
|
|
48
|
-
* @example
|
|
49
|
-
* const list = html`<ul>${['<foo', 'bar>'].map(item => html`<li>${item}</li>`)}</ul>`
|
|
50
|
-
* // '<ul><li><foo</li><li>bar></li></ul>'
|
|
51
|
-
*/
|
|
52
|
-
export const html = (strings, ...values) =>
|
|
53
|
-
unsafeHtml(
|
|
54
|
-
String.raw(
|
|
55
|
-
{ raw: strings },
|
|
56
|
-
...values.map((val) =>
|
|
57
|
-
Array.isArray(val) ? val.map(escHtml).join('') : escHtml(val)
|
|
58
|
-
)
|
|
59
|
-
)
|
|
60
|
-
)
|
package/types/escape.d.ts
DELETED