Skip to content

Commit

Permalink
feat: Correctly handle all case modifications
Browse files Browse the repository at this point in the history
in HTML docs or namespaces
  • Loading branch information
karfau committed Oct 26, 2021
1 parent 06a6d48 commit 65c8368
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 28 deletions.
115 changes: 102 additions & 13 deletions lib/dom.js
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
},
Expand All @@ -1003,19 +1048,25 @@ 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)
},
removeAttribute : function(name){
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);
Expand All @@ -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;
},
Expand All @@ -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 `<linearGradient>`) 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;
Expand All @@ -1075,7 +1164,7 @@ Element.prototype = {
}
});
return ls;

});
}
};
Expand Down
37 changes: 37 additions & 0 deletions test/dom/document.test.js
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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')
})
})
})
9 changes: 6 additions & 3 deletions test/dom/dom-implementation.test.js
Expand Up @@ -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')
Expand All @@ -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()
Expand All @@ -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()
Expand Down

0 comments on commit 65c8368

Please sign in to comment.