"use strict"; // Copyright 2012 United States Government, as represented by the Secretary of Defense, Under // Secretary of Defense (Personnel & Readiness). // // 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. /// XPath resolution functions. /// /// @module vwf/utility/xpath define( [ "module" ], function( module ) { var exports = { // -- 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: function( 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: function( 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: function( 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: function( 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: function( 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: function( 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: function( 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" ); } }, // -- regex ---------------------------------------------------------------------------- /// Regexes to crack the XPath string. 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; } )(), }; return exports; } );