parse-html-dom 1.0.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/LICENSE +22 -0
- package/README.md +127 -0
- package/lib/character-reference-table.js +2249 -0
- package/lib/character-references.js +164 -0
- package/lib/document.js +125 -0
- package/lib/element.js +258 -0
- package/lib/html-tags.js +129 -0
- package/lib/node.js +259 -0
- package/lib/parse-html.js +96 -0
- package/lib/selector-matcher.js +315 -0
- package/lib/selector-parser.js +565 -0
- package/lib/serialize.js +142 -0
- package/lib/tokenizer.js +481 -0
- package/lib/tree-builder.js +319 -0
- package/mod.js +9 -0
- package/package.json +43 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module character-references
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CHARACTER_REFERENCE_TABLE } from './character-reference-table.js';
|
|
6
|
+
|
|
7
|
+
const REPLACEMENT_CHARACTER = '�';
|
|
8
|
+
|
|
9
|
+
// The HTML spec's "numeric character reference end state" remaps this
|
|
10
|
+
// Windows-1252 C1 control range onto Unicode punctuation, because that is
|
|
11
|
+
// what every browser actually does with a numeric reference in this range.
|
|
12
|
+
// A code point in the range with no entry here (0x81, 0x8D, 0x8F, 0x90, 0x9D)
|
|
13
|
+
// is used as-is.
|
|
14
|
+
const WINDOWS_1252_C1_REMAP = new Map([
|
|
15
|
+
[ 0x80, 0x20AC ],
|
|
16
|
+
[ 0x82, 0x201A ],
|
|
17
|
+
[ 0x83, 0x0192 ],
|
|
18
|
+
[ 0x84, 0x201E ],
|
|
19
|
+
[ 0x85, 0x2026 ],
|
|
20
|
+
[ 0x86, 0x2020 ],
|
|
21
|
+
[ 0x87, 0x2021 ],
|
|
22
|
+
[ 0x88, 0x02C6 ],
|
|
23
|
+
[ 0x89, 0x2030 ],
|
|
24
|
+
[ 0x8A, 0x0160 ],
|
|
25
|
+
[ 0x8B, 0x2039 ],
|
|
26
|
+
[ 0x8C, 0x0152 ],
|
|
27
|
+
[ 0x8E, 0x017D ],
|
|
28
|
+
[ 0x91, 0x2018 ],
|
|
29
|
+
[ 0x92, 0x2019 ],
|
|
30
|
+
[ 0x93, 0x201C ],
|
|
31
|
+
[ 0x94, 0x201D ],
|
|
32
|
+
[ 0x95, 0x2022 ],
|
|
33
|
+
[ 0x96, 0x2013 ],
|
|
34
|
+
[ 0x97, 0x2014 ],
|
|
35
|
+
[ 0x98, 0x02DC ],
|
|
36
|
+
[ 0x99, 0x2122 ],
|
|
37
|
+
[ 0x9A, 0x0161 ],
|
|
38
|
+
[ 0x9B, 0x203A ],
|
|
39
|
+
[ 0x9C, 0x0153 ],
|
|
40
|
+
[ 0x9E, 0x017E ],
|
|
41
|
+
[ 0x9F, 0x0178 ],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function isAsciiAlphanumeric(char) {
|
|
45
|
+
return /^[A-Za-z0-9]$/.test(char);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isAsciiDigit(char) {
|
|
49
|
+
return char >= '0' && char <= '9';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isAsciiHexDigit(char) {
|
|
53
|
+
return /^[0-9A-Fa-f]$/.test(char);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function codePointToString(codePoint) {
|
|
57
|
+
if (codePoint === 0 || (codePoint >= 0xD800 && codePoint <= 0xDFFF) || codePoint > 0x10FFFF) {
|
|
58
|
+
return REPLACEMENT_CHARACTER;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (codePoint >= 0x80 && codePoint <= 0x9F) {
|
|
62
|
+
const remapped = WINDOWS_1252_C1_REMAP.get(codePoint);
|
|
63
|
+
return String.fromCodePoint(remapped === undefined ? codePoint : remapped);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return String.fromCodePoint(codePoint);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Numeric references are required to carry a trailing ";" in this project,
|
|
70
|
+
// matching the named-reference rule below. Returns null when the text at
|
|
71
|
+
// ampersandIndex is not a well-formed "&#...;" reference so the caller can
|
|
72
|
+
// fall back to literal text.
|
|
73
|
+
function decodeNumericReference(text, ampersandIndex) {
|
|
74
|
+
const isHex = text[ampersandIndex + 2] === 'x' || text[ampersandIndex + 2] === 'X';
|
|
75
|
+
const digitsStart = ampersandIndex + (isHex ? 3 : 2);
|
|
76
|
+
const isDigit = isHex ? isAsciiHexDigit : isAsciiDigit;
|
|
77
|
+
|
|
78
|
+
let cursor = digitsStart;
|
|
79
|
+
|
|
80
|
+
while (cursor < text.length && isDigit(text[cursor])) {
|
|
81
|
+
cursor += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (cursor === digitsStart || text[cursor] !== ';') {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const digits = text.slice(digitsStart, cursor);
|
|
89
|
+
const codePoint = parseInt(digits, isHex ? 16 : 10);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
text: codePointToString(codePoint),
|
|
93
|
+
nextIndex: cursor + 1,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Named references are required to carry a trailing ";" in this project.
|
|
98
|
+
// Every entity name is ASCII alphanumeric, so the run of alphanumeric
|
|
99
|
+
// characters after "&" up to the first ";" is the only possible name
|
|
100
|
+
// boundary — no shorter or longer candidate needs to be tried.
|
|
101
|
+
function decodeNamedReference(text, ampersandIndex) {
|
|
102
|
+
let cursor = ampersandIndex + 1;
|
|
103
|
+
|
|
104
|
+
while (cursor < text.length && isAsciiAlphanumeric(text[cursor])) {
|
|
105
|
+
cursor += 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (text[cursor] !== ';') {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const name = text.slice(ampersandIndex + 1, cursor + 1);
|
|
113
|
+
const replacement = CHARACTER_REFERENCE_TABLE[name];
|
|
114
|
+
|
|
115
|
+
if (replacement === undefined) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
text: replacement,
|
|
121
|
+
nextIndex: cursor + 1,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Decodes HTML character references (named, decimal, and hexadecimal) in a
|
|
127
|
+
* string of text. A malformed or unknown reference is left as literal text
|
|
128
|
+
* rather than raising an error, matching how a browser recovers from one.
|
|
129
|
+
* @param {string} text - Text that may contain character references.
|
|
130
|
+
* @returns {string} The text with every well-formed reference replaced.
|
|
131
|
+
*/
|
|
132
|
+
export function decodeCharacterReferences(text) {
|
|
133
|
+
if (!text.includes('&')) {
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let result = '';
|
|
138
|
+
let index = 0;
|
|
139
|
+
|
|
140
|
+
while (index < text.length) {
|
|
141
|
+
const ampersandIndex = text.indexOf('&', index);
|
|
142
|
+
|
|
143
|
+
if (ampersandIndex === -1) {
|
|
144
|
+
result += text.slice(index);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
result += text.slice(index, ampersandIndex);
|
|
149
|
+
|
|
150
|
+
const decoded = text[ampersandIndex + 1] === '#'
|
|
151
|
+
? decodeNumericReference(text, ampersandIndex)
|
|
152
|
+
: decodeNamedReference(text, ampersandIndex);
|
|
153
|
+
|
|
154
|
+
if (decoded) {
|
|
155
|
+
result += decoded.text;
|
|
156
|
+
index = decoded.nextIndex;
|
|
157
|
+
} else {
|
|
158
|
+
result += '&';
|
|
159
|
+
index = ampersandIndex + 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
package/lib/document.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module document
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ParentNode } from './node.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} DocumentType
|
|
9
|
+
* @property {string} name - The doctype name, lowercased.
|
|
10
|
+
* @property {string|null} publicId - The public identifier, or `null`.
|
|
11
|
+
* @property {string|null} systemId - The system identifier, or `null`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} ParseError
|
|
16
|
+
* @property {string} code - A stable, kebab-case error identifier. Adding a
|
|
17
|
+
* code is a compatible change; renaming one is a breaking change.
|
|
18
|
+
* @property {string} message - A human-readable description.
|
|
19
|
+
* @property {number} offset - 0-based index into the preprocessed source.
|
|
20
|
+
* @property {number} line - 1-based line number.
|
|
21
|
+
* @property {number} column - 1-based column number.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The root of a parsed document. Construction is internal to this package —
|
|
26
|
+
* `parseHTML` is the supported way to obtain one — but the class is
|
|
27
|
+
* exported so callers can use `instanceof Document`.
|
|
28
|
+
*
|
|
29
|
+
* `documentElement`, `head`, and `body` are always non-null: `parseHTML`
|
|
30
|
+
* synthesizes them even for empty input, so callers never need to guard
|
|
31
|
+
* against a missing one.
|
|
32
|
+
*/
|
|
33
|
+
export class Document extends ParentNode {
|
|
34
|
+
#documentElement = null;
|
|
35
|
+
#head = null;
|
|
36
|
+
#body = null;
|
|
37
|
+
#doctype = null;
|
|
38
|
+
#parseErrors = Object.freeze([]);
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super(9);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @returns {string} Always `'#document'`.
|
|
46
|
+
*/
|
|
47
|
+
get nodeName() {
|
|
48
|
+
return '#document';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @returns {Element} The root `<html>` element.
|
|
53
|
+
*/
|
|
54
|
+
get documentElement() {
|
|
55
|
+
return this.#documentElement;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @returns {Element} The `<head>` element.
|
|
60
|
+
*/
|
|
61
|
+
get head() {
|
|
62
|
+
return this.#head;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @returns {Element} The `<body>` element.
|
|
67
|
+
*/
|
|
68
|
+
get body() {
|
|
69
|
+
return this.#body;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @returns {DocumentType|null} The doctype, or `null` when the source
|
|
74
|
+
* had none. Not a node: it is not in `childNodes` or `children`.
|
|
75
|
+
*/
|
|
76
|
+
get doctype() {
|
|
77
|
+
return this.#doctype;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @returns {ParseError[]} Every recovery the parser performed, in
|
|
82
|
+
* source order. Empty for a clean document.
|
|
83
|
+
*/
|
|
84
|
+
get parseErrors() {
|
|
85
|
+
return this.#parseErrors;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @returns {Element[]} Always `[documentElement]` once parsing has set
|
|
90
|
+
* it, otherwise an empty array.
|
|
91
|
+
*/
|
|
92
|
+
get children() {
|
|
93
|
+
return this.#documentElement ? Object.freeze([ this.#documentElement ]) : Object.freeze([]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Internal: called only by the tree builder while assembling the
|
|
97
|
+
// document.
|
|
98
|
+
setDocumentElement(element) {
|
|
99
|
+
this.#documentElement = element;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Internal: called only by the tree builder while assembling the
|
|
103
|
+
// document.
|
|
104
|
+
setHead(element) {
|
|
105
|
+
this.#head = element;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Internal: called only by the tree builder while assembling the
|
|
109
|
+
// document.
|
|
110
|
+
setBody(element) {
|
|
111
|
+
this.#body = element;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Internal: called only by the tree builder while assembling the
|
|
115
|
+
// document.
|
|
116
|
+
setDoctype(doctype) {
|
|
117
|
+
this.#doctype = doctype ? Object.freeze(doctype) : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Internal: called only by the tree builder once parsing has finished
|
|
121
|
+
// collecting errors.
|
|
122
|
+
setParseErrors(errors) {
|
|
123
|
+
this.#parseErrors = Object.freeze(errors);
|
|
124
|
+
}
|
|
125
|
+
}
|
package/lib/element.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module element
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ParentNode } from './node.js';
|
|
6
|
+
import { serializeInnerHTML, serializeOuterHTML } from './serialize.js';
|
|
7
|
+
import { closest as findClosest } from './selector-matcher.js';
|
|
8
|
+
|
|
9
|
+
// Frozen {name, value} pairs, exposed through the array-like `attributes`
|
|
10
|
+
// getter below. Index access and Symbol.iterator are implemented with own
|
|
11
|
+
// numeric properties rather than extending Array, since an Array instance
|
|
12
|
+
// also carries push/splice/etc. that would misleadingly suggest the list
|
|
13
|
+
// can be mutated.
|
|
14
|
+
class AttributeList {
|
|
15
|
+
constructor(entries) {
|
|
16
|
+
entries.forEach((entry, index) => {
|
|
17
|
+
this[index] = entry;
|
|
18
|
+
});
|
|
19
|
+
this.length = entries.length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} name - Attribute name to find.
|
|
24
|
+
* @returns {{name: string, value: string}|null} The matching entry, or
|
|
25
|
+
* `null` when no attribute has this name.
|
|
26
|
+
*/
|
|
27
|
+
getNamedItem(name) {
|
|
28
|
+
for (let index = 0; index < this.length; index += 1) {
|
|
29
|
+
if (this[index].name === name) {
|
|
30
|
+
return this[index];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[Symbol.iterator]() {
|
|
37
|
+
let index = 0;
|
|
38
|
+
const { length } = this;
|
|
39
|
+
const list = this;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
next() {
|
|
43
|
+
if (index >= length) {
|
|
44
|
+
return { value: undefined, done: true };
|
|
45
|
+
}
|
|
46
|
+
const value = list[index];
|
|
47
|
+
index += 1;
|
|
48
|
+
return { value, done: false };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A read-only DOMTokenList-alike over an attribute's whitespace-separated
|
|
55
|
+
// value. Tokens are deduplicated preserving first occurrence, per the DOM's
|
|
56
|
+
// ordered-set parser, so `class="a a"` has length 1 while `value` keeps the
|
|
57
|
+
// raw string `'a a'`.
|
|
58
|
+
class ClassList {
|
|
59
|
+
#tokens;
|
|
60
|
+
#value;
|
|
61
|
+
|
|
62
|
+
constructor(value) {
|
|
63
|
+
this.#value = value;
|
|
64
|
+
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
this.#tokens = [];
|
|
67
|
+
|
|
68
|
+
for (const token of value.split(/\s+/u).filter(Boolean)) {
|
|
69
|
+
if (!seen.has(token)) {
|
|
70
|
+
seen.add(token);
|
|
71
|
+
this.#tokens.push(token);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.#tokens.forEach((token, index) => {
|
|
76
|
+
this[index] = token;
|
|
77
|
+
});
|
|
78
|
+
this.length = this.#tokens.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @returns {string} The raw, undeduplicated attribute value.
|
|
83
|
+
*/
|
|
84
|
+
get value() {
|
|
85
|
+
return this.#value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} token - Class name to test for.
|
|
90
|
+
* @returns {boolean} Whether `token` is present.
|
|
91
|
+
*/
|
|
92
|
+
contains(token) {
|
|
93
|
+
return this.#tokens.includes(token);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[Symbol.iterator]() {
|
|
97
|
+
return this.#tokens[Symbol.iterator]();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* An element node. Construction is internal to this package — `parseHTML`
|
|
103
|
+
* is the supported way to obtain one — but the class is exported so callers
|
|
104
|
+
* can use `instanceof Element`.
|
|
105
|
+
*/
|
|
106
|
+
export class Element extends ParentNode {
|
|
107
|
+
#localName;
|
|
108
|
+
#rawName;
|
|
109
|
+
#isForeign;
|
|
110
|
+
#attributesArray;
|
|
111
|
+
#attributesMap;
|
|
112
|
+
#attributeList = null;
|
|
113
|
+
#classList = null;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} localName - Lowercase element name.
|
|
117
|
+
* @param {string} rawName - Element name as the author wrote it.
|
|
118
|
+
* @param {{name: string, rawName: string, value: string}[]} attributes -
|
|
119
|
+
* Attributes in source order. Names are already lowercase.
|
|
120
|
+
* @param {boolean} isForeign - Whether this element is inside an `svg`
|
|
121
|
+
* or `math` subtree.
|
|
122
|
+
*/
|
|
123
|
+
constructor(localName, rawName, attributes, isForeign) {
|
|
124
|
+
super(1);
|
|
125
|
+
this.#localName = localName;
|
|
126
|
+
this.#rawName = rawName;
|
|
127
|
+
this.#isForeign = isForeign;
|
|
128
|
+
this.#attributesArray = attributes;
|
|
129
|
+
this.#attributesMap = new Map(attributes.map((attr) => [ attr.name, attr.value ]));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @returns {string} Same value as `tagName`, matching the DOM's
|
|
134
|
+
* `Element.nodeName`.
|
|
135
|
+
*/
|
|
136
|
+
get nodeName() {
|
|
137
|
+
return this.tagName;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @returns {string} The lowercase element name.
|
|
142
|
+
*/
|
|
143
|
+
get localName() {
|
|
144
|
+
return this.#localName;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @returns {string} The element name exactly as the author wrote it.
|
|
149
|
+
*/
|
|
150
|
+
get rawName() {
|
|
151
|
+
return this.#rawName;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @returns {boolean} Whether this element is inside an `svg` or `math`
|
|
156
|
+
* subtree.
|
|
157
|
+
*/
|
|
158
|
+
get isForeign() {
|
|
159
|
+
return this.#isForeign;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @returns {string} `rawName` for a foreign element (so `svg` reports
|
|
164
|
+
* `'svg'` and `clipPath` reports `'clipPath'`), otherwise the
|
|
165
|
+
* uppercased local name.
|
|
166
|
+
*/
|
|
167
|
+
get tagName() {
|
|
168
|
+
return this.#isForeign ? this.#rawName : this.#localName.toUpperCase();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @returns {string} The `id` attribute's value, or `''` when absent.
|
|
173
|
+
*/
|
|
174
|
+
get id() {
|
|
175
|
+
const value = this.getAttribute('id');
|
|
176
|
+
return value === null ? '' : value;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} name - Attribute name, matched case-insensitively.
|
|
181
|
+
* @returns {string|null} The attribute's value, or `null` when absent.
|
|
182
|
+
*/
|
|
183
|
+
getAttribute(name) {
|
|
184
|
+
const value = this.#attributesMap.get(name.toLowerCase());
|
|
185
|
+
return value === undefined ? null : value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {string} name - Attribute name, matched case-insensitively.
|
|
190
|
+
* @returns {boolean} Whether the attribute is present.
|
|
191
|
+
*/
|
|
192
|
+
hasAttribute(name) {
|
|
193
|
+
return this.#attributesMap.has(name.toLowerCase());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @returns {string[]} Attribute names in source order.
|
|
198
|
+
*/
|
|
199
|
+
getAttributeNames() {
|
|
200
|
+
return Object.freeze(this.#attributesArray.map((attr) => attr.name));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @returns {AttributeList} A frozen, iterable, array-like collection of
|
|
205
|
+
* `{name, value}` entries in source order.
|
|
206
|
+
*/
|
|
207
|
+
get attributes() {
|
|
208
|
+
if (!this.#attributeList) {
|
|
209
|
+
const entries = this.#attributesArray.map((attr) => Object.freeze({ name: attr.name, value: attr.value }));
|
|
210
|
+
this.#attributeList = Object.freeze(new AttributeList(entries));
|
|
211
|
+
}
|
|
212
|
+
return this.#attributeList;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @returns {ClassList} A read-only, iterable, array-like view of the
|
|
217
|
+
* `class` attribute's tokens.
|
|
218
|
+
*/
|
|
219
|
+
get classList() {
|
|
220
|
+
if (!this.#classList) {
|
|
221
|
+
this.#classList = Object.freeze(new ClassList(this.getAttribute('class') ?? ''));
|
|
222
|
+
}
|
|
223
|
+
return this.#classList;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @returns {string} HTML for this element's children only.
|
|
228
|
+
*/
|
|
229
|
+
get innerHTML() {
|
|
230
|
+
return serializeInnerHTML(this);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @returns {string} HTML for this element, including its own tags.
|
|
235
|
+
*/
|
|
236
|
+
get outerHTML() {
|
|
237
|
+
return serializeOuterHTML(this);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} selector - A CSS selector.
|
|
242
|
+
* @returns {Element|null} This element if it matches `selector`,
|
|
243
|
+
* otherwise the nearest matching ancestor, stopping at
|
|
244
|
+
* `documentElement`. `null` when nothing matches.
|
|
245
|
+
* @throws {SelectorSyntaxError} When `selector` is invalid.
|
|
246
|
+
*/
|
|
247
|
+
closest(selector) {
|
|
248
|
+
return findClosest(this, selector);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Internal: called only by serialize.js. The public `attributes` getter
|
|
252
|
+
// and `getAttributeNames()` intentionally hide rawName casing behind
|
|
253
|
+
// lowercase names; serialization is the one place that needs it back,
|
|
254
|
+
// to reproduce an author's "viewBox" rather than "viewbox".
|
|
255
|
+
getAttributesForSerialization() {
|
|
256
|
+
return this.#attributesArray;
|
|
257
|
+
}
|
|
258
|
+
}
|
package/lib/html-tags.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module html-tags
|
|
3
|
+
*
|
|
4
|
+
* Lookup tables that classify HTML element names. Every table is keyed (or,
|
|
5
|
+
* for the auto-close table, valued) by lowercase element name. Consumers must
|
|
6
|
+
* not mutate the exported `Set` and `Map` instances; ES module bindings keep
|
|
7
|
+
* the exported names themselves from being reassigned, but the collections
|
|
8
|
+
* are ordinary mutable objects by convention only.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Elements that never have children and never have an end tag.
|
|
13
|
+
* @type {Set<string>}
|
|
14
|
+
*/
|
|
15
|
+
export const VOID_ELEMENTS = new Set([
|
|
16
|
+
'area',
|
|
17
|
+
'base',
|
|
18
|
+
'br',
|
|
19
|
+
'col',
|
|
20
|
+
'embed',
|
|
21
|
+
'hr',
|
|
22
|
+
'img',
|
|
23
|
+
'input',
|
|
24
|
+
'link',
|
|
25
|
+
'meta',
|
|
26
|
+
'source',
|
|
27
|
+
'track',
|
|
28
|
+
'wbr',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Elements whose content is raw text: no tags and no character references
|
|
33
|
+
* are recognized inside.
|
|
34
|
+
* @type {Set<string>}
|
|
35
|
+
*/
|
|
36
|
+
export const RAW_TEXT_ELEMENTS = new Set([
|
|
37
|
+
'script',
|
|
38
|
+
'style',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Elements whose content is RCDATA: character references are recognized,
|
|
43
|
+
* tags are not.
|
|
44
|
+
* @type {Set<string>}
|
|
45
|
+
*/
|
|
46
|
+
export const RCDATA_ELEMENTS = new Set([
|
|
47
|
+
'title',
|
|
48
|
+
'textarea',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Elements that root a foreign-content subtree.
|
|
53
|
+
* @type {Set<string>}
|
|
54
|
+
*/
|
|
55
|
+
export const FOREIGN_ROOT_ELEMENTS = new Set([
|
|
56
|
+
'svg',
|
|
57
|
+
'math',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Elements that belong in the implied `<head>` while it is still open.
|
|
62
|
+
* @type {Set<string>}
|
|
63
|
+
*/
|
|
64
|
+
export const HEAD_ELEMENTS = new Set([
|
|
65
|
+
'base',
|
|
66
|
+
'link',
|
|
67
|
+
'meta',
|
|
68
|
+
'noscript',
|
|
69
|
+
'script',
|
|
70
|
+
'style',
|
|
71
|
+
'template',
|
|
72
|
+
'title',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Maps an open element's name to the set of start tag names that close it.
|
|
77
|
+
* Read an entry as: "if this element is open and one of these start tags
|
|
78
|
+
* arrives, close it first." The tree builder applies this repeatedly so
|
|
79
|
+
* nested cases unwind correctly.
|
|
80
|
+
* @type {Map<string, Set<string>>}
|
|
81
|
+
*/
|
|
82
|
+
export const AUTO_CLOSED_BY = new Map([
|
|
83
|
+
[ 'p', new Set([
|
|
84
|
+
'address',
|
|
85
|
+
'article',
|
|
86
|
+
'aside',
|
|
87
|
+
'blockquote',
|
|
88
|
+
'details',
|
|
89
|
+
'div',
|
|
90
|
+
'dl',
|
|
91
|
+
'fieldset',
|
|
92
|
+
'figcaption',
|
|
93
|
+
'figure',
|
|
94
|
+
'footer',
|
|
95
|
+
'form',
|
|
96
|
+
'h1',
|
|
97
|
+
'h2',
|
|
98
|
+
'h3',
|
|
99
|
+
'h4',
|
|
100
|
+
'h5',
|
|
101
|
+
'h6',
|
|
102
|
+
'header',
|
|
103
|
+
'hgroup',
|
|
104
|
+
'hr',
|
|
105
|
+
'main',
|
|
106
|
+
'menu',
|
|
107
|
+
'nav',
|
|
108
|
+
'ol',
|
|
109
|
+
'p',
|
|
110
|
+
'pre',
|
|
111
|
+
'section',
|
|
112
|
+
'table',
|
|
113
|
+
'ul',
|
|
114
|
+
]) ],
|
|
115
|
+
[ 'li', new Set([ 'li' ]) ],
|
|
116
|
+
[ 'dt', new Set([ 'dt', 'dd' ]) ],
|
|
117
|
+
[ 'dd', new Set([ 'dt', 'dd' ]) ],
|
|
118
|
+
[ 'option', new Set([ 'option', 'optgroup' ]) ],
|
|
119
|
+
[ 'optgroup', new Set([ 'optgroup' ]) ],
|
|
120
|
+
[ 'tr', new Set([ 'tr' ]) ],
|
|
121
|
+
[ 'td', new Set([ 'td', 'th', 'tr' ]) ],
|
|
122
|
+
[ 'th', new Set([ 'td', 'th', 'tr' ]) ],
|
|
123
|
+
[ 'thead', new Set([ 'tbody', 'tfoot' ]) ],
|
|
124
|
+
[ 'tbody', new Set([ 'tbody', 'tfoot' ]) ],
|
|
125
|
+
[ 'caption', new Set([ 'thead', 'tbody', 'tfoot', 'tr' ]) ],
|
|
126
|
+
[ 'colgroup', new Set([ 'thead', 'tbody', 'tfoot', 'tr' ]) ],
|
|
127
|
+
[ 'rt', new Set([ 'rt', 'rp' ]) ],
|
|
128
|
+
[ 'rp', new Set([ 'rt', 'rp' ]) ],
|
|
129
|
+
]);
|