@oscarpalmer/toretto 0.41.0 → 0.43.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.
Files changed (55) hide show
  1. package/dist/attribute/{get.d.mts → get.attribute.d.mts} +3 -2
  2. package/dist/attribute/{get.mjs → get.attribute.mjs} +4 -3
  3. package/dist/attribute/index.d.mts +3 -16
  4. package/dist/attribute/index.mjs +4 -7
  5. package/dist/attribute/{set.d.mts → set.attribute.d.mts} +4 -4
  6. package/dist/attribute/{set.mjs → set.attribute.mjs} +2 -2
  7. package/dist/create.d.mts +25 -0
  8. package/dist/create.mjs +17 -0
  9. package/dist/data.mjs +7 -7
  10. package/dist/event/delegation.mjs +8 -1
  11. package/dist/html/index.d.mts +23 -26
  12. package/dist/html/index.mjs +85 -18
  13. package/dist/html/sanitize.mjs +6 -5
  14. package/dist/index.d.mts +117 -54
  15. package/dist/index.mjs +541 -380
  16. package/dist/internal/attribute.d.mts +4 -3
  17. package/dist/internal/attribute.mjs +13 -23
  18. package/dist/internal/element-value.d.mts +2 -2
  19. package/dist/internal/element-value.mjs +12 -6
  20. package/dist/internal/get-value.mjs +3 -1
  21. package/dist/internal/property.d.mts +4 -0
  22. package/dist/internal/property.mjs +24 -0
  23. package/dist/property/get.property.d.mts +20 -0
  24. package/dist/property/get.property.mjs +35 -0
  25. package/dist/property/index.d.mts +3 -0
  26. package/dist/property/index.mjs +3 -0
  27. package/dist/property/set.property.d.mts +32 -0
  28. package/dist/property/set.property.mjs +32 -0
  29. package/dist/style.d.mts +16 -9
  30. package/dist/style.mjs +22 -21
  31. package/package.json +14 -6
  32. package/src/attribute/{get.ts → get.attribute.ts} +14 -3
  33. package/src/attribute/index.ts +10 -22
  34. package/src/attribute/{set.ts → set.attribute.ts} +9 -5
  35. package/src/create.ts +81 -0
  36. package/src/data.ts +16 -8
  37. package/src/event/delegation.ts +24 -3
  38. package/src/event/index.ts +9 -3
  39. package/src/find/index.ts +11 -3
  40. package/src/find/relative.ts +4 -0
  41. package/src/focusable.ts +10 -2
  42. package/src/html/index.ts +166 -58
  43. package/src/html/sanitize.ts +14 -11
  44. package/src/index.ts +2 -1
  45. package/src/internal/attribute.ts +23 -42
  46. package/src/internal/element-value.ts +25 -6
  47. package/src/internal/get-value.ts +14 -0
  48. package/src/internal/is.ts +4 -0
  49. package/src/internal/property.ts +42 -0
  50. package/src/is.ts +10 -2
  51. package/src/property/get.property.ts +73 -0
  52. package/src/property/index.ts +2 -0
  53. package/src/property/set.property.ts +103 -0
  54. package/src/style.ts +81 -36
  55. package/src/touch.ts +14 -2
@@ -2,11 +2,13 @@ import {updateAttribute} from '../internal/attribute';
2
2
  import {setElementValue, setElementValues} from '../internal/element-value';
3
3
  import type {Attribute} from '../models';
4
4
 
5
- //
5
+ // #region Types
6
6
 
7
- type DispatchedAttribute = 'checked' | 'open' | 'value';
7
+ export type DispatchedAttributeName = 'checked' | 'open' | 'value';
8
8
 
9
- //
9
+ // #endregion
10
+
11
+ // #region Functions
10
12
 
11
13
  /**
12
14
  * Set an attribute on an element
@@ -17,9 +19,9 @@ type DispatchedAttribute = 'checked' | 'open' | 'value';
17
19
  * @param value Attribute value
18
20
  * @param dispatch Dispatch event for attribute? _(defaults to `true`)_
19
21
  */
20
- export function setAttribute<Name extends DispatchedAttribute>(
22
+ export function setAttribute(
21
23
  element: Element,
22
- name: Name,
24
+ name: DispatchedAttributeName,
23
25
  value?: unknown,
24
26
  dispatch?: boolean,
25
27
  ): void;
@@ -92,3 +94,5 @@ export function setAttributes(
92
94
  ): void {
93
95
  setElementValues(element, attributes, null, dispatch, updateAttribute);
94
96
  }
97
+
98
+ // #endregion
package/src/create.ts ADDED
@@ -0,0 +1,81 @@
1
+ import type {Primitive} from '@oscarpalmer/atoms/models';
2
+ import {setAttributes} from './attribute';
3
+ import {setStyles} from './style';
4
+ import {setProperties} from './property';
5
+
6
+ // #region Types
7
+
8
+ type Properties<Target extends Element> = {
9
+ [Property in keyof Target]?: Target[Property] extends Primitive ? Target[Property] : never;
10
+ };
11
+
12
+ type Styles = Partial<Record<keyof CSSStyleDeclaration, unknown>>;
13
+
14
+ // #endregion
15
+
16
+ // #region Functions
17
+
18
+ /**
19
+ * Creates an HTML element with the specified tag name together with optional properties, attributes, and styles
20
+ * @param tag Tag name
21
+ * @param properties Element properties
22
+ * @param attributes Element attributes
23
+ * @param styles Element styles
24
+ * @returns Created element
25
+ */
26
+ export function createElement<TagName extends keyof HTMLElementTagNameMap>(
27
+ tag: TagName,
28
+ properties?: Properties<HTMLElementTagNameMap[TagName]>,
29
+ attributes?: Record<string, unknown>,
30
+ styles?: Styles,
31
+ ): HTMLElementTagNameMap[TagName];
32
+
33
+ /**
34
+ * Creates an HTML element with the specified tag name together with optional properties, attributes, and styles
35
+ * @param tag Tag name
36
+ * @param properties Element properties
37
+ * @param attributes Element attributes
38
+ * @param styles Element styles
39
+ * @returns Created element
40
+ */
41
+ export function createElement(
42
+ tag: string,
43
+ properties?: Properties<HTMLElement>,
44
+ attributes?: Record<string, unknown>,
45
+ styles?: Styles,
46
+ ): HTMLUnknownElement;
47
+
48
+ export function createElement(
49
+ tag: string,
50
+ properties?: Properties<HTMLElement>,
51
+ attributes?: Record<string, unknown>,
52
+ styles?: Styles,
53
+ ): HTMLElement {
54
+ if (typeof tag !== 'string') {
55
+ throw new TypeError(MESSAGE);
56
+ }
57
+
58
+ const element = document.createElement(tag);
59
+
60
+ if (properties != null) {
61
+ setProperties(element, properties);
62
+ }
63
+
64
+ if (attributes != null) {
65
+ setAttributes(element, attributes);
66
+ }
67
+
68
+ if (styles != null) {
69
+ setStyles(element, styles);
70
+ }
71
+
72
+ return element;
73
+ }
74
+
75
+ // #endregion
76
+
77
+ // #region Variables
78
+
79
+ const MESSAGE = 'Tag name must be a string';
80
+
81
+ // #endregion
package/src/data.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type {PlainObject} from '@oscarpalmer/atoms/models';
2
2
  import {parse} from '@oscarpalmer/atoms/string';
3
- import {kebabCase} from '@oscarpalmer/atoms/string/case';
3
+ import {camelCase, kebabCase} from '@oscarpalmer/atoms/string/case';
4
4
  import {setElementValues, updateElementValue} from './internal/element-value';
5
5
  import {EXPRESSION_DATA_PREFIX} from './internal/get-value';
6
6
  import {isHTMLOrSVGElement} from './internal/is';
7
7
 
8
+ // #region Functions
9
+
8
10
  /**
9
11
  * Get a keyed data value from an element
10
12
  * @param element Element to get data from
@@ -32,16 +34,16 @@ export function getData(element: Element, keys: string | string[], parseValues?:
32
34
  return;
33
35
  }
34
36
 
35
- const shouldParse = parseValues !== false;
37
+ const noParse = parseValues === false;
36
38
 
37
39
  if (typeof keys === 'string') {
38
- const value = element.dataset[keys];
40
+ const value = element.dataset[camelCase(keys)];
39
41
 
40
42
  if (value === undefined) {
41
43
  return;
42
44
  }
43
45
 
44
- return shouldParse ? parse(value) : value;
46
+ return noParse ? value : parse(value);
45
47
  }
46
48
 
47
49
  const {length} = keys;
@@ -50,12 +52,12 @@ export function getData(element: Element, keys: string | string[], parseValues?:
50
52
 
51
53
  for (let index = 0; index < length; index += 1) {
52
54
  const key = keys[index];
53
- const value = element.dataset[key];
55
+ const value = element.dataset[camelCase(key)];
54
56
 
55
57
  if (value == null) {
56
58
  data[key] = undefined;
57
59
  } else {
58
- data[key] = shouldParse ? parse(value) : value;
60
+ data[key] = noParse ? value : parse(value);
59
61
  }
60
62
  }
61
63
 
@@ -63,7 +65,7 @@ export function getData(element: Element, keys: string | string[], parseValues?:
63
65
  }
64
66
 
65
67
  function getName(original: string): string {
66
- return `${ATTRIBUTE_DATA_PREFIX}${kebabCase(original).replace(EXPRESSION_DATA_PREFIX, '')}`;
68
+ return `${ATTRIBUTE_DATA_PREFIX}${kebabCase(original.replace(EXPRESSION_DATA_PREFIX, ''))}`;
67
69
  }
68
70
 
69
71
  /**
@@ -90,13 +92,19 @@ function updateDataAttribute(element: Element, key: string, value: unknown): voi
90
92
  element,
91
93
  getName(key),
92
94
  value,
95
+ // oxlint-disable-next-line typescript/unbound-method: using .call in `updateElementValue`
93
96
  element.setAttribute,
97
+ // oxlint-disable-next-line typescript/unbound-method: using .call in `updateElementValue`
94
98
  element.removeAttribute,
95
99
  false,
96
100
  true,
97
101
  );
98
102
  }
99
103
 
100
- //
104
+ // #endregion
105
+
106
+ // #region Variables
101
107
 
102
108
  const ATTRIBUTE_DATA_PREFIX = 'data-';
109
+
110
+ // #endregion
@@ -1,13 +1,17 @@
1
1
  import {isEventTarget} from '../internal/is';
2
2
  import type {CustomEventListener, RemovableEventListener} from '../models';
3
3
 
4
- //
4
+ // #region Types
5
5
 
6
6
  export type EventTargetWithListeners = EventTarget &
7
7
  Partial<{
8
8
  [key: string]: Set<EventListener | CustomEventListener>;
9
9
  }>;
10
10
 
11
+ // #endregion
12
+
13
+ // #region Functions
14
+
11
15
  function addDelegatedHandler(doc: Document, type: string, name: string, passive: boolean): void {
12
16
  if (DELEGATED.has(name)) {
13
17
  return;
@@ -44,8 +48,21 @@ function delegatedEventHandler(this: boolean, event: Event): void {
44
48
  const items = event.composedPath();
45
49
  const {length} = items;
46
50
 
51
+ let cancelled = false;
47
52
  let target = items[0];
48
53
 
54
+ // oxlint-disable-next-line typescript/unbound-method: using `.call` to ensure correct `this` context
55
+ const originalStopPropagation = event.stopPropagation;
56
+
57
+ event.stopPropagation = function () {
58
+ cancelled = true;
59
+
60
+ originalStopPropagation.call(event);
61
+ };
62
+
63
+ // Event is one and the same for all listeners, so stopping propagation should work the same, regardless of stop type
64
+ event.stopImmediatePropagation = event.stopPropagation.bind(event);
65
+
49
66
  Object.defineProperties(event, {
50
67
  currentTarget: {
51
68
  configurable: true,
@@ -72,7 +89,7 @@ function delegatedEventHandler(this: boolean, event: Event): void {
72
89
  for (const listener of listeners) {
73
90
  (listener as EventListener).call(item, event);
74
91
 
75
- if (event.cancelBubble) {
92
+ if (cancelled) {
76
93
  return;
77
94
  }
78
95
  }
@@ -115,7 +132,9 @@ export function removeDelegatedListener(
115
132
  return true;
116
133
  }
117
134
 
118
- //
135
+ // #endregion
136
+
137
+ // #region Variables
119
138
 
120
139
  const DELEGATED = new Set<string>();
121
140
 
@@ -153,3 +172,5 @@ const EVENT_TYPES: Set<string> = new Set([
153
172
  const HANDLER_ACTIVE = delegatedEventHandler.bind(false);
154
173
 
155
174
  const HANDLER_PASSIVE = delegatedEventHandler.bind(true);
175
+
176
+ // #endregion
@@ -10,7 +10,7 @@ import {
10
10
  removeDelegatedListener,
11
11
  } from './delegation';
12
12
 
13
- //
13
+ // #region Types
14
14
 
15
15
  type EventOptions = {
16
16
  capture: boolean;
@@ -19,7 +19,9 @@ type EventOptions = {
19
19
  signal?: AbortSignal;
20
20
  };
21
21
 
22
- //
22
+ // #endregion
23
+
24
+ // #region Functions
23
25
 
24
26
  function createDispatchOptions(options: EventInit): EventInit {
25
27
  return {
@@ -219,6 +221,10 @@ export function on(
219
221
  };
220
222
  }
221
223
 
222
- //
224
+ // #endregion
225
+
226
+ // #region Variables
223
227
 
224
228
  const PROPERTY_DETAIL = 'detail';
229
+
230
+ // #endregion
package/src/find/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type {PlainObject} from '@oscarpalmer/atoms/models';
2
2
  import type {Selector} from '../models';
3
3
 
4
+ // #region Functions
5
+
4
6
  /**
5
7
  * Find the first element that matches the tag name
6
8
  * @param tagName Tag name of element to find
@@ -180,7 +182,9 @@ function isContext(value: unknown): boolean {
180
182
  );
181
183
  }
182
184
 
183
- //
185
+ // #endregion
186
+
187
+ // #region Variables
184
188
 
185
189
  const QUERY_SELECTOR_ALL = 'querySelectorAll';
186
190
 
@@ -194,7 +198,11 @@ const SUFFIX_HOVER = ':hover';
194
198
 
195
199
  const TAG_HEAD = 'HEAD';
196
200
 
197
- //
201
+ // #endregion
202
+
203
+ // #region Exports
198
204
 
199
- export {findElement as $, findElements as $$};
200
205
  export {findAncestor, findRelatives, getDistance} from './relative';
206
+ export {findElement as $, findElements as $$};
207
+
208
+ // #endregion
@@ -1,3 +1,5 @@
1
+ // #region Functions
2
+
1
3
  /**
2
4
  * Find the closest ancestor element that matches the tag name
3
5
  *
@@ -211,3 +213,5 @@ function traverse(from: Element, to: Element): number | undefined {
211
213
  parent = parent.parentElement;
212
214
  }
213
215
  }
216
+
217
+ // #endregion
package/src/focusable.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  // Based on https://github.com/focus-trap/tabbable :-)
2
2
 
3
+ // #region Types
4
+
3
5
  type ElementWithTabIndex = {
4
6
  element: Element;
5
7
  tabIndex: number;
@@ -9,7 +11,9 @@ type Filter = (item: ElementWithTabIndex) => boolean;
9
11
 
10
12
  type InertElement = Element & {inert: boolean};
11
13
 
12
- //
14
+ // #endregion
15
+
16
+ // #region Functions
13
17
 
14
18
  /**
15
19
  * Get a list of focusable elements within a parent element
@@ -230,7 +234,9 @@ function isValidElement(element: Element, filters: Filter[], tabbable: boolean):
230
234
  return !filters.some(filter => filter(item));
231
235
  }
232
236
 
233
- //
237
+ // #endregion
238
+
239
+ // #region Variables
234
240
 
235
241
  const ATTRIBUTE_CONTENTEDITABLE = 'contenteditable';
236
242
 
@@ -294,3 +300,5 @@ const TABINDEX_BASE = 0;
294
300
  const TABINDEX_DEFAULT = -1;
295
301
 
296
302
  const TYPE_RADIO = 'radio';
303
+
304
+ // #endregion
package/src/html/index.ts CHANGED
@@ -1,36 +1,9 @@
1
1
  import {isPlainObject} from '@oscarpalmer/atoms/is';
2
+ import {SizedMap} from '@oscarpalmer/atoms/sized/map';
3
+ import {getString} from '@oscarpalmer/atoms/string';
2
4
  import {sanitizeNodes} from './sanitize';
3
5
 
4
- //
5
-
6
- type Html = {
7
- /**
8
- * Create nodes from an HTML string or a template element
9
- * @param value HTML string or id for a template element
10
- * @param options Options for creating nodes
11
- * @returns Created nodes
12
- */
13
- (value: string, options?: HtmlOptions): Node[];
14
-
15
- /**
16
- * Create nodes from a template element
17
- * @param template Template element
18
- * @param options Options for creating nodes
19
- * @returns Created nodes
20
- */
21
- (template: HTMLTemplateElement, options?: HtmlOptions): Node[];
22
-
23
- /**
24
- * Clear cache of template elements
25
- */
26
- clear(): void;
27
-
28
- /**
29
- * Remove cached template element for an HTML string or id
30
- * @param template HTML string or id for a template element
31
- */
32
- remove(template: string): void;
33
- };
6
+ // #region Types
34
7
 
35
8
  type HtmlOptions = {
36
9
  /**
@@ -41,7 +14,14 @@ type HtmlOptions = {
41
14
 
42
15
  type Options = Required<HtmlOptions>;
43
16
 
44
- //
17
+ type Tagged = {
18
+ nodes: Node[];
19
+ template: string;
20
+ };
21
+
22
+ // #endregion
23
+
24
+ // #region Functions
45
25
 
46
26
  function createHtml(value: string | HTMLTemplateElement): string {
47
27
  const parsed = getParser().parseFromString(getHtml(value), PARSE_TYPE_HTML);
@@ -62,27 +42,41 @@ function createTemplate(
62
42
  template.innerHTML = createHtml(value);
63
43
 
64
44
  if (typeof value === 'string' && options.cache) {
65
- templates[value] = template;
45
+ templates.set(value, template);
66
46
  }
67
47
 
68
48
  return template;
69
49
  }
70
50
 
51
+ function getComment(index: number): string {
52
+ return COMMENT_TEMPLATE.replace(COMMENT_INDEX, String(index));
53
+ }
54
+
71
55
  function getHtml(value: string | HTMLTemplateElement): string {
72
56
  return `${TEMPORARY_ELEMENT}${typeof value === 'string' ? value : value.innerHTML}${TEMPORARY_ELEMENT}`;
73
57
  }
74
58
 
75
- function getNodes(value: string | HTMLTemplateElement, options: Options): Node[] {
59
+ function getNodes(value: unknown, options: Options, nodes?: Node[]): Node[] {
76
60
  if (typeof value !== 'string' && !(value instanceof HTMLTemplateElement)) {
77
61
  return [];
78
62
  }
79
63
 
80
64
  const template = getTemplate(value, options);
81
65
 
82
- return template == null ? [] : [...template.content.cloneNode(true).childNodes];
66
+ if (template == null) {
67
+ return [];
68
+ }
69
+
70
+ const cloned = [...template.content.cloneNode(true).childNodes];
71
+
72
+ if (nodes != null) {
73
+ replaceComments(cloned, nodes);
74
+ }
75
+
76
+ return cloned;
83
77
  }
84
78
 
85
- function getOptions(input?: HtmlOptions): Options {
79
+ function getOptions(input?: unknown): Options {
86
80
  const options = isPlainObject(input) ? input : {};
87
81
 
88
82
  options.cache = typeof options.cache === 'boolean' ? options.cache : true;
@@ -96,6 +90,58 @@ function getParser(): DOMParser {
96
90
  return parser;
97
91
  }
98
92
 
93
+ function getTagged(strings: TemplateStringsArray, values: unknown[]): Tagged {
94
+ const tagged: Tagged = {
95
+ nodes: [],
96
+ template: '',
97
+ };
98
+
99
+ const stringsLength = strings.length;
100
+
101
+ let nodeIndex = 0;
102
+
103
+ for (let stringIndex = 0; stringIndex < stringsLength; stringIndex += 1) {
104
+ const value = values[stringIndex];
105
+
106
+ tagged.template += strings[stringIndex];
107
+
108
+ if (value instanceof Node) {
109
+ tagged.nodes.push(value);
110
+
111
+ tagged.template += getComment(nodeIndex);
112
+
113
+ nodeIndex += 1;
114
+ } else if (hasNodes(value)) {
115
+ const items = [...value];
116
+ const itemsLength = items.length;
117
+
118
+ for (let itemIndex = 0; itemIndex < itemsLength; itemIndex += 1) {
119
+ const item = items[itemIndex];
120
+
121
+ if (item instanceof Node) {
122
+ tagged.nodes.push(item);
123
+
124
+ tagged.template += getComment(nodeIndex);
125
+
126
+ nodeIndex += 1;
127
+ } else {
128
+ tagged.template += getString(item);
129
+ }
130
+ }
131
+ } else if (Array.isArray(value)) {
132
+ const valueLength = value.length;
133
+
134
+ for (let valueIndex = 0; valueIndex < valueLength; valueIndex += 1) {
135
+ tagged.template += getString(value[valueIndex]);
136
+ }
137
+ } else {
138
+ tagged.template += getString(value);
139
+ }
140
+ }
141
+
142
+ return tagged;
143
+ }
144
+
99
145
  function getTemplate(
100
146
  value: string | HTMLTemplateElement,
101
147
  options: Options,
@@ -108,7 +154,7 @@ function getTemplate(
108
154
  return;
109
155
  }
110
156
 
111
- let template = templates[value];
157
+ let template = templates.get(value);
112
158
 
113
159
  if (template != null) {
114
160
  return template;
@@ -119,34 +165,87 @@ function getTemplate(
119
165
  return createTemplate(element instanceof HTMLTemplateElement ? element : value, options);
120
166
  }
121
167
 
122
- const html = ((value: string | HTMLTemplateElement, options?: Options): Node[] => {
123
- return getNodes(value, getOptions(options));
124
- }) as Html;
168
+ function hasNodes(value: unknown): value is HTMLCollection | NodeList | Node[] {
169
+ if (value instanceof HTMLCollection || value instanceof NodeList) {
170
+ return true;
171
+ }
172
+
173
+ return Array.isArray(value) && value.some(item => item instanceof Node);
174
+ }
175
+
176
+ /**
177
+ * Create nodes from a template string
178
+ * @returns Created nodes
179
+ */
180
+ export function html(strings: TemplateStringsArray, ...values: unknown[]): Node[];
181
+
182
+ /**
183
+ * Create nodes from an HTML string or a template element
184
+ * @param value HTML string or id for a template element
185
+ * @param options Options for creating nodes
186
+ * @returns Created nodes
187
+ */
188
+ export function html(value: string, options?: HtmlOptions): Node[];
189
+
190
+ /**
191
+ * Create nodes from a template element
192
+ * @param template Template element
193
+ * @param options Options for creating nodes
194
+ * @returns Created nodes
195
+ */
196
+ export function html(template: HTMLTemplateElement, options?: HtmlOptions): Node[];
197
+
198
+ export function html(first: unknown, ...second: unknown[]): Node[] {
199
+ if (isTagged(first)) {
200
+ const tagged = getTagged(first, second);
201
+
202
+ return getNodes(tagged.template, getOptions(), tagged.nodes);
203
+ }
204
+
205
+ return getNodes(first, getOptions(second[0]));
206
+ }
125
207
 
208
+ /**
209
+ * Clear cache of template elements
210
+ */
126
211
  html.clear = (): void => {
127
- templates = {};
212
+ templates.clear();
128
213
  };
129
214
 
215
+ /**
216
+ * Remove cached template element for an HTML string or id
217
+ * @param template HTML string or id for a template element
218
+ */
130
219
  html.remove = (template: string): void => {
131
- if (typeof template !== 'string' || templates[template] == null) {
132
- return;
133
- }
220
+ templates.delete(template);
221
+ };
134
222
 
135
- const keys = Object.keys(templates);
136
- const {length} = keys;
223
+ function isTagged(value: unknown): value is TemplateStringsArray {
224
+ return Array.isArray(value) && Array.isArray((value as unknown as TemplateStringsArray).raw);
225
+ }
226
+
227
+ function replaceComments(origin: NodeList | Node[], replacements: Node[]): void {
228
+ const nodes = [...origin];
229
+ const {length} = nodes;
230
+
231
+ for (let nodeIndex = 0; nodeIndex < length; nodeIndex += 1) {
232
+ const node = nodes[nodeIndex];
137
233
 
138
- const updated: Record<string, HTMLTemplateElement> = {};
234
+ if (node instanceof Comment) {
235
+ const [, index] = EXPRESSION_COMMENT.exec(node.textContent) ?? [];
139
236
 
140
- for (let index = 0; index < length; index += 1) {
141
- const key = keys[index];
237
+ if (index != null) {
238
+ node.replaceWith(replacements[Number(index)]);
239
+ }
142
240
 
143
- if (key !== template) {
144
- updated[key] = templates[key];
241
+ continue;
145
242
  }
146
- }
147
243
 
148
- templates = updated;
149
- };
244
+ if (node.hasChildNodes()) {
245
+ replaceComments(node.childNodes, replacements);
246
+ }
247
+ }
248
+ }
150
249
 
151
250
  /**
152
251
  * Sanitize one or more nodes, recursively
@@ -158,7 +257,15 @@ export function sanitize(value: Node | Node[]): Node[] {
158
257
  return sanitizeNodes(Array.isArray(value) ? value : [value], 0);
159
258
  }
160
259
 
161
- //
260
+ // #endregion
261
+
262
+ // #region Variables
263
+
264
+ const COMMENT_INDEX = '<index>';
265
+
266
+ const COMMENT_TEMPLATE = `<!--toretto.node:${COMMENT_INDEX}-->`;
267
+
268
+ const EXPRESSION_COMMENT = /^toretto\.node:(\d+)$/;
162
269
 
163
270
  const EXPRESSION_ID = /^[a-z][\w-]*$/i;
164
271
 
@@ -168,10 +275,11 @@ const TEMPLATE_TAG = 'template';
168
275
 
169
276
  const TEMPORARY_ELEMENT = '<toretto-temporary></toretto-temporary>';
170
277
 
171
- let parser: DOMParser;
278
+ const templates = new SizedMap<string, HTMLTemplateElement>(128);
172
279
 
173
- let templates: Record<string, HTMLTemplateElement> = {};
280
+ let parser: DOMParser;
174
281
 
175
- //
282
+ // @ts-expect-error debug
283
+ window.templates = templates;
176
284
 
177
- export {html};
285
+ // #endregion