feat(compiler): parse5 DOM adapter

Closes #841
This commit is contained in:
Marc Laval
2015-03-01 22:17:36 +01:00
parent 537f943f57
commit 1d4ff9bcdc
13 changed files with 647 additions and 140 deletions

View File

@ -184,7 +184,9 @@ export class ElementBinderBuilder extends CompileStep {
} else {
property = this._resolvePropertyName(property);
//TODO(pk): special casing innerHtml, see: https://github.com/angular/angular/issues/789
if (DOM.hasProperty(compileElement.element, property) || StringWrapper.equals(property, 'innerHtml')) {
if (StringWrapper.equals(property, 'innerHTML')) {
setterFn = (element, value) => DOM.setInnerHTML(element, value);
} else if (DOM.hasProperty(compileElement.element, property) || StringWrapper.equals(property, 'innerHtml')) {
setterFn = reflector.setter(property);
}
}

View File

@ -0,0 +1,454 @@
var parse5 = require('parse5');
var parser = new parse5.Parser(parse5.TreeAdapters.htmlparser2);
var serializer = new parse5.Serializer(parse5.TreeAdapters.htmlparser2);
var treeAdapter = parser.treeAdapter;
var cssParse = require('css-parse');
var url = require('url');
import {List, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DomAdapter, setRootDomAdapter} from './dom_adapter';
import {BaseException, isPresent, isBlank} from 'angular2/src/facade/lang';
var _attrToPropMap = {
'inner-html': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex',
};
var defDoc = null;
function _notImplemented(methodName) {
return new BaseException('This method is not implemented in Parse5DomAdapter: ' + methodName);
}
export class Parse5DomAdapter extends DomAdapter {
static makeCurrent() {
setRootDomAdapter(new Parse5DomAdapter());
}
get attrToPropMap() {
return _attrToPropMap;
}
query(selector) {
throw _notImplemented('query');
}
querySelector(el, selector:string) {
throw _notImplemented('querySelector');
}
querySelectorAll(el, selector:string) {
//TODO: use selector class from core. For now, only works for .classname ...
var res = ListWrapper.create();
var _recursive = (result, node, className) => {
if (this.hasClass(node, className)) {
ListWrapper.push(result, node);
}
var cNodes = node.childNodes;
if (cNodes && cNodes.length > 0) {
for (var i = 0; i < cNodes.length; i++) {
_recursive(result, cNodes[i], className);
}
}
};
_recursive(res, el, selector.substring(1));
return res;
}
on(el, evt, listener) {
throw _notImplemented('on');
}
dispatchEvent(el, evt) {
throw _notImplemented('dispatchEvent');
}
createMouseEvent(eventType) {
throw _notImplemented('createMouseEvent');
}
createEvent(eventType) {
throw _notImplemented('createEvent');
}
getInnerHTML(el) {
return serializer.serialize(this.templateAwareRoot(el));
}
getOuterHTML(el) {
serializer.html = '';
serializer._serializeElement(el);
return serializer.html;
}
nodeName(node):string {
return node.tagName;
}
nodeValue(node):string {
return node.nodeValue;
}
type(node:string) {
throw _notImplemented('type');
}
content(node) {
return node.childNodes[0];
}
firstChild(el) {
return el.firstChild;
}
nextSibling(el) {
return el.nextSibling;
}
parentElement(el) {
return el.parent;
}
childNodes(el) {
return el.childNodes;
}
childNodesAsList(el):List {
var childNodes = el.childNodes;
var res = ListWrapper.createFixedSize(childNodes.length);
for (var i = 0; i < childNodes.length; i++) {
res[i] = childNodes[i];
}
return res;
}
clearNodes(el) {
while (el.childNodes.length > 0) {
this.remove(el.childNodes[0]);
}
}
appendChild(el, node) {
this.remove(node);
treeAdapter.appendChild(this.templateAwareRoot(el), node);
}
removeChild(el, node) {
if (ListWrapper.contains(el.childNodes, node)) {
this.remove(node);
}
}
remove(el) {
var parent = el.parent;
if (parent) {
var index = parent.childNodes.indexOf(el);
parent.childNodes.splice(index, 1);
}
var prev = el.previousSibling;
var next = el.nextSibling;
if (prev) {
prev.next = next;
}
if (next) {
next.prev = prev;
}
el.prev = null;
el.next = null;
el.parent = null;
return el;
}
insertBefore(el, node) {
this.remove(node);
treeAdapter.insertBefore(el.parent, node, el);
}
insertAllBefore(el, nodes) {
ListWrapper.forEach(nodes, (n) => {
this.insertBefore(el, n);
});
}
insertAfter(el, node) {
if (el.nextSibling) {
this.insertBefore(el.nextSibling, node);
} else {
this.appendChild(el.parent, node);
}
}
setInnerHTML(el, value) {
this.clearNodes(el);
var content = parser.parseFragment(value);
for (var i = 0; i < content.childNodes.length; i++) {
treeAdapter.appendChild(el, content.childNodes[i]);
}
}
getText(el) {
if (this.isTextNode(el)) {
return el.data;
} else if (el.childNodes.length == 0) {
return "";
} else {
var textContent = "";
for (var i = 0; i < el.childNodes.length; i++) {
textContent += this.getText(el.childNodes[i]);
}
return textContent;
}
}
setText(el, value:string) {
if (this.isTextNode(el)) {
el.data = value;
} else {
this.clearNodes(el);
treeAdapter.insertText(el, value);
}
}
getValue(el) {
return el.value;
}
setValue(el, value:string) {
el.value = value;
}
getChecked(el) {
return el.checked;
}
setChecked(el, value:boolean) {
el.checked = value;
}
createTemplate(html) {
var template = treeAdapter.createElement("template", 'http://www.w3.org/1999/xhtml', []);
var content = parser.parseFragment(html);
treeAdapter.appendChild(template, content);
return template;
}
createElement(tagName) {
return treeAdapter.createElement(tagName, 'http://www.w3.org/1999/xhtml', []);
}
createTextNode(text: string) {
throw _notImplemented('createTextNode');
}
createScriptTag(attrName:string, attrValue:string) {
return treeAdapter.createElement("script", 'http://www.w3.org/1999/xhtml', [{name: attrName, value: attrValue}]);
}
createStyleElement(css:string) {
var style = this.createElement('style');
this.setText(style, css);
return style;
}
createShadowRoot(el) {
el.shadowRoot = treeAdapter.createDocumentFragment();
el.shadowRoot.parent = el;
return el.shadowRoot;
}
getShadowRoot(el) {
return el.shadowRoot;
}
clone(node) {
var temp = treeAdapter.createElement("template", null, []);
treeAdapter.appendChild(temp, node);
var serialized = serializer.serialize(temp);
var newParser = new parse5.Parser(parse5.TreeAdapters.htmlparser2);
return newParser.parseFragment(serialized).childNodes[0];
}
hasProperty(element, name:string) {
return _HTMLElementPropertyList.indexOf(name) > -1;
}
getElementsByClassName(element, name:string) {
return this.querySelectorAll(element, "." + name);
}
getElementsByTagName(element, name:string) {
throw _notImplemented('getElementsByTagName');
}
classList(element):List {
var classAttrValue = null;
var attributes = element.attribs;
if (attributes && attributes.hasOwnProperty("class")) {
classAttrValue = attributes["class"];
}
return classAttrValue ? classAttrValue.trim().split(/\s+/g) : [];
}
addClass(element, classname:string) {
var classList = this.classList(element);
var index = classList.indexOf(classname);
if (index == -1) {
ListWrapper.push(classList, classname);
element.attribs["class"] = element.className = ListWrapper.join(classList, " ");
}
}
removeClass(element, classname:string) {
var classList = this.classList(element);
var index = classList.indexOf(classname);
if (index > -1) {
classList.splice(index, 1);
element.attribs["class"] = element.className = ListWrapper.join(classList, " ");
}
}
hasClass(element, classname:string) {
return ListWrapper.contains(this.classList(element), classname);
}
_readStyleAttribute(element) {
var styleMap = {};
var attributes = element.attribs;
if (attributes && attributes.hasOwnProperty("style")) {
var styleAttrValue = attributes["style"];
var styleList = styleAttrValue.split(/;+/g);
for (var i = 0; i < styleList.length; i++) {
if (styleList[i].length > 0) {
var elems = styleList[i].split(/:+/g);
styleMap[elems[0].trim()] = elems[1].trim();
}
}
}
return styleMap;
}
_writeStyleAttribute(element, styleMap) {
var styleAttrValue = "";
for (var key in styleMap) {
var newValue = styleMap[key];
if (newValue && newValue.length > 0) {
styleAttrValue += key + ":" + styleMap[key] + ";";
}
}
element.attribs["style"] = styleAttrValue;
}
setStyle(element, stylename:string, stylevalue:string) {
var styleMap = this._readStyleAttribute(element);
styleMap[stylename] = stylevalue;
this._writeStyleAttribute(element, styleMap);
}
removeStyle(element, stylename:string) {
this.setStyle(element, stylename, null);
}
getStyle(element, stylename:string) {
var styleMap = this._readStyleAttribute(element);
return styleMap.hasOwnProperty(stylename) ? styleMap[stylename] : "";
}
tagName(element):string {
return element.tagName == "style" ? "STYLE" : element.tagName;
}
attributeMap(element) {
var res = MapWrapper.create();
var elAttrs = treeAdapter.getAttrList(element);
for (var i = 0; i < elAttrs.length; i++) {
var attrib = elAttrs[i];
MapWrapper.set(res, attrib.name, attrib.value);
}
return res;
}
getAttribute(element, attribute:string) {
return element.attribs.hasOwnProperty(attribute) ? element.attribs[attribute] : null;
}
setAttribute(element, attribute:string, value:string) {
if (attribute) {
element.attribs[attribute] = value;
}
}
removeAttribute(element, attribute:string) {
if (attribute) {
delete element.attribs[attribute];
}
}
templateAwareRoot(el) {
return this.isTemplateElement(el) ? this.content(el) : el;
}
createHtmlDocument() {
throw _notImplemented('createHtmlDocument');
}
defaultDoc() {
if (defDoc === null) {
defDoc = StringMapWrapper.create();
StringMapWrapper.set(defDoc, "head", treeAdapter.createElement("head", null, []));
}
return defDoc;
}
elementMatches(n, selector:string):boolean {
//TODO: use selector class from core.
if (selector && selector.charAt(0) == ".") {
return this.hasClass(n, selector.substring(1));
} else {
return n.tagName == selector;
}
}
isTemplateElement(el:any):boolean {
return this.isElementNode(el) && this.tagName(el) === "template";
}
isTextNode(node):boolean {
return treeAdapter.isTextNode(node);
}
isCommentNode(node):boolean {
throw treeAdapter.isCommentNode(node);
}
isElementNode(node):boolean {
return node ? treeAdapter.isElementNode(node) : false;
}
hasShadowRoot(node):boolean {
return isPresent(node.shadowRoot);
}
importIntoDoc(node) {
return this.clone(node);
}
isPageRule(rule): boolean {
return rule.type === 6; //CSSRule.PAGE_RULE
}
isStyleRule(rule): boolean {
return rule.type === 1; //CSSRule.MEDIA_RULE
}
isMediaRule(rule): boolean {
return rule.type === 4; //CSSRule.MEDIA_RULE
}
isKeyframesRule(rule): boolean {
return rule.type === 7; //CSSRule.KEYFRAMES_RULE
}
getHref(el): string {
return el.href;
}
resolveAndSetHref(el, baseUrl:string, href:string) {
if (href == null) {
el.href = baseUrl;
} else {
el.href = url.resolve(baseUrl, href);
}
}
_buildRules(parsedRules, css) {
var rules = ListWrapper.create();
for (var i = 0; i < parsedRules.length; i++) {
var parsedRule = parsedRules[i];
var rule = {cssText: css};
rule.style = {content: "", cssText: ""};
if (parsedRule.type == "rule") {
rule.type = 1;
rule.selectorText = parsedRule.selectors.join(", ").replace(/\s{2,}/g, " ").replace(/\s*~\s*/g, " ~ ")
.replace(/\s*\+\s*/g, " + ").replace(/\s*>\s*/g, " > ").replace(/\[(\w+)=(\w+)\]/g, '[$1="$2"]');
if (isBlank(parsedRule.declarations)) {
continue;
}
for (var j = 0; j < parsedRule.declarations.length; j++) {
var declaration = parsedRule.declarations[j];
rule.style[declaration.property] = declaration.value;
rule.style.cssText += declaration.property + ": " + declaration.value + ";";
}
} else if (parsedRule.type == "media") {
rule.type = 4;
rule.media = {mediaText: parsedRule.media};
if (parsedRule.rules) {
rule.cssRules = this._buildRules(parsedRule.rules);
}
}
ListWrapper.push(rules, rule);
}
return rules;
}
cssToRules(css:string): List {
css = css.replace(/url\(\'(.+)\'\)/g, 'url($1)');
var rules = ListWrapper.create();
var parsedCSS = cssParse(css, {silent: true});
if (parsedCSS.stylesheet && parsedCSS.stylesheet.rules) {
rules = this._buildRules(parsedCSS.stylesheet.rules, css);
}
return rules;
}
}
//TODO: build a proper list, this one is all the keys of a HTMLInputElement
var _HTMLElementPropertyList = ["webkitEntries","incremental","webkitdirectory","selectionDirection","selectionEnd",
"selectionStart","labels","validationMessage","validity","willValidate","width","valueAsNumber","valueAsDate",
"value","useMap","defaultValue","type","step","src","size","required","readOnly","placeholder","pattern","name",
"multiple","min","minLength","maxLength","max","list","indeterminate","height","formTarget","formNoValidate",
"formMethod","formEnctype","formAction","files","form","disabled","dirName","checked","defaultChecked","autofocus",
"autocomplete","alt","align","accept","onautocompleteerror","onautocomplete","onwaiting","onvolumechange",
"ontoggle","ontimeupdate","onsuspend","onsubmit","onstalled","onshow","onselect","onseeking","onseeked","onscroll",
"onresize","onreset","onratechange","onprogress","onplaying","onplay","onpause","onmousewheel","onmouseup",
"onmouseover","onmouseout","onmousemove","onmouseleave","onmouseenter","onmousedown","onloadstart",
"onloadedmetadata","onloadeddata","onload","onkeyup","onkeypress","onkeydown","oninvalid","oninput","onfocus",
"onerror","onended","onemptied","ondurationchange","ondrop","ondragstart","ondragover","ondragleave","ondragenter",
"ondragend","ondrag","ondblclick","oncuechange","oncontextmenu","onclose","onclick","onchange","oncanplaythrough",
"oncanplay","oncancel","onblur","onabort","spellcheck","isContentEditable","contentEditable","outerText",
"innerText","accessKey","hidden","webkitdropzone","draggable","tabIndex","dir","translate","lang","title",
"childElementCount","lastElementChild","firstElementChild","children","onwebkitfullscreenerror",
"onwebkitfullscreenchange","nextElementSibling","previousElementSibling","onwheel","onselectstart",
"onsearch","onpaste","oncut","oncopy","onbeforepaste","onbeforecut","onbeforecopy","shadowRoot","dataset",
"classList","className","outerHTML","innerHTML","scrollHeight","scrollWidth","scrollTop","scrollLeft",
"clientHeight","clientWidth","clientTop","clientLeft","offsetParent","offsetHeight","offsetWidth","offsetTop",
"offsetLeft","localName","prefix","namespaceURI","id","style","attributes","tagName","parentElement","textContent",
"baseURI","ownerDocument","nextSibling","previousSibling","lastChild","firstChild","childNodes","parentNode",
"nodeType","nodeValue","nodeName","closure_lm_714617","__jsaction"];

View File

@ -11,6 +11,7 @@ import 'package:collection/equality.dart';
import 'package:angular2/src/dom/dom_adapter.dart' show DOM;
bool IS_DARTIUM = true;
bool IS_NODEJS = false;
Expect expect(actual, [matcher]) {
final expect = new Expect(actual);

View File

@ -1,23 +1,26 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
var _global = typeof window === 'undefined' ? global : window;
export {proxy} from 'rtts_assert/rtts_assert';
export var describe = window.describe;
export var xdescribe = window.xdescribe;
export var ddescribe = window.ddescribe;
export var it = window.it;
export var xit = window.xit;
export var iit = window.iit;
export var beforeEach = window.beforeEach;
export var afterEach = window.afterEach;
export var expect = window.expect;
export var describe = _global.describe;
export var xdescribe = _global.xdescribe;
export var ddescribe = _global.ddescribe;
export var it = _global.it;
export var xit = _global.xit;
export var iit = _global.iit;
export var beforeEach = _global.beforeEach;
export var afterEach = _global.afterEach;
export var expect = _global.expect;
export var IS_DARTIUM = false;
export var IS_NODEJS = typeof window === 'undefined';
// To make testing consistent between dart and js
window.print = function(msg) {
if (window.dump) {
window.dump(msg);
_global.print = function(msg) {
if (_global.dump) {
_global.dump(msg);
} else {
window.console.log(msg);
_global.console.log(msg);
}
};
@ -25,7 +28,7 @@ window.print = function(msg) {
// gives us bad error messages in tests.
// The only way to do this in Jasmine is to monkey patch a method
// to the object :-(
window.Map.prototype.jasmineToString = function() {
_global.Map.prototype.jasmineToString = function() {
var m = this;
if (!m) {
return ''+m;
@ -37,7 +40,7 @@ window.Map.prototype.jasmineToString = function() {
return `{ ${res.join(',')} }`;
}
window.beforeEach(function() {
_global.beforeEach(function() {
jasmine.addMatchers({
// Custom handler for Map as Jasmine does not support it yet
toEqual: function(util, customEqualityTesters) {
@ -150,15 +153,48 @@ export class SpyObject {
function elementText(n) {
var hasNodes = (n) => {var children = DOM.childNodes(n); return children && children.length > 0;}
if (!IS_NODEJS) {
var hasNodes = (n) => {var children = DOM.childNodes(n); return children && children.length > 0;}
if (n instanceof Comment) return '';
if (n instanceof Comment) return '';
if (n instanceof Array) return n.map((nn) => elementText(nn)).join("");
if (n instanceof Element && DOM.tagName(n) == 'CONTENT')
return elementText(Array.prototype.slice.apply(n.getDistributedNodes()));
if (DOM.hasShadowRoot(n)) return elementText(DOM.childNodesAsList(n.shadowRoot));
if (hasNodes(n)) return elementText(DOM.childNodesAsList(n));
if (n instanceof Array) return n.map((nn) => elementText(nn)).join("");
if (n instanceof Element && DOM.tagName(n) == 'CONTENT')
return elementText(Array.prototype.slice.apply(n.getDistributedNodes()));
if (DOM.hasShadowRoot(n)) return elementText(DOM.childNodesAsList(n.shadowRoot));
if (hasNodes(n)) return elementText(DOM.childNodesAsList(n));
return n.textContent;
return n.textContent;
} else {
if (DOM.hasShadowRoot(n)) {
return elementText(DOM.getShadowRoot(n).childNodes);
} else if (n instanceof Array) {
return n.map((nn) => elementText(nn)).join("");
} else if (DOM.tagName(n) == 'content') {
//minimal implementation of getDistributedNodes()
var host = null;
var temp = n;
while (temp.parent) {
if (DOM.hasShadowRoot(temp)) {
host = temp;
}
temp = temp.parent;
}
if (host) {
var list = [];
var select = DOM.getAttribute(n, "select");
var selectClass = select? select.substring(1): null;
DOM.childNodes(host).forEach((child) => {
var classList = DOM.classList(child);
if (selectClass && classList.indexOf(selectClass) > -1 || selectClass == null && classList.length == 0) {
list.push(child);
}
});
return list.length > 0? elementText(list): "";
}
return "";
} else {
return DOM.getText(n);
}
}
}