xpath.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. "use strict";
  2. // Copyright 2012 United States Government, as represented by the Secretary of Defense, Under
  3. // Secretary of Defense (Personnel & Readiness).
  4. //
  5. // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
  6. // in compliance with the License. You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software distributed under the License
  11. // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
  12. // or implied. See the License for the specific language governing permissions and limitations under
  13. // the License.
  14. /// XPath resolution functions.
  15. ///
  16. /// @module vwf/utility/xpath
  17. define( [ "module" ], function( module ) {
  18. var exports = {
  19. // -- resolve --------------------------------------------------------------------------
  20. /// Resolve an XPath expression, using a callback function to interpret each step.
  21. ///
  22. /// @param {String|String[]|Object[]} xpath
  23. /// @param {ID} rootID
  24. /// @param {ID|ID[]} contextIDs
  25. /// @param {Function} callback
  26. /// @param {Object} [thisArg]
  27. ///
  28. /// @returns {ID[]|undefined}
  29. resolve: function( xpath, rootID, contextIDs, callback /* ( step, id, resolveAttributes ) */, thisArg ) {
  30. // Accept contextIDs as either a single id or an array of ids.
  31. if ( ! ( contextIDs instanceof Array ) ) {
  32. contextIDs = [ contextIDs ];
  33. }
  34. // Parse the expression.
  35. var steps = this.parse( xpath );
  36. if ( steps ) {
  37. // Reset the context if it's an absolute path.
  38. if ( steps.absolute ) {
  39. contextIDs = rootID ? [ rootID ] : [];
  40. }
  41. // Resolve each step.
  42. steps.forEach( function( step ) {
  43. contextIDs = Array.prototype.concat.apply( [], contextIDs.map( function( id ) {
  44. var stepIDs = callback.call( thisArg, step, id );
  45. step.predicates && step.predicates.forEach( function( predicate ) {
  46. stepIDs = stepIDs.filter( function( stepID ) {
  47. return this.resolve( predicate, rootID, stepID, function( step, id ) {
  48. return callback.call( this, step, id, true );
  49. }, thisArg ).length;
  50. }, this );
  51. }, this );
  52. return stepIDs;
  53. }, this ) );
  54. }, this );
  55. return contextIDs;
  56. }
  57. },
  58. // -- parse ----------------------------------------------------------------------------
  59. /// Parse an XPath expression into a series of steps.
  60. ///
  61. /// @param {String|String[]|Object[]} xpath
  62. ///
  63. /// @returns {Object[]|undefined}
  64. parse: function( xpath ) {
  65. var steps = [], step;
  66. if ( typeof xpath == "string" || xpath instanceof String ) {
  67. if ( xpath[0] == "/" ) {
  68. steps.absolute = true;
  69. xpath = { string: xpath, index: 1 };
  70. } else {
  71. xpath = { string: xpath, index: 0 };
  72. }
  73. while ( xpath.index < xpath.string.length &&
  74. ( step = /* assignment! */ this.parseStep( xpath ) ) ) {
  75. steps.push( step );
  76. }
  77. if ( xpath.index < xpath.string.length ) {
  78. return undefined;
  79. }
  80. } else if ( typeof xpath[0] == "string" || xpath[0] instanceof String ) {
  81. var valid = true;
  82. steps = xpath.map( function( step ) {
  83. step = this.parseStep( step );
  84. valid = valid && step;
  85. return step;
  86. }, this );
  87. if ( ! valid ) {
  88. return undefined;
  89. }
  90. } else {
  91. steps = xpath;
  92. }
  93. return steps;
  94. },
  95. // -- parseStep ------------------------------------------------------------------------
  96. /// Parse an XPath step expression.
  97. ///
  98. /// @param {String|Object} xpath
  99. ///
  100. /// @returns {Object|undefined}
  101. parseStep: function( xpath ) {
  102. if ( typeof xpath == "string" || xpath instanceof String ) {
  103. xpath = { string: xpath, index: 0 };
  104. }
  105. if ( xpath.index < xpath.string.length && ! this.regex.separator.test( xpath.string.slice( xpath.index ) ) ) {
  106. var step_match = this.regex.step.exec( xpath.string.slice( xpath.index ) );
  107. } else {
  108. var step_match = [].concat( "", new Array( 11 ), "" /* abbreviated_step */ ); // special case for "//"
  109. }
  110. if ( step_match ) {
  111. xpath.index += step_match[0].length;
  112. var axis_name = step_match[1],
  113. abbreviated_axis_specifier = step_match[2],
  114. node_kind = step_match[3],
  115. node_name = step_match[4],
  116. node_name_quoted = step_match[5],
  117. node_name_wildcard = step_match[6],
  118. type_name = step_match[7],
  119. type_name_quoted = step_match[8],
  120. name_test = step_match[9],
  121. name_test_quoted = step_match[10],
  122. name_test_wildcard = step_match[11],
  123. abbreviated_step = step_match[12];
  124. if ( name_test || name_test_quoted || name_test_wildcard ) {
  125. node_name = name_test;
  126. node_name_quoted = name_test_quoted;
  127. node_name_wildcard = name_test_wildcard;
  128. }
  129. if ( node_name_quoted ) {
  130. node_name = this.unquoteName( node_name_quoted );
  131. }
  132. if ( type_name_quoted ) {
  133. type_name = this.unquoteName( type_name_quoted );
  134. }
  135. switch ( abbreviated_step ) {
  136. case "": // "" == "descendant-or-self:node()"
  137. axis_name = "descendant-or-self";
  138. node_kind = "node";
  139. break;
  140. case ".": // "." == "self::node()"
  141. axis_name = "self";
  142. node_kind = "node";
  143. break;
  144. case "..": // ".." == "parent::node()"
  145. axis_name = "parent";
  146. node_kind = "node";
  147. break;
  148. }
  149. switch ( abbreviated_axis_specifier ) {
  150. case "": // "name" == "child::name"
  151. axis_name = "child";
  152. break;
  153. case "@": // "@name" == "attribute::name"
  154. axis_name = "attribute";
  155. break;
  156. }
  157. // // * == element()
  158. // "preceding::"
  159. // "preceding-sibling::"
  160. // "ancestor-or-self::"
  161. // "ancestor::"
  162. // "parent::"
  163. // "self::"
  164. // "child::"
  165. // "descendant::"
  166. // "descendant-or-self::"
  167. // "following-sibling::"
  168. // "following::"
  169. // // * == attribute()
  170. // "attribute::"
  171. // // * == namespace()
  172. // "namespace::"
  173. if ( node_name_wildcard && ! node_kind ) {
  174. switch ( axis_name ) {
  175. default:
  176. node_kind = "element";
  177. break;
  178. case "attribute":
  179. node_kind = "attribute";
  180. break;
  181. case "namespace":
  182. node_kind = "namespace";
  183. break;
  184. }
  185. }
  186. // Parse the predicates.
  187. var predicates = [], predicate;
  188. while ( predicate = /* assignment! */ this.parsePredicate( xpath ) ) {
  189. predicates.push( predicate );
  190. }
  191. // Absorb the separator.
  192. this.parseSeparator( xpath );
  193. // Now have: axis_name and name_test | node_kind(node_name,type_name)
  194. var step = {
  195. axis: axis_name,
  196. kind: node_kind,
  197. name: node_name,
  198. type: type_name,
  199. };
  200. if ( predicates.length ) {
  201. step.predicates = predicates;
  202. }
  203. return step;
  204. }
  205. },
  206. // -- parsePredicate -------------------------------------------------------------------
  207. /// Parse an XPath step predicate.
  208. ///
  209. /// @param {String|Object} xpath
  210. ///
  211. /// @returns {Object[]|undefined}
  212. parsePredicate: function( xpath ) {
  213. if ( typeof xpath == "string" || xpath instanceof String ) {
  214. xpath = { string: xpath, index: 0 };
  215. }
  216. var predicate_match = this.regex.predicate.exec( xpath.string.slice( xpath.index ) );
  217. if ( predicate_match ) {
  218. xpath.index += predicate_match[0].length;
  219. return this.parse( predicate_match[1] );
  220. }
  221. },
  222. // -- parseSeparator -------------------------------------------------------------------
  223. /// Parse an XPath step separator.
  224. ///
  225. /// @param {String|Object} xpath
  226. ///
  227. /// @returns {Boolean|undefined}
  228. parseSeparator: function( xpath ) {
  229. if ( typeof xpath == "string" || xpath instanceof String ) {
  230. xpath = { string: xpath, index: 0 };
  231. }
  232. var separator_match = this.regex.separator.exec( xpath.string.slice( xpath.index ) );
  233. if ( separator_match ) {
  234. xpath.index += separator_match[0].length;
  235. return true;
  236. }
  237. },
  238. // -- quoteName --------------------------------------------------------------------------
  239. /// Apply quotation marks around a name and escape internal quotation marks and escape
  240. /// characters.
  241. ///
  242. /// @param {String} unquoted_name
  243. ///
  244. /// @returns {String}
  245. quoteName: function( unquoted_name ) {
  246. return '"' + unquoted_name.replace( /(["\\])/g, "\\$1" ) + '"';
  247. },
  248. // -- unquoteName ------------------------------------------------------------------------
  249. /// Remove the enclosing quotation marks and unescape internal quotation marks and escape
  250. /// characters of a quoted name.
  251. ///
  252. /// @param {String} quoted_name
  253. ///
  254. /// @returns {String}
  255. unquoteName: function( quoted_name ) {
  256. if ( quoted_name[0] == "'" ) {
  257. return quoted_name.slice( 1, -1 ).replace( /\\(['\\])/g, "$1" );
  258. } else if ( quoted_name[0] == '"' ) {
  259. return quoted_name.slice( 1, -1 ).replace( /\\(["\\])/g, "$1" );
  260. }
  261. },
  262. // -- regex ----------------------------------------------------------------------------
  263. /// Regexes to crack the XPath string.
  264. regex: ( function() {
  265. var name = "[A-Za-z_][A-Za-z_0-9.-]*", // XPath QName: http://www.w3.org/TR/xpath20/#prod-xpath-QName
  266. singleQuotedName = "'(?:[^'\\\\]|\\'|\\\\)+'", // Single-quoted QName (VWF extension)
  267. doubleQuotedName = '"(?:[^"\\\\]|\\"|\\\\)+"', // Double-quoted QName (VWF extension)
  268. wildcard = "\\*"; // XPath Wildcard: http://www.w3.org/TR/xpath20/#prod-xpath-Wildcard
  269. /// @field
  270. var step = // XPath StepExpr: http://www.w3.org/TR/xpath20/#prod-xpath-StepExpr
  271. "(?:" +
  272. "(?:" +
  273. "(?:" +
  274. "(?:(" + name + ")::)" + // "axis", as in "axis::"" (axis_name)
  275. "|" +
  276. "(@|)" + // "@", "" (abbreviated_axis_specifier)
  277. ")" +
  278. "(?:" +
  279. "(?:" +
  280. "(" + name + ")" + // "kind" (node_kind)
  281. "\\(" + // "("
  282. "(?:" +
  283. "(?:" +
  284. "(" + name + ")" + // "node" (node_name)
  285. "|" +
  286. "(" +
  287. "(?:" + singleQuotedName + ")" + // "'node'" (node_name_quoted, quoted and with internal escapes)
  288. "|" +
  289. "(?:" + doubleQuotedName + ")" + // "\"node\"" (node_name_quoted, quoted and with internal escapes)
  290. ")" +
  291. "|" +
  292. "(" + wildcard + ")" + // "*" (node_name_wildcard)
  293. ")" +
  294. "(?:" +
  295. "," + // ","
  296. "(?:" +
  297. "(" + name + ")" + // "type" (type_name)
  298. "|" +
  299. "(" +
  300. "(?:" + singleQuotedName + ")" + // "'type'" (type_name_quoted, quoted and with internal escapes)
  301. "|" +
  302. "(?:" + doubleQuotedName + ")" + // '"type"' (type_name_quoted, quoted and with internal escapes)
  303. ")" +
  304. ")" +
  305. ")?" +
  306. ")?" +
  307. "\\)" + // ")"
  308. ")" +
  309. "|" +
  310. "(?:" +
  311. "(" + name + ")" + // "name" (name_test)
  312. "|" +
  313. "(" +
  314. "(?:" + singleQuotedName + ")" + // "'name'" (name_test_quoted, quoted and with internal escapes)
  315. "|" +
  316. "(?:" + doubleQuotedName + ")" + // '"name"' (name_test_quoted, quoted and with internal escapes)
  317. ")" +
  318. "|" +
  319. "(" + wildcard + ")" + // "*" (name_test_wildcard)
  320. ")" +
  321. ")" +
  322. ")" +
  323. "|" +
  324. "(\\.\\.|\\.)" + // "..", "." (abbreviated_step)
  325. ")";
  326. /// @field
  327. var predicate = // XPath Predicate: http://www.w3.org/TR/xpath20/#prod-xpath-Predicate
  328. "\\[" +
  329. "(" +
  330. step + // "[^\\]]*" +
  331. ")" +
  332. "\\]";
  333. /// @field
  334. var separator = "/";
  335. var regexes = {
  336. /// @field
  337. step: new RegExp( "^" + step ),
  338. /// @field
  339. predicate: new RegExp( "^" + predicate ),
  340. /// @field
  341. separator: new RegExp( "^" + separator ),
  342. };
  343. return regexes;
  344. } )(),
  345. };
  346. return exports;
  347. } );