xpath.js 15 KB

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