@angular/platform-server 22.2.0 → 22.3.0-next.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/platform-server",
3
- "version": "22.2.0",
3
+ "version": "22.3.0-next.0",
4
4
  "description": "Angular - library for using Angular in Node.js",
5
5
  "author": "angular",
6
6
  "license": "MIT",
@@ -8,10 +8,10 @@
8
8
  "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
9
9
  },
10
10
  "peerDependencies": {
11
- "@angular/common": "22.2.0",
12
- "@angular/compiler": "22.2.0",
13
- "@angular/core": "22.2.0",
14
- "@angular/platform-browser": "22.2.0",
11
+ "@angular/common": "22.3.0-next.0",
12
+ "@angular/compiler": "22.3.0-next.0",
13
+ "@angular/core": "22.3.0-next.0",
14
+ "@angular/platform-browser": "22.3.0-next.0",
15
15
  "rxjs": "^6.5.3 || ^7.4.0"
16
16
  },
17
17
  "dependencies": {
@@ -758,6 +758,18 @@ function requireNodeUtils () {
758
758
  NOFRAMES: true
759
759
  };
760
760
 
761
+ // HTML's *escapable raw text* elements (a.k.a. RCDATA). Unlike the raw-text
762
+ // elements above, their text content IS escaped for character references --
763
+ // but the element is still terminated only by its own closing tag. So any
764
+ // payload that we emit verbatim underneath one of them (comment data,
765
+ // processing-instruction data, or the serialization of a nested raw-content
766
+ // element) must have that closing tag escaped, or it breaks the element open.
767
+ // https://html.spec.whatwg.org/multipage/syntax.html#escapable-raw-text-elements
768
+ var hasEscapableRawContent = {
769
+ TEXTAREA: true,
770
+ TITLE: true
771
+ };
772
+
761
773
  var emptyElements = {
762
774
  area: true,
763
775
  base: true,
@@ -779,14 +791,6 @@ function requireNodeUtils () {
779
791
  wbr: true
780
792
  };
781
793
 
782
- var extraNewLine = {
783
- /* Removed in https://github.com/whatwg/html/issues/944
784
- pre: true,
785
- textarea: true,
786
- listing: true
787
- */
788
- };
789
-
790
794
  const ESCAPE_REGEXP = /[&<>\u00A0]/g;
791
795
  const ESCAPE_ATTR_REGEXP = /[&"<>\u00A0]/g;
792
796
 
@@ -848,12 +852,22 @@ function requireNodeUtils () {
848
852
  return a.name;
849
853
  }
850
854
 
855
+ function serializedTagName(node) {
856
+ var ns = node.namespaceURI;
857
+ return (ns === NAMESPACE.HTML || ns === NAMESPACE.SVG || ns === NAMESPACE.MATHML)
858
+ ? node.localName
859
+ : node.tagName;
860
+ }
861
+
851
862
  function fallbackRawContentTags(node) {
852
863
  const tags = [];
853
864
  while (node) {
854
865
  if (node.nodeType === 1 /*ELEMENT_NODE*/) {
855
- if (node.namespaceURI === NAMESPACE.HTML && hasRawContentFallback[node.tagName]) {
856
- tags.push(node.localName);
866
+ const tagname = serializedTagName(node);
867
+ if (tagname &&
868
+ (hasRawContentFallback[tagname.toUpperCase()] ||
869
+ hasEscapableRawContent[tagname.toUpperCase()])) {
870
+ tags.push(tagname);
857
871
  }
858
872
  node = node.parentNode;
859
873
  } else if (node.nodeType === 11 /*DOCUMENT_FRAGMENT_NODE*/ && node._host) {
@@ -996,7 +1010,7 @@ function requireNodeUtils () {
996
1010
  case 1: //ELEMENT_NODE
997
1011
  var ns = kid.namespaceURI;
998
1012
  var html = ns === NAMESPACE.HTML;
999
- var tagname = (html || ns === NAMESPACE.SVG || ns === NAMESPACE.MATHML) ? kid.localName : kid.tagName;
1013
+ var tagname = serializedTagName(kid);
1000
1014
 
1001
1015
  s += '<' + tagname;
1002
1016
 
@@ -1019,7 +1033,6 @@ function requireNodeUtils () {
1019
1033
  ss = escapeMatchingClosingTag(ss, fallbackTag);
1020
1034
  }
1021
1035
  }
1022
- if (html && extraNewLine[tagname] && ss.charAt(0)==='\n') s += '\n';
1023
1036
  // Serialize children and add end tag for all others
1024
1037
  s += ss;
1025
1038
  s += '</' + tagname + '>';
@@ -3058,6 +3071,9 @@ function requireSelect () {
3058
3071
  return attr.indexOf(val) !== -1;
3059
3072
  },
3060
3073
  '~=': function(attr, val) {
3074
+ // An empty token never matches and would prevent the search from advancing.
3075
+ if (val === '') return false;
3076
+
3061
3077
  var i
3062
3078
  , s
3063
3079
  , f
@@ -3174,7 +3190,14 @@ function requireSelect () {
3174
3190
  */
3175
3191
 
3176
3192
  var rules = {
3177
- escape: /\\(?:[^0-9A-Fa-f\r\n]|[0-9A-Fa-f]{1,6}[\r\n\t ]?)/g,
3193
+ // A hexadecimal escape takes the longest run of hex digits it can, up to six.
3194
+ // Spelling that as `[0-9A-Fa-f]{1,6}` also lets the engine hand digits back to
3195
+ // the `[-_a-zA-Z0-9]` branch of `cssid`, so `\aaa` has three readings and an
3196
+ // identifier built from n such runs has 3^n of them: a selector that ultimately
3197
+ // fails to parse costs exponential time. Pinning the run to either "exactly six"
3198
+ // or "fewer than six, and not followed by another hex digit" leaves a single
3199
+ // reading without changing which escapes are accepted.
3200
+ escape: /\\(?:[^0-9A-Fa-f\r\n]|(?:[0-9A-Fa-f]{6}|[0-9A-Fa-f]{1,5}(?![0-9A-Fa-f]))[\r\n\t ]?)/g,
3178
3201
  str_escape: /(escape)|\\(\n|\r\n?|\f)/g,
3179
3202
  nonascii: /[\u00A0-\uFFFF]/,
3180
3203
  cssid: /(?:(?!-?[0-9])(?:escape|nonascii|[-_a-zA-Z0-9])+)/,
@@ -3184,7 +3207,15 @@ function requireSelect () {
3184
3207
  combinator: /^(?: +([^ \w*.#\\]) +|( )+|([^ \w*.#\\]))(?! *$)/,
3185
3208
  attr: /^\[(cssid)(?:([^\w]?=)(inside))?\]/,
3186
3209
  pseudo: /^(:cssid)(?:\((inside)\))?/,
3187
- inside: /(?:"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|<[^"'>]*>|\\["'>]|[^"'>])*/,
3210
+ // Every alternative below is disjoint on its first character, so a value has
3211
+ // exactly one reading: `\` always starts an escape pair (the plain branch and
3212
+ // the in-string branch both exclude it) and `<` always opens a nested group.
3213
+ // Letting them overlap instead -- `\\["'>]` against `[^"'>]`, which also
3214
+ // matches a lone `\` -- gives `\"` two readings and a value of n of them 2^n,
3215
+ // the same exponential parse as `escape` above. The trailing `\\?` keeps a
3216
+ // lone backslash before the closing delimiter acceptable, as browsers do,
3217
+ // without putting that choice inside the loop.
3218
+ inside: /(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|<[^"'>]*>|\\[\s\S]|[^\\"'<>])*\\?/,
3188
3219
  ident: /^(cssid)$/
3189
3220
  };
3190
3221
 
@@ -3208,7 +3239,10 @@ function requireSelect () {
3208
3239
  */
3209
3240
 
3210
3241
  var compile = function(sel_) {
3211
- var sel = sel_.replace(/^\s+|\s+$/g, '')
3242
+ // `String.prototype.trim` removes exactly the code points `\s` matches, but scans
3243
+ // instead of backtracking. `/^\s+|\s+$/g` makes `\s+$` retry from every position in
3244
+ // an interior whitespace run, which is quadratic in the length of that run.
3245
+ var sel = sel_.trim()
3212
3246
  , test
3213
3247
  , filter = []
3214
3248
  , buff = []
@@ -3321,9 +3355,16 @@ function requireSelect () {
3321
3355
  // attr value
3322
3356
  if (cap[4]) {
3323
3357
  var value = cap[6];
3324
- var i = /["'\s]\s*I$/i.test(value);
3358
+ // `["'\s]\s*I$` is equivalent to `["'\s]I$`: when `\s*` takes anything at all, the
3359
+ // character before the `I` is itself whitespace and so satisfies `["'\s]` on its own.
3360
+ // The shorter spelling cannot retry from every position of a long whitespace run.
3361
+ var i = /["'\s]I$/i.test(value);
3325
3362
  if (i) {
3326
- value = value.replace(/\s*I$/i, '');
3363
+ // Drop the `I` and the whitespace run immediately before it -- the same text
3364
+ // `/\s*I$/` matched, found by scanning back rather than by backtracking.
3365
+ var end = value.length - 1;
3366
+ while (end > 0 && /\s/.test(value.charAt(end - 1))) { end--; }
3367
+ value = value.slice(0, end);
3327
3368
  }
3328
3369
  return selectors.attr(decodeid(cap[4]), cap[5] || '-', unquote(value), i);
3329
3370
  }
@@ -3733,7 +3774,8 @@ function requireElement () {
3733
3774
  this._tagName = undefined;
3734
3775
 
3735
3776
  // These properties maintain the set of attributes
3736
- this._attrsByQName = Object.create(null); // The qname->Attr map
3777
+ // Map keeps numeric attribute names out of sparse object-index storage.
3778
+ this._attrsByQName = new Map(); // The qname->Attr map
3737
3779
  this._attrsByLName = Object.create(null); // The ns|lname->Attr map
3738
3780
  this._attrKeys = []; // attr index -> ns|lname
3739
3781
  }
@@ -4239,7 +4281,7 @@ function requireElement () {
4239
4281
  qname = String(qname);
4240
4282
  if (/[A-Z]/.test(qname) && this.isHTML)
4241
4283
  qname = utils.toASCIILowerCase(qname);
4242
- var attr = this._attrsByQName[qname];
4284
+ var attr = this._attrsByQName.get(qname);
4243
4285
  if (!attr) return null;
4244
4286
 
4245
4287
  if (Array.isArray(attr)) // If there is more than one
@@ -4259,7 +4301,7 @@ function requireElement () {
4259
4301
  qname = String(qname);
4260
4302
  if (/[A-Z]/.test(qname) && this.isHTML)
4261
4303
  qname = utils.toASCIILowerCase(qname);
4262
- return this._attrsByQName[qname] !== undefined;
4304
+ return this._attrsByQName.has(qname);
4263
4305
  }},
4264
4306
 
4265
4307
  hasAttributeNS: { value: function hasAttributeNS(ns, lname) {
@@ -4278,7 +4320,7 @@ function requireElement () {
4278
4320
  if (!xml.isValidName(qname)) utils.InvalidCharacterError();
4279
4321
  if (/[A-Z]/.test(qname) && this.isHTML)
4280
4322
  qname = utils.toASCIILowerCase(qname);
4281
- var a = this._attrsByQName[qname];
4323
+ var a = this._attrsByQName.get(qname);
4282
4324
  if (a === undefined) {
4283
4325
  if (force === undefined || force === true) {
4284
4326
  this._setAttribute(qname, '');
@@ -4299,7 +4341,7 @@ function requireElement () {
4299
4341
  // XXX: the spec says that this next search should be done
4300
4342
  // on the local name, but I think that is an error.
4301
4343
  // email pending on www-dom about it.
4302
- var attr = this._attrsByQName[qname];
4344
+ var attr = this._attrsByQName.get(qname);
4303
4345
  var isnew;
4304
4346
  if (!attr) {
4305
4347
  attr = this._newattr(qname);
@@ -4312,7 +4354,7 @@ function requireElement () {
4312
4354
  // Now set the attribute value on the new or existing Attr object.
4313
4355
  // The Attr.value setter method handles mutation events, etc.
4314
4356
  attr.value = value;
4315
- if (this._attributes) this._attributes[qname] = attr;
4357
+ setNamedProperty(this._attributes, qname, attr);
4316
4358
  if (isnew && this._newattrhook) this._newattrhook(qname, value);
4317
4359
  }},
4318
4360
 
@@ -4387,7 +4429,7 @@ function requireElement () {
4387
4429
  utils.InUseAttributeError();
4388
4430
  }
4389
4431
  var result = null;
4390
- var oldAttrs = this._attrsByQName[attr.name];
4432
+ var oldAttrs = this._attrsByQName.get(attr.name);
4391
4433
  if (oldAttrs) {
4392
4434
  if (!Array.isArray(oldAttrs)) { oldAttrs = [ oldAttrs ]; }
4393
4435
  if (oldAttrs.some(function(a) { return a===attr; })) {
@@ -4426,7 +4468,7 @@ function requireElement () {
4426
4468
  if (/[A-Z]/.test(qname) && this.isHTML)
4427
4469
  qname = utils.toASCIILowerCase(qname);
4428
4470
 
4429
- var attr = this._attrsByQName[qname];
4471
+ var attr = this._attrsByQName.get(qname);
4430
4472
  if (!attr) return;
4431
4473
 
4432
4474
  // If there is more than one match for this qname
@@ -4437,13 +4479,13 @@ function requireElement () {
4437
4479
  attr = attr.shift(); // remove it from the array
4438
4480
  }
4439
4481
  else {
4440
- this._attrsByQName[qname] = attr[1];
4482
+ this._attrsByQName.set(qname, attr[1]);
4441
4483
  attr = attr[0];
4442
4484
  }
4443
4485
  }
4444
4486
  else {
4445
4487
  // only a single match, so remove the qname mapping
4446
- this._attrsByQName[qname] = undefined;
4488
+ this._attrsByQName.delete(qname);
4447
4489
  }
4448
4490
 
4449
4491
  var ns = attr.namespaceURI;
@@ -4455,7 +4497,7 @@ function requireElement () {
4455
4497
  var i = this._attrKeys.indexOf(key);
4456
4498
  if (this._attributes) {
4457
4499
  Array.prototype.splice.call(this._attributes, i, 1);
4458
- this._attributes[qname] = undefined;
4500
+ setNamedProperty(this._attributes, qname, undefined);
4459
4501
  }
4460
4502
  this._attrKeys.splice(i, 1);
4461
4503
 
@@ -4524,20 +4566,20 @@ function requireElement () {
4524
4566
  // prefix will never have two matching Attr objects (because
4525
4567
  // setAttributeNS doesn't allow a non-null namespace with a
4526
4568
  // null prefix.
4527
- var attr = this._attrsByQName[qname];
4569
+ var attr = this._attrsByQName.get(qname);
4528
4570
  return attr ? attr.value : null;
4529
4571
  }},
4530
4572
 
4531
4573
  // The raw version of setAttribute for reflected idl attributes.
4532
4574
  _setattr: { value: function _setattr(qname, value) {
4533
- var attr = this._attrsByQName[qname];
4575
+ var attr = this._attrsByQName.get(qname);
4534
4576
  var isnew;
4535
4577
  if (!attr) {
4536
4578
  attr = this._newattr(qname);
4537
4579
  isnew = true;
4538
4580
  }
4539
4581
  attr.value = String(value);
4540
- if (this._attributes) this._attributes[qname] = attr;
4582
+ setNamedProperty(this._attributes, qname, attr);
4541
4583
  if (isnew && this._newattrhook) this._newattrhook(qname, value);
4542
4584
  }},
4543
4585
 
@@ -4546,7 +4588,7 @@ function requireElement () {
4546
4588
  _newattr: { value: function _newattr(qname) {
4547
4589
  var attr = new Attr(this, qname, null, null);
4548
4590
  var key = '|' + qname;
4549
- this._attrsByQName[qname] = attr;
4591
+ this._attrsByQName.set(qname, attr);
4550
4592
  this._attrsByLName[key] = attr;
4551
4593
  if (this._attributes) {
4552
4594
  this._attributes[this._attrKeys.length] = attr;
@@ -4555,52 +4597,48 @@ function requireElement () {
4555
4597
  return attr;
4556
4598
  }},
4557
4599
 
4558
- // Add a qname->Attr mapping to the _attrsByQName object, taking into
4600
+ // Add a qname->Attr mapping to the _attrsByQName map, taking into
4559
4601
  // account that there may be more than one attr object with the
4560
4602
  // same qname
4561
4603
  _addQName: { value: function(attr) {
4562
4604
  var qname = attr.name;
4563
- var existing = this._attrsByQName[qname];
4605
+ var existing = this._attrsByQName.get(qname);
4564
4606
  if (!existing) {
4565
- this._attrsByQName[qname] = attr;
4607
+ this._attrsByQName.set(qname, attr);
4566
4608
  }
4567
4609
  else if (Array.isArray(existing)) {
4568
4610
  existing.push(attr);
4569
4611
  }
4570
4612
  else {
4571
- this._attrsByQName[qname] = [existing, attr];
4613
+ this._attrsByQName.set(qname, [existing, attr]);
4572
4614
  }
4573
- if (this._attributes) this._attributes[qname] = attr;
4615
+ setNamedProperty(this._attributes, qname, attr);
4574
4616
  }},
4575
4617
 
4576
- // Remove a qname->Attr mapping to the _attrsByQName object, taking into
4618
+ // Remove a qname->Attr mapping from the _attrsByQName map, taking into
4577
4619
  // account that there may be more than one attr object with the
4578
4620
  // same qname
4579
4621
  _removeQName: { value: function(attr) {
4580
4622
  var qname = attr.name;
4581
- var target = this._attrsByQName[qname];
4623
+ var target = this._attrsByQName.get(qname);
4582
4624
 
4583
4625
  if (Array.isArray(target)) {
4584
4626
  var idx = target.indexOf(attr);
4585
4627
  utils.assert(idx !== -1); // It must be here somewhere
4586
4628
  if (target.length === 2) {
4587
- this._attrsByQName[qname] = target[1-idx];
4588
- if (this._attributes) {
4589
- this._attributes[qname] = this._attrsByQName[qname];
4590
- }
4629
+ this._attrsByQName.set(qname, target[1-idx]);
4630
+ setNamedProperty(this._attributes, qname, this._attrsByQName.get(qname));
4591
4631
  } else {
4592
4632
  target.splice(idx, 1);
4593
4633
  if (this._attributes && this._attributes[qname] === attr) {
4594
- this._attributes[qname] = target[0];
4634
+ setNamedProperty(this._attributes, qname, target[0]);
4595
4635
  }
4596
4636
  }
4597
4637
  }
4598
4638
  else {
4599
4639
  utils.assert(target === attr); // If only one, it must match
4600
- this._attrsByQName[qname] = undefined;
4601
- if (this._attributes) {
4602
- this._attributes[qname] = undefined;
4603
- }
4640
+ this._attrsByQName.delete(qname);
4641
+ setNamedProperty(this._attributes, qname, undefined);
4604
4642
  }
4605
4643
  }},
4606
4644
 
@@ -4782,15 +4820,30 @@ function requireElement () {
4782
4820
  // Sneakily export this class for use by Document.createAttribute()
4783
4821
  Element._Attr = Attr;
4784
4822
 
4823
+ // WebIDL reserves array indices for indexed access, even outside the list.
4824
+ // Mirroring numeric names would also create sparse object-index storage.
4825
+ function isArrayIndex(qname) {
4826
+ if (qname.length > 10) { return false; }
4827
+ var index = qname >>> 0;
4828
+ // Reject noncanonical spellings and 2^32-1, which is not an array index.
4829
+ return index !== 0xFFFFFFFF && String(index) === qname;
4830
+ }
4831
+
4832
+ // Mirror a qname->Attr mapping onto an already-created NamedNodeMap.
4833
+ function setNamedProperty(attributes, qname, attr) {
4834
+ if (attributes && !isArrayIndex(qname)) { attributes[qname] = attr; }
4835
+ }
4836
+
4785
4837
  // The attributes property of an Element will be an instance of this class.
4786
4838
  // This class is really just a dummy, though. It only defines a length
4787
4839
  // property and an item() method. The AttrArrayProxy that
4788
4840
  // defines the public API just uses the Element object itself.
4789
4841
  function AttributesArray(elt) {
4790
4842
  NamedNodeMap.call(this, elt);
4791
- for (var name in elt._attrsByQName) {
4792
- this[name] = elt._attrsByQName[name];
4793
- }
4843
+ var self = this;
4844
+ elt._attrsByQName.forEach(function(attr, qname) {
4845
+ setNamedProperty(self, qname, attr);
4846
+ });
4794
4847
  for (var i = 0; i < elt._attrKeys.length; i++) {
4795
4848
  this[i] = elt._attrsByLName[elt._attrKeys[i]];
4796
4849
  }
@@ -8698,7 +8751,18 @@ function requireDocument () {
8698
8751
  // We may need to update this mapping every time a node is rooted
8699
8752
  // or uprooted, and any time an attribute is added, removed or changed
8700
8753
  // on a rooted element.
8701
- this.byId = Object.create(null);
8754
+ //
8755
+ // The mapping is built lazily: it stays null until something actually
8756
+ // looks an element up by id, and only then is it populated from the
8757
+ // nodes currently rooted in this document. After that it is kept up to
8758
+ // date by root()/uproot() and by the id attribute change handler, just as
8759
+ // it always was. Documents that are never queried by id -- in particular
8760
+ // the private documents the fragment parser creates and throws away for
8761
+ // every innerHTML assignment -- no longer pay for an index nobody reads.
8762
+ //
8763
+ // A Map keeps ids such as `1023` out of sparse object-index storage, the
8764
+ // same reason Element uses one for qualified attribute names.
8765
+ this._byId = null;
8702
8766
 
8703
8767
  // This property holds a monotonically increasing value akin to
8704
8768
  // a timestamp used to record the last modification time of nodes
@@ -8966,7 +9030,9 @@ function requireDocument () {
8966
9030
  }},
8967
9031
 
8968
9032
  getElementById: { value: function(id) {
8969
- var n = this.byId[id];
9033
+ // Coercion can mutate the document, so do it before building the index.
9034
+ id = String(id);
9035
+ var n = this._idIndex().get(id);
8970
9036
  if (!n) return null;
8971
9037
  if (n instanceof MultiId) { // there was more than one element with this id
8972
9038
  return n.getFirst();
@@ -8976,7 +9042,29 @@ function requireDocument () {
8976
9042
 
8977
9043
  _hasMultipleElementsWithId: { value: function(id) {
8978
9044
  // Used internally by querySelectorAll optimization
8979
- return (this.byId[id] instanceof MultiId);
9045
+ return (this._idIndex().get(id) instanceof MultiId);
9046
+ }},
9047
+
9048
+ // The id -> element map, materialized on demand. Everything that reads the
9049
+ // index goes through here; everything that writes it (addId/delId) leaves it
9050
+ // alone while it is absent.
9051
+ _idIndex: { value: function _idIndex() {
9052
+ var byId = this._byId;
9053
+ if (byId !== null) return byId;
9054
+ byId = this._byId = new Map();
9055
+ // Populate from the elements rooted in this document right now. _nodes
9056
+ // is the registry root() and uproot() maintain, so walking it -- rather
9057
+ // than the tree -- gives exactly the entries the index would hold had it
9058
+ // been kept up to date all along, and needs no traversal state.
9059
+ var nodes = this._nodes;
9060
+ for (var nid = 1, end = this._nextnid; nid < end; nid++) {
9061
+ var n = nodes[nid];
9062
+ if (n !== undefined && n.nodeType === Node.ELEMENT_NODE) {
9063
+ var id = n.getAttributeNS(null, 'id');
9064
+ if (id) this.addId(id, n);
9065
+ }
9066
+ }
9067
+ return byId;
8980
9068
  }},
8981
9069
 
8982
9070
  // Just copy this method from the Element prototype
@@ -9269,16 +9357,18 @@ function requireDocument () {
9269
9357
 
9270
9358
  // Add a mapping from id to n for n.ownerDocument
9271
9359
  addId: { value: function addId(id, n) {
9272
- var val = this.byId[id];
9360
+ var byId = this._byId;
9361
+ if (byId === null) return; // no index yet; it will pick n up when built
9362
+ var val = byId.get(id);
9273
9363
  if (!val) {
9274
- this.byId[id] = n;
9364
+ byId.set(id, n);
9275
9365
  }
9276
9366
  else {
9277
9367
  // TODO: Add a way to opt-out console warnings
9278
9368
  //console.warn('Duplicate element id ' + id);
9279
9369
  if (!(val instanceof MultiId)) {
9280
9370
  val = new MultiId(val);
9281
- this.byId[id] = val;
9371
+ byId.set(id, val);
9282
9372
  }
9283
9373
  val.add(n);
9284
9374
  }
@@ -9286,17 +9376,19 @@ function requireDocument () {
9286
9376
 
9287
9377
  // Delete the mapping from id to n for n.ownerDocument
9288
9378
  delId: { value: function delId(id, n) {
9289
- var val = this.byId[id];
9379
+ var byId = this._byId;
9380
+ if (byId === null) return; // no index yet; nothing to remove n from
9381
+ var val = byId.get(id);
9290
9382
  utils.assert(val);
9291
9383
 
9292
9384
  if (val instanceof MultiId) {
9293
9385
  val.del(n);
9294
9386
  if (val.length === 1) { // convert back to a single node
9295
- this.byId[id] = val.downgrade();
9387
+ byId.set(id, val.downgrade());
9296
9388
  }
9297
9389
  }
9298
9390
  else {
9299
- this.byId[id] = undefined;
9391
+ byId.delete(id);
9300
9392
  }
9301
9393
  }},
9302
9394
 
@@ -9412,8 +9504,10 @@ function requireDocument () {
9412
9504
  n.ownerDocument._nodes[n._nid] = n;
9413
9505
  // Manage id to element mapping
9414
9506
  if (n.nodeType === Node.ELEMENT_NODE) {
9415
- var id = n.getAttribute('id');
9416
- if (id) n.ownerDocument.addId(id, n);
9507
+ if (n.ownerDocument._byId !== null) {
9508
+ var id = n.getAttributeNS(null, 'id');
9509
+ if (id) n.ownerDocument.addId(id, n);
9510
+ }
9417
9511
 
9418
9512
  // Script elements need to know when they're inserted
9419
9513
  // into the document
@@ -9423,8 +9517,8 @@ function requireDocument () {
9423
9517
 
9424
9518
  function uproot(n) {
9425
9519
  // Manage id to element mapping
9426
- if (n.nodeType === Node.ELEMENT_NODE) {
9427
- var id = n.getAttribute('id');
9520
+ if (n.nodeType === Node.ELEMENT_NODE && n.ownerDocument._byId !== null) {
9521
+ var id = n.getAttributeNS(null, 'id');
9428
9522
  if (id) n.ownerDocument.delId(id, n);
9429
9523
  }
9430
9524
  n.ownerDocument._nodes[n._nid] = undefined;
@@ -9467,18 +9561,23 @@ function requireDocument () {
9467
9561
  recursivelySetOwner(kid, owner);
9468
9562
  }
9469
9563
 
9470
- // A class for storing multiple nodes with the same ID
9564
+ // A class for storing multiple nodes with the same ID.
9565
+ //
9566
+ // A Map keeps the node ids used as keys out of sparse object-index storage:
9567
+ // a plain object keyed by integers gets an elements backing store sized to
9568
+ // the largest key, which for node ids means one as large as the document for
9569
+ // every id that is duplicated.
9471
9570
  function MultiId(node) {
9472
- this.nodes = Object.create(null);
9473
- this.nodes[node._nid] = node;
9571
+ this.nodes = new Map();
9572
+ this.nodes.set(node._nid, node);
9474
9573
  this.length = 1;
9475
9574
  this.firstNode = undefined;
9476
9575
  }
9477
9576
 
9478
9577
  // Add a node to the list, with O(1) time
9479
9578
  MultiId.prototype.add = function(node) {
9480
- if (!this.nodes[node._nid]) {
9481
- this.nodes[node._nid] = node;
9579
+ if (!this.nodes.has(node._nid)) {
9580
+ this.nodes.set(node._nid, node);
9482
9581
  this.length++;
9483
9582
  this.firstNode = undefined;
9484
9583
  }
@@ -9486,8 +9585,7 @@ function requireDocument () {
9486
9585
 
9487
9586
  // Remove a node from the list, with O(1) time
9488
9587
  MultiId.prototype.del = function(node) {
9489
- if (this.nodes[node._nid]) {
9490
- delete this.nodes[node._nid];
9588
+ if (this.nodes.delete(node._nid)) {
9491
9589
  this.length--;
9492
9590
  this.firstNode = undefined;
9493
9591
  }
@@ -9499,13 +9597,13 @@ function requireDocument () {
9499
9597
  MultiId.prototype.getFirst = function() {
9500
9598
  /* jshint bitwise: false */
9501
9599
  if (!this.firstNode) {
9502
- var nid;
9503
- for (nid in this.nodes) {
9504
- if (this.firstNode === undefined ||
9505
- this.firstNode.compareDocumentPosition(this.nodes[nid]) & Node.DOCUMENT_POSITION_PRECEDING) {
9506
- this.firstNode = this.nodes[nid];
9600
+ var self = this;
9601
+ this.nodes.forEach(function(node) {
9602
+ if (self.firstNode === undefined ||
9603
+ self.firstNode.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_PRECEDING) {
9604
+ self.firstNode = node;
9507
9605
  }
9508
- }
9606
+ });
9509
9607
  }
9510
9608
  return this.firstNode;
9511
9609
  };
@@ -9513,10 +9611,7 @@ function requireDocument () {
9513
9611
  // If there is only one node left, return it. Otherwise return "this".
9514
9612
  MultiId.prototype.downgrade = function() {
9515
9613
  if (this.length === 1) {
9516
- var nid;
9517
- for (nid in this.nodes) {
9518
- return this.nodes[nid];
9519
- }
9614
+ return this.nodes.values().next().value;
9520
9615
  }
9521
9616
  return this;
9522
9617
  };
@@ -11437,6 +11532,8 @@ function requireHTMLParser () {
11437
11532
  for(var i = 0, n = oldattrs.length; i < n; i++) {
11438
11533
  var oldname = oldattrs[i][0];
11439
11534
  var oldval = oldattrs[i][1];
11535
+ // Bare attributes have an empty string value in the DOM.
11536
+ if (oldval === undefined) oldval = "";
11440
11537
  if (!newelt.hasAttribute(oldname)) return false;
11441
11538
  if (newelt.getAttribute(oldname) !== oldval) return false;
11442
11539
  }