575 lines
16 KiB
JavaScript
575 lines
16 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
"use strict";
|
|
/// <reference path="./custom_typings/main.d.ts" />
|
|
var cloneObject = require('clone');
|
|
var parse5 = require('parse5');
|
|
function getAttributeIndex(element, name) {
|
|
if (!element.attrs) {
|
|
return -1;
|
|
}
|
|
var n = name.toLowerCase();
|
|
for (var i = 0; i < element.attrs.length; i++) {
|
|
if (element.attrs[i].name.toLowerCase() === n) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
/**
|
|
* @returns `true` iff [element] has the attribute [name], `false` otherwise.
|
|
*/
|
|
function hasAttribute(element, name) {
|
|
return getAttributeIndex(element, name) !== -1;
|
|
}
|
|
exports.hasAttribute = hasAttribute;
|
|
function hasSpaceSeparatedAttrValue(name, value) {
|
|
return function (element) {
|
|
var attributeValue = getAttribute(element, name);
|
|
if (typeof attributeValue !== 'string') {
|
|
return false;
|
|
}
|
|
return attributeValue.split(' ').indexOf(value) !== -1;
|
|
};
|
|
}
|
|
exports.hasSpaceSeparatedAttrValue = hasSpaceSeparatedAttrValue;
|
|
/**
|
|
* @returns The string value of attribute `name`, or `null`.
|
|
*/
|
|
function getAttribute(element, name) {
|
|
var i = getAttributeIndex(element, name);
|
|
if (i > -1) {
|
|
return element.attrs[i].value;
|
|
}
|
|
return null;
|
|
}
|
|
exports.getAttribute = getAttribute;
|
|
function setAttribute(element, name, value) {
|
|
var i = getAttributeIndex(element, name);
|
|
if (i > -1) {
|
|
element.attrs[i].value = value;
|
|
}
|
|
else {
|
|
element.attrs.push({ name: name, value: value });
|
|
}
|
|
}
|
|
exports.setAttribute = setAttribute;
|
|
function removeAttribute(element, name) {
|
|
var i = getAttributeIndex(element, name);
|
|
if (i > -1) {
|
|
element.attrs.splice(i, 1);
|
|
}
|
|
}
|
|
exports.removeAttribute = removeAttribute;
|
|
function hasTagName(name) {
|
|
var n = name.toLowerCase();
|
|
return function (node) {
|
|
if (!node.tagName) {
|
|
return false;
|
|
}
|
|
return node.tagName.toLowerCase() === n;
|
|
};
|
|
}
|
|
/**
|
|
* Returns true if `regex.match(tagName)` finds a match.
|
|
*
|
|
* This will use the lowercased tagName for comparison.
|
|
*/
|
|
function hasMatchingTagName(regex) {
|
|
return function (node) {
|
|
if (!node.tagName) {
|
|
return false;
|
|
}
|
|
return regex.test(node.tagName.toLowerCase());
|
|
};
|
|
}
|
|
function hasClass(name) {
|
|
return hasSpaceSeparatedAttrValue('class', name);
|
|
}
|
|
function collapseTextRange(parent, start, end) {
|
|
if (!parent.childNodes) {
|
|
return;
|
|
}
|
|
var text = '';
|
|
for (var i = start; i <= end; i++) {
|
|
text += getTextContent(parent.childNodes[i]);
|
|
}
|
|
parent.childNodes.splice(start, (end - start) + 1);
|
|
if (text) {
|
|
var tn = newTextNode(text);
|
|
tn.parentNode = parent;
|
|
parent.childNodes.splice(start, 0, tn);
|
|
}
|
|
}
|
|
/**
|
|
* Normalize the text inside an element
|
|
*
|
|
* Equivalent to `element.normalize()` in the browser
|
|
* See https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
|
|
*/
|
|
function normalize(node) {
|
|
if (!(isElement(node) || isDocument(node) || isDocumentFragment(node))) {
|
|
return;
|
|
}
|
|
if (!node.childNodes) {
|
|
return;
|
|
}
|
|
var textRangeStart = -1;
|
|
for (var i = node.childNodes.length - 1, n = void 0; i >= 0; i--) {
|
|
n = node.childNodes[i];
|
|
if (isTextNode(n)) {
|
|
if (textRangeStart === -1) {
|
|
textRangeStart = i;
|
|
}
|
|
if (i === 0) {
|
|
// collapse leading text nodes
|
|
collapseTextRange(node, 0, textRangeStart);
|
|
}
|
|
}
|
|
else {
|
|
// recurse
|
|
normalize(n);
|
|
// collapse the range after this node
|
|
if (textRangeStart > -1) {
|
|
collapseTextRange(node, i + 1, textRangeStart);
|
|
textRangeStart = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
exports.normalize = normalize;
|
|
/**
|
|
* Return the text value of a node or element
|
|
*
|
|
* Equivalent to `node.textContent` in the browser
|
|
*/
|
|
function getTextContent(node) {
|
|
if (isCommentNode(node)) {
|
|
return node.data;
|
|
}
|
|
if (isTextNode(node)) {
|
|
return node.value || '';
|
|
}
|
|
var subtree = nodeWalkAll(node, isTextNode);
|
|
return subtree.map(getTextContent).join('');
|
|
}
|
|
exports.getTextContent = getTextContent;
|
|
/**
|
|
* Set the text value of a node or element
|
|
*
|
|
* Equivalent to `node.textContent = value` in the browser
|
|
*/
|
|
function setTextContent(node, value) {
|
|
if (isCommentNode(node)) {
|
|
node.data = value;
|
|
}
|
|
else if (isTextNode(node)) {
|
|
node.value = value;
|
|
}
|
|
else {
|
|
var tn = newTextNode(value);
|
|
tn.parentNode = node;
|
|
node.childNodes = [tn];
|
|
}
|
|
}
|
|
exports.setTextContent = setTextContent;
|
|
/**
|
|
* Match the text inside an element, textnode, or comment
|
|
*
|
|
* Note: nodeWalkAll with hasTextValue may return an textnode and its parent if
|
|
* the textnode is the only child in that parent.
|
|
*/
|
|
function hasTextValue(value) {
|
|
return function (node) {
|
|
return getTextContent(node) === value;
|
|
};
|
|
}
|
|
function OR() {
|
|
var rules = new Array(arguments.length);
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
rules[i] = arguments[i];
|
|
}
|
|
return function (node) {
|
|
for (var i = 0; i < rules.length; i++) {
|
|
if (rules[i](node)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
function AND() {
|
|
var rules = new Array(arguments.length);
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
rules[i] = arguments[i];
|
|
}
|
|
return function (node) {
|
|
for (var i = 0; i < rules.length; i++) {
|
|
if (!rules[i](node)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
/**
|
|
* negate an individual predicate, or a group with AND or OR
|
|
*/
|
|
function NOT(predicateFn) {
|
|
return function (node) {
|
|
return !predicateFn(node);
|
|
};
|
|
}
|
|
/**
|
|
* Returns a predicate that matches any node with a parent matching
|
|
* `predicateFn`.
|
|
*/
|
|
function parentMatches(predicateFn) {
|
|
return function (node) {
|
|
var parent = node.parentNode;
|
|
while (parent !== undefined) {
|
|
if (predicateFn(parent)) {
|
|
return true;
|
|
}
|
|
parent = parent.parentNode;
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
function hasAttr(attr) {
|
|
return function (node) {
|
|
return getAttributeIndex(node, attr) > -1;
|
|
};
|
|
}
|
|
function hasAttrValue(attr, value) {
|
|
return function (node) {
|
|
return getAttribute(node, attr) === value;
|
|
};
|
|
}
|
|
function isDocument(node) {
|
|
return node.nodeName === '#document';
|
|
}
|
|
exports.isDocument = isDocument;
|
|
function isDocumentFragment(node) {
|
|
return node.nodeName === '#document-fragment';
|
|
}
|
|
exports.isDocumentFragment = isDocumentFragment;
|
|
function isElement(node) {
|
|
return node.nodeName === node.tagName;
|
|
}
|
|
exports.isElement = isElement;
|
|
function isTextNode(node) {
|
|
return node.nodeName === '#text';
|
|
}
|
|
exports.isTextNode = isTextNode;
|
|
function isCommentNode(node) {
|
|
return node.nodeName === '#comment';
|
|
}
|
|
exports.isCommentNode = isCommentNode;
|
|
/**
|
|
* Applies `mapfn` to `node` and the tree below `node`, returning a flattened
|
|
* list of results.
|
|
*/
|
|
function treeMap(node, mapfn) {
|
|
var results = [];
|
|
nodeWalk(node, function (node) {
|
|
results = results.concat(mapfn(node));
|
|
return false;
|
|
});
|
|
return results;
|
|
}
|
|
exports.treeMap = treeMap;
|
|
/**
|
|
* Walk the tree down from `node`, applying the `predicate` function.
|
|
* Return the first node that matches the given predicate.
|
|
*
|
|
* @returns `null` if no node matches, parse5 node object if a node matches.
|
|
*/
|
|
function nodeWalk(node, predicate) {
|
|
if (predicate(node)) {
|
|
return node;
|
|
}
|
|
var match = null;
|
|
if (node.childNodes) {
|
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
match = nodeWalk(node.childNodes[i], predicate);
|
|
if (match) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
exports.nodeWalk = nodeWalk;
|
|
/**
|
|
* Walk the tree down from `node`, applying the `predicate` function.
|
|
* All nodes matching the predicate function from `node` to leaves will be
|
|
* returned.
|
|
*/
|
|
function nodeWalkAll(node, predicate, matches) {
|
|
if (!matches) {
|
|
matches = [];
|
|
}
|
|
if (predicate(node)) {
|
|
matches.push(node);
|
|
}
|
|
if (node.childNodes) {
|
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
nodeWalkAll(node.childNodes[i], predicate, matches);
|
|
}
|
|
}
|
|
return matches;
|
|
}
|
|
exports.nodeWalkAll = nodeWalkAll;
|
|
function _reverseNodeWalkAll(node, predicate, matches) {
|
|
if (!matches) {
|
|
matches = [];
|
|
}
|
|
if (node.childNodes) {
|
|
for (var i = node.childNodes.length - 1; i >= 0; i--) {
|
|
nodeWalkAll(node.childNodes[i], predicate, matches);
|
|
}
|
|
}
|
|
if (predicate(node)) {
|
|
matches.push(node);
|
|
}
|
|
return matches;
|
|
}
|
|
/**
|
|
* Equivalent to `nodeWalk`, but only returns nodes that are either
|
|
* ancestors or earlier cousins/siblings in the document.
|
|
*
|
|
* Nodes are searched in reverse document order, starting from the sibling
|
|
* prior to `node`.
|
|
*/
|
|
function nodeWalkPrior(node, predicate) {
|
|
// Search our earlier siblings and their descendents.
|
|
var parent = node.parentNode;
|
|
if (parent) {
|
|
var idx = parent.childNodes.indexOf(node);
|
|
var siblings = parent.childNodes.slice(0, idx);
|
|
for (var i = siblings.length - 1; i >= 0; i--) {
|
|
var sibling = siblings[i];
|
|
if (predicate(sibling)) {
|
|
return sibling;
|
|
}
|
|
var found = nodeWalk(sibling, predicate);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
if (predicate(parent)) {
|
|
return parent;
|
|
}
|
|
return nodeWalkPrior(parent, predicate);
|
|
}
|
|
return undefined;
|
|
}
|
|
exports.nodeWalkPrior = nodeWalkPrior;
|
|
/**
|
|
* Walk the tree up from the parent of `node`, to its grandparent and so on to
|
|
* the root of the tree. Return the first ancestor that matches the given
|
|
* predicate.
|
|
*/
|
|
function nodeWalkAncestors(node, predicate) {
|
|
var parent = node.parentNode;
|
|
if (!parent) {
|
|
return undefined;
|
|
}
|
|
if (predicate(parent)) {
|
|
return parent;
|
|
}
|
|
return nodeWalkAncestors(parent, predicate);
|
|
}
|
|
exports.nodeWalkAncestors = nodeWalkAncestors;
|
|
/**
|
|
* Equivalent to `nodeWalkAll`, but only returns nodes that are either
|
|
* ancestors or earlier cousins/siblings in the document.
|
|
*
|
|
* Nodes are returned in reverse document order, starting from `node`.
|
|
*/
|
|
function nodeWalkAllPrior(node, predicate, matches) {
|
|
if (!matches) {
|
|
matches = [];
|
|
}
|
|
if (predicate(node)) {
|
|
matches.push(node);
|
|
}
|
|
// Search our earlier siblings and their descendents.
|
|
var parent = node.parentNode;
|
|
if (parent) {
|
|
var idx = parent.childNodes.indexOf(node);
|
|
var siblings = parent.childNodes.slice(0, idx);
|
|
for (var i = siblings.length - 1; i >= 0; i--) {
|
|
_reverseNodeWalkAll(siblings[i], predicate, matches);
|
|
}
|
|
nodeWalkAllPrior(parent, predicate, matches);
|
|
}
|
|
return matches;
|
|
}
|
|
exports.nodeWalkAllPrior = nodeWalkAllPrior;
|
|
/**
|
|
* Equivalent to `nodeWalk`, but only matches elements
|
|
*/
|
|
function query(node, predicate) {
|
|
var elementPredicate = AND(isElement, predicate);
|
|
return nodeWalk(node, elementPredicate);
|
|
}
|
|
exports.query = query;
|
|
/**
|
|
* Equivalent to `nodeWalkAll`, but only matches elements
|
|
*/
|
|
function queryAll(node, predicate, matches) {
|
|
var elementPredicate = AND(isElement, predicate);
|
|
return nodeWalkAll(node, elementPredicate, matches);
|
|
}
|
|
exports.queryAll = queryAll;
|
|
function newTextNode(value) {
|
|
return {
|
|
nodeName: '#text',
|
|
value: value,
|
|
parentNode: undefined,
|
|
attrs: [],
|
|
__location: undefined
|
|
};
|
|
}
|
|
function newCommentNode(comment) {
|
|
return {
|
|
nodeName: '#comment',
|
|
data: comment,
|
|
parentNode: undefined,
|
|
attrs: [],
|
|
__location: undefined
|
|
};
|
|
}
|
|
function newElement(tagName, namespace) {
|
|
return {
|
|
nodeName: tagName,
|
|
tagName: tagName,
|
|
childNodes: [],
|
|
namespaceURI: namespace || 'http://www.w3.org/1999/xhtml',
|
|
attrs: [],
|
|
parentNode: undefined,
|
|
__location: undefined
|
|
};
|
|
}
|
|
function newDocumentFragment() {
|
|
return {
|
|
nodeName: '#document-fragment',
|
|
childNodes: [],
|
|
parentNode: null,
|
|
quirksMode: false
|
|
};
|
|
}
|
|
function cloneNode(node) {
|
|
// parent is a backreference, and we don't want to clone the whole tree, so
|
|
// make it null before cloning.
|
|
var parent = node.parentNode;
|
|
node.parentNode = undefined;
|
|
var clone = cloneObject(node);
|
|
node.parentNode = parent;
|
|
return clone;
|
|
}
|
|
exports.cloneNode = cloneNode;
|
|
/**
|
|
* Inserts `newNode` into `parent` at `index`, optionally replaceing the
|
|
* current node at `index`. If `newNode` is a DocumentFragment, its childNodes
|
|
* are inserted and removed from the fragment.
|
|
*/
|
|
function insertNode(parent, index, newNode, replace) {
|
|
if (!parent.childNodes) {
|
|
throw new Error("Parent node has no childNodes, can't insert.");
|
|
}
|
|
var newNodes = [];
|
|
var removedNode = replace ? parent.childNodes[index] : null;
|
|
if (newNode) {
|
|
if (isDocumentFragment(newNode)) {
|
|
newNodes = newNode.childNodes || [];
|
|
newNode.childNodes = [];
|
|
}
|
|
else {
|
|
newNodes = [newNode];
|
|
remove(newNode);
|
|
}
|
|
}
|
|
if (replace) {
|
|
removedNode = parent.childNodes[index];
|
|
}
|
|
Array.prototype.splice.apply(parent.childNodes, [index, replace ? 1 : 0].concat(newNodes));
|
|
newNodes.forEach(function (n) {
|
|
n.parentNode = parent;
|
|
});
|
|
if (removedNode) {
|
|
removedNode.parentNode = undefined;
|
|
}
|
|
}
|
|
function replace(oldNode, newNode) {
|
|
var parent = oldNode.parentNode;
|
|
var index = parent.childNodes.indexOf(oldNode);
|
|
insertNode(parent, index, newNode, true);
|
|
}
|
|
exports.replace = replace;
|
|
function remove(node) {
|
|
var parent = node.parentNode;
|
|
if (parent && parent.childNodes) {
|
|
var idx = parent.childNodes.indexOf(node);
|
|
parent.childNodes.splice(idx, 1);
|
|
}
|
|
node.parentNode = undefined;
|
|
}
|
|
exports.remove = remove;
|
|
function insertBefore(parent, oldNode, newNode) {
|
|
var index = parent.childNodes.indexOf(oldNode);
|
|
insertNode(parent, index, newNode);
|
|
}
|
|
exports.insertBefore = insertBefore;
|
|
function append(parent, newNode) {
|
|
insertNode(parent, parent.childNodes.length, newNode);
|
|
}
|
|
exports.append = append;
|
|
function parse(text, options) {
|
|
var parser = new parse5.Parser(parse5.TreeAdapters.default, options);
|
|
return parser.parse(text);
|
|
}
|
|
exports.parse = parse;
|
|
function parseFragment(text) {
|
|
var parser = new parse5.Parser();
|
|
return parser.parseFragment(text);
|
|
}
|
|
exports.parseFragment = parseFragment;
|
|
function serialize(ast) {
|
|
var serializer = new parse5.Serializer();
|
|
return serializer.serialize(ast);
|
|
}
|
|
exports.serialize = serialize;
|
|
exports.predicates = {
|
|
hasClass: hasClass,
|
|
hasAttr: hasAttr,
|
|
hasAttrValue: hasAttrValue,
|
|
hasMatchingTagName: hasMatchingTagName,
|
|
hasSpaceSeparatedAttrValue: hasSpaceSeparatedAttrValue,
|
|
hasTagName: hasTagName,
|
|
hasTextValue: hasTextValue,
|
|
AND: AND,
|
|
OR: OR,
|
|
NOT: NOT,
|
|
parentMatches: parentMatches
|
|
};
|
|
exports.constructors = {
|
|
text: newTextNode,
|
|
comment: newCommentNode,
|
|
element: newElement,
|
|
fragment: newDocumentFragment
|
|
};
|