fontoxpath
Version:
A minimalistic XPath 3.1 engine in JavaScript
214 lines (185 loc) • 7.52 kB
JavaScript
import Selector from '../Selector';
import Specificity from '../Specificity';
import { DONE_TOKEN, ready } from '../util/iterators';
import createNodeValue from '../dataTypes/createNodeValue';
import isSubtypeOf from '../dataTypes/isSubtypeOf';
import atomize from '../dataTypes/atomize';
import Sequence from '../dataTypes/Sequence';
import castToType from '../dataTypes/castToType';
import concatSequences from '../util/concatSequences';
function getAttributeValueForNamespaceDeclaration (partialValueSelectors) {
if (!partialValueSelectors.length) {
return null;
}
if (partialValueSelectors.length > 1 || typeof partialValueSelectors[0] !== 'string') {
throw new Error('XQST0022: The value of namespace declaration attributes may not contain enclosed expressions.');
}
return partialValueSelectors[0];
}
/**
* @extends {Selector}
*/
class DirElementConstructor extends Selector {
/**
* @param {string} prefix
* @param {string} name
* @param {!Array<!{name:!Array<!string>,partialValues:!Array<(!string|!Selector)>}>} attributes
* @param {!Array<!Selector>} contents Strings and enclosed expressions
*/
constructor (prefix, name, attributes, contents) {
super(new Specificity({}), {
canBeStaticallyEvaluated: false,
resultOrder: Selector.RESULT_ORDERINGS.UNSORTED
});
this._prefix = prefix;
this._name = name;
/**
* @type {!Object<!string, !string>}
*/
this._namespacesInScope = {};
/**
* @type {!Array<!{qualifiedName:!{prefix:!string,localPart:!string},partialValues:!Array<(!string|!Selector)>}>}
*/
this._attributes = [];
attributes.forEach(({ name, partialValues }) => {
if (!(name[1] === 'xmlns' && name[0] === null) && name[0] !== 'xmlns') {
this._attributes.push({
qualifiedName: {
prefix: name[0],
localPart: name[1]
},
partialValues
});
return;
}
const namespaceURI = getAttributeValueForNamespaceDeclaration(partialValues);
const namespacePrefix = name[0] === 'xmlns' ? name[1] : '';
if (namespacePrefix in this._namespacesInScope) {
throw new Error(`XQST0071: The namespace declaration with the prefix ${namespacePrefix} has already been declared on the constructed element.`);
}
this._namespacesInScope[namespacePrefix] = namespaceURI;
});
this._contents = contents;
}
/**
* @param {!../DynamicContext} dynamicContext
* @return {!Sequence}
*/
evaluate (dynamicContext) {
/**
* @type {!../DynamicContext}
*/
const dynamicContextWithNamespaces = dynamicContext.scopeWithNamespaceResolver(
prefix => {
prefix = prefix || '';
return prefix in this._namespacesInScope ?
this._namespacesInScope[prefix] :
dynamicContext.resolveNamespacePrefix(prefix);
});
/**
* @type INodesFactory
*/
const nodesFactory = dynamicContext.nodesFactory;
const attributes = this._attributes.map(({ qualifiedName, partialValues }) => ({
qualifiedName,
valueSequences: partialValues
.map(
value => typeof value === 'string' ?
Sequence.singleton({ value, type: 'xs:string' }) :
value.evaluateMaybeStatically(dynamicContextWithNamespaces).atomize(dynamicContextWithNamespaces)),
hasAllValues: false,
value: null
}));
let attributePhaseDone = false;
let childNodesPhaseDone = false;
let childNodesSequences;
let done = false;
return new Sequence({
next: () => {
if (done) {
return DONE_TOKEN;
}
if (!attributePhaseDone) {
let unfinishedAttributeRetrieve = attributes.find(attr => !attr.hasAllValues);
while (unfinishedAttributeRetrieve) {
const vals = unfinishedAttributeRetrieve.valueSequences.map(sequence => sequence.tryGetAllValues());
if (vals.some(val => !val.ready)) {
return vals.find(val => !val.ready);
}
unfinishedAttributeRetrieve.value = vals
.map(val => val.value.map(v => castToType(v, 'xs:string').value).join(' ')).join('');
unfinishedAttributeRetrieve.hasAllValues = true;
unfinishedAttributeRetrieve = attributes.find(attr => !attr.hasAllValues);
}
attributePhaseDone = true;
}
if (!childNodesPhaseDone) {
// Accumulate all children
childNodesSequences = concatSequences(
this._contents.map(
contentSelector => contentSelector.evaluateMaybeStatically(dynamicContextWithNamespaces)
.mapAll(allValues => new Sequence([allValues]))));
childNodesPhaseDone = true;
}
const allChildNodesItrResult = childNodesSequences.tryGetAllValues();
if (!allChildNodesItrResult.ready) {
return allChildNodesItrResult;
}
const elementNamespaceURI = dynamicContextWithNamespaces.resolveNamespacePrefix(this._prefix);
const element = nodesFactory.createElementNS(elementNamespaceURI, this._prefix ? this._prefix + ':' + this._name : this._name);
// Plonk all attribute on the element
attributes.forEach(attr => {
const attrName = attr.qualifiedName.prefix ? attr.qualifiedName.prefix + ':' + attr.qualifiedName.localPart : attr.qualifiedName.localPart;
const attributeNamespaceURI = attr.qualifiedName.prefix ? dynamicContextWithNamespaces.resolveNamespacePrefix(attr.qualifiedName.prefix) : null;
if (element.hasAttributeNS(attributeNamespaceURI, attr.qualifiedName.localPart)) {
throw new Error(`XQST0040: The attribute ${attrName} is already present on a constructed element.`);
}
element.setAttributeNS(attributeNamespaceURI, attrName, attr.value);
});
// Plonk all childNodes, these are special though
allChildNodesItrResult.value.forEach(childNodes => {
childNodes.forEach((childNode, i) => {
if (isSubtypeOf(childNode.type, 'xs:anyAtomicType')) {
const atomizedValue = castToType(atomize(childNode, dynamicContextWithNamespaces), 'xs:string').value;
if (i !== 0 && isSubtypeOf(childNodes[i - 1].type, 'xs:anyAtomicType')) {
element.appendChild(nodesFactory.createTextNode(' ' + atomizedValue));
return;
}
element.appendChild(nodesFactory.createTextNode(atomizedValue));
return;
}
if (isSubtypeOf(childNode.type, 'attribute()')) {
// The contents may include attributes, 'clone' them and set them on the element
if (element.hasAttributeNS(childNode.value.namespaceURI, childNode.value.localName)) {
throw new Error(
`XQST0040: The attribute ${childNode.value.name} is already present on a constructed element.`);
}
element.setAttributeNS(
childNode.value.namespaceURI,
childNode.value.prefix ?
childNode.value.prefix + ':' + childNode.value.localName : childNode.value.localName,
childNode.value.value);
return;
}
if (isSubtypeOf(childNode.type, 'node()')) {
// Deep clone child elements
// TODO: skip copy if the childNode has already been created in the expression
element.appendChild(childNode.value.cloneNode(true));
return;
}
// We now only have unatomizable types left
// (function || map) && !array
if (isSubtypeOf(childNode.type, 'function(*)') && !isSubtypeOf(childNode.type, 'array(*)')) {
throw new Error(`FOTY0013: Atomization is not supported for ${childNode.type}.`);
}
throw new Error(`Atomizing ${childNode.type} is not implemented.`);
});
});
element.normalize();
done = true;
return ready(createNodeValue(element));
}
});
}
}
export default DirElementConstructor;