1 | const selfClosing = ['input', 'link', 'meta', 'hr', 'br', 'source', 'img'];
|
2 |
|
3 | function without(arr, element, attr) {
|
4 | let idx;
|
5 | arr.forEach((node, index) => {
|
6 | if ((attr && node[attr] === element) || node === element) {
|
7 | idx = index;
|
8 | }
|
9 | });
|
10 | arr.splice(idx, 1);
|
11 | return arr;
|
12 | }
|
13 |
|
14 | function isCustomSelfClosing(tagName) {
|
15 | let tagList = module.exports.options.customSelfClosingTags;
|
16 | if (!tagList) return false;
|
17 | if (Array.isArray(tagList)) {
|
18 | return tagList.indexOf(tagName) >= 0;
|
19 | }
|
20 | if (tagList instanceof RegExp) {
|
21 | return tagList.test(tagName);
|
22 | }
|
23 | if (typeof tagList === 'string') {
|
24 | return (new RegExp(tagList)).test(tagName);
|
25 | }
|
26 | throw new Error('Unknown custom self closing tag list format. Please use an array with tag names, a regular expression or a string');
|
27 | }
|
28 |
|
29 | function parseAttributes(node, attributes) {
|
30 | attributes = (attributes || '').trim();
|
31 | if (attributes.length <= 0) {
|
32 | return;
|
33 | }
|
34 | let match = [];
|
35 | let position = 0;
|
36 | let charCode = attributes.charCodeAt(position);
|
37 | while (charCode >= 65 && charCode <= 90 ||
|
38 | charCode >= 97 && charCode <= 122 ||
|
39 | charCode >= 48 && charCode <= 57 ||
|
40 | charCode === 58 || charCode === 45 ||
|
41 | charCode === 95) {
|
42 | match[1] = (match[1] || '') + attributes.charAt(position);
|
43 | charCode = attributes.charCodeAt(++position);
|
44 | }
|
45 | attributes = attributes.substr(position).trim();
|
46 | if (attributes[0] !== '=') {
|
47 | if (attributes[0] === '<') {
|
48 | throw new Error('Unexpected markup while parsing attributes for ' + node.tagName + ' at ' + attributes);
|
49 | }
|
50 | node.setAttribute(match[1], match[1]);
|
51 | parseAttributes(node, attributes);
|
52 | } else {
|
53 | attributes = attributes.substr(1).trim();
|
54 | if (attributes[0] === '"' || attributes[0] === "'") {
|
55 |
|
56 | position = 1;
|
57 | while (attributes[position] !== attributes[0]) {
|
58 | match[2] = (match[2] || '') + attributes[position];
|
59 | position += 1;
|
60 | }
|
61 | attributes = attributes.substr(position + 1);
|
62 | } else {
|
63 | match[2] = attributes.split(' ')[0];
|
64 | attributes = attributes.split(' ').slice(1).join(' ');
|
65 | }
|
66 | node.setAttribute(match[1], match[2]);
|
67 | if (match[1] === 'class') {
|
68 | node.classList.add(match[2]);
|
69 | } else if (match[1] === 'title' || match[1] === 'id' || match[1] === 'name') {
|
70 | node[match[1]] = match[2];
|
71 | }
|
72 | return parseAttributes(node, attributes);
|
73 | }
|
74 | }
|
75 |
|
76 | function getNextTag(html, position = -1) {
|
77 | let match = null;
|
78 | if (position < 0) {
|
79 | position = html.indexOf('<');
|
80 | }
|
81 |
|
82 | if (position >= 0 && position < html.length) {
|
83 | match = [];
|
84 | match.index = position;
|
85 | position += 1;
|
86 | if (html[position] === '/') {
|
87 | match[1] = '/';
|
88 | position += 1;
|
89 | }
|
90 | let charCode = html.charCodeAt(position);
|
91 |
|
92 | while (charCode >= 65 && charCode <= 90 ||
|
93 | charCode >= 97 && charCode <= 122 ||
|
94 | charCode >= 48 && charCode <= 57 ||
|
95 | charCode === 58 || charCode === 45 ||
|
96 | charCode === 95) {
|
97 | match[2] = (match[2] || '') + html.charAt(position);
|
98 | charCode = html.charCodeAt(++position);
|
99 | }
|
100 | if (!match[2]) {
|
101 | return getNextTag(html, html.indexOf('<', position));
|
102 | }
|
103 | let startAttrs = position;
|
104 | let isInAttributeValue = false;
|
105 | while (position < html.length && (html[position] !== '>' || isInAttributeValue)) {
|
106 | if (html[position] === '"') isInAttributeValue = !isInAttributeValue;
|
107 | position++;
|
108 | }
|
109 | if (position < html.length) {
|
110 | let endAttrs = position;
|
111 | if (html[position - 1] === '/') {
|
112 | match[4] = '/';
|
113 | endAttrs = position - 1;
|
114 | }
|
115 | if (endAttrs - startAttrs > 1) {
|
116 |
|
117 | match[3] = html.substring(startAttrs, endAttrs);
|
118 | }
|
119 | }
|
120 | match[0] = html.substring(match.index, position + 1);
|
121 | }
|
122 | return match;
|
123 | }
|
124 |
|
125 | let level = [];
|
126 | function parse(document, html, parentNode) {
|
127 | let match;
|
128 |
|
129 | while (match = getNextTag(html)) {
|
130 | if (match[1]) {
|
131 |
|
132 | if (level.length === 0) throw new Error('Unexpected closing tag ' + match[2]);
|
133 | let closed = level.pop();
|
134 | if (closed !== match[2]) throw new Error('Unexpected closing tag ' + match[2] + '; expected ' + closed);
|
135 | let content = html.substring(0, match.index);
|
136 | if (content) {
|
137 | parentNode.appendChild(document.createTextNode(content));
|
138 | }
|
139 | return html.substr(match.index + match[0].length);
|
140 | } else {
|
141 |
|
142 | let content = html.substring(0, match.index);
|
143 | if (content) {
|
144 | parentNode.appendChild(document.createTextNode(content));
|
145 | }
|
146 | let node = document.createElement(match[2]);
|
147 | parseAttributes(node, match[3]);
|
148 | if (!match[4] && selfClosing.indexOf(match[2]) < 0 && !isCustomSelfClosing(match[2])) {
|
149 | level.push(match[2]);
|
150 | html = parse(document, html.substr(match.index + match[0].length), node);
|
151 | } else {
|
152 | html = html.substr(match.index + match[0].length);
|
153 | }
|
154 | parentNode.appendChild(node);
|
155 | }
|
156 | }
|
157 | if (level.length > 0) {
|
158 | throw new Error('Unclosed tag' + (level.length > 1 ? 's ' : ' ') + level.join(', '));
|
159 | }
|
160 | if (html.length > 0) {
|
161 | parentNode.appendChild(document.createTextNode(html));
|
162 | }
|
163 | return html;
|
164 | }
|
165 |
|
166 |
|
167 | const regExp = function(name) {
|
168 | return new RegExp('(^| )'+ name +'( |$)');
|
169 | };
|
170 | const forEach = function(list, fn, scope) {
|
171 | for (let i = 0; i < list.length; i++) {
|
172 | fn.call(scope, list[i]);
|
173 | }
|
174 | };
|
175 |
|
176 |
|
177 | function ClassList(element) {
|
178 | this.element = element;
|
179 | }
|
180 |
|
181 | ClassList.prototype = {
|
182 | add: function() {
|
183 | forEach(arguments, function(name) {
|
184 | if (!this.contains(name)) {
|
185 | this.element.className += this.element.className.length > 0 ? ' ' + name : name;
|
186 | }
|
187 | }, this);
|
188 | },
|
189 | remove: function() {
|
190 | forEach(arguments, function(name) {
|
191 | this.element.className =
|
192 | this.element.className.replace(regExp(name), '');
|
193 | }, this);
|
194 | },
|
195 | toggle: function(name) {
|
196 | return this.contains(name)
|
197 | ? (this.remove(name), false) : (this.add(name), true);
|
198 | },
|
199 | contains: function(name) {
|
200 | return regExp(name).test(this.element.className);
|
201 | },
|
202 |
|
203 | replace: function(oldName, newName) {
|
204 | this.remove(oldName), this.add(newName);
|
205 | }
|
206 | };
|
207 |
|
208 | function matchesSelector(tag, selector) {
|
209 | let selectors = selector.split(/\s*,\s*/),
|
210 | match;
|
211 | for (let all in selectors) {
|
212 | if (match = selectors[all].match(/(?:([\w*:_-]+)?\[([\w:_-]+)(?:(\$|\^|\*)?=(?:(?:'([^']*)')|(?:"([^"]*)")))?\])|(?:\.([\w_-]+))|([\w*:_-]+)/g)) {
|
213 | let value = RegExp.$4 || RegExp.$5;
|
214 | if (RegExp.$7 === tag.tagName || RegExp.$7 === '*') return true;
|
215 | if (RegExp.$6 && tag.classList.contains(RegExp.$6)) return true;
|
216 | if (RegExp.$1 && tag.tagName !== RegExp.$1) continue;
|
217 | let attribute = tag.getAttribute(RegExp.$2);
|
218 | if (!RegExp.$3 && !value && typeof tag.attributes[RegExp.$2] !== 'undefined') return true;
|
219 | if (!RegExp.$3 && value && attribute === value) return true;
|
220 | if (RegExp.$3 && RegExp.$3 === '^' && attribute.indexOf(value) === 0) return true;
|
221 | if (RegExp.$3 && RegExp.$3 === '$' && attribute.match(new RegExp(value + '$'))) return true;
|
222 | if (RegExp.$3 && RegExp.$3 === '*' && attribute.indexOf(value) >= 0) return true;
|
223 | }
|
224 | }
|
225 | return false;
|
226 | }
|
227 |
|
228 | function findElements(start, filterFn) {
|
229 | let result = [];
|
230 | start.children.forEach(child => {
|
231 | result = result.concat(filterFn(child) ? child : [], findElements(child, filterFn));
|
232 | });
|
233 | return result;
|
234 | }
|
235 |
|
236 | function HTMLElement(name, owner) {
|
237 | this.nodeType = 1;
|
238 | this.nodeName = name;
|
239 | this.tagName = name;
|
240 | this.className = '';
|
241 | this.childNodes = [];
|
242 | this.style = {};
|
243 | this.ownerDocument = owner;
|
244 | this.parentNode = null;
|
245 | this.attributes = [];
|
246 | }
|
247 |
|
248 | Object.defineProperty(HTMLElement.prototype, 'children', {
|
249 | get: function() { return this.childNodes.filter(node => node.nodeType === 1) }
|
250 | });
|
251 | Object.defineProperty(HTMLElement.prototype, 'classList', {
|
252 | get: function() { return new ClassList(this); }
|
253 | });
|
254 | Object.defineProperty(HTMLElement.prototype, 'innerHTML', {
|
255 | get: function() {
|
256 | return this.childNodes.map(tag => tag.nodeType === 1 ? tag.outerHTML : tag.nodeValue).join('');
|
257 | },
|
258 | set: function (value) {
|
259 | this.childNodes = [];
|
260 | level = [];
|
261 | parse(this.ownerDocument, value, this);
|
262 | }
|
263 | });
|
264 | Object.defineProperty(HTMLElement.prototype, 'outerHTML', {
|
265 | get: function() {
|
266 | if (Object.prototype.toString.call(this.attributes) !== '[object Array]') {
|
267 | this.attributes = Object.keys(this.attributes).map(entry => ({name: entry, value: this.attributes[entry]}));
|
268 | this.attributes.forEach((attr, idx, arr) => {
|
269 | this.attributes[attr.name] = attr.value;
|
270 | });
|
271 | }
|
272 | let attributes = this.attributes.map(attr => `${attr.name}="${typeof attr.value === 'undefined'?'':attr.value}"`).join(' ');
|
273 | if (selfClosing.indexOf(this.tagName) >= 0 || isCustomSelfClosing(this.tagName)) {
|
274 | return `<${this.tagName}${attributes ? ' ' + attributes : ''}/>`;
|
275 | } else {
|
276 | return `<${this.tagName}${attributes ? ' ' + attributes : ''}>${this.innerHTML}</${this.tagName}>`;
|
277 | }
|
278 | }
|
279 | });
|
280 | HTMLElement.prototype.appendChild = function(child) {
|
281 | this.childNodes.push(child);
|
282 | child.parentNode = this;
|
283 | };
|
284 | HTMLElement.prototype.removeChild = function(child) {
|
285 | let idx = this.childNodes.indexOf(child);
|
286 | if (idx >= 0) this.childNodes.splice(idx, 1);
|
287 | };
|
288 | HTMLElement.prototype.setAttribute = function(name, value) {
|
289 | let obj = {name, value};
|
290 | if (this.attributes[name]) {
|
291 | this.attributes[this.attributes.indexOf(this.attributes[name])] = obj;
|
292 | } else {
|
293 | this.attributes.push(obj);
|
294 | }
|
295 | this.attributes[name] = obj;
|
296 | if (name === 'class') this.className = value;
|
297 | };
|
298 | HTMLElement.prototype.removeAttribute = function(name) {
|
299 | let idx = this.attributes.indexOf(this.attributes[name]);
|
300 | if (idx >= 0) {
|
301 | this.attributes.splice(idx, 1);
|
302 | }
|
303 | delete this.attributes[name];
|
304 | };
|
305 | HTMLElement.prototype.getAttribute = function(name) { return this.attributes[name] && this.attributes[name].value || ''; };
|
306 | HTMLElement.prototype.replaceChild = function(newChild, toReplace) {
|
307 | let idx = this.childNodes.indexOf(toReplace);
|
308 | this.childNodes.splice(idx, 1, newChild);
|
309 | newChild.parentNode = this;
|
310 | };
|
311 | HTMLElement.prototype.addEventListener = function() {};
|
312 | HTMLElement.prototype.removeEventListener = function() {};
|
313 | HTMLElement.prototype.getElementsByTagName = function(tagName) {
|
314 | return findElements(this, el => ((tagName === '*' && el.tagName) || el.tagName === tagName));
|
315 | };
|
316 | HTMLElement.prototype.getElementsByClassName = function(className) {
|
317 | return findElements(this, el => el.classList.contains(className));
|
318 | };
|
319 | HTMLElement.prototype.querySelectorAll = function(selector) {
|
320 | return findElements(this, el => matchesSelector(el, selector));
|
321 | };
|
322 | HTMLElement.prototype.getElementById = function(id) {
|
323 | return findElements(this, el => el.getAttribute('id') === id)[0];
|
324 | };
|
325 |
|
326 | function DOMText(content, owner) {
|
327 | this.nodeValue = content;
|
328 | this.nodeType = 3;
|
329 | this.parentNode = null;
|
330 | this.ownerDocument = owner;
|
331 | }
|
332 |
|
333 | export default function Document(html) {
|
334 | if (!this instanceof Document) {
|
335 | return new Document(html);
|
336 | }
|
337 |
|
338 | this.createElement = name => new HTMLElement(name, this);
|
339 | this.createTextNode = content => new DOMText(content, this);
|
340 | this.getElementById = HTMLElement.prototype.getElementById.bind(this);
|
341 | this.getElementsByTagName = HTMLElement.prototype.getElementsByTagName.bind(this);
|
342 | this.getElementsByClassName = HTMLElement.prototype.getElementsByClassName.bind(this);
|
343 | this.querySelectorAll = HTMLElement.prototype.querySelectorAll.bind(this);
|
344 | this.addEventListener = () => {};
|
345 | this.removeEventListener = () => {};
|
346 |
|
347 | this.documentElement = this.createElement('html');
|
348 | this.childNodes = [this.documentElement];
|
349 | this.children = [this.documentElement];
|
350 | this.nodeType = 9;
|
351 |
|
352 | if (typeof html !== 'string' || html.trim().indexOf('<!DOCTYPE') < 0) {
|
353 | this.head = this.createElement('head');
|
354 | this.body = this.createElement('body');
|
355 | this.documentElement.appendChild(this.head);
|
356 | this.documentElement.appendChild(this.body);
|
357 | if (typeof html === 'string') {
|
358 | level = [];
|
359 | parse(this, html, this.body);
|
360 | }
|
361 | } else {
|
362 | html.match(/<html([^>]*)>/);
|
363 | if (RegExp.$1) {
|
364 | parseAttributes(this.documentElement, RegExp.$1);
|
365 | }
|
366 | html = html.replace(/<!DOCTYPE[^>]+>[\n\s]*<html([^>]*)>/g, '').replace(/<\/html>/g, '');
|
367 | level = [];
|
368 | parse(this, html, this.documentElement);
|
369 | this.head = this.getElementsByTagName('head')[0];
|
370 | this.body = this.getElementsByTagName('body')[0];
|
371 | }
|
372 | }
|
373 |
|
374 | module.exports = Document;
|
375 | module.exports.DOMElement = HTMLElement;
|
376 | module.exports.DOMText = DOMText;
|
377 | module.exports.options = {
|
378 | customSelfClosingTags: null
|
379 | };
|
380 | typeof global !== 'undefined' && (global.HTMLElement = HTMLElement);
|