@linxiraos/pi-utils 1.1.26 → 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/CHANGELOG.md +6 -4
- package/README.md +1 -1
- package/dist/types/ar/index.d.ts +1 -0
- package/dist/types/ar/zip-package.d.ts +21 -0
- package/dist/types/color.d.ts +2 -0
- package/dist/types/dirs.d.ts +37 -12
- package/dist/types/docx/converter.d.ts +2 -0
- package/dist/types/dom/core.d.ts +15 -4
- package/dist/types/dom/parser.d.ts +5 -3
- package/dist/types/dom/selector.d.ts +7 -1
- package/dist/types/logger/rotating-file.d.ts +4 -2
- package/dist/types/loop-phase.d.ts +12 -7
- package/dist/types/marked/core.d.ts +7 -6
- package/dist/types/marked-list.d.ts +38 -0
- package/dist/types/math-delimiters.d.ts +48 -0
- package/dist/types/procmgr.d.ts +14 -20
- package/dist/types/readability/types.d.ts +2 -0
- package/dist/types/temp.d.ts +7 -0
- package/dist/types/turndown/types.d.ts +7 -0
- package/dist/types/vterm/buffer.d.ts +16 -5
- package/dist/types/vterm/query-responder.d.ts +15 -0
- package/package.json +2 -2
- package/src/ar/index.ts +1 -0
- package/src/ar/open.ts +29 -8
- package/src/ar/zip-package.ts +57 -0
- package/src/chalk.ts +66 -23
- package/src/color.ts +6 -0
- package/src/dirs.ts +93 -24
- package/src/docx/converter.ts +26 -12
- package/src/dom/core.ts +354 -111
- package/src/dom/parser.ts +42 -19
- package/src/dom/selector.ts +328 -95
- package/src/fetch-retry.ts +2 -0
- package/src/format.ts +5 -1
- package/src/json-lexer.ts +54 -0
- package/src/json-parse.ts +21 -6
- package/src/logger/rotating-file.ts +5 -3
- package/src/logger.ts +39 -23
- package/src/loop-phase.ts +66 -21
- package/src/marked/core.ts +45 -19
- package/src/marked-list.ts +73 -0
- package/src/math-delimiters.ts +125 -5
- package/src/module-timer.ts +5 -5
- package/src/postmortem.ts +1 -1
- package/src/process-name.ts +2 -2
- package/src/procmgr.ts +58 -4
- package/src/prompt.ts +3 -1
- package/src/ptree.ts +28 -35
- package/src/readability/readability.ts +349 -39
- package/src/readability/types.ts +2 -0
- package/src/runtime-install.ts +2 -2
- package/src/sqlite.ts +58 -1
- package/src/stream.ts +34 -13
- package/src/temp.ts +13 -0
- package/src/template.ts +7 -4
- package/src/timing-buffer.ts +1 -1
- package/src/tls-fetch.ts +1 -1
- package/src/turndown/gfm.ts +16 -5
- package/src/turndown/html.ts +28 -8
- package/src/turndown/service.ts +43 -15
- package/src/turndown/types.ts +7 -0
- package/src/vterm/buffer.ts +31 -11
- package/src/vterm/query-responder.ts +31 -2
- package/src/vterm/terminal.ts +213 -100
package/src/dom/core.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Behavior-compatible reimplementation of linkedom's used surface. */
|
|
2
2
|
|
|
3
|
-
import { parseFragment } from "./parser";
|
|
4
|
-
import { matchesSelector, querySelectorAllFrom } from "./selector";
|
|
3
|
+
import { parseFragment, parseInto } from "./parser";
|
|
4
|
+
import { closestMatching, hasClassToken, matchesSelector, querySelectorAllFrom, querySelectorFrom } from "./selector";
|
|
5
5
|
|
|
6
6
|
/** DOM node type constants. */
|
|
7
7
|
export const enum NodeType {
|
|
@@ -83,29 +83,32 @@ type EventListener = ((event: Event) => void) | { handleEvent(event: Event): voi
|
|
|
83
83
|
|
|
84
84
|
/** Minimal event-target implementation used by DOM nodes and window. */
|
|
85
85
|
export class EventTarget {
|
|
86
|
-
|
|
86
|
+
/** Allocated on the first `addEventListener`; most nodes never get listeners. */
|
|
87
|
+
#listeners: Map<string, Set<EventListener>> | undefined;
|
|
87
88
|
|
|
88
89
|
/** Register an event listener. */
|
|
89
90
|
addEventListener(type: string, listener: EventListener | null): void {
|
|
90
91
|
if (!listener) return;
|
|
91
|
-
|
|
92
|
+
const listenerMap = (this.#listeners ??= new Map());
|
|
93
|
+
let listeners = listenerMap.get(type);
|
|
92
94
|
if (!listeners) {
|
|
93
95
|
listeners = new Set();
|
|
94
|
-
|
|
96
|
+
listenerMap.set(type, listeners);
|
|
95
97
|
}
|
|
96
98
|
listeners.add(listener);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/** Remove an event listener. */
|
|
100
102
|
removeEventListener(type: string, listener: EventListener | null): void {
|
|
101
|
-
if (listener) this.#listeners
|
|
103
|
+
if (listener) this.#listeners?.get(type)?.delete(listener);
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/** Dispatch an event to listeners and optionally through node ancestors. */
|
|
105
107
|
dispatchEvent(event: Event): boolean {
|
|
106
108
|
if (!event.target) event.target = this;
|
|
107
109
|
event.currentTarget = this;
|
|
108
|
-
|
|
110
|
+
const listeners = this.#listeners?.get(event.type);
|
|
111
|
+
for (const listener of listeners ? Array.from(listeners) : []) {
|
|
109
112
|
if (typeof listener === "function") listener.call(this, event);
|
|
110
113
|
else listener.handleEvent(event);
|
|
111
114
|
if (event.propagationStopped) break;
|
|
@@ -117,6 +120,9 @@ export class EventTarget {
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
/** Index of a node in its parent's `childNodes`, or -1; bound to Node's private index hint. */
|
|
124
|
+
let indexInParent: (node: Node) => number;
|
|
125
|
+
|
|
120
126
|
/** Base class for the implemented DOM tree. */
|
|
121
127
|
export class Node extends EventTarget {
|
|
122
128
|
static readonly ELEMENT_NODE = NodeType.ELEMENT;
|
|
@@ -129,7 +135,17 @@ export class Node extends EventTarget {
|
|
|
129
135
|
readonly nodeName: string;
|
|
130
136
|
parentNode: Node | null = null;
|
|
131
137
|
ownerDocument: Document | null;
|
|
132
|
-
|
|
138
|
+
/** Allocated on first access or insertion, so leaf nodes (text, comments, attributes) usually never allocate. */
|
|
139
|
+
#childNodes: Node[] | undefined;
|
|
140
|
+
/** Last known index in the parent's `childNodes`; always verified before use, so a stale value only costs a rescan. */
|
|
141
|
+
#index = 0;
|
|
142
|
+
|
|
143
|
+
static {
|
|
144
|
+
indexInParent = node => {
|
|
145
|
+
const siblings = node.#siblings();
|
|
146
|
+
return siblings ? node.#indexIn(siblings) : -1;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
133
149
|
|
|
134
150
|
constructor(nodeType: number, nodeName: string, ownerDocument: Document | null = null) {
|
|
135
151
|
super();
|
|
@@ -138,28 +154,105 @@ export class Node extends EventTarget {
|
|
|
138
154
|
this.ownerDocument = ownerDocument;
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
/** Child nodes in tree order. */
|
|
158
|
+
get childNodes(): Node[] {
|
|
159
|
+
this.#childNodes ??= [];
|
|
160
|
+
return this.#childNodes;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
set childNodes(value: Node[]) {
|
|
164
|
+
this.#childNodes = value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Whether this node has any children (without allocating a child list). */
|
|
168
|
+
hasChildNodes(): boolean {
|
|
169
|
+
return this.#childNodes !== undefined && this.#childNodes.length > 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
141
172
|
/** First child node, if present. */
|
|
142
173
|
get firstChild(): Node | null {
|
|
143
|
-
return this
|
|
174
|
+
return this.#childNodes?.[0] ?? null;
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
/** Last child node, if present. */
|
|
147
178
|
get lastChild(): Node | null {
|
|
148
|
-
|
|
179
|
+
const children = this.#childNodes;
|
|
180
|
+
return children ? (children[children.length - 1] ?? null) : null;
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
/** Previous node with the same parent. */
|
|
152
184
|
get previousSibling(): Node | null {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
185
|
+
const siblings = this.#siblings();
|
|
186
|
+
if (!siblings) return null;
|
|
187
|
+
const index = this.#indexIn(siblings);
|
|
188
|
+
return index > 0 ? siblings[index - 1] : null;
|
|
156
189
|
}
|
|
157
190
|
|
|
158
191
|
/** Next node with the same parent. */
|
|
159
192
|
get nextSibling(): Node | null {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
193
|
+
const siblings = this.#siblings();
|
|
194
|
+
if (!siblings) return null;
|
|
195
|
+
const index = this.#indexIn(siblings);
|
|
196
|
+
return index >= 0 ? (siblings[index + 1] ?? null) : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The parent's child list, without allocating one. */
|
|
200
|
+
#siblings(): Node[] | undefined {
|
|
201
|
+
const parent = this.parentNode;
|
|
202
|
+
return parent ? parent.#childNodes : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Locate this node in `siblings` via the index hint, re-indexing all siblings on a miss. */
|
|
206
|
+
#indexIn(siblings: Node[]): number {
|
|
207
|
+
const hint = this.#index;
|
|
208
|
+
if (siblings[hint] === this) return hint;
|
|
209
|
+
if (siblings[hint + 1] === this) return ++this.#index;
|
|
210
|
+
if (hint > 0 && siblings[hint - 1] === this) return --this.#index;
|
|
211
|
+
let found = -1;
|
|
212
|
+
for (let index = 0; index < siblings.length; index++) {
|
|
213
|
+
const sibling = siblings[index];
|
|
214
|
+
sibling.#index = index;
|
|
215
|
+
if (found < 0 && sibling === this) found = index;
|
|
216
|
+
}
|
|
217
|
+
if (found >= 0) this.#index = found;
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Index of a child in this node's `childNodes`, or -1. */
|
|
222
|
+
#childIndex(child: Node): number {
|
|
223
|
+
const children = this.#childNodes;
|
|
224
|
+
if (!children) return -1;
|
|
225
|
+
return child?.parentNode === this ? child.#indexIn(children) : children.indexOf(child);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Move every child of a fragment into this node at `index` (append for null). */
|
|
229
|
+
#adoptChildren(fragment: DocumentFragment, index: number | null): void {
|
|
230
|
+
const moved = fragment.#childNodes;
|
|
231
|
+
if (!moved?.length) return;
|
|
232
|
+
if (moved.some(nested => nested.parentNode !== fragment || nested instanceof DocumentFragment)) {
|
|
233
|
+
// Inconsistent fragment state: keep the one-at-a-time semantics.
|
|
234
|
+
const reference = index === null ? null : (this.#childNodes?.[index] ?? null);
|
|
235
|
+
for (const nested of Array.from(moved)) {
|
|
236
|
+
if (reference === null) this.appendChild(nested);
|
|
237
|
+
else this.insertBefore(nested, reference);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const document = this.documentForCreation();
|
|
242
|
+
const nodes = moved.slice();
|
|
243
|
+
moved.length = 0;
|
|
244
|
+
const children = (this.#childNodes ??= []);
|
|
245
|
+
const tail = index === null ? [] : children.splice(index);
|
|
246
|
+
for (const node of nodes) {
|
|
247
|
+
node.parentNode = this;
|
|
248
|
+
node.setOwnerDocument(document);
|
|
249
|
+
node.#index = children.length;
|
|
250
|
+
children.push(node);
|
|
251
|
+
}
|
|
252
|
+
for (const node of tail) {
|
|
253
|
+
node.#index = children.length;
|
|
254
|
+
children.push(node);
|
|
255
|
+
}
|
|
163
256
|
}
|
|
164
257
|
|
|
165
258
|
/** Connectedness to a document. */
|
|
@@ -178,7 +271,11 @@ export class Node extends EventTarget {
|
|
|
178
271
|
|
|
179
272
|
/** Text contained by this node. */
|
|
180
273
|
get textContent(): string | null {
|
|
181
|
-
|
|
274
|
+
const children = this.#childNodes;
|
|
275
|
+
if (!children?.length) return "";
|
|
276
|
+
const only = children[0];
|
|
277
|
+
if (children.length === 1 && only instanceof Text) return only.data;
|
|
278
|
+
return collectText(children);
|
|
182
279
|
}
|
|
183
280
|
|
|
184
281
|
set textContent(value: string | null) {
|
|
@@ -196,44 +293,48 @@ export class Node extends EventTarget {
|
|
|
196
293
|
const node: Node = child;
|
|
197
294
|
if (node === this || node.contains(this)) throw new Error("The new child is an ancestor of this node");
|
|
198
295
|
if (child instanceof DocumentFragment) {
|
|
199
|
-
|
|
296
|
+
this.#adoptChildren(child, null);
|
|
200
297
|
return child;
|
|
201
298
|
}
|
|
202
299
|
child.parentNode?.removeChild(child);
|
|
203
300
|
child.parentNode = this;
|
|
204
301
|
child.setOwnerDocument(this.documentForCreation());
|
|
205
|
-
this
|
|
302
|
+
const children = (this.#childNodes ??= []);
|
|
303
|
+
node.#index = children.length;
|
|
304
|
+
children.push(child);
|
|
206
305
|
return child;
|
|
207
306
|
}
|
|
208
307
|
|
|
209
308
|
/** Insert a node before a current child, or append for null. */
|
|
210
309
|
insertBefore<T extends Node>(child: T, reference: Node | null): T {
|
|
211
310
|
if (reference === null) return this.appendChild(child);
|
|
212
|
-
const index = this
|
|
311
|
+
const index = this.#childIndex(reference);
|
|
213
312
|
if (index < 0) throw new Error("The reference node is not a child of this node");
|
|
214
313
|
if (child instanceof DocumentFragment) {
|
|
215
|
-
|
|
314
|
+
this.#adoptChildren(child, index);
|
|
216
315
|
return child;
|
|
217
316
|
}
|
|
218
317
|
child.parentNode?.removeChild(child);
|
|
219
318
|
child.parentNode = this;
|
|
220
319
|
child.setOwnerDocument(this.documentForCreation());
|
|
320
|
+
const node: Node = child;
|
|
321
|
+
node.#index = index;
|
|
221
322
|
this.childNodes.splice(index, 0, child);
|
|
222
323
|
return child;
|
|
223
324
|
}
|
|
224
325
|
|
|
225
326
|
/** Replace a current child with another node. */
|
|
226
327
|
replaceChild<T extends Node>(child: Node, previous: T): T {
|
|
227
|
-
const index = this
|
|
328
|
+
const index = this.#childIndex(previous);
|
|
228
329
|
if (index < 0) throw new Error("The node to replace is not a child of this node");
|
|
229
330
|
this.removeChild(previous);
|
|
230
|
-
this.insertBefore(child, this
|
|
331
|
+
this.insertBefore(child, this.#childNodes?.[index] ?? null);
|
|
231
332
|
return previous;
|
|
232
333
|
}
|
|
233
334
|
|
|
234
335
|
/** Remove a current child. */
|
|
235
336
|
removeChild<T extends Node>(child: T): T {
|
|
236
|
-
const index = this
|
|
337
|
+
const index = this.#childIndex(child);
|
|
237
338
|
if (index < 0) throw new Error("The node to remove is not a child of this node");
|
|
238
339
|
this.childNodes.splice(index, 1);
|
|
239
340
|
child.parentNode = null;
|
|
@@ -242,8 +343,9 @@ export class Node extends EventTarget {
|
|
|
242
343
|
|
|
243
344
|
/** Replace all children with nodes or strings. */
|
|
244
345
|
replaceChildren(...children: Array<Node | string>): void {
|
|
245
|
-
|
|
246
|
-
|
|
346
|
+
const previous = this.#childNodes;
|
|
347
|
+
if (previous) for (const child of previous) child.parentNode = null;
|
|
348
|
+
this.#childNodes = undefined;
|
|
247
349
|
this.append(...children);
|
|
248
350
|
}
|
|
249
351
|
|
|
@@ -289,7 +391,7 @@ export class Node extends EventTarget {
|
|
|
289
391
|
/** Clone this node, optionally including descendants. */
|
|
290
392
|
cloneNode(deep = false): Node {
|
|
291
393
|
const clone = new Node(this.nodeType, this.nodeName, this.ownerDocument);
|
|
292
|
-
if (deep) for (const child of this
|
|
394
|
+
if (deep && this.#childNodes) for (const child of this.#childNodes) clone.appendChild(child.cloneNode(true));
|
|
293
395
|
return clone;
|
|
294
396
|
}
|
|
295
397
|
|
|
@@ -302,7 +404,7 @@ export class Node extends EventTarget {
|
|
|
302
404
|
const nodes: Node[] = [];
|
|
303
405
|
const visit = (node: Node): void => {
|
|
304
406
|
nodes.push(node);
|
|
305
|
-
for (const child of node
|
|
407
|
+
for (const child of node.#childNodes ?? []) visit(child);
|
|
306
408
|
};
|
|
307
409
|
visit(root);
|
|
308
410
|
return nodes.indexOf(this) < nodes.indexOf(other) ? 4 : 2;
|
|
@@ -310,7 +412,8 @@ export class Node extends EventTarget {
|
|
|
310
412
|
|
|
311
413
|
setOwnerDocument(document: Document): void {
|
|
312
414
|
if (this.nodeType !== NodeType.DOCUMENT) this.ownerDocument = document;
|
|
313
|
-
|
|
415
|
+
const children = this.#childNodes;
|
|
416
|
+
if (children) for (const child of children) child.setOwnerDocument(document);
|
|
314
417
|
}
|
|
315
418
|
|
|
316
419
|
documentForCreation(): Document {
|
|
@@ -417,7 +520,11 @@ export class NamedNodeMap extends Array<Attr> {
|
|
|
417
520
|
/** Find an attribute case-insensitively. */
|
|
418
521
|
getNamedItem(name: string): Attr | null {
|
|
419
522
|
const normalized = name.toLowerCase();
|
|
420
|
-
|
|
523
|
+
for (let index = 0; index < this.length; index++) {
|
|
524
|
+
const attr = this[index];
|
|
525
|
+
if (attr.name === normalized || attr.name.toLowerCase() === normalized) return attr;
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
421
528
|
}
|
|
422
529
|
|
|
423
530
|
/** Set an attribute object and return the prior one. */
|
|
@@ -480,7 +587,7 @@ export class DOMTokenList implements Iterable<string> {
|
|
|
480
587
|
|
|
481
588
|
/** Whether a class token exists. */
|
|
482
589
|
contains(token: string): boolean {
|
|
483
|
-
return this.#
|
|
590
|
+
return hasClassToken(this.#element.className, token);
|
|
484
591
|
}
|
|
485
592
|
|
|
486
593
|
/** Toggle a class token. */
|
|
@@ -518,15 +625,20 @@ export class DOMTokenList implements Iterable<string> {
|
|
|
518
625
|
|
|
519
626
|
/** Mutable inline style declaration. */
|
|
520
627
|
export class CSSStyleDeclaration {
|
|
521
|
-
|
|
628
|
+
/** Allocated on the first non-empty declaration. */
|
|
629
|
+
#values: Map<string, string> | undefined;
|
|
630
|
+
/** Serialized declarations; `undefined` after a change until the next read. */
|
|
631
|
+
#cssText: string | undefined = "";
|
|
522
632
|
|
|
523
633
|
/** Serialized declarations. */
|
|
524
634
|
get cssText(): string {
|
|
525
|
-
|
|
635
|
+
this.#cssText ??= this.#values ? [...this.#values].map(([name, value]) => `${name}: ${value};`).join(" ") : "";
|
|
636
|
+
return this.#cssText;
|
|
526
637
|
}
|
|
527
638
|
|
|
528
639
|
set cssText(value: string) {
|
|
529
|
-
this.#values
|
|
640
|
+
this.#values?.clear();
|
|
641
|
+
this.#cssText = undefined;
|
|
530
642
|
for (const declaration of value.split(";")) {
|
|
531
643
|
const colon = declaration.indexOf(":");
|
|
532
644
|
if (colon > 0) this.setProperty(declaration.slice(0, colon).trim(), declaration.slice(colon + 1).trim());
|
|
@@ -535,19 +647,24 @@ export class CSSStyleDeclaration {
|
|
|
535
647
|
|
|
536
648
|
/** Read a CSS property. */
|
|
537
649
|
getPropertyValue(name: string): string {
|
|
538
|
-
return this.#values
|
|
650
|
+
return this.#values?.get(name) ?? "";
|
|
539
651
|
}
|
|
540
652
|
|
|
541
653
|
/** Set a CSS property. */
|
|
542
654
|
setProperty(name: string, value: string | null, _priority?: string): void {
|
|
543
|
-
if (value === null || value === "")
|
|
544
|
-
|
|
655
|
+
if (value === null || value === "") {
|
|
656
|
+
if (this.#values?.delete(name)) this.#cssText = undefined;
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
this.#values ??= new Map();
|
|
660
|
+
this.#values.set(name, String(value));
|
|
661
|
+
this.#cssText = undefined;
|
|
545
662
|
}
|
|
546
663
|
|
|
547
664
|
/** Remove and return a CSS property. */
|
|
548
665
|
removeProperty(name: string): string {
|
|
549
666
|
const value = this.getPropertyValue(name);
|
|
550
|
-
this.#values
|
|
667
|
+
if (this.#values?.delete(name)) this.#cssText = undefined;
|
|
551
668
|
return value;
|
|
552
669
|
}
|
|
553
670
|
}
|
|
@@ -555,15 +672,23 @@ export class CSSStyleDeclaration {
|
|
|
555
672
|
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
556
673
|
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
557
674
|
|
|
675
|
+
/** An element's inline style when one has been created; bound to Element's private field. */
|
|
676
|
+
let inlineStyleOf: (element: Element) => CSSStyleDeclaration | undefined;
|
|
677
|
+
|
|
558
678
|
/** DOM element with attributes, selectors, and HTML serialization. */
|
|
559
679
|
export class Element extends Node {
|
|
560
680
|
readonly localName: string;
|
|
561
681
|
readonly tagName: string;
|
|
562
682
|
readonly namespaceURI: string | null;
|
|
563
683
|
readonly qualifiedName: string;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
684
|
+
/** Attribute, class-list and style objects are created on first use. */
|
|
685
|
+
#attributes: NamedNodeMap | undefined;
|
|
686
|
+
#classList: DOMTokenList | undefined;
|
|
687
|
+
#style: CSSStyleDeclaration | undefined;
|
|
688
|
+
|
|
689
|
+
static {
|
|
690
|
+
inlineStyleOf = element => element.#style;
|
|
691
|
+
}
|
|
567
692
|
|
|
568
693
|
constructor(tagName: string, ownerDocument: Document | null = null, namespaceURI: string | null = HTML_NAMESPACE) {
|
|
569
694
|
const localName = tagName.toLowerCase();
|
|
@@ -573,34 +698,67 @@ export class Element extends Node {
|
|
|
573
698
|
this.tagName = qualified;
|
|
574
699
|
this.qualifiedName = tagName;
|
|
575
700
|
this.namespaceURI = namespaceURI;
|
|
576
|
-
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** Attribute collection. */
|
|
704
|
+
get attributes(): NamedNodeMap {
|
|
705
|
+
this.#attributes ??= new NamedNodeMap();
|
|
706
|
+
return this.#attributes;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Class token list over the `class` attribute. */
|
|
710
|
+
get classList(): DOMTokenList {
|
|
711
|
+
this.#classList ??= new DOMTokenList(this);
|
|
712
|
+
return this.#classList;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Inline style declaration. */
|
|
716
|
+
get style(): CSSStyleDeclaration {
|
|
717
|
+
this.#style ??= new CSSStyleDeclaration();
|
|
718
|
+
return this.#style;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Whether the element has any attribute objects (without allocating the collection). */
|
|
722
|
+
hasAttributes(): boolean {
|
|
723
|
+
return this.#attributes !== undefined && this.#attributes.length > 0;
|
|
577
724
|
}
|
|
578
725
|
|
|
579
726
|
/** Element children. */
|
|
580
727
|
get children(): Element[] {
|
|
581
|
-
return this
|
|
728
|
+
return childElements(this);
|
|
582
729
|
}
|
|
583
730
|
|
|
584
731
|
/** First element child. */
|
|
585
732
|
get firstElementChild(): Element | null {
|
|
586
|
-
return this
|
|
733
|
+
return firstElementIn(this);
|
|
587
734
|
}
|
|
588
735
|
|
|
589
736
|
/** Last element child. */
|
|
590
737
|
get lastElementChild(): Element | null {
|
|
591
|
-
|
|
592
|
-
return children[children.length - 1] ?? null;
|
|
738
|
+
return lastElementIn(this);
|
|
593
739
|
}
|
|
594
740
|
|
|
595
741
|
/** Previous sibling that is an element. */
|
|
596
742
|
get previousElementSibling(): Element | null {
|
|
597
|
-
|
|
743
|
+
const index = indexInParent(this);
|
|
744
|
+
if (index < 0) return null;
|
|
745
|
+
const siblings = (this.parentNode as Node).childNodes;
|
|
746
|
+
for (let position = index - 1; position >= 0; position--) {
|
|
747
|
+
const sibling = siblings[position];
|
|
748
|
+
if (sibling instanceof Element) return sibling;
|
|
749
|
+
}
|
|
598
750
|
return null;
|
|
599
751
|
}
|
|
600
752
|
|
|
601
753
|
/** Next sibling that is an element. */
|
|
602
754
|
get nextElementSibling(): Element | null {
|
|
603
|
-
|
|
755
|
+
const index = indexInParent(this);
|
|
756
|
+
if (index < 0) return null;
|
|
757
|
+
const siblings = (this.parentNode as Node).childNodes;
|
|
758
|
+
for (let position = index + 1; position < siblings.length; position++) {
|
|
759
|
+
const sibling = siblings[position];
|
|
760
|
+
if (sibling instanceof Element) return sibling;
|
|
761
|
+
}
|
|
604
762
|
return null;
|
|
605
763
|
}
|
|
606
764
|
|
|
@@ -663,13 +821,12 @@ export class Element extends Node {
|
|
|
663
821
|
|
|
664
822
|
/** HTML contained inside this element. */
|
|
665
823
|
get innerHTML(): string {
|
|
666
|
-
return this
|
|
824
|
+
return serializeChildren(this);
|
|
667
825
|
}
|
|
668
826
|
|
|
669
827
|
set innerHTML(value: string) {
|
|
670
828
|
this.replaceChildren();
|
|
671
|
-
|
|
672
|
-
this.appendChild(child);
|
|
829
|
+
parseInto(value, this.documentForCreation(), this);
|
|
673
830
|
}
|
|
674
831
|
|
|
675
832
|
/** Serialized element and descendants. */
|
|
@@ -680,26 +837,30 @@ export class Element extends Node {
|
|
|
680
837
|
set outerHTML(value: string) {
|
|
681
838
|
const parent = this.parentNode;
|
|
682
839
|
if (!parent) return;
|
|
683
|
-
|
|
684
|
-
for (const child of Array.from(fragment.childNodes)) parent.insertBefore(child, this);
|
|
840
|
+
parent.insertBefore(parseFragment(value, this.documentForCreation()), this);
|
|
685
841
|
parent.removeChild(this);
|
|
686
842
|
}
|
|
687
843
|
|
|
688
844
|
/** Read an attribute. */
|
|
689
845
|
getAttribute(name: string): string | null {
|
|
690
|
-
|
|
691
|
-
|
|
846
|
+
const style = this.#style;
|
|
847
|
+
if (style && name.toLowerCase() === "style") {
|
|
848
|
+
const cssText = style.cssText;
|
|
849
|
+
if (cssText) return cssText;
|
|
850
|
+
}
|
|
851
|
+
return this.#attributes?.getNamedItem(name)?.value ?? null;
|
|
692
852
|
}
|
|
693
853
|
|
|
694
854
|
/** Read an attribute object. */
|
|
695
855
|
getAttributeNode(name: string): Attr | null {
|
|
696
|
-
return this
|
|
856
|
+
return this.#attributes?.getNamedItem(name) ?? null;
|
|
697
857
|
}
|
|
698
858
|
|
|
699
859
|
/** Whether an attribute exists. */
|
|
700
860
|
hasAttribute(name: string): boolean {
|
|
701
861
|
return (
|
|
702
|
-
this
|
|
862
|
+
(this.#attributes?.getNamedItem(name) ?? null) !== null ||
|
|
863
|
+
(this.#style !== undefined && name.toLowerCase() === "style" && Boolean(this.#style.cssText))
|
|
703
864
|
);
|
|
704
865
|
}
|
|
705
866
|
|
|
@@ -707,9 +868,10 @@ export class Element extends Node {
|
|
|
707
868
|
setAttribute(name: string, value: string): void {
|
|
708
869
|
const normalized = name.toLowerCase();
|
|
709
870
|
if (normalized === "style") this.style.cssText = String(value);
|
|
710
|
-
const
|
|
871
|
+
const attributes = this.attributes;
|
|
872
|
+
const existing = attributes.getNamedItem(normalized);
|
|
711
873
|
if (existing) existing.value = String(value);
|
|
712
|
-
else
|
|
874
|
+
else attributes.unshift(new Attr(name, String(value), this.ownerDocument));
|
|
713
875
|
}
|
|
714
876
|
|
|
715
877
|
/** Set a namespaced attribute. */
|
|
@@ -719,14 +881,15 @@ export class Element extends Node {
|
|
|
719
881
|
|
|
720
882
|
/** Remove an attribute. */
|
|
721
883
|
removeAttribute(name: string): void {
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
if (
|
|
884
|
+
const attributes = this.#attributes;
|
|
885
|
+
const existing = attributes?.getNamedItem(name);
|
|
886
|
+
if (attributes && existing) attributes.splice(attributes.indexOf(existing), 1);
|
|
887
|
+
if (this.#style && name.toLowerCase() === "style") this.#style.cssText = "";
|
|
725
888
|
}
|
|
726
889
|
|
|
727
890
|
/** Find the first descendant matching a selector. */
|
|
728
891
|
querySelector(selector: string): Element | null {
|
|
729
|
-
return
|
|
892
|
+
return querySelectorFrom(this, selector);
|
|
730
893
|
}
|
|
731
894
|
|
|
732
895
|
/** Find all descendants matching a selector. */
|
|
@@ -741,10 +904,7 @@ export class Element extends Node {
|
|
|
741
904
|
|
|
742
905
|
/** Find the nearest matching ancestor including this element. */
|
|
743
906
|
closest(selector: string): Element | null {
|
|
744
|
-
|
|
745
|
-
if (element.matches(selector)) return element;
|
|
746
|
-
}
|
|
747
|
-
return null;
|
|
907
|
+
return closestMatching(this, selector);
|
|
748
908
|
}
|
|
749
909
|
|
|
750
910
|
/** Descendant elements with a tag name. */
|
|
@@ -755,9 +915,10 @@ export class Element extends Node {
|
|
|
755
915
|
/** Descendant elements containing all requested class tokens. */
|
|
756
916
|
getElementsByClassName(classNames: string): Element[] {
|
|
757
917
|
const tokens = classNames.trim().split(/\s+/).filter(Boolean);
|
|
758
|
-
return querySelectorAllFrom(this, "*", false).filter(element =>
|
|
759
|
-
|
|
760
|
-
|
|
918
|
+
return querySelectorAllFrom(this, "*", false).filter(element => {
|
|
919
|
+
const className = element.className;
|
|
920
|
+
return tokens.every(token => hasClassToken(className, token));
|
|
921
|
+
});
|
|
761
922
|
}
|
|
762
923
|
|
|
763
924
|
/**
|
|
@@ -798,11 +959,14 @@ export class Element extends Node {
|
|
|
798
959
|
const clone =
|
|
799
960
|
this.ownerDocument?.createElementNS(this.namespaceURI, this.qualifiedName) ??
|
|
800
961
|
new Element(this.qualifiedName, null, this.namespaceURI);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
962
|
+
const attributes = this.#attributes;
|
|
963
|
+
if (attributes) {
|
|
964
|
+
for (let index = attributes.length - 1; index >= 0; index--) {
|
|
965
|
+
const attr = attributes[index];
|
|
966
|
+
clone.setAttribute(attr.name, attr.value);
|
|
967
|
+
}
|
|
804
968
|
}
|
|
805
|
-
if (deep) for (const child of this.childNodes) clone.appendChild(child.cloneNode(true));
|
|
969
|
+
if (deep && this.hasChildNodes()) for (const child of this.childNodes) clone.appendChild(child.cloneNode(true));
|
|
806
970
|
return clone;
|
|
807
971
|
}
|
|
808
972
|
}
|
|
@@ -885,23 +1049,22 @@ export class DocumentFragment extends Node {
|
|
|
885
1049
|
|
|
886
1050
|
/** Element children. */
|
|
887
1051
|
get children(): Element[] {
|
|
888
|
-
return this
|
|
1052
|
+
return childElements(this);
|
|
889
1053
|
}
|
|
890
1054
|
|
|
891
1055
|
/** First element child. */
|
|
892
1056
|
get firstElementChild(): Element | null {
|
|
893
|
-
return this
|
|
1057
|
+
return firstElementIn(this);
|
|
894
1058
|
}
|
|
895
1059
|
|
|
896
1060
|
/** Last element child. */
|
|
897
1061
|
get lastElementChild(): Element | null {
|
|
898
|
-
|
|
899
|
-
return children[children.length - 1] ?? null;
|
|
1062
|
+
return lastElementIn(this);
|
|
900
1063
|
}
|
|
901
1064
|
|
|
902
1065
|
/** First matching descendant. */
|
|
903
1066
|
querySelector(selector: string): Element | null {
|
|
904
|
-
return
|
|
1067
|
+
return querySelectorFrom(this, selector);
|
|
905
1068
|
}
|
|
906
1069
|
|
|
907
1070
|
/** All matching descendants. */
|
|
@@ -926,14 +1089,12 @@ export class HTMLTemplateElement extends HTMLElement {
|
|
|
926
1089
|
}
|
|
927
1090
|
|
|
928
1091
|
override get innerHTML(): string {
|
|
929
|
-
return this.content
|
|
1092
|
+
return serializeChildren(this.content);
|
|
930
1093
|
}
|
|
931
1094
|
|
|
932
1095
|
override set innerHTML(value: string) {
|
|
933
1096
|
this.content.replaceChildren();
|
|
934
|
-
|
|
935
|
-
this.content.appendChild(child);
|
|
936
|
-
}
|
|
1097
|
+
parseInto(value, this.documentForCreation(), this.content);
|
|
937
1098
|
}
|
|
938
1099
|
|
|
939
1100
|
override get textContent(): string {
|
|
@@ -984,33 +1145,33 @@ export class Document extends Node {
|
|
|
984
1145
|
|
|
985
1146
|
/** Root element of the document. */
|
|
986
1147
|
get documentElement(): Element | null {
|
|
987
|
-
return this
|
|
1148
|
+
return firstElementIn(this);
|
|
988
1149
|
}
|
|
989
1150
|
|
|
990
1151
|
/** Element children. */
|
|
991
1152
|
get children(): Element[] {
|
|
992
|
-
return this
|
|
1153
|
+
return childElements(this);
|
|
993
1154
|
}
|
|
994
1155
|
|
|
995
1156
|
/** HTML body, or a detached empty body when absent like linkedom. */
|
|
996
1157
|
get body(): HTMLElement {
|
|
997
|
-
const body = this
|
|
1158
|
+
const body = querySelectorFrom(this, "body");
|
|
998
1159
|
return body instanceof HTMLElement ? body : this.createElement("body");
|
|
999
1160
|
}
|
|
1000
1161
|
|
|
1001
1162
|
/** HTML head, or a detached empty head when absent. */
|
|
1002
1163
|
get head(): HTMLElement {
|
|
1003
|
-
const head = this
|
|
1164
|
+
const head = querySelectorFrom(this, "head");
|
|
1004
1165
|
return head instanceof HTMLElement ? head : this.createElement("head");
|
|
1005
1166
|
}
|
|
1006
1167
|
|
|
1007
1168
|
/** Document title text. */
|
|
1008
1169
|
get title(): string {
|
|
1009
|
-
return this
|
|
1170
|
+
return querySelectorFrom(this, "title")?.textContent ?? "";
|
|
1010
1171
|
}
|
|
1011
1172
|
|
|
1012
1173
|
set title(value: string) {
|
|
1013
|
-
let title = this
|
|
1174
|
+
let title = querySelectorFrom(this, "title");
|
|
1014
1175
|
if (!title) {
|
|
1015
1176
|
title = this.createElement("title");
|
|
1016
1177
|
this.head.appendChild(title);
|
|
@@ -1067,13 +1228,16 @@ export class Document extends Node {
|
|
|
1067
1228
|
|
|
1068
1229
|
/** Find the first element by id. */
|
|
1069
1230
|
getElementById(id: string): HTMLElement | null {
|
|
1070
|
-
|
|
1231
|
+
// Ids free of selector syntax compare directly; the rest keep the escaped-selector path's exact behaviour.
|
|
1232
|
+
const element = SELECTOR_SAFE_ID.test(id)
|
|
1233
|
+
? findElementById(this, id)
|
|
1234
|
+
: querySelectorFrom(this, `#${cssEscapeIdentifier(id)}`);
|
|
1071
1235
|
return element instanceof HTMLElement ? element : null;
|
|
1072
1236
|
}
|
|
1073
1237
|
|
|
1074
1238
|
/** Find the first matching descendant. */
|
|
1075
1239
|
querySelector(selector: string): Element | null {
|
|
1076
|
-
return
|
|
1240
|
+
return querySelectorFrom(this, selector);
|
|
1077
1241
|
}
|
|
1078
1242
|
|
|
1079
1243
|
/** Find all matching descendants. */
|
|
@@ -1089,7 +1253,10 @@ export class Document extends Node {
|
|
|
1089
1253
|
/** Descendant elements containing all class tokens. */
|
|
1090
1254
|
getElementsByClassName(classNames: string): Element[] {
|
|
1091
1255
|
const tokens = classNames.trim().split(/\s+/).filter(Boolean);
|
|
1092
|
-
return this.querySelectorAll("*").filter(element =>
|
|
1256
|
+
return this.querySelectorAll("*").filter(element => {
|
|
1257
|
+
const className = element.className;
|
|
1258
|
+
return tokens.every(token => hasClassToken(className, token));
|
|
1259
|
+
});
|
|
1093
1260
|
}
|
|
1094
1261
|
|
|
1095
1262
|
/** Adopt a node into this document. */
|
|
@@ -1186,6 +1353,7 @@ const VOID_ELEMENTS: Record<string, true> = {
|
|
|
1186
1353
|
track: true,
|
|
1187
1354
|
wbr: true,
|
|
1188
1355
|
};
|
|
1356
|
+
|
|
1189
1357
|
const BOOLEAN_ATTRIBUTES: Record<string, true> = {
|
|
1190
1358
|
allowfullscreen: true,
|
|
1191
1359
|
async: true,
|
|
@@ -1214,41 +1382,116 @@ const BOOLEAN_ATTRIBUTES: Record<string, true> = {
|
|
|
1214
1382
|
selected: true,
|
|
1215
1383
|
};
|
|
1216
1384
|
|
|
1385
|
+
const TEXT_ESCAPE_PATTERN = /[&\u00a0<>]/;
|
|
1386
|
+
/** Ids whose escaped `#id` selector parses as one plain id compound (no combinators, lists, quotes or groups). */
|
|
1387
|
+
const SELECTOR_SAFE_ID = /^[^\s,>+~"'[\]()\\]+$/;
|
|
1388
|
+
|
|
1217
1389
|
function escapeText(value: string): string {
|
|
1218
|
-
|
|
1390
|
+
if (!TEXT_ESCAPE_PATTERN.test(value)) return value;
|
|
1391
|
+
return value
|
|
1392
|
+
.replace(/&/g, "&")
|
|
1393
|
+
.replace(/\u00a0/g, " ")
|
|
1394
|
+
.replace(/</g, "<")
|
|
1395
|
+
.replace(/>/g, ">");
|
|
1219
1396
|
}
|
|
1220
1397
|
|
|
1221
1398
|
function escapeAttribute(value: string): string {
|
|
1222
|
-
return value.replace(/"/g, """);
|
|
1399
|
+
return value.includes('"') ? value.replace(/"/g, """) : value;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/** Serialize a node's children; `raw` emits text data unescaped (script/style content). */
|
|
1403
|
+
function serializeChildren(parent: Node, raw = false): string {
|
|
1404
|
+
if (!parent.hasChildNodes()) return "";
|
|
1405
|
+
let html = "";
|
|
1406
|
+
for (const child of parent.childNodes) html += raw && child instanceof Text ? child.data : serializeNode(child);
|
|
1407
|
+
return html;
|
|
1223
1408
|
}
|
|
1224
1409
|
|
|
1225
1410
|
/** Serialize a DOM node as HTML. */
|
|
1226
1411
|
export function serializeNode(node: Node): string {
|
|
1227
1412
|
if (node instanceof Comment) return `<!--${node.data}-->`;
|
|
1228
1413
|
if (node instanceof Text) return node.serializeRaw ? node.data : escapeText(node.data);
|
|
1229
|
-
if (node instanceof Document || node instanceof DocumentFragment) return node
|
|
1414
|
+
if (node instanceof Document || node instanceof DocumentFragment) return serializeChildren(node);
|
|
1230
1415
|
if (!(node instanceof Element)) return "";
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1416
|
+
let opening = `<${node.qualifiedName}`;
|
|
1417
|
+
const hasAttributes = node.hasAttributes();
|
|
1418
|
+
if (hasAttributes) {
|
|
1419
|
+
for (const attr of node.attributes) {
|
|
1420
|
+
opening +=
|
|
1421
|
+
attr.value === "" && BOOLEAN_ATTRIBUTES[attr.name.toLowerCase()]
|
|
1422
|
+
? ` ${attr.name}`
|
|
1423
|
+
: ` ${attr.name}="${escapeAttribute(attr.value)}"`;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const cssText = inlineStyleOf(node)?.cssText;
|
|
1427
|
+
if (cssText && !(hasAttributes && node.attributes.getNamedItem("style"))) {
|
|
1428
|
+
opening += ` style="${escapeAttribute(cssText)}"`;
|
|
1429
|
+
}
|
|
1430
|
+
opening += ">";
|
|
1243
1431
|
if (VOID_ELEMENTS[node.localName]) return opening;
|
|
1244
1432
|
const raw = node.localName === "script" || node.localName === "style";
|
|
1245
|
-
const
|
|
1246
|
-
const content = raw
|
|
1247
|
-
? childNodes.map(child => (child instanceof Text ? child.data : serializeNode(child))).join("")
|
|
1248
|
-
: childNodes.map(serializeNode).join("");
|
|
1433
|
+
const content = serializeChildren(node instanceof HTMLTemplateElement ? node.content : node, raw);
|
|
1249
1434
|
return `${opening}${content}</${node.qualifiedName}>`;
|
|
1250
1435
|
}
|
|
1251
1436
|
|
|
1437
|
+
function childElements(parent: Node): Element[] {
|
|
1438
|
+
return parent.hasChildNodes() ? parent.childNodes.filter((child): child is Element => child instanceof Element) : [];
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function firstElementIn(parent: Node): Element | null {
|
|
1442
|
+
if (!parent.hasChildNodes()) return null;
|
|
1443
|
+
for (const child of parent.childNodes) if (child instanceof Element) return child;
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function lastElementIn(parent: Node): Element | null {
|
|
1448
|
+
if (!parent.hasChildNodes()) return null;
|
|
1449
|
+
const children = parent.childNodes;
|
|
1450
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
1451
|
+
const child = children[index];
|
|
1452
|
+
if (child instanceof Element) return child;
|
|
1453
|
+
}
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** First descendant element in document order whose `id` equals `id`. */
|
|
1458
|
+
function findElementById(parent: Node, id: string): Element | null {
|
|
1459
|
+
if (!parent.hasChildNodes()) return null;
|
|
1460
|
+
for (const child of parent.childNodes) {
|
|
1461
|
+
if (child instanceof Element && child.id === id) return child;
|
|
1462
|
+
const found = findElementById(child, id);
|
|
1463
|
+
if (found) return found;
|
|
1464
|
+
}
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Concatenate descendant text iteratively, mirroring each node type's `textContent`: character data and
|
|
1470
|
+
* attributes contribute their value, documents nothing, templates their content fragment.
|
|
1471
|
+
*/
|
|
1472
|
+
function collectText(children: Node[]): string {
|
|
1473
|
+
let text = "";
|
|
1474
|
+
const pending: Node[] = [];
|
|
1475
|
+
for (let index = children.length - 1; index >= 0; index--) pending.push(children[index]);
|
|
1476
|
+
while (pending.length > 0) {
|
|
1477
|
+
const node = pending.pop() as Node;
|
|
1478
|
+
if (node instanceof Text) {
|
|
1479
|
+
text += node.data;
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
if (node instanceof Attr) {
|
|
1483
|
+
text += node.value;
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (node instanceof Document) continue;
|
|
1487
|
+
const owner = node instanceof HTMLTemplateElement ? node.content : node;
|
|
1488
|
+
if (!owner.hasChildNodes()) continue;
|
|
1489
|
+
const nested = owner.childNodes;
|
|
1490
|
+
for (let index = nested.length - 1; index >= 0; index--) pending.push(nested[index]);
|
|
1491
|
+
}
|
|
1492
|
+
return text;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1252
1495
|
function cssEscapeIdentifier(value: string): string {
|
|
1253
1496
|
return value.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
|
|
1254
1497
|
}
|