diff --git a/lib/dom.js b/lib/dom.js index 59922be6c..f09a174da 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -362,7 +362,7 @@ DOMImplementation.prototype = { * __It behaves slightly different from the description in the living standard__: * - There is no interface/class `XMLDocument`, it returns a `Document` instance (with it's `type` set to `'xml'`). * - `encoding`, `mode`, `origin`, `url` fields are currently not declared. - * - This implementation is not validating names or qualified names wen being called. + * - This methods provided by this implementation are not validating names or qualified names. * (They are only validated by the SAX parser when calling `DOMParser.parseFromString`) * * @param {string | null} namespaceURI @@ -884,7 +884,25 @@ Document.prototype = { }); }, - //document factory method: + /** + * Creates a new `Element` that is owned by this `Document`. + * In HTML Documents `localName` is the lower cased `tagName`, + * otherwise no transformation is being applied. + * When `contentType` implies the HTML namespace, it will be set as `namespaceURI`. + * + * __This implementation differs from the specification:__ + * - The provided name is not checked against the `Name` production, + * so no related error will be thrown. + * - There is no interface `HTMLElement`, it is always an `Element`. + * - There is no support for a second argument to indicate using custom elements. + * + * @param {string} tagName + * @return {Element} + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement + * @see https://dom.spec.whatwg.org/#dom-document-createelement + * @see https://dom.spec.whatwg.org/#concept-create-element + */ createElement : function(tagName){ var node = new Element(); node.ownerDocument = this; @@ -933,9 +951,30 @@ Document.prototype = { node.nodeValue= node.data = data; return node; }, - createAttribute : function(name){ + /** + * Creates an `Attr` node that is owned by this document. + * In HTML Documents `localName` is the lower cased `name`, + * otherwise no transformation is being applied. + * + * __This implementation differs from the specification:__ + * - The provided name is not checked against the `Name` production, + * so no related error will be thrown. + * + * @param {string} name + * @return {Attr} + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createAttribute + * @see https://dom.spec.whatwg.org/#dom-document-createattribute + */ + createAttribute: function(name){ + if (this.type === 'html') { + name = name.toLowerCase() + } + return this._createAttribute(name); + }, + _createAttribute: function(name){ var node = new Attr(); - node.ownerDocument = this; + node.ownerDocument = this; node.name = name; node.nodeName = name; node.localName = name; @@ -995,6 +1034,12 @@ function Element() { }; Element.prototype = { nodeType : ELEMENT_NODE, + getQualifiedName: function () { + return this.prefix ? this.prefix+':'+this.localName : this.localName + }, + _isInHTMLDocumentAndNamespace: function () { + return this.ownerDocument.type === 'html' && this.namespaceURI === NAMESPACE.HTML; + }, hasAttribute : function(name){ return this.getAttributeNode(name)!=null; }, @@ -1003,10 +1048,16 @@ Element.prototype = { return attr && attr.value || ''; }, getAttributeNode : function(name){ + if (this._isInHTMLDocumentAndNamespace()) { + name = name.toLowerCase() + } return this.attributes.getNamedItem(name); }, setAttribute : function(name, value){ - var attr = this.ownerDocument.createAttribute(name); + if (this._isInHTMLDocumentAndNamespace()) { + name = name.toLowerCase() + } + var attr = this.ownerDocument._createAttribute(name) attr.value = attr.nodeValue = "" + value; this.setAttributeNode(attr) }, @@ -1014,8 +1065,8 @@ Element.prototype = { var attr = this.getAttributeNode(name) attr && this.removeAttributeNode(attr); }, - - //four real opeartion method + + // four real operation method appendChild:function(newChild){ if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){ return this.insertBefore(newChild,null); @@ -1038,7 +1089,7 @@ Element.prototype = { var old = this.getAttributeNodeNS(namespaceURI, localName); old && this.removeAttributeNode(old); }, - + hasAttributeNS : function(namespaceURI, localName){ return this.getAttributeNodeNS(namespaceURI, localName)!=null; }, @@ -1054,13 +1105,51 @@ Element.prototype = { getAttributeNodeNS : function(namespaceURI, localName){ return this.attributes.getNamedItemNS(namespaceURI, localName); }, - - getElementsByTagName : function(tagName){ + + /** + * Returns a LiveNodeList of elements with the given qualifiedName. + * Searching for all descendants can be done by passing `*` as `qualifiedName`. + * + * All descendants of the specified element are searched, but not the element itself. + * The returned list is live, which means it updates itself with the DOM tree automatically. + * Therefore, there is no need to call `Element.getElementsByTagName()` + * with the same element and arguments repeatedly if the DOM changes in between calls. + * + * When called on an HTML element in an HTML document, + * `getElementsByTagName` lower-cases the argument before searching for it. + * This is undesirable when trying to match camel-cased SVG elements + * (such as ``) in an HTML document. + * Instead, use `Element.getElementsByTagNameNS()`, + * which preserves the capitalization of the tag name. + * + * `Element.getElementsByTagName` is similar to `Document.getElementsByTagName()`, + * except that it only searches for elements that are descendants of the specified element. + * + * @param {string} qualifiedName + * @return {LiveNodeList} + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByTagName + * @see https://dom.spec.whatwg.org/#concept-getelementsbytagname + */ + getElementsByTagName : function(qualifiedName){ + var isHTMLDocument = (this.nodeType === DOCUMENT_NODE ? this : this.ownerDocument).type === 'html' + var lowerQualifiedName = qualifiedName.toLowerCase() return new LiveNodeList(this,function(base){ var ls = []; - _visitNode(base,function(node){ - if(node !== base && node.nodeType == ELEMENT_NODE && (tagName === '*' || node.tagName == tagName)){ + _visitNode(base, function(node) { + if (node === base || node.nodeType !== ELEMENT_NODE) { + return + } + if (qualifiedName === '*') { ls.push(node); + } else { + var nodeQualifiedName = node.getQualifiedName(); + var matchingQName = isHTMLDocument && node.namespaceURI === NAMESPACE.HTML + ? lowerQualifiedName + : qualifiedName + if(nodeQualifiedName === matchingQName){ + ls.push(node); + } } }); return ls; @@ -1075,7 +1164,7 @@ Element.prototype = { } }); return ls; - + }); } }; diff --git a/test/dom/document.test.js b/test/dom/document.test.js index 891bcdfc3..5258ef08b 100644 --- a/test/dom/document.test.js +++ b/test/dom/document.test.js @@ -97,6 +97,7 @@ describe('Document.prototype', () => { const element = doc.createElement('XmL') expect(element.nodeName).toBe('XmL') + expect(element.localName).toBe(element.nodeName) }) it('should create elements with exact cased name in an XHTML document', () => { const impl = new DOMImplementation() @@ -105,6 +106,7 @@ describe('Document.prototype', () => { const element = doc.createElement('XmL') expect(element.nodeName).toBe('XmL') + expect(element.localName).toBe(element.nodeName) }) it('should create elements with lower cased name in an HTML document', () => { // https://dom.spec.whatwg.org/#dom-document-createelement @@ -113,7 +115,9 @@ describe('Document.prototype', () => { const element = doc.createElement('XmL') + expect(element.localName).toBe('xml') expect(element.nodeName).toBe('xml') + expect(element.tagName).toBe(element.nodeName) }) it('should create elements with no namespace in an XML document without default namespace', () => { const impl = new DOMImplementation() @@ -140,4 +144,37 @@ describe('Document.prototype', () => { expect(element.namespaceURI).toBe(NAMESPACE.HTML) }) }) + describe('createAttribute', () => { + const NAME = 'NaMe' + test('should create name as passed in XML documents', () => { + const doc = new DOMImplementation().createDocument(null, '') + + const attr = doc.createAttribute(NAME) + + expect(attr.ownerDocument).toBe(doc) + expect(attr.name).toBe(NAME) + expect(attr.localName).toBe(NAME) + expect(attr.nodeName).toBe(NAME) + }) + test('should create name as passed in XHTML documents', () => { + const doc = new DOMImplementation().createDocument(NAMESPACE.HTML, '') + + const attr = doc.createAttribute(NAME) + + expect(attr.ownerDocument).toBe(doc) + expect(attr.name).toBe(NAME) + expect(attr.localName).toBe(NAME) + expect(attr.nodeName).toBe(NAME) + }) + test('should create lower cased name when passed in HTML document', () => { + const doc = new DOMImplementation().createHTMLDocument(false) + + const attr = doc.createAttribute(NAME) + + expect(attr.ownerDocument).toBe(doc) + expect(attr.name).toBe('name') + expect(attr.localName).toBe('name') + expect(attr.nodeName).toBe('name') + }) + }) }) diff --git a/test/dom/dom-implementation.test.js b/test/dom/dom-implementation.test.js index efc0674b2..cec7101ce 100644 --- a/test/dom/dom-implementation.test.js +++ b/test/dom/dom-implementation.test.js @@ -219,8 +219,9 @@ describe('DOMImplementation', () => { expect(doc.firstChild).toBe(doc.doctype) expect(doc.documentElement).not.toBeNull() + expect(doc.documentElement.localName).toBe('html') expect(doc.documentElement.nodeName).toBe('html') - expect(doc.documentElement.tagName).toBe('html') + expect(doc.documentElement.tagName).toBe(doc.documentElement.nodeName) const htmlNode = doc.documentElement expect(htmlNode.firstChild).not.toBeNull() expect(htmlNode.firstChild.nodeName).toBe('head') @@ -246,8 +247,9 @@ describe('DOMImplementation', () => { expect(doc.firstChild).toBe(doc.doctype) expect(doc.documentElement).not.toBeNull() + expect(doc.documentElement.localName).toBe('html') expect(doc.documentElement.nodeName).toBe('html') - expect(doc.documentElement.tagName).toBe('html') + expect(doc.documentElement.tagName).toBe(doc.documentElement.nodeName) const htmlNode = doc.documentElement expect(htmlNode.firstChild).not.toBeNull() @@ -270,8 +272,9 @@ describe('DOMImplementation', () => { expect(doc.type).toBe('html') expect(doc.documentElement).not.toBeNull() + expect(doc.documentElement.localName).toBe('html') expect(doc.documentElement.nodeName).toBe('html') - expect(doc.documentElement.tagName).toBe('html') + expect(doc.documentElement.tagName).toBe(doc.documentElement.nodeName) const htmlNode = doc.documentElement expect(htmlNode.firstChild).not.toBeNull() diff --git a/test/dom/element.test.js b/test/dom/element.test.js index 7e59b669f..666e7f29c 100644 --- a/test/dom/element.test.js +++ b/test/dom/element.test.js @@ -1,14 +1,57 @@ 'use strict' const { DOMParser, DOMImplementation, XMLSerializer } = require('../../lib') +const { MIME_TYPE, NAMESPACE } = require('../../lib/conventions') describe('Document', () => { // See: http://jsfiddle.net/bigeasy/ShcXP/1/ describe('getElementsByTagName', () => { - it('should return the correct number of elements', () => { - const doc = new DOMParser().parseFromString('') - expect(doc.getElementsByTagName('*')).toHaveLength(2) - expect(doc.documentElement.getElementsByTagName('*')).toHaveLength(1) + it('should return the correct number of elements in XML documents', () => { + const doc = new DOMParser().parseFromString( + ` + Title + +

+
+ +
` + ) + expect(doc.getElementsByTagName('*')).toHaveLength(8) + expect(doc.documentElement.getElementsByTagName('*')).toHaveLength(7) + expect(doc.getElementsByTagName('div')).toHaveLength(2) + expect(doc.documentElement.getElementsByTagName('div')).toHaveLength(2) + + // in HTML documents inside the HTML namespace case doesn't have to match, + // this is not an HTML document, so no div will be found, + // not even the second one inside the HTML namespace + expect(doc.getElementsByTagName('DIV')).toHaveLength(0) + expect(doc.documentElement.getElementsByTagName('DIV')).toHaveLength(0) + }) + + it('should return the correct number of elements in HTML documents', () => { + const doc = new DOMParser().parseFromString( + ` + Title + +

+
+ + `, + MIME_TYPE.HTML + ) + expect(doc.getElementsByTagName('*')).toHaveLength(8) + expect(doc.documentElement.getElementsByTagName('*')).toHaveLength(7) + expect(doc.getElementsByTagName('div')).toHaveLength(2) + expect(doc.documentElement.getElementsByTagName('div')).toHaveLength(2) + + // in HTML documents inside the HTML namespace case doesn't have to match, + // but the second one is not in the HTML namespace + const documentDIVs = doc.getElementsByTagName('DIV') + expect(documentDIVs).toHaveLength(1) + expect(documentDIVs.item(0).getAttribute('id')).toBe('4') + const elementDIVs = doc.documentElement.getElementsByTagName('DIV') + expect(elementDIVs).toHaveLength(1) + expect(elementDIVs.item(0).getAttribute('id')).toBe('4') }) it('should support API on element (this test needs to be split)', () => { @@ -165,17 +208,69 @@ describe('Document', () => { expect(doc.documentElement.toString()).toBe('bye') }) - describe('createElement', () => { - it('should set localName', () => { - const doc = new DOMImplementation().createDocument(null, 'test', null) + xit('nested append failed', () => {}) + + xit('self append failed', () => {}) +}) + +describe('Element', () => { + const ATTR_MIXED_CASE = 'AttR' + const ATTR_LOWER_CASE = 'attr' + const VALUE = '2039e2dk' + describe('setAttribute', () => { + test.each([null, NAMESPACE.HTML])( + 'should set attribute as is in XML document with namespace %s', + (ns) => { + const doc = new DOMImplementation().createDocument(ns, 'xml') + + doc.documentElement.setAttribute(ATTR_MIXED_CASE, VALUE) + + expect(doc.documentElement.attributes).toHaveLength(1) + expect(doc.documentElement.attributes.item(0)).toMatchObject({ + name: ATTR_MIXED_CASE, + value: VALUE, + }) + expect(doc.documentElement.hasAttribute(ATTR_MIXED_CASE)).toBe(true) + expect(doc.documentElement.hasAttribute(ATTR_LOWER_CASE)).toBe(false) + } + ) + test('should set attribute lower cased in HTML document', () => { + const doc = new DOMImplementation().createHTMLDocument() - const elem = doc.createElement('foo') + doc.documentElement.setAttribute(ATTR_MIXED_CASE, VALUE) - expect(elem.localName === 'foo') + expect(doc.documentElement.attributes).toHaveLength(1) + expect(doc.documentElement.attributes.item(0)).toMatchObject({ + name: ATTR_LOWER_CASE, + value: VALUE, + }) + // the attribute is accessible with the lower cased name + expect(doc.documentElement.hasAttribute(ATTR_LOWER_CASE)).toBe(true) + // and with the original name (and any other one that is the same lower case name) + expect(doc.documentElement.hasAttribute(ATTR_MIXED_CASE)).toBe(true) + // the value is the same for "both" attribute names + expect(doc.documentElement.getAttribute(ATTR_MIXED_CASE)).toBe( + doc.documentElement.getAttribute(ATTR_LOWER_CASE) + ) + // since it's the same node it resolves to + expect(doc.documentElement.getAttributeNode(ATTR_MIXED_CASE)).toBe( + doc.documentElement.getAttributeNode(ATTR_LOWER_CASE) + ) }) - }) + test('should set attribute as is in HTML document with different namespace', () => { + const doc = new DOMImplementation().createHTMLDocument() + const nameSpacedElement = doc.createElementNS(NAMESPACE.SVG, 'svg') + expect(nameSpacedElement.namespaceURI).toBe(NAMESPACE.SVG) - xit('nested append failed', () => {}) + nameSpacedElement.setAttribute(ATTR_MIXED_CASE, VALUE) - xit('self append failed', () => {}) + expect(nameSpacedElement.attributes).toHaveLength(1) + expect(nameSpacedElement.attributes.item(0)).toMatchObject({ + name: ATTR_MIXED_CASE, + value: VALUE, + }) + expect(nameSpacedElement.hasAttribute(ATTR_MIXED_CASE)).toBe(true) + expect(doc.documentElement.hasAttribute(ATTR_LOWER_CASE)).toBe(false) + }) + }) })