domino 2.1.0 → 2.1.4

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 CHANGED
@@ -1,3 +1,22 @@
1
+ # domino 2.1.4 (16 Dec 2019)
2
+ * Bug fix for `Element#closest` when selector doesn't match (#154)
3
+
4
+ # domino 2.1.3 (6 Mar 2019)
5
+ * Bug fix for CSS `$=` selector and for matches on root `<html>` element.
6
+ * Renamed CSS `:matches` to `:is`
7
+ ( https://github.com/w3c/csswg-drafts/issues/3258 )
8
+ * Bug fix for CSS matches with escape characters in tag name.
9
+
10
+ # domino 2.1.2 (14 Feb 2019)
11
+ * Allow writable Element constructors unless __domino_frozen__ is set to true (#138)
12
+ * Bug fix for CSS `$=` selector. (#135)
13
+ * Move `Node#_serializeOne()` to `NodeUtils.serializeOne()` to reduce pressure
14
+ on the megamorphic stub cache in V8, and thereby improve throughput (#142).
15
+ * Implemented `HTMLOptionElement#text` and `HTMLOptionElement#value` (#136)
16
+
17
+ # domino 2.1.1 (30 Nov 2018)
18
+ * Add `domino.createIncrementalHTMLParser` interface.
19
+
1
20
  # domino 2.1.0 (13 Aug 2018)
2
21
  * Fix `ContainerNode#removeChildren()` when there is more than one child (#129)
3
22
  * Implement `Document#scrollingElement` (#107)
package/README.md CHANGED
@@ -63,6 +63,31 @@ console.log(h1.innerHTML);
63
63
  console.log(h1 instanceof Element);
64
64
  ```
65
65
 
66
+ There is also an incremental parser available, if you need to interleave
67
+ parsing with other processing:
68
+ ```javascript
69
+ var domino = require('domino');
70
+
71
+ var pauseAfter = function(ms) {
72
+ var start = Date.now();
73
+ return function() { return (Date.now() - start) >= ms; };
74
+ };
75
+
76
+ var incrParser = domino.createIncrementalHTMLParser();
77
+ incrParser.write('<p>hello<');
78
+ incrParser.write('b>&am');
79
+ incrParser.process(pauseAfter(1/*ms*/)); // can interleave processing
80
+ incrParser.write('p;');
81
+ // ...etc...
82
+ incrParser.end(); // when done writing the document
83
+
84
+ while (incrParser.process(pauseAfter(10/*ms*/))) {
85
+ // ...do other housekeeping...
86
+ }
87
+
88
+ console.log(incrParser.document().outerHTML);
89
+ ```
90
+
66
91
  If you want a more standards-compliant way to create a `Document`, you can
67
92
  also use [DOMImplementation](https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation):
68
93
  ```javascript
package/lib/Element.js CHANGED
@@ -7,6 +7,7 @@ var NAMESPACE = utils.NAMESPACE;
7
7
  var attributes = require('./attributes');
8
8
  var Node = require('./Node');
9
9
  var NodeList = require('./NodeList');
10
+ var NodeUtils = require('./NodeUtils');
10
11
  var FilteredElementList = require('./FilteredElementList');
11
12
  var DOMException = require('./DOMException');
12
13
  var DOMTokenList = require('./DOMTokenList');
@@ -98,7 +99,15 @@ Element.prototype = Object.create(ContainerNode.prototype, {
98
99
  // "the attribute must return the result of running the HTML fragment
99
100
  // serialization algorithm on a fictional node whose only child is
100
101
  // the context object"
101
- return this._serializeOne({ nodeType: 0 });
102
+ //
103
+ // The serialization logic is intentionally implemented in a separate
104
+ // `NodeUtils` helper instead of the more obvious choice of a private
105
+ // `_serializeOne()` method on the `Node.prototype` in order to avoid
106
+ // the megamorphic `this._serializeOne` property access, which reduces
107
+ // performance unnecessarily. If you need specialized behavior for a
108
+ // certain subclass, you'll need to implement that in `NodeUtils`.
109
+ // See https://github.com/fgnass/domino/pull/142 for more information.
110
+ return NodeUtils.serializeOne(this, { nodeType: 0 });
102
111
  },
103
112
  set: function(v) {
104
113
  var document = this.ownerDocument;
@@ -913,8 +922,11 @@ Element.prototype = Object.create(ContainerNode.prototype, {
913
922
 
914
923
  closest: { value: function(selector) {
915
924
  var el = this;
916
- while (el.matches && !el.matches(selector)) el = el.parentNode;
917
- return el.matches ? el : null;
925
+ do {
926
+ if (el.matches && el.matches(selector)) { return el; }
927
+ el = el.parentElement || el.parentNode;
928
+ } while (el !== null && el.nodeType === Node.ELEMENT_NODE);
929
+ return null;
918
930
  }},
919
931
 
920
932
  querySelector: { value: function(selector) {
package/lib/HTMLParser.js CHANGED
@@ -2035,13 +2035,15 @@ function HTMLParser(address, fragmentContext, options) {
2035
2035
  // text to be parsed, and should be false or omitted otherwise.
2036
2036
  // The second argument must not be set for recursive invocations
2037
2037
  // from document.write()
2038
- parse: function(s, end) {
2038
+ parse: function(s, end, shouldPauseFunc) {
2039
+ var moreToDo;
2039
2040
 
2040
2041
  // If we're paused, remember the text to parse, but
2041
2042
  // don't parse it now.
2043
+ // (Don't invoke shouldPauseFunc because we haven't handled 'end' yet.)
2042
2044
  if (paused > 0) {
2043
2045
  leftovers += s;
2044
- return;
2046
+ return true; // more to do
2045
2047
  }
2046
2048
 
2047
2049
 
@@ -2077,7 +2079,7 @@ function HTMLParser(address, fragmentContext, options) {
2077
2079
  }
2078
2080
 
2079
2081
  reentrant_invocations++;
2080
- scanChars();
2082
+ moreToDo = scanChars(shouldPauseFunc);
2081
2083
  leftovers = chars.substring(nextchar, numchars);
2082
2084
  reentrant_invocations--;
2083
2085
  }
@@ -2096,6 +2098,7 @@ function HTMLParser(address, fragmentContext, options) {
2096
2098
 
2097
2099
  // Now scan as many of these new chars as we can
2098
2100
  scanChars();
2101
+ moreToDo = false;
2099
2102
 
2100
2103
  leftovers = chars.substring(nextchar, numchars);
2101
2104
 
@@ -2117,6 +2120,7 @@ function HTMLParser(address, fragmentContext, options) {
2117
2120
  // Decrement the counter
2118
2121
  reentrant_invocations--;
2119
2122
  }
2123
+ return moreToDo;
2120
2124
  }
2121
2125
  };
2122
2126
 
@@ -2191,7 +2195,7 @@ function HTMLParser(address, fragmentContext, options) {
2191
2195
  // (This may leave 1 or more characters in the buffer: like a CR
2192
2196
  // waiting to see if the next char is LF, or for states that require
2193
2197
  // lookahead...)
2194
- function scanChars() {
2198
+ function scanChars(shouldPauseFunc) {
2195
2199
  var codepoint, s, pattern, eof;
2196
2200
 
2197
2201
  while(nextchar < numchars) {
@@ -2199,8 +2203,8 @@ function HTMLParser(address, fragmentContext, options) {
2199
2203
  // If we just tokenized a </script> tag, then the paused flag
2200
2204
  // may have been set to tell us to stop tokenizing while
2201
2205
  // the script is loading
2202
- if (paused > 0) {
2203
- return;
2206
+ if (paused > 0 || (shouldPauseFunc && shouldPauseFunc())) {
2207
+ return true;
2204
2208
  }
2205
2209
 
2206
2210
 
@@ -2274,7 +2278,7 @@ function HTMLParser(address, fragmentContext, options) {
2274
2278
  }
2275
2279
  else {
2276
2280
  // Return now and wait for more chars later
2277
- return;
2281
+ return true;
2278
2282
  }
2279
2283
  }
2280
2284
  tokenizer(codepoint, s, eof);
@@ -2291,7 +2295,7 @@ function HTMLParser(address, fragmentContext, options) {
2291
2295
  }
2292
2296
  else { // No match
2293
2297
  // If more characters coming, wait for them
2294
- if (!input_complete) return;
2298
+ if (!input_complete) return true;
2295
2299
 
2296
2300
  // Otherwise, we've got to return what we've got
2297
2301
  s = chars.substring(nextchar, numchars);
@@ -2307,6 +2311,7 @@ function HTMLParser(address, fragmentContext, options) {
2307
2311
  break;
2308
2312
  }
2309
2313
  }
2314
+ return false; // no more characters to scan!
2310
2315
  }
2311
2316
 
2312
2317
 
@@ -2472,6 +2477,7 @@ function HTMLParser(address, fragmentContext, options) {
2472
2477
  // A shortcut: look ahead and if this is a open or close tag
2473
2478
  // in lowercase with no spaces and no attributes, just emit it now.
2474
2479
  function emitSimpleTag() {
2480
+ if (nextchar === numchars) { return false; /* not even 1 char left */ }
2475
2481
  SIMPLETAG.lastIndex = nextchar;
2476
2482
  var matched = SIMPLETAG.exec(chars);
2477
2483
  if (!matched) throw new Error("should never happen");
package/lib/Node.js CHANGED
@@ -3,8 +3,8 @@ module.exports = Node;
3
3
 
4
4
  var EventTarget = require('./EventTarget');
5
5
  var LinkedList = require('./LinkedList');
6
+ var NodeUtils = require('./NodeUtils');
6
7
  var utils = require('./utils');
7
- var NAMESPACE = utils.NAMESPACE;
8
8
 
9
9
  // All nodes have a nodeType and an ownerDocument.
10
10
  // Once inserted, they also have a parentNode.
@@ -38,45 +38,6 @@ var DOCUMENT_POSITION_CONTAINS = Node.DOCUMENT_POSITION_CONTAINS
38
38
  var DOCUMENT_POSITION_CONTAINED_BY = Node.DOCUMENT_POSITION_CONTAINED_BY = 0x10;
39
39
  var DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
40
40
 
41
- var hasRawContent = {
42
- STYLE: true,
43
- SCRIPT: true,
44
- XMP: true,
45
- IFRAME: true,
46
- NOEMBED: true,
47
- NOFRAMES: true,
48
- PLAINTEXT: true
49
- };
50
-
51
- var emptyElements = {
52
- area: true,
53
- base: true,
54
- basefont: true,
55
- bgsound: true,
56
- br: true,
57
- col: true,
58
- embed: true,
59
- frame: true,
60
- hr: true,
61
- img: true,
62
- input: true,
63
- keygen: true,
64
- link: true,
65
- meta: true,
66
- param: true,
67
- source: true,
68
- track: true,
69
- wbr: true
70
- };
71
-
72
- var extraNewLine = {
73
- /* Removed in https://github.com/whatwg/html/issues/944
74
- pre: true,
75
- textarea: true,
76
- listing: true
77
- */
78
- };
79
-
80
41
  Node.prototype = Object.create(EventTarget.prototype, {
81
42
 
82
43
  // Node that are not inserted into the tree inherit a null parent
@@ -729,78 +690,18 @@ Node.prototype = Object.create(EventTarget.prototype, {
729
690
  // This is used by the innerHTML getter
730
691
  // The serialization spec is at:
731
692
  // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#serializing-html-fragments
693
+ //
694
+ // The serialization logic is intentionally implemented in a separate
695
+ // `NodeUtils` helper instead of the more obvious choice of a private
696
+ // `_serializeOne()` method on the `Node.prototype` in order to avoid
697
+ // the megamorphic `this._serializeOne` property access, which reduces
698
+ // performance unnecessarily. If you need specialized behavior for a
699
+ // certain subclass, you'll need to implement that in `NodeUtils`.
700
+ // See https://github.com/fgnass/domino/pull/142 for more information.
732
701
  serialize: { value: function() {
733
702
  var s = '';
734
703
  for (var kid = this.firstChild; kid !== null; kid = kid.nextSibling) {
735
- s += kid._serializeOne(this);
736
- }
737
- return s;
738
- }},
739
- _serializeOne: { value: function(parent) {
740
- var kid = this, s = '';
741
- switch(kid.nodeType) {
742
- case 1: //ELEMENT_NODE
743
- var ns = kid.namespaceURI;
744
- var html = ns === NAMESPACE.HTML;
745
- var tagname = (html || ns === NAMESPACE.SVG || ns === NAMESPACE.MATHML) ? kid.localName : kid.tagName;
746
-
747
- s += '<' + tagname;
748
-
749
- for(var j = 0, k = kid._numattrs; j < k; j++) {
750
- var a = kid._attr(j);
751
- s += ' ' + attrname(a);
752
- if (a.value !== undefined) s += '="' + escapeAttr(a.value) + '"';
753
- }
754
- s += '>';
755
-
756
- if (!(html && emptyElements[tagname])) {
757
- var ss = kid.serialize();
758
- if (html && extraNewLine[tagname] && ss.charAt(0)==='\n') s += '\n';
759
- // Serialize children and add end tag for all others
760
- s += ss;
761
- s += '</' + tagname + '>';
762
- }
763
- break;
764
- case 3: //TEXT_NODE
765
- case 4: //CDATA_SECTION_NODE
766
- var parenttag;
767
- if (parent.nodeType === ELEMENT_NODE &&
768
- parent.namespaceURI === NAMESPACE.HTML)
769
- parenttag = parent.tagName;
770
- else
771
- parenttag = '';
772
-
773
- if (hasRawContent[parenttag] ||
774
- (parenttag==='NOSCRIPT' && parent.ownerDocument._scripting_enabled)) {
775
- s += kid.data;
776
- } else {
777
- s += escape(kid.data);
778
- }
779
- break;
780
- case 8: //COMMENT_NODE
781
- s += '<!--' + kid.data + '-->';
782
- break;
783
- case 7: //PROCESSING_INSTRUCTION_NODE
784
- s += '<?' + kid.target + ' ' + kid.data + '?>';
785
- break;
786
- case 10: //DOCUMENT_TYPE_NODE
787
- s += '<!DOCTYPE ' + kid.name;
788
-
789
- if (false) {
790
- // Latest HTML serialization spec omits the public/system ID
791
- if (kid.publicID) {
792
- s += ' PUBLIC "' + kid.publicId + '"';
793
- }
794
-
795
- if (kid.systemId) {
796
- s += ' "' + kid.systemId + '"';
797
- }
798
- }
799
-
800
- s += '>';
801
- break;
802
- default:
803
- utils.InvalidState();
704
+ s += NodeUtils.serializeOne(kid, this);
804
705
  }
805
706
  return s;
806
707
  }},
@@ -808,7 +709,7 @@ Node.prototype = Object.create(EventTarget.prototype, {
808
709
  // Non-standard, but often useful for debugging.
809
710
  outerHTML: {
810
711
  get: function() {
811
- return this._serializeOne({ nodeType: 0 });
712
+ return NodeUtils.serializeOne(this, { nodeType: 0 });
812
713
  },
813
714
  set: utils.nyi,
814
715
  },
@@ -835,46 +736,3 @@ Node.prototype = Object.create(EventTarget.prototype, {
835
736
  DOCUMENT_POSITION_CONTAINED_BY: { value: DOCUMENT_POSITION_CONTAINED_BY },
836
737
  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: { value: DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC },
837
738
  });
838
-
839
- function escape(s) {
840
- return s.replace(/[&<>\u00A0]/g, function(c) {
841
- switch(c) {
842
- case '&': return '&amp;';
843
- case '<': return '&lt;';
844
- case '>': return '&gt;';
845
- case '\u00A0': return '&nbsp;';
846
- }
847
- });
848
- }
849
-
850
- function escapeAttr(s) {
851
- var toEscape = /[&"\u00A0]/g;
852
- if (!toEscape.test(s)) {
853
- // nothing to do, fast path
854
- return s;
855
- } else {
856
- return s.replace(toEscape, function(c) {
857
- switch(c) {
858
- case '&': return '&amp;';
859
- case '"': return '&quot;';
860
- case '\u00A0': return '&nbsp;';
861
- }
862
- });
863
- }
864
- }
865
-
866
- function attrname(a) {
867
- var ns = a.namespaceURI;
868
- if (!ns)
869
- return a.localName;
870
- if (ns === NAMESPACE.XML)
871
- return 'xml:' + a.localName;
872
- if (ns === NAMESPACE.XLINK)
873
- return 'xlink:' + a.localName;
874
-
875
- if (ns === NAMESPACE.XMLNS) {
876
- if (a.localName === 'xmlns') return 'xmlns';
877
- else return 'xmlns:' + a.localName;
878
- }
879
- return a.name;
880
- }
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ module.exports = {
3
+ // NOTE: The `serializeOne()` function used to live on the `Node.prototype`
4
+ // as a private method `Node#_serializeOne(child)`, however that requires
5
+ // a megamorphic property access `this._serializeOne` just to get to the
6
+ // method, and this is being done on lots of different `Node` subclasses,
7
+ // which puts a lot of pressure on V8's megamorphic stub cache. So by
8
+ // moving the helper off of the `Node.prototype` and into a separate
9
+ // function in this helper module, we get a monomorphic property access
10
+ // `NodeUtils.serializeOne` to get to the function and reduce pressure
11
+ // on the megamorphic stub cache.
12
+ // See https://github.com/fgnass/domino/pull/142 for more information.
13
+ serializeOne: serializeOne
14
+ };
15
+
16
+ var utils = require('./utils');
17
+ var NAMESPACE = utils.NAMESPACE;
18
+
19
+ var hasRawContent = {
20
+ STYLE: true,
21
+ SCRIPT: true,
22
+ XMP: true,
23
+ IFRAME: true,
24
+ NOEMBED: true,
25
+ NOFRAMES: true,
26
+ PLAINTEXT: true
27
+ };
28
+
29
+ var emptyElements = {
30
+ area: true,
31
+ base: true,
32
+ basefont: true,
33
+ bgsound: true,
34
+ br: true,
35
+ col: true,
36
+ embed: true,
37
+ frame: true,
38
+ hr: true,
39
+ img: true,
40
+ input: true,
41
+ keygen: true,
42
+ link: true,
43
+ meta: true,
44
+ param: true,
45
+ source: true,
46
+ track: true,
47
+ wbr: true
48
+ };
49
+
50
+ var extraNewLine = {
51
+ /* Removed in https://github.com/whatwg/html/issues/944
52
+ pre: true,
53
+ textarea: true,
54
+ listing: true
55
+ */
56
+ };
57
+
58
+ function escape(s) {
59
+ return s.replace(/[&<>\u00A0]/g, function(c) {
60
+ switch(c) {
61
+ case '&': return '&amp;';
62
+ case '<': return '&lt;';
63
+ case '>': return '&gt;';
64
+ case '\u00A0': return '&nbsp;';
65
+ }
66
+ });
67
+ }
68
+
69
+ function escapeAttr(s) {
70
+ var toEscape = /[&"\u00A0]/g;
71
+ if (!toEscape.test(s)) {
72
+ // nothing to do, fast path
73
+ return s;
74
+ } else {
75
+ return s.replace(toEscape, function(c) {
76
+ switch(c) {
77
+ case '&': return '&amp;';
78
+ case '"': return '&quot;';
79
+ case '\u00A0': return '&nbsp;';
80
+ }
81
+ });
82
+ }
83
+ }
84
+
85
+ function attrname(a) {
86
+ var ns = a.namespaceURI;
87
+ if (!ns)
88
+ return a.localName;
89
+ if (ns === NAMESPACE.XML)
90
+ return 'xml:' + a.localName;
91
+ if (ns === NAMESPACE.XLINK)
92
+ return 'xlink:' + a.localName;
93
+
94
+ if (ns === NAMESPACE.XMLNS) {
95
+ if (a.localName === 'xmlns') return 'xmlns';
96
+ else return 'xmlns:' + a.localName;
97
+ }
98
+ return a.name;
99
+ }
100
+
101
+ function serializeOne(kid, parent) {
102
+ var s = '';
103
+ switch(kid.nodeType) {
104
+ case 1: //ELEMENT_NODE
105
+ var ns = kid.namespaceURI;
106
+ var html = ns === NAMESPACE.HTML;
107
+ var tagname = (html || ns === NAMESPACE.SVG || ns === NAMESPACE.MATHML) ? kid.localName : kid.tagName;
108
+
109
+ s += '<' + tagname;
110
+
111
+ for(var j = 0, k = kid._numattrs; j < k; j++) {
112
+ var a = kid._attr(j);
113
+ s += ' ' + attrname(a);
114
+ if (a.value !== undefined) s += '="' + escapeAttr(a.value) + '"';
115
+ }
116
+ s += '>';
117
+
118
+ if (!(html && emptyElements[tagname])) {
119
+ var ss = kid.serialize();
120
+ if (html && extraNewLine[tagname] && ss.charAt(0)==='\n') s += '\n';
121
+ // Serialize children and add end tag for all others
122
+ s += ss;
123
+ s += '</' + tagname + '>';
124
+ }
125
+ break;
126
+ case 3: //TEXT_NODE
127
+ case 4: //CDATA_SECTION_NODE
128
+ var parenttag;
129
+ if (parent.nodeType === 1 /*ELEMENT_NODE*/ &&
130
+ parent.namespaceURI === NAMESPACE.HTML)
131
+ parenttag = parent.tagName;
132
+ else
133
+ parenttag = '';
134
+
135
+ if (hasRawContent[parenttag] ||
136
+ (parenttag==='NOSCRIPT' && parent.ownerDocument._scripting_enabled)) {
137
+ s += kid.data;
138
+ } else {
139
+ s += escape(kid.data);
140
+ }
141
+ break;
142
+ case 8: //COMMENT_NODE
143
+ s += '<!--' + kid.data + '-->';
144
+ break;
145
+ case 7: //PROCESSING_INSTRUCTION_NODE
146
+ s += '<?' + kid.target + ' ' + kid.data + '?>';
147
+ break;
148
+ case 10: //DOCUMENT_TYPE_NODE
149
+ s += '<!DOCTYPE ' + kid.name;
150
+
151
+ if (false) {
152
+ // Latest HTML serialization spec omits the public/system ID
153
+ if (kid.publicID) {
154
+ s += ' PUBLIC "' + kid.publicId + '"';
155
+ }
156
+
157
+ if (kid.systemId) {
158
+ s += ' "' + kid.systemId + '"';
159
+ }
160
+ }
161
+
162
+ s += '>';
163
+ break;
164
+ default:
165
+ utils.InvalidStateError();
166
+ }
167
+ return s;
168
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  var attributes = require('./attributes');
4
4
  var sloppy = require('./sloppy');
5
+ var isApiWritable = require("./config").isApiWritable;
5
6
 
6
7
  module.exports = function(spec, defaultConstructor, tagList, tagNameToImpl) {
7
8
  var c = spec.ctor;
@@ -17,7 +18,7 @@ module.exports = function(spec, defaultConstructor, tagList, tagNameToImpl) {
17
18
  }
18
19
  }
19
20
 
20
- props.constructor = { value : c };
21
+ props.constructor = { value : c, writable: isApiWritable };
21
22
  c.prototype = Object.create((spec.superclass || defaultConstructor).prototype, props);
22
23
  if (spec.events) {
23
24
  addEventHandlers(c, spec.events);
package/lib/htmlelts.js CHANGED
@@ -830,13 +830,24 @@ define({
830
830
  if (p.localName === 'select') return p.form;
831
831
  p = p.parentNode;
832
832
  }
833
- }}
833
+ }},
834
+ value: {
835
+ get: function() { return this._getattr('value') || this.text; },
836
+ set: function(v) { this._setattr('value', v); },
837
+ },
838
+ text: {
839
+ get: function() {
840
+ // Strip and collapse whitespace
841
+ return this.textContent.replace(/[ \t\n\f\r]+/g, ' ').trim();
842
+ },
843
+ set: function(v) { this.textContent = v; },
844
+ },
845
+ // missing: index
834
846
  },
835
847
  attributes: {
836
848
  disabled: Boolean,
837
849
  defaultSelected: {name: 'selected', type: Boolean},
838
850
  label: String,
839
- value: String,
840
851
  }
841
852
  });
842
853
 
package/lib/index.js CHANGED
@@ -19,6 +19,57 @@ exports.createDocument = function(html, force) {
19
19
  return new DOMImplementation(null).createHTMLDocument("");
20
20
  };
21
21
 
22
+ exports.createIncrementalHTMLParser = function() {
23
+ var parser = new HTMLParser();
24
+ /** API for incremental parser. */
25
+ return {
26
+ /** Provide an additional chunk of text to be parsed. */
27
+ write: function(s) {
28
+ if (s.length > 0) {
29
+ parser.parse(s, false, function() { return true; });
30
+ }
31
+ },
32
+ /**
33
+ * Signal that we are done providing input text, optionally
34
+ * providing one last chunk as a parameter.
35
+ */
36
+ end: function(s) {
37
+ parser.parse(s || '', true, function() { return true; });
38
+ },
39
+ /**
40
+ * Performs a chunk of parsing work, returning at the end of
41
+ * the next token as soon as shouldPauseFunc() returns true.
42
+ * Returns true iff there is more work to do.
43
+ *
44
+ * For example:
45
+ * ```
46
+ * var incrParser = domino.createIncrementalHTMLParser();
47
+ * incrParser.end('...long html document...');
48
+ * while (true) {
49
+ * // Pause every 10ms
50
+ * var start = Date.now();
51
+ * var pauseIn10 = function() { return (Date.now() - start) >= 10; };
52
+ * if (!incrParser.process(pauseIn10)) {
53
+ * break;
54
+ * }
55
+ * ...yield to other tasks, do other housekeeping, etc...
56
+ * }
57
+ * ```
58
+ */
59
+ process: function(shouldPauseFunc) {
60
+ return parser.parse('', false, shouldPauseFunc);
61
+ },
62
+ /**
63
+ * Returns the result of the incremental parse. Valid after
64
+ * `this.end()` has been called and `this.process()` has returned
65
+ * false.
66
+ */
67
+ document: function() {
68
+ return parser.document();
69
+ },
70
+ };
71
+ };
72
+
22
73
  exports.createWindow = function(html, address) {
23
74
  var document = exports.createDocument(html);
24
75
  if (address !== undefined) { document._address = address; }
package/lib/select.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * Zest (https://github.com/chjj/zest)
5
5
  * A css selector engine.
6
6
  * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed)
7
+ * Domino version based on Zest v0.1.3 with bugfixes applied.
7
8
  */
8
9
 
9
10
  /**
@@ -55,6 +56,13 @@ var lastChild = function(el) {
55
56
  return el;
56
57
  };
57
58
 
59
+ var parentIsElement = function(n) {
60
+ if (!n.parentNode) { return false; }
61
+ var nodeType = n.parentNode.nodeType;
62
+ // The root `html` element can be a first- or last-child, too.
63
+ return nodeType === 1 || nodeType === 9;
64
+ };
65
+
58
66
  var unquote = function(str) {
59
67
  if (!str) return str;
60
68
  var ch = str[0];
@@ -164,7 +172,7 @@ var nth = function(param_, test, last) {
164
172
  , advance = !last ? next : prev;
165
173
 
166
174
  return function(el) {
167
- if (el.parentNode.nodeType !== 1) return;
175
+ if (!parentIsElement(el)) return;
168
176
 
169
177
  var rel = find(el.parentNode)
170
178
  , pos = 0;
@@ -263,14 +271,13 @@ var selectors = {
263
271
  };
264
272
  },
265
273
  ':first-child': function(el) {
266
- return !prev(el) && el.parentNode.nodeType === 1;
274
+ return !prev(el) && parentIsElement(el);
267
275
  },
268
276
  ':last-child': function(el) {
269
- return !next(el) && el.parentNode.nodeType === 1;
277
+ return !next(el) && parentIsElement(el);
270
278
  },
271
279
  ':only-child': function(el) {
272
- return !prev(el) && !next(el)
273
- && el.parentNode.nodeType === 1;
280
+ return !prev(el) && !next(el) && parentIsElement(el);
274
281
  },
275
282
  ':nth-child': function(param, last) {
276
283
  return nth(param, function() {
@@ -293,7 +300,7 @@ var selectors = {
293
300
  };
294
301
  },
295
302
  ':first-of-type': function(el) {
296
- if (el.parentNode.nodeType !== 1) return;
303
+ if (!parentIsElement(el)) return;
297
304
  var type = el.nodeName;
298
305
  /*jshint -W084 */
299
306
  while (el = prev(el)) {
@@ -302,7 +309,7 @@ var selectors = {
302
309
  return true;
303
310
  },
304
311
  ':last-of-type': function(el) {
305
- if (el.parentNode.nodeType !== 1) return;
312
+ if (!parentIsElement(el)) return;
306
313
  var type = el.nodeName;
307
314
  /*jshint -W084 */
308
315
  while (el = next(el)) {
@@ -340,9 +347,14 @@ var selectors = {
340
347
  ':focus': function(el) {
341
348
  return el === el.ownerDocument.activeElement;
342
349
  },
343
- ':matches': function(sel) {
350
+ ':is': function(sel) {
344
351
  return compileGroup(sel);
345
352
  },
353
+ // :matches is an older name for :is; see
354
+ // https://github.com/w3c/csswg-drafts/issues/3258
355
+ ':matches': function(sel) {
356
+ return selectors[':is'](sel);
357
+ },
346
358
  ':nth-match': function(param, last) {
347
359
  var args = param.split(/\s*,\s*/)
348
360
  , arg = args.shift()
@@ -519,7 +531,8 @@ var operators = {
519
531
  return attr.indexOf(val) === 0;
520
532
  },
521
533
  '$=': function(attr, val) {
522
- return attr.indexOf(val) + val.length === attr.length;
534
+ var i = attr.lastIndexOf(val);
535
+ return i !== -1 && i + val.length === attr.length;
523
536
  },
524
537
  // non-standard
525
538
  '!=': function(attr, val) {
@@ -656,7 +669,7 @@ var compile = function(sel_) {
656
669
  while (sel) {
657
670
  if (cap = rules.qname.exec(sel)) {
658
671
  sel = sel.substring(cap[0].length);
659
- qname = cap[1];
672
+ qname = decodeid(cap[1]);
660
673
  buff.push(tok(qname, true));
661
674
  } else if (cap = rules.simple.exec(sel)) {
662
675
  sel = sel.substring(cap[0].length);
@@ -731,7 +744,7 @@ var tok = function(cap, qname) {
731
744
  if (qname) {
732
745
  return cap === '*'
733
746
  ? selectors['*']
734
- : selectors.type(decodeid(cap));
747
+ : selectors.type(cap);
735
748
  }
736
749
 
737
750
  // class/id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domino",
3
- "version": "2.1.0",
3
+ "version": "2.1.4",
4
4
  "license": "BSD-2-Clause",
5
5
  "author": "Felix Gnass <fgnass@gmail.com>",
6
6
  "description": "Server-side DOM implementation based on Mozilla's dom.js",
@@ -22,9 +22,9 @@
22
22
  },
23
23
  "types": "lib/index.d.ts",
24
24
  "devDependencies": {
25
- "jquery": "^3.3.1",
26
- "jshint": "^2.9.6",
27
- "mocha": "^4.1.0",
28
- "should": "^13.2.1"
25
+ "jquery": "^3.4.1",
26
+ "jshint": "^2.10.1",
27
+ "mocha": "^5.2.0",
28
+ "should": "^13.2.3"
29
29
  }
30
30
  }
package/test/domino.js CHANGED
@@ -16,6 +16,41 @@ exports.matches = function() {
16
16
  h1.matches('h2,h1').should.equal(true);
17
17
  };
18
18
 
19
+ exports.closest = function() {
20
+ // see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
21
+ var html =
22
+ '<article>\n' +
23
+ ' <div id="div-01">Here is div-01\n' +
24
+ ' <div id="div-02">Here is div-02\n' +
25
+ ' <div id="div-03">Here is div-03</div>\n' +
26
+ ' </div>\n' +
27
+ ' </div>\n' +
28
+ '</article>';
29
+ var document = domino.createWindow(html).document;
30
+ var el = document.getElementById('div-03');
31
+ var r1 = el.closest('#div-02');
32
+ r1.id.should.equal('div-02');
33
+ var r2 = el.closest('div div');
34
+ r2.id.should.equal('div-03');
35
+ var r3 = el.closest('article > div');
36
+ r3.id.should.equal('div-01');
37
+ var r4 = el.closest(':not(div)');
38
+ r4.tagName.should.equal('ARTICLE');
39
+ var r5 = el.closest('#nothere');
40
+ (r5 === null).should.be.true();
41
+ // GH #154
42
+ var disconnected = document.createElement('div');
43
+ var r6 = disconnected.closest('#nothere');
44
+ (r6 === null).should.be.true();
45
+ var fragment = document.createDocumentFragment();
46
+ fragment.appendChild(disconnected);
47
+ var r7 = disconnected.closest('#nothere');
48
+ (r7 === null).should.be.true();
49
+ var r8 = disconnected.closest(':not(div)');
50
+ // This should not return DocumentFragment! Always should return an Element.
51
+ (r8 === null).should.be.true();
52
+ };
53
+
19
54
  exports.querySelectorAll = function() {
20
55
  var window = domino.createWindow(html);
21
56
  var d = window.document;
@@ -45,6 +80,39 @@ exports.orphanQSA = function() {
45
80
  p.querySelectorAll('p').should.have.length(0);
46
81
  };
47
82
 
83
+ // This is https://github.com/chjj/zest/issues/22
84
+ exports.tildeQSA = function() {
85
+ var document = domino.createDocument('<div class="foo-bar foo">');
86
+ var els = document.querySelectorAll('.foo');
87
+ els.should.have.length(1);
88
+ };
89
+
90
+ exports.rootQSA = function() {
91
+ // the root element should be both first-child and last-child
92
+ var document = domino.createDocument('foo');
93
+ var html = document.documentElement;
94
+ html.matches(':first-child').should.be.true();
95
+ html.matches(':last-child').should.be.true();
96
+ html.matches(':only-child').should.be.true();
97
+ html.matches(':first-of-type').should.be.true();
98
+ html.matches(':last-of-type').should.be.true();
99
+ html.matches(':nth-child(1)').should.be.true();
100
+ html.matches(':nth-child(2)').should.be.false();
101
+ html.matches(':nth-last-child(1)').should.be.true();
102
+ html.matches(':nth-last-child(2)').should.be.false();
103
+ html.matches(':nth-of-type(1)').should.be.true();
104
+ html.matches(':nth-of-type(2)').should.be.false();
105
+ html.matches(':nth-last-of-type(1)').should.be.true();
106
+ html.matches(':nth-last-of-type(2)').should.be.false();
107
+ };
108
+
109
+ exports.escapeQSA = function() {
110
+ var document = domino.createDocument('<p>foo');
111
+ // ensure that selector parsing can handle escaped characters
112
+ document.querySelectorAll('p\\22\\27p').should.have.length(0);
113
+ document.querySelectorAll('\\50').should.have.length(1);
114
+ };
115
+
48
116
  exports.gh20 = function() {
49
117
  var window = domino.createWindow('');
50
118
  var frag = window.document.createDocumentFragment();
@@ -1180,6 +1248,27 @@ exports.gh129 = function() {
1180
1248
  (span.parentNode === null).should.equal(true);
1181
1249
  };
1182
1250
 
1251
+ exports.gh135 = function() {
1252
+ var document = domino.createDocument('<a target="foobar"></a>');
1253
+ var a = document.querySelectorAll('*[target$="aaaaaaa"]');
1254
+ a.length.should.equal(0);
1255
+ };
1256
+
1257
+ exports.gh136 = function() {
1258
+ var document = domino.createDocument('<option> \f\tabc\n');
1259
+ var o = document.querySelector('option');
1260
+ o.value.should.equal('abc');
1261
+ o.text.should.equal('abc');
1262
+ o.textContent.should.equal(' \f\tabc\n');
1263
+ o.value = ' def ';
1264
+ o.value.should.equal(' def ');
1265
+ o.text.should.equal('abc');
1266
+ o.text = ' ghi ';
1267
+ o.text.should.equal('ghi');
1268
+ o.textContent.should.equal(' ghi ');
1269
+ o.outerHTML.should.equal("<option value=\" def \"> ghi </option>");
1270
+ };
1271
+
1183
1272
  // Examples from
1184
1273
  // https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML
1185
1274
  exports.outerHTML1 = function() {
@@ -1271,3 +1360,76 @@ exports.insertForeignElement = function() {
1271
1360
  '</iframe></xsl:template>'
1272
1361
  );
1273
1362
  };
1363
+
1364
+ exports.bodyNL = function() {
1365
+ var document = domino.createDocument(
1366
+ '<body></body>\n'
1367
+ );
1368
+ // This may be surprising, but it's what the spec requires.
1369
+ document.body.textContent.should.equal('\n');
1370
+ };
1371
+
1372
+ exports.testIncompleteTag = function() {
1373
+ var document = domino.createDocument('<p>hello<');
1374
+ document.body.outerHTML.should.equal('<body><p>hello&lt;</p></body>');
1375
+ };
1376
+
1377
+ exports.incrementalHTMLParser1 = function() {
1378
+ // Verify correct operation of incremental parser, fed chunks which split
1379
+ // up tokens.
1380
+ var incrParser = domino.createIncrementalHTMLParser();
1381
+ var neverPause = function() { return false; };
1382
+ incrParser.write('<p>hello<');
1383
+ incrParser.process(neverPause);
1384
+
1385
+ incrParser.write('b>foo&am');
1386
+ incrParser.process(neverPause);
1387
+
1388
+ incrParser.write('p;<');
1389
+ incrParser.process(neverPause);
1390
+
1391
+ incrParser.write('/p>');
1392
+ incrParser.process(neverPause);
1393
+
1394
+ incrParser.end();
1395
+ // omitting the pauseFunc is equivalent to passing neverPause
1396
+ incrParser.process().should.equal(/*no more to do*/false);
1397
+
1398
+ incrParser.document().outerHTML.should.equal(
1399
+ '<html><head></head><body><p>hello<b>foo&amp;</b></p></body></html>'
1400
+ );
1401
+ };
1402
+
1403
+ exports.incrementalHTMLParser2 = function() {
1404
+ // Verify correct operation of incremental parser when we only manage to
1405
+ // scan one token at each step.
1406
+ var justOneStep = function() {
1407
+ var counter = 1;
1408
+ return function() { return (counter--) <= 0; };
1409
+ };
1410
+ var incrParser = domino.createIncrementalHTMLParser();
1411
+ incrParser.write('<p>hello<');
1412
+ incrParser.write('b>foo&am');
1413
+ incrParser.write('p;<');
1414
+ incrParser.write('/p>');
1415
+ incrParser.end();
1416
+
1417
+ [
1418
+ '<html><head></head><body><p></p></body></html>',
1419
+ '<html><head></head><body><p>hello</p></body></html>',
1420
+ '<html><head></head><body><p>hello<b></b></p></body></html>',
1421
+ '<html><head></head><body><p>hello<b>foo</b></p></body></html>',
1422
+ '<html><head></head><body><p>hello<b>foo</b></p></body></html>',
1423
+ '<html><head></head><body><p>hello<b>foo</b></p></body></html>',
1424
+ '<html><head></head><body><p>hello<b>foo</b></p></body></html>',
1425
+ '<html><head></head><body><p>hello<b>foo</b></p></body></html>',
1426
+ '<html><head></head><body><p>hello<b>foo&amp;</b></p></body></html>',
1427
+ ].forEach(function(step) {
1428
+ incrParser.process(justOneStep()).should.equal(/*more to do!*/true);
1429
+ incrParser.document().outerHTML.should.equal(step);
1430
+ });
1431
+ incrParser.process(justOneStep()).should.equal(/*no more to do*/false);
1432
+ incrParser.document().outerHTML.should.equal(
1433
+ '<html><head></head><body><p>hello<b>foo&amp;</b></p></body></html>'
1434
+ );
1435
+ };
@@ -2277,10 +2277,12 @@
2277
2277
  "SVG elements should have a .dataset"
2278
2278
  ],
2279
2279
  "html/dom/elements/global-attributes/dir_auto-contained-script-L-ref.html": [
2280
- "Uncaught: Unexpected token &"
2280
+ "Uncaught: Unexpected token &",
2281
+ "Uncaught: Unexpected token '&'"
2281
2282
  ],
2282
2283
  "html/dom/elements/global-attributes/dir_auto-contained-script-L.html": [
2283
- "Uncaught: Unexpected token &"
2284
+ "Uncaught: Unexpected token &",
2285
+ "Uncaught: Unexpected token '&'"
2284
2286
  ],
2285
2287
  "html/dom/elements/global-attributes/id-attribute.html": [
2286
2288
  "User agents must associate the element with an id value for purposes of CSS.",
@@ -2944,19 +2946,24 @@
2944
2946
  "getElementsByTagNameNS() should be a live collection"
2945
2947
  ],
2946
2948
  "dom/nodes/Element-childElement-null-xhtml.xhtml": [
2947
- "Uncaught: Unexpected token <"
2949
+ "Uncaught: Unexpected token <",
2950
+ "Uncaught: Unexpected token '<'"
2948
2951
  ],
2949
2952
  "dom/nodes/Element-childElementCount-dynamic-add-xhtml.xhtml": [
2950
- "Uncaught: Unexpected token <"
2953
+ "Uncaught: Unexpected token <",
2954
+ "Uncaught: Unexpected token '<'"
2951
2955
  ],
2952
2956
  "dom/nodes/Element-childElementCount-dynamic-remove-xhtml.xhtml": [
2953
- "Uncaught: Unexpected token <"
2957
+ "Uncaught: Unexpected token <",
2958
+ "Uncaught: Unexpected token '<'"
2954
2959
  ],
2955
2960
  "dom/nodes/Element-childElementCount-nochild-xhtml.xhtml": [
2956
- "Uncaught: Unexpected token <"
2961
+ "Uncaught: Unexpected token <",
2962
+ "Uncaught: Unexpected token '<'"
2957
2963
  ],
2958
2964
  "dom/nodes/Element-childElementCount-xhtml.xhtml": [
2959
- "Uncaught: Unexpected token <"
2965
+ "Uncaught: Unexpected token <",
2966
+ "Uncaught: Unexpected token '<'"
2960
2967
  ],
2961
2968
  "dom/nodes/Element-children.html": [
2962
2969
  "HTMLCollection edge cases",
@@ -2991,13 +2998,16 @@
2991
2998
  "Element.closest with context node 'test4' and selector ':has(> :scope)'"
2992
2999
  ],
2993
3000
  "dom/nodes/Element-firstElementChild-entity-xhtml.xhtml": [
2994
- "Uncaught: Unexpected token <"
3001
+ "Uncaught: Unexpected token <",
3002
+ "Uncaught: Unexpected token '<'"
2995
3003
  ],
2996
3004
  "dom/nodes/Element-firstElementChild-namespace-xhtml.xhtml": [
2997
- "Uncaught: Unexpected token <"
3005
+ "Uncaught: Unexpected token <",
3006
+ "Uncaught: Unexpected token '<'"
2998
3007
  ],
2999
3008
  "dom/nodes/Element-firstElementChild-xhtml.xhtml": [
3000
- "Uncaught: Unexpected token <"
3009
+ "Uncaught: Unexpected token <",
3010
+ "Uncaught: Unexpected token '<'"
3001
3011
  ],
3002
3012
  "dom/nodes/Element-getElementsByClassName.html": [
3003
3013
  "Interface should be correct.",
@@ -3022,19 +3032,23 @@
3022
3032
  "getElementsByTagNameNS() should be a live collection"
3023
3033
  ],
3024
3034
  "dom/nodes/Element-lastElementChild-xhtml.xhtml": [
3025
- "Uncaught: Unexpected token <"
3035
+ "Uncaught: Unexpected token <",
3036
+ "Uncaught: Unexpected token '<'"
3026
3037
  ],
3027
3038
  "dom/nodes/Element-matches.html": [
3028
3039
  "Selectors-API Level 2 Test Suite: HTML with Selectors Level 3"
3029
3040
  ],
3030
3041
  "dom/nodes/Element-nextElementSibling-xhtml.xhtml": [
3031
- "Uncaught: Unexpected token <"
3042
+ "Uncaught: Unexpected token <",
3043
+ "Uncaught: Unexpected token '<'"
3032
3044
  ],
3033
3045
  "dom/nodes/Element-previousElementSibling-xhtml.xhtml": [
3034
- "Uncaught: Unexpected token <"
3046
+ "Uncaught: Unexpected token <",
3047
+ "Uncaught: Unexpected token '<'"
3035
3048
  ],
3036
3049
  "dom/nodes/Element-siblingElement-null-xhtml.xhtml": [
3037
- "Uncaught: Unexpected token <"
3050
+ "Uncaught: Unexpected token <",
3051
+ "Uncaught: Unexpected token '<'"
3038
3052
  ],
3039
3053
  "dom/nodes/Element-webkitMatchesSelector.html": [
3040
3054
  "Selectors-API Level 2 Test Suite: HTML with Selectors Level 3"
@@ -4424,7 +4438,8 @@
4424
4438
  "DocumentFrgment.prepend() with one element and text as argument, on a parent having a child."
4425
4439
  ],
4426
4440
  "dom/nodes/ProcessingInstruction-escapes-1.xhtml": [
4427
- "Uncaught: Unexpected token <"
4441
+ "Uncaught: Unexpected token <",
4442
+ "Uncaught: Unexpected token '<'"
4428
4443
  ],
4429
4444
  "dom/nodes/ProcessingInstruction-literal-1.xhtml": [
4430
4445
  "<?xml?> is not a ProcessingInstruction"
@@ -4614,10 +4629,12 @@
4614
4629
  "innerHTML in XHTML: getting while the document is in an invalid state 1"
4615
4630
  ],
4616
4631
  "domparsing/innerhtml-03.xhtml": [
4617
- "Uncaught: Unexpected token <"
4632
+ "Uncaught: Unexpected token <",
4633
+ "Uncaught: Unexpected token '<'"
4618
4634
  ],
4619
4635
  "domparsing/innerhtml-05.xhtml": [
4620
- "Uncaught: Unexpected token <"
4636
+ "Uncaught: Unexpected token <",
4637
+ "Uncaught: Unexpected token '<'"
4621
4638
  ],
4622
4639
  "domparsing/innerhtml-mxss.sub.html": [
4623
4640
  "href before setter: 1680",
@@ -4652,7 +4669,8 @@
4652
4669
  "href after setter: 3000"
4653
4670
  ],
4654
4671
  "domparsing/insert_adjacent_html-xhtml.xhtml": [
4655
- "Uncaught: Unexpected token <"
4672
+ "Uncaught: Unexpected token <",
4673
+ "Uncaught: Unexpected token '<'"
4656
4674
  ],
4657
4675
  "domparsing/style_attribute_html.html": [
4658
4676
  "Parsing of initial style attribute",
@@ -4661,7 +4679,8 @@
4661
4679
  "Update style.backgroundColor"
4662
4680
  ],
4663
4681
  "domparsing/xml-serialization.xhtml": [
4664
- "Uncaught: Unexpected token <"
4682
+ "Uncaught: Unexpected token <",
4683
+ "Uncaught: Unexpected token '<'"
4665
4684
  ],
4666
4685
  "domparsing/xmldomparser.html": [
4667
4686
  "XML Dom Parse readyState Test"