blob: 2e93f47e96e81a4b57fb145ee167b201a9ba3b16 [file] [log] [blame]
/**
* Copyright 2011 Rackspace
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
var sprintf = require('./sprintf').sprintf;
var utils = require('./utils');
var SyntaxError = require('./errors').SyntaxError;
var _cache = {};
var RE = new RegExp(
"(" +
"'[^']*'|\"[^\"]*\"|" +
"::|" +
"//?|" +
"\\.\\.|" +
"\\(\\)|" +
"[/.*:\\[\\]\\(\\)@=])|" +
"((?:\\{[^}]+\\})?[^/\\[\\]\\(\\)@=\\s]+)|" +
"\\s+", 'g'
);
var xpath_tokenizer = utils.findall.bind(null, RE);
function prepare_tag(next, token) {
var tag = token[0];
function select(context, result) {
var i, len, elem, rv = [];
for (i = 0, len = result.length; i < len; i++) {
elem = result[i];
elem._children.forEach(function(e) {
if (e.tag === tag) {
rv.push(e);
}
});
}
return rv;
}
return select;
}
function prepare_star(next, token) {
function select(context, result) {
var i, len, elem, rv = [];
for (i = 0, len = result.length; i < len; i++) {
elem = result[i];
elem._children.forEach(function(e) {
rv.push(e);
});
}
return rv;
}
return select;
}
function prepare_dot(next, token) {
function select(context, result) {
var i, len, elem, rv = [];
for (i = 0, len = result.length; i < len; i++) {
elem = result[i];
rv.push(elem);
}
return rv;
}
return select;
}
function prepare_iter(next, token) {
var tag;
token = next();
if (token[1] === '*') {
tag = '*';
}
else if (!token[1]) {
tag = token[0] || '';
}
else {
throw new SyntaxError(token);
}
function select(context, result) {
var i, len, elem, rv = [];
for (i = 0, len = result.length; i < len; i++) {
elem = result[i];
elem.iter(tag, function(e) {
if (e !== elem) {
rv.push(e);
}
});
}
return rv;
}
return select;
}
function prepare_dot_dot(next, token) {
function select(context, result) {
var i, len, elem, rv = [], parent_map = context.parent_map;
if (!parent_map) {
context.parent_map = parent_map = {};
context.root.iter(null, function(p) {
p._children.forEach(function(e) {
parent_map[e] = p;
});
});
}
for (i = 0, len = result.length; i < len; i++) {
elem = result[i];
if (parent_map.hasOwnProperty(elem)) {
rv.push(parent_map[elem]);
}
}
return rv;
}
return select;
}
function prepare_predicate(next, token) {
var tag, key, value, select;
token = next();
if (token[1] === '@') {
// attribute
token = next();
if (token[1]) {
throw new SyntaxError(token, 'Invalid attribute predicate');
}
key = token[0];
token = next();
if (token[1] === ']') {
select = function(context, result) {
var i, len, elem, rv = [];
for (i = 0, len = result.length; i < len; i++) {
elem = result[i];
if (elem.get(key)) {
rv.push(elem);
}
}
return rv;
};
}
else if (token[1] === '=') {
value = next()[1];
if (value[0] === '"' || value[value.length - 1] === '\'') {
value = value.slice(1, value.length - 1);
}
else {
throw new SyntaxError(token, 'Ivalid comparison target');
}
token = next();
select = function(context, result) {
var i, len, elem, rv = [];
for (i = 0, len = result.length; i < len; i++) {
elem = result[i];
if (elem.get(key) === value) {
rv.push(elem);
}
}
return rv;
};
}
if (token[1] !== ']') {
throw new SyntaxError(token, 'Invalid attribute predicate');
}
}
else if (!token[1]) {
tag = token[0] || '';
token = next();
if (token[1] !== ']') {
throw new SyntaxError(token, 'Invalid node predicate');
}
select = function(context, result) {
var i, len, elem, rv = [];
for (i = 0, len = result.length; i < len; i++) {
elem = result[i];
if (elem.find(tag)) {
rv.push(elem);
}
}
return rv;
};
}
else {
throw new SyntaxError(null, 'Invalid predicate');
}
return select;
}
var ops = {
"": prepare_tag,
"*": prepare_star,
".": prepare_dot,
"..": prepare_dot_dot,
"//": prepare_iter,
"[": prepare_predicate,
};
function _SelectorContext(root) {
this.parent_map = null;
this.root = root;
}
function findall(elem, path) {
var selector, result, i, len, token, value, select, context;
if (_cache.hasOwnProperty(path)) {
selector = _cache[path];
}
else {
// TODO: Use smarter cache purging approach
if (Object.keys(_cache).length > 100) {
_cache = {};
}
if (path.charAt(0) === '/') {
throw new SyntaxError(null, 'Cannot use absolute path on element');
}
result = xpath_tokenizer(path);
selector = [];
function getToken() {
return result.shift();
}
token = getToken();
while (true) {
var c = token[1] || '';
value = ops[c](getToken, token);
if (!value) {
throw new SyntaxError(null, sprintf('Invalid path: %s', path));
}
selector.push(value);
token = getToken();
if (!token) {
break;
}
else if (token[1] === '/') {
token = getToken();
}
if (!token) {
break;
}
}
_cache[path] = selector;
}
// Execute slector pattern
result = [elem];
context = new _SelectorContext(elem);
for (i = 0, len = selector.length; i < len; i++) {
select = selector[i];
result = select(context, result);
}
return result || [];
}
function find(element, path) {
var resultElements = findall(element, path);
if (resultElements && resultElements.length > 0) {
return resultElements[0];
}
return null;
}
function findtext(element, path, defvalue) {
var resultElements = findall(element, path);
if (resultElements && resultElements.length > 0) {
return resultElements[0].text;
}
return defvalue;
}
exports.find = find;
exports.findall = findall;
exports.findtext = findtext;