@gesslar/toolkit 1.0.3 → 1.2.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/package.json +1 -1
- package/src/browser/index.js +3 -0
- package/src/browser/lib/Disposable.js +73 -0
- package/src/browser/lib/HTML.js +137 -0
- package/src/browser/lib/Notify.js +89 -0
- package/src/browser/lib/vendor/dompurify.esm.js +1515 -0
- package/src/index.js +1 -0
- package/src/lib/Contract.js +2 -2
- package/src/types/browser/index.d.ts +3 -0
- package/src/types/browser/lib/Base.d.ts +45 -0
- package/src/types/browser/lib/Base.d.ts.map +1 -0
- package/src/types/browser/lib/Disposable.d.ts +33 -0
- package/src/types/browser/lib/Disposable.d.ts.map +1 -0
- package/src/types/browser/lib/HTML.d.ts +38 -0
- package/src/types/browser/lib/HTML.d.ts.map +1 -0
- package/src/types/browser/lib/Notify.d.ts +60 -0
- package/src/types/browser/lib/Notify.d.ts.map +1 -0
- package/src/types/browser/lib/vendor/dompurify.esm.d.ts +29 -0
- package/src/types/browser/lib/vendor/dompurify.esm.d.ts.map +1 -0
- package/src/types/index.d.ts +1 -0
- package/src/types/lib/Contract.d.ts +4 -4
- package/src/types/lib/Contract.d.ts.map +1 -1
- package/src/types/lib/Disposable.d.ts +33 -0
- package/src/types/lib/Disposable.d.ts.map +1 -0
package/package.json
CHANGED
package/src/browser/index.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
export {default as Collection} from "./lib/Collection.js"
|
|
5
5
|
export {default as Data} from "./lib/Data.js"
|
|
6
|
+
export {default as Disposable} from "./lib/Disposable.js"
|
|
7
|
+
export {default as HTML} from "./lib/HTML.js"
|
|
8
|
+
export {default as Notify} from "./lib/Notify.js"
|
|
6
9
|
export {default as Sass} from "./lib/Sass.js"
|
|
7
10
|
export {default as Tantrum} from "./lib/Tantrum.js"
|
|
8
11
|
export {default as Type} from "./lib/TypeSpec.js"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple lifecycle helper that tracks disposer callbacks.
|
|
3
|
+
* Register any teardown functions and call dispose() to run them in reverse.
|
|
4
|
+
*/
|
|
5
|
+
export default class Disposable {
|
|
6
|
+
#disposers = []
|
|
7
|
+
#disposed = false
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registers a disposer callback to be executed when disposed.
|
|
11
|
+
*
|
|
12
|
+
* @param {() => void} disposer - Cleanup callback.
|
|
13
|
+
* @returns {() => void} Function to unregister the disposer.
|
|
14
|
+
*/
|
|
15
|
+
registerDisposer(disposer) {
|
|
16
|
+
if(this.#disposed || typeof disposer !== "function")
|
|
17
|
+
return () => {}
|
|
18
|
+
|
|
19
|
+
this.#disposers.push(disposer)
|
|
20
|
+
|
|
21
|
+
return () => this.#removeDisposer(disposer)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Runs all registered disposers in reverse order.
|
|
26
|
+
*
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
29
|
+
dispose() {
|
|
30
|
+
if(this.#disposed)
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
this.#disposed = true
|
|
34
|
+
|
|
35
|
+
const errors = []
|
|
36
|
+
this.#disposers.toReversed().forEach(disposer => {
|
|
37
|
+
try {
|
|
38
|
+
disposer()
|
|
39
|
+
} catch(error) {
|
|
40
|
+
errors.push(error)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
this.#disposers.length = 0
|
|
44
|
+
|
|
45
|
+
if(errors.length > 0)
|
|
46
|
+
throw new AggregateError(errors, "Errors occurred during disposal.")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether disposal has run.
|
|
51
|
+
*
|
|
52
|
+
* @returns {boolean} True when dispose() has already been called.
|
|
53
|
+
*/
|
|
54
|
+
get disposed() {
|
|
55
|
+
return this.#disposed
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read-only list of registered disposers.
|
|
60
|
+
*
|
|
61
|
+
* @returns {Array<() => void>} Snapshot of disposer callbacks.
|
|
62
|
+
*/
|
|
63
|
+
get disposers() {
|
|
64
|
+
return Object.freeze([...this.#disposers])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#removeDisposer(disposer) {
|
|
68
|
+
const index = this.#disposers.indexOf(disposer)
|
|
69
|
+
|
|
70
|
+
if(index >= 0)
|
|
71
|
+
this.#disposers.splice(index, 1)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import DOMPurify from "./vendor/dompurify.esm.js"
|
|
2
|
+
import Sass from "./Sass.js"
|
|
3
|
+
|
|
4
|
+
export default class HTML {
|
|
5
|
+
#domPurify
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Lightweight HTML helper utilities for browser contexts.
|
|
9
|
+
*
|
|
10
|
+
* @param {object|(() => unknown)} domPurify - Optional DOMPurify instance or factory.
|
|
11
|
+
*/
|
|
12
|
+
constructor(domPurify=DOMPurify) {
|
|
13
|
+
this.#domPurify = domPurify
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetches an HTML fragment and returns the contents inside the <body> tag when present.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} url - Location of the HTML resource to load.
|
|
20
|
+
* @param {boolean} filterBodyContent - If true, returns only content found between the <body> tags. Defaults to false.
|
|
21
|
+
* @returns {Promise<string>} Sanitized HTML string or empty string on missing content.
|
|
22
|
+
*/
|
|
23
|
+
async loadHTML(url, filterBodyContent=false) {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(url)
|
|
26
|
+
const html = await response?.text()
|
|
27
|
+
|
|
28
|
+
if(!html)
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
const {body} = /<body[^>]*>(?<body>[\s\S]*?)<\/body>/i.exec(html)?.groups ?? {}
|
|
32
|
+
|
|
33
|
+
if(filterBodyContent)
|
|
34
|
+
return body ?? html
|
|
35
|
+
|
|
36
|
+
return html
|
|
37
|
+
} catch(error) {
|
|
38
|
+
throw Sass.new(`Loading HTML from '${url}'.`, error)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sanitizes arbitrary HTML using DOMPurify.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} text - HTML string to sanitize. Defaults to "".
|
|
46
|
+
* @returns {string} Sanitized HTML.
|
|
47
|
+
*/
|
|
48
|
+
sanitise(text="") {
|
|
49
|
+
const sanitizer = this.#resolveSanitizer()
|
|
50
|
+
|
|
51
|
+
return sanitizer(String(text ?? ""))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sanitizes an HTML string and replaces the element's children with the result.
|
|
56
|
+
*
|
|
57
|
+
* @param {Element} element - Target element to replace content within.
|
|
58
|
+
* @param {string} htmlString - HTML string to sanitize and insert.
|
|
59
|
+
*/
|
|
60
|
+
setHTMLContent(element, htmlString) {
|
|
61
|
+
if(!element)
|
|
62
|
+
throw Sass.new("setHTMLContent requires a valid element.")
|
|
63
|
+
|
|
64
|
+
const sanitised = this.sanitise(htmlString)
|
|
65
|
+
const doc = element.ownerDocument ?? globalThis.document
|
|
66
|
+
|
|
67
|
+
if(doc?.createRange && typeof element.replaceChildren === "function") {
|
|
68
|
+
const range = doc.createRange()
|
|
69
|
+
const fragment = range.createContextualFragment(sanitised)
|
|
70
|
+
|
|
71
|
+
element.replaceChildren(fragment)
|
|
72
|
+
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if("innerHTML" in element) {
|
|
77
|
+
element.innerHTML = sanitised
|
|
78
|
+
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if(typeof element.replaceChildren === "function") {
|
|
83
|
+
element.replaceChildren(sanitised)
|
|
84
|
+
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw Sass.new("Unable to set HTML content: unsupported element.")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Removes all child nodes from the given element.
|
|
93
|
+
*
|
|
94
|
+
* @param {Element} element - Element to clear.
|
|
95
|
+
*/
|
|
96
|
+
clearHTMLContent(element) {
|
|
97
|
+
if(!element)
|
|
98
|
+
throw Sass.new("clearHTMLContent requires a valid element.")
|
|
99
|
+
|
|
100
|
+
if(typeof element.replaceChildren === "function") {
|
|
101
|
+
element.replaceChildren()
|
|
102
|
+
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if("innerHTML" in element) {
|
|
107
|
+
element.innerHTML = ""
|
|
108
|
+
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw Sass.new("Unable to clear HTML content: unsupported element.")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolves the DOMPurify sanitize function.
|
|
117
|
+
*
|
|
118
|
+
* @returns {(input: string) => string} Sanitizer function.
|
|
119
|
+
*/
|
|
120
|
+
#resolveSanitizer() {
|
|
121
|
+
if(this.#domPurify?.sanitize)
|
|
122
|
+
return this.#domPurify.sanitize
|
|
123
|
+
|
|
124
|
+
if(typeof this.#domPurify === "function") {
|
|
125
|
+
try {
|
|
126
|
+
const configured = this.#domPurify(globalThis.window ?? globalThis)
|
|
127
|
+
|
|
128
|
+
if(configured?.sanitize)
|
|
129
|
+
return configured.sanitize
|
|
130
|
+
} catch(error) {
|
|
131
|
+
throw Sass.new("DOMPurify sanitization is unavailable in this environment.", error)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw Sass.new("DOMPurify sanitization is unavailable in this environment.")
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrapper around `window` event handling to centralize emit/on/off
|
|
3
|
+
* helpers. Used to dispatch simple CustomEvents and manage listeners in one
|
|
4
|
+
* place.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} NotifyEventOptions
|
|
9
|
+
* @property {boolean} [bubbles] - Whether the event bubbles up the DOM tree.
|
|
10
|
+
* @property {boolean} [cancelable] - Whether the event can be canceled.
|
|
11
|
+
* @property {boolean} [composed] - Whether the event can cross the shadow DOM boundary.
|
|
12
|
+
*/
|
|
13
|
+
export default new class Notify {
|
|
14
|
+
/** @type {string} Display name for debugging. */
|
|
15
|
+
name = "Notify"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Emits a CustomEvent without expecting a return value.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} type - Event name to dispatch.
|
|
21
|
+
* @param {unknown} [payload] - Value assigned to `event.detail`.
|
|
22
|
+
* @param {boolean | NotifyEventOptions} [options] - CustomEvent options or boolean to set `bubbles`.
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
emit(type, payload=undefined, options=undefined) {
|
|
26
|
+
const evt = new CustomEvent(type, this.#buildEventInit(payload, options))
|
|
27
|
+
window.dispatchEvent(evt)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Emits a CustomEvent and returns the detail for simple request/response flows.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} type - Event name to dispatch.
|
|
34
|
+
* @param {unknown} [payload] - Value assigned to `event.detail`.
|
|
35
|
+
* @param {boolean | NotifyEventOptions} [options] - CustomEvent options or boolean to set `bubbles`.
|
|
36
|
+
* @returns {unknown} The detail placed on the CustomEvent.
|
|
37
|
+
*/
|
|
38
|
+
request(type, payload={}, options=undefined) {
|
|
39
|
+
const evt = new CustomEvent(type, this.#buildEventInit(payload, options))
|
|
40
|
+
window.dispatchEvent(evt)
|
|
41
|
+
|
|
42
|
+
return evt.detail
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers a listener for the given event type on an HTMLElement (or
|
|
47
|
+
* window, if not specified).
|
|
48
|
+
*
|
|
49
|
+
* @param {string} type - Event name to listen for.
|
|
50
|
+
* @param {(evt: Notify) => void} handler - Listener callback.
|
|
51
|
+
* @param {HTMLElement | Window} [element] - The object to which to attach the handler. Default is window.
|
|
52
|
+
* @param {boolean | object} [options] - Options to pass to addEventListener.
|
|
53
|
+
* @returns {() => void} Dispose function to unregister the handler.
|
|
54
|
+
*/
|
|
55
|
+
on(type, handler, element=window, options=undefined) {
|
|
56
|
+
if(!(typeof type === "string" && type))
|
|
57
|
+
throw new Error("No event 'type' specified to listen for.")
|
|
58
|
+
|
|
59
|
+
if(typeof handler !== "function")
|
|
60
|
+
throw new Error("No handler function specified.")
|
|
61
|
+
|
|
62
|
+
element.addEventListener(type, handler, options)
|
|
63
|
+
|
|
64
|
+
return () => this.off(type, handler, element, options)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Removes a previously registered listener for the given event type.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} type - Event name to remove.
|
|
71
|
+
* @param {(evt: Notify) => void} handler - Listener callback to detach.
|
|
72
|
+
* @param {HTMLElement | Window} [element] - The object from which to remove the handler. Default is window.
|
|
73
|
+
* @param {boolean | object} [options] - Options to pass to removeEventListener.
|
|
74
|
+
* @returns {void}
|
|
75
|
+
*/
|
|
76
|
+
off(type, handler, element=window, options=undefined) {
|
|
77
|
+
element.removeEventListener(type, handler, options)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#buildEventInit(detail, options) {
|
|
81
|
+
if(typeof options === "boolean")
|
|
82
|
+
return {detail, bubbles: options}
|
|
83
|
+
|
|
84
|
+
if(typeof options === "object" && options !== null)
|
|
85
|
+
return {detail, ...options}
|
|
86
|
+
|
|
87
|
+
return {detail}
|
|
88
|
+
}
|
|
89
|
+
}
|