123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- /*
- The MIT License (MIT)
- Copyright (c) 2014-2020 Nikolai Suslov and the Krestianstvo.org project contributors. (https://github.com/NikolaySuslov/livecodingspace/blob/master/LICENSE.md)
- Virtual World Framework Apache 2.0 license (https://github.com/NikolaySuslov/livecodingspace/blob/master/licenses/LICENSE_VWF.md)
- */
- /// XPath resolution functions.
- ///
- /// @module vwf/utility/xpath
- class XPath {
- constructor() {
- console.log("XPath constructor");
- let self = this;
- // -- regex ----------------------------------------------------------------------------
- /// Regexes to crack the XPath string.
- this.regex = ( function() {
- var name = "[A-Za-z_][A-Za-z_0-9.-]*", // XPath QName: http://www.w3.org/TR/xpath20/#prod-xpath-QName
- singleQuotedName = "'(?:[^'\\\\]|\\'|\\\\)+'", // Single-quoted QName (VWF extension)
- doubleQuotedName = '"(?:[^"\\\\]|\\"|\\\\)+"', // Double-quoted QName (VWF extension)
- wildcard = "\\*"; // XPath Wildcard: http://www.w3.org/TR/xpath20/#prod-xpath-Wildcard
- /// @field
- var step = // XPath StepExpr: http://www.w3.org/TR/xpath20/#prod-xpath-StepExpr
- "(?:" +
- "(?:" +
- "(?:" +
- "(?:(" + name + ")::)" + // "axis", as in "axis::"" (axis_name)
- "|" +
- "(@|)" + // "@", "" (abbreviated_axis_specifier)
- ")" +
- "(?:" +
- "(?:" +
- "(" + name + ")" + // "kind" (node_kind)
- "\\(" + // "("
- "(?:" +
- "(?:" +
- "(" + name + ")" + // "node" (node_name)
- "|" +
- "(" +
- "(?:" + singleQuotedName + ")" + // "'node'" (node_name_quoted, quoted and with internal escapes)
- "|" +
- "(?:" + doubleQuotedName + ")" + // "\"node\"" (node_name_quoted, quoted and with internal escapes)
- ")" +
- "|" +
- "(" + wildcard + ")" + // "*" (node_name_wildcard)
- ")" +
- "(?:" +
- "," + // ","
- "(?:" +
- "(" + name + ")" + // "type" (type_name)
- "|" +
- "(" +
- "(?:" + singleQuotedName + ")" + // "'type'" (type_name_quoted, quoted and with internal escapes)
- "|" +
- "(?:" + doubleQuotedName + ")" + // '"type"' (type_name_quoted, quoted and with internal escapes)
- ")" +
- ")" +
- ")?" +
- ")?" +
- "\\)" + // ")"
- ")" +
- "|" +
- "(?:" +
- "(" + name + ")" + // "name" (name_test)
- "|" +
- "(" +
- "(?:" + singleQuotedName + ")" + // "'name'" (name_test_quoted, quoted and with internal escapes)
- "|" +
- "(?:" + doubleQuotedName + ")" + // '"name"' (name_test_quoted, quoted and with internal escapes)
- ")" +
- "|" +
- "(" + wildcard + ")" + // "*" (name_test_wildcard)
- ")" +
- ")" +
- ")" +
- "|" +
- "(\\.\\.|\\.)" + // "..", "." (abbreviated_step)
- ")";
- /// @field
- var predicate = // XPath Predicate: http://www.w3.org/TR/xpath20/#prod-xpath-Predicate
- "\\[" +
- "(" +
- step + // "[^\\]]*" +
- ")" +
- "\\]";
- /// @field
- var separator = "/";
- var regexes = {
- /// @field
- step: new RegExp( "^" + step ),
- /// @field
- predicate: new RegExp( "^" + predicate ),
- /// @field
- separator: new RegExp( "^" + separator ),
- };
- return regexes;
- } )()
- }
- // -- resolve --------------------------------------------------------------------------
- /// Resolve an XPath expression, using a callback function to interpret each step.
- ///
- /// @param {String|String[]|Object[]} xpath
- /// @param {ID} rootID
- /// @param {ID|ID[]} contextIDs
- /// @param {Function} callback
- /// @param {Object} [thisArg]
- ///
- /// @returns {ID[]|undefined}
- resolve ( xpath, rootID, contextIDs, callback /* ( step, id, resolveAttributes ) */, thisArg ) {
- // Accept contextIDs as either a single id or an array of ids.
- if ( ! ( contextIDs instanceof Array ) ) {
- contextIDs = [ contextIDs ];
- }
- // Parse the expression.
- var steps = this.parse( xpath );
- if ( steps ) {
- // Reset the context if it's an absolute path.
- if ( steps.absolute ) {
- contextIDs = rootID ? [ rootID ] : [];
- }
- // Resolve each step.
- steps.forEach( function( step ) {
- contextIDs = Array.prototype.concat.apply( [], contextIDs.map( function( id ) {
- var stepIDs = callback.call( thisArg, step, id );
- step.predicates && step.predicates.forEach( function( predicate ) {
- stepIDs = stepIDs.filter( function( stepID ) {
- return this.resolve( predicate, rootID, stepID, function( step, id ) {
- return callback.call( this, step, id, true );
- }, thisArg ).length;
- }, this );
- }, this );
- return stepIDs;
- }, this ) );
- }, this );
- return contextIDs;
- }
- }
- // -- parse ----------------------------------------------------------------------------
- /// Parse an XPath expression into a series of steps.
- ///
- /// @param {String|String[]|Object[]} xpath
- ///
- /// @returns {Object[]|undefined}
- parse ( xpath ) {
- var steps = [], step;
- if ( typeof xpath == "string" || xpath instanceof String ) {
- if ( xpath[0] == "/" ) {
- steps.absolute = true;
- xpath = { string: xpath, index: 1 };
- } else {
- xpath = { string: xpath, index: 0 };
- }
- while ( xpath.index < xpath.string.length &&
- ( step = /* assignment! */ this.parseStep( xpath ) ) ) {
- steps.push( step );
- }
- if ( xpath.index < xpath.string.length ) {
- return undefined;
- }
- } else if ( typeof xpath[0] == "string" || xpath[0] instanceof String ) {
- var valid = true;
- steps = xpath.map( function( step ) {
- step = this.parseStep( step );
- valid = valid && step;
- return step;
- }, this );
- if ( ! valid ) {
- return undefined;
- }
- } else {
- steps = xpath;
- }
- return steps;
- }
- // -- parseStep ------------------------------------------------------------------------
- /// Parse an XPath step expression.
- ///
- /// @param {String|Object} xpath
- ///
- /// @returns {Object|undefined}
- parseStep ( xpath ) {
- if ( typeof xpath == "string" || xpath instanceof String ) {
- xpath = { string: xpath, index: 0 };
- }
- if ( xpath.index < xpath.string.length && ! this.regex.separator.test( xpath.string.slice( xpath.index ) ) ) {
- var step_match = this.regex.step.exec( xpath.string.slice( xpath.index ) );
- } else {
- var step_match = [].concat( "", new Array( 11 ), "" /* abbreviated_step */ ); // special case for "//"
- }
- if ( step_match ) {
- xpath.index += step_match[0].length;
- var axis_name = step_match[1],
- abbreviated_axis_specifier = step_match[2],
- node_kind = step_match[3],
- node_name = step_match[4],
- node_name_quoted = step_match[5],
- node_name_wildcard = step_match[6],
- type_name = step_match[7],
- type_name_quoted = step_match[8],
- name_test = step_match[9],
- name_test_quoted = step_match[10],
- name_test_wildcard = step_match[11],
- abbreviated_step = step_match[12];
- if ( name_test || name_test_quoted || name_test_wildcard ) {
- node_name = name_test;
- node_name_quoted = name_test_quoted;
- node_name_wildcard = name_test_wildcard;
- }
- if ( node_name_quoted ) {
- node_name = this.unquoteName( node_name_quoted );
- }
- if ( type_name_quoted ) {
- type_name = this.unquoteName( type_name_quoted );
- }
- switch ( abbreviated_step ) {
- case "": // "" == "descendant-or-self:node()"
- axis_name = "descendant-or-self";
- node_kind = "node";
- break;
- case ".": // "." == "self::node()"
- axis_name = "self";
- node_kind = "node";
- break;
- case "..": // ".." == "parent::node()"
- axis_name = "parent";
- node_kind = "node";
- break;
- }
- switch ( abbreviated_axis_specifier ) {
- case "": // "name" == "child::name"
- axis_name = "child";
- break;
- case "@": // "@name" == "attribute::name"
- axis_name = "attribute";
- break;
- }
- // // * == element()
- // "preceding::"
- // "preceding-sibling::"
- // "ancestor-or-self::"
- // "ancestor::"
- // "parent::"
- // "self::"
- // "child::"
- // "descendant::"
- // "descendant-or-self::"
- // "following-sibling::"
- // "following::"
- // // * == attribute()
- // "attribute::"
- // // * == namespace()
- // "namespace::"
- if ( node_name_wildcard && ! node_kind ) {
- switch ( axis_name ) {
- default:
- node_kind = "element";
- break;
- case "attribute":
- node_kind = "attribute";
- break;
- case "namespace":
- node_kind = "namespace";
- break;
- }
- }
- // Parse the predicates.
- var predicates = [], predicate;
- while ( predicate = /* assignment! */ this.parsePredicate( xpath ) ) {
- predicates.push( predicate );
- }
- // Absorb the separator.
- this.parseSeparator( xpath );
- // Now have: axis_name and name_test | node_kind(node_name,type_name)
- var step = {
- axis: axis_name,
- kind: node_kind,
- name: node_name,
- type: type_name,
- };
- if ( predicates.length ) {
- step.predicates = predicates;
- }
- return step;
- }
- }
- // -- parsePredicate -------------------------------------------------------------------
- /// Parse an XPath step predicate.
- ///
- /// @param {String|Object} xpath
- ///
- /// @returns {Object[]|undefined}
- parsePredicate ( xpath ) {
- if ( typeof xpath == "string" || xpath instanceof String ) {
- xpath = { string: xpath, index: 0 };
- }
- var predicate_match = this.regex.predicate.exec( xpath.string.slice( xpath.index ) );
- if ( predicate_match ) {
- xpath.index += predicate_match[0].length;
- return this.parse( predicate_match[1] );
- }
- }
- // -- parseSeparator -------------------------------------------------------------------
- /// Parse an XPath step separator.
- ///
- /// @param {String|Object} xpath
- ///
- /// @returns {Boolean|undefined}
- parseSeparator ( xpath ) {
- if ( typeof xpath == "string" || xpath instanceof String ) {
- xpath = { string: xpath, index: 0 };
- }
- var separator_match = this.regex.separator.exec( xpath.string.slice( xpath.index ) );
- if ( separator_match ) {
- xpath.index += separator_match[0].length;
- return true;
- }
- }
- // -- quoteName --------------------------------------------------------------------------
- /// Apply quotation marks around a name and escape internal quotation marks and escape
- /// characters.
- ///
- /// @param {String} unquoted_name
- ///
- /// @returns {String}
- quoteName ( unquoted_name ) {
- return '"' + unquoted_name.replace( /(["\\])/g, "\\$1" ) + '"';
- }
- // -- unquoteName ------------------------------------------------------------------------
- /// Remove the enclosing quotation marks and unescape internal quotation marks and escape
- /// characters of a quoted name.
- ///
- /// @param {String} quoted_name
- ///
- /// @returns {String}
- unquoteName ( quoted_name ) {
- if ( quoted_name[0] == "'" ) {
- return quoted_name.slice( 1, -1 ).replace( /\\(['\\])/g, "$1" );
- } else if ( quoted_name[0] == '"' ) {
- return quoted_name.slice( 1, -1 ).replace( /\\(["\\])/g, "$1" );
- }
- }
- }
- export {
- XPath
- }
|