PageRenderTime 55ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/common/content/javascript.js

https://bitbucket.org/teramako/vimperator
JavaScript | 619 lines | 436 code | 73 blank | 110 comment | 120 complexity | f0920a8c15c6bf7e71fc8d4b56e1ce58 MD5 | raw file
  1. // Copyright (c) 2008-2009 by Kris Maglione <maglione.k at Gmail>
  2. //
  3. // This work is licensed for reuse under an MIT license. Details are
  4. // given in the License.txt file included with this file.
  5. // TODO: Clean this up.
  6. const JavaScript = Module("javascript", {
  7. init: function () {
  8. this._stack = [];
  9. this._functions = [];
  10. this._top = {}; // The element on the top of the stack.
  11. this._last = ""; // The last opening char pushed onto the stack.
  12. this._lastNonwhite = ""; // Last non-whitespace character we saw.
  13. this._lastChar = ""; // Last character we saw, used for \ escaping quotes.
  14. this._str = "";
  15. this._lastIdx = 0;
  16. this._cacheKey = null;
  17. },
  18. get completers() JavaScript.completers, // For backward compatibility
  19. // Some object members are only accessible as function calls
  20. getKey: function (obj, key) {
  21. try {
  22. return obj[key];
  23. }
  24. catch (e) {
  25. return undefined;
  26. }
  27. },
  28. iter: function iter(obj, toplevel) {
  29. toplevel = !!toplevel;
  30. let seen = {};
  31. try {
  32. let orig = obj;
  33. function iterObj(obj, toplevel) {
  34. function isXPCNativeWrapper(obj) isobject(obj) && XPCNativeWrapper.unwrap(obj) !== obj
  35. if (isXPCNativeWrapper(obj)) {
  36. if (toplevel) {
  37. yield {get wrappedJSObject() 0};
  38. }
  39. // according to http://getfirebug.com/wiki/index.php/Using_win.wrappedJSObject
  40. // using a wrappedJSObject itself is safe, as potential functions are always
  41. // run in page context, not in chrome context.
  42. // However, as we really need to make sure, values coming
  43. // from content scope are never used in unsecured eval(),
  44. // we dissallow unwrapping objects for now, unless the user
  45. // uses an (undocumented) option 'unwrapjsobjects'
  46. else if (options["inspectcontentobjects"]) {
  47. obj = obj.wrappedJSObject;
  48. }
  49. }
  50. if (toplevel)
  51. yield obj;
  52. else
  53. for (let o = obj.__proto__; o; o = o.__proto__)
  54. yield o;
  55. }
  56. for (let obj in iterObj(orig, toplevel)) {
  57. for (let [, k] in Iterator(Object.getOwnPropertyNames(obj))) {
  58. let name = "|" + k;
  59. if (name in seen)
  60. continue;
  61. seen[name] = 1;
  62. yield [k, this.getKey(orig, k)];
  63. }
  64. }
  65. }
  66. catch (ex) {
  67. // TODO: report error?
  68. }
  69. },
  70. // Search the object for strings starting with @key.
  71. // If @last is defined, key is a quoted string, it's
  72. // wrapped in @last after @offset characters are sliced
  73. // off of it and it's quoted.
  74. objectKeys: function objectKeys(obj, toplevel) {
  75. // Things we can dereference
  76. if (["object", "string", "function"].indexOf(typeof obj) == -1)
  77. return [];
  78. if (!obj)
  79. return [];
  80. let completions;
  81. if (modules.isPrototypeOf(obj))
  82. completions = [v for (v in Iterator(obj))];
  83. else {
  84. completions = [k for (k in this.iter(obj, toplevel))];
  85. if (!toplevel)
  86. completions = util.Array.uniq(completions, true);
  87. }
  88. // Add keys for sorting later.
  89. // Numbers are parsed to ints.
  90. // Constants, which should be unsorted, are found and marked null.
  91. completions.forEach(function (item) {
  92. let key = item[0];
  93. if (!isNaN(key))
  94. key = parseInt(key);
  95. else if (/^[A-Z_][A-Z0-9_]*$/.test(key))
  96. key = "";
  97. item.key = key;
  98. });
  99. return completions;
  100. },
  101. eval: function eval(arg, key, tmp) {
  102. let cache = this.context.cache.eval;
  103. let context = this.context.cache.evalContext;
  104. if (!key)
  105. key = arg;
  106. if (key in cache)
  107. return cache[key];
  108. context[JavaScript.EVAL_TMP] = tmp;
  109. try {
  110. return cache[key] = liberator.eval(arg, context);
  111. }
  112. catch (e) {
  113. return null;
  114. }
  115. finally {
  116. delete context[JavaScript.EVAL_TMP];
  117. }
  118. },
  119. // Get an element from the stack. If @frame is negative,
  120. // count from the top of the stack, otherwise, the bottom.
  121. // If @nth is provided, return the @mth value of element @type
  122. // of the stack entry at @frame.
  123. _get: function (frame, nth, type) {
  124. let a = this._stack[frame >= 0 ? frame : this._stack.length + frame];
  125. if (type != null)
  126. a = a[type];
  127. if (nth == null)
  128. return a;
  129. return a[a.length - nth - 1];
  130. },
  131. // Push and pop the stack, maintaining references to 'top' and 'last'.
  132. _push: function push(arg) {
  133. this._top = {
  134. offset: this._i,
  135. char: arg,
  136. statements: [this._i],
  137. dots: [],
  138. fullStatements: [],
  139. comma: [],
  140. functions: []
  141. };
  142. this._last = this._top.char;
  143. this._stack.push(this._top);
  144. },
  145. _pop: function pop(arg) {
  146. if (this._top.char != arg) {
  147. this.context.highlight(this._top.offset, this._i - this._top.offset, "SPELLCHECK");
  148. this.context.highlight(this._top.offset, 1, "FIND");
  149. throw new Error("Invalid JS");
  150. }
  151. if (this._i == this.context.caret - 1)
  152. this.context.highlight(this._top.offset, 1, "FIND");
  153. // The closing character of this stack frame will have pushed a new
  154. // statement, leaving us with an empty statement. This doesn't matter,
  155. // now, as we simply throw away the frame when we pop it, but it may later.
  156. if (this._top.statements[this._top.statements.length - 1] == this._i)
  157. this._top.statements.pop();
  158. this._top = this._get(-2);
  159. this._last = this._top.char;
  160. let ret = this._stack.pop();
  161. return ret;
  162. },
  163. _buildStack: function (filter) {
  164. let self = this;
  165. // Todo: Fix these one-letter variable names.
  166. this._i = 0;
  167. this._c = ""; // Current index and character, respectively.
  168. // Reuse the old stack.
  169. if (this._str && filter.substr(0, this._str.length) == this._str) {
  170. this._i = this._str.length;
  171. if (this.popStatement)
  172. this._top.statements.pop();
  173. }
  174. else {
  175. this._stack = [];
  176. this._functions = [];
  177. this._push("#root");
  178. }
  179. // Build a parse stack, discarding entries as opening characters
  180. // match closing characters. The stack is walked from the top entry
  181. // and down as many levels as it takes us to figure out what it is
  182. // that we're completing.
  183. this._str = filter;
  184. let length = this._str.length;
  185. for (; this._i < length; this._lastChar = this._c, this._i++) {
  186. this._c = this._str[this._i];
  187. if (this._last == '"' || this._last == "'" || this._last == "/") {
  188. if (this._lastChar == "\\") { // Escape. Skip the next char, whatever it may be.
  189. this._c = "";
  190. this._i++;
  191. }
  192. else if (this._c == this._last)
  193. this._pop(this._c);
  194. }
  195. else {
  196. // A word character following a non-word character, or simply a non-word
  197. // character. Start a new statement.
  198. if (/[a-zA-Z_$]/.test(this._c) && !/[\w$]/.test(this._lastChar) || !/[\w\s$]/.test(this._c))
  199. this._top.statements.push(this._i);
  200. // A "." or a "[" dereferences the last "statement" and effectively
  201. // joins it to this logical statement.
  202. if ((this._c == "." || this._c == "[") && /[\w$\])"']/.test(this._lastNonwhite)
  203. || this._lastNonwhite == "." && /[a-zA-Z_$]/.test(this._c))
  204. this._top.statements.pop();
  205. switch (this._c) {
  206. case "(":
  207. // Function call, or if/while/for/...
  208. if (/[\w$]/.test(this._lastNonwhite)) {
  209. this._functions.push(this._i);
  210. this._top.functions.push(this._i);
  211. this._top.statements.pop();
  212. }
  213. case '"':
  214. case "'":
  215. case "/":
  216. case "{":
  217. this._push(this._c);
  218. break;
  219. case "[":
  220. this._push(this._c);
  221. break;
  222. case ".":
  223. this._top.dots.push(this._i);
  224. break;
  225. case ")": this._pop("("); break;
  226. case "]": this._pop("["); break;
  227. case "}": this._pop("{"); // Fallthrough
  228. case ";":
  229. this._top.fullStatements.push(this._i);
  230. break;
  231. case ",":
  232. this._top.comma.push(this._i);
  233. break;
  234. }
  235. if (/\S/.test(this._c))
  236. this._lastNonwhite = this._c;
  237. }
  238. }
  239. this.popStatement = false;
  240. if (!/[\w$]/.test(this._lastChar) && this._lastNonwhite != ".") {
  241. this.popStatement = true;
  242. this._top.statements.push(this._i);
  243. }
  244. this._lastIdx = this._i;
  245. },
  246. // Don't eval any function calls unless the user presses tab.
  247. _checkFunction: function (start, end, key) {
  248. let res = this._functions.some(function (idx) idx >= start && idx < end);
  249. if (!res || this.context.tabPressed || key in this.cache.eval)
  250. return false;
  251. this.context.waitingForTab = true;
  252. return true;
  253. },
  254. // For each DOT in a statement, prefix it with TMP, eval it,
  255. // and save the result back to TMP. The point of this is to
  256. // cache the entire path through an object chain, mainly in
  257. // the presence of function calls. There are drawbacks. For
  258. // instance, if the value of a variable changes in the course
  259. // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
  260. // we'll still use the old value. But, it's worth it.
  261. _getObj: function (frame, stop) {
  262. let statement = this._get(frame, 0, "statements") || 0; // Current statement.
  263. let prev = statement;
  264. let obj;
  265. let cacheKey;
  266. for (let [, dot] in Iterator(this._get(frame).dots.concat(stop))) {
  267. if (dot < statement)
  268. continue;
  269. if (dot > stop || dot <= prev)
  270. break;
  271. let s = this._str.substring(prev, dot);
  272. if (prev != statement)
  273. s = JavaScript.EVAL_TMP + "." + s;
  274. cacheKey = this._str.substring(statement, dot);
  275. if (this._checkFunction(prev, dot, cacheKey))
  276. return [];
  277. prev = dot + 1;
  278. obj = this.eval(s, cacheKey, obj);
  279. }
  280. return [[obj, cacheKey]];
  281. },
  282. _getObjKey: function (frame) {
  283. let dot = this._get(frame, 0, "dots") || -1; // Last dot in frame.
  284. let statement = this._get(frame, 0, "statements") || 0; // Current statement.
  285. let end = (frame == -1 ? this._lastIdx : this._get(frame + 1).offset);
  286. this._cacheKey = null;
  287. let obj = [[this.cache.evalContext, "Local Variables"],
  288. [userContext, "Global Variables"],
  289. [modules, "modules"],
  290. [window, "window"]]; // Default objects;
  291. // Is this an object dereference?
  292. if (dot < statement) // No.
  293. dot = statement - 1;
  294. else // Yes. Set the object to the string before the dot.
  295. obj = this._getObj(frame, dot);
  296. let [, space, key] = this._str.substring(dot + 1, end).match(/^(\s*)(.*)/);
  297. return [dot + 1 + space.length, obj, key];
  298. },
  299. _fill: function (context, obj, name, compl, anchored, key, last, offset) {
  300. context.title = [name];
  301. context.anchored = anchored;
  302. context.filter = key;
  303. context.itemCache = context.parent.itemCache;
  304. context.key = name;
  305. if (last != null)
  306. context.quote = [last, function (text) util.escapeString(text.substr(offset), ""), last];
  307. else // We're not looking for a quoted string, so filter out anything that's not a valid identifier
  308. context.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
  309. compl.call(self, context, obj);
  310. },
  311. _complete: function (objects, key, compl, string, last) {
  312. const self = this;
  313. let orig = compl;
  314. if (!compl) {
  315. compl = function (context, obj, recurse) {
  316. context.process = [null, function highlight(item, v) template.highlight(v, true)];
  317. // Sort in a logical fashion for object keys:
  318. // Numbers are sorted as numbers, rather than strings, and appear first.
  319. // Constants are unsorted, and appear before other non-null strings.
  320. // Other strings are sorted in the default manner.
  321. let compare = context.compare;
  322. function isnan(item) item != '' && isNaN(item);
  323. context.compare = function (a, b) {
  324. if (!isnan(a.item.key) && !isnan(b.item.key))
  325. return a.item.key - b.item.key;
  326. return isnan(b.item.key) - isnan(a.item.key) || compare(a, b);
  327. };
  328. if (!context.anchored) // We've already listed anchored matches, so don't list them again here.
  329. context.filters.push(function (item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter));
  330. if (obj == self.cache.evalContext)
  331. context.regenerate = true;
  332. context.generate = function () self.objectKeys(obj, !recurse);
  333. };
  334. }
  335. // TODO: Make this a generic completion helper function.
  336. let filter = key + (string || "");
  337. for (let [, obj] in Iterator(objects)) {
  338. this.context.fork(obj[1], this._top.offset, this, this._fill,
  339. obj[0], obj[1], compl,
  340. true, filter, last, key.length);
  341. }
  342. if (orig)
  343. return;
  344. for (let [, obj] in Iterator(objects)) {
  345. let name = obj[1] + " (prototypes)";
  346. this.context.fork(name, this._top.offset, this, this._fill,
  347. obj[0], name, function (a, b) compl(a, b, true),
  348. true, filter, last, key.length);
  349. }
  350. for (let [, obj] in Iterator(objects)) {
  351. let name = obj[1] + " (substrings)";
  352. this.context.fork(name, this._top.offset, this, this._fill,
  353. obj[0], name, compl,
  354. false, filter, last, key.length);
  355. }
  356. for (let [, obj] in Iterator(objects)) {
  357. let name = obj[1] + " (prototype substrings)";
  358. this.context.fork(name, this._top.offset, this, this._fill,
  359. obj[0], name, function (a, b) compl(a, b, true),
  360. false, filter, last, key.length);
  361. }
  362. },
  363. _getKey: function () {
  364. if (this._last == "")
  365. return "";
  366. // After the opening [ upto the opening ", plus '' to take care of any operators before it
  367. let key = this._str.substring(this._get(-2, 0, "statements"), this._get(-1, null, "offset")) + "''";
  368. // Now eval the key, to process any referenced variables.
  369. return this.eval(key);
  370. },
  371. get cache() this.context.cache,
  372. complete: function _complete(context) {
  373. const self = this;
  374. this.context = context;
  375. try {
  376. this._buildStack.call(this, context.filter);
  377. }
  378. catch (e) {
  379. if (e.message != "Invalid JS")
  380. liberator.echoerr(e);
  381. this._lastIdx = 0;
  382. return null;
  383. }
  384. this.context.getCache("eval", Object);
  385. this.context.getCache("evalContext", function () ({ __proto__: userContext }));
  386. // Okay, have parse stack. Figure out what we're completing.
  387. // Find any complete statements that we can eval before we eval our object.
  388. // This allows for things like: let doc = window.content.document; let elem = doc.createElement...; elem.<Tab>
  389. let prev = 0;
  390. for (let [, v] in Iterator(this._get(0).fullStatements)) {
  391. let key = this._str.substring(prev, v + 1);
  392. if (this._checkFunction(prev, v, key))
  393. return null;
  394. this.eval(key);
  395. prev = v + 1;
  396. }
  397. // In a string. Check if we're dereferencing an object.
  398. // Otherwise, do nothing.
  399. if (this._last == "'" || this._last == '"') {
  400. //
  401. // str = "foo[bar + 'baz"
  402. // obj = "foo"
  403. // key = "bar + ''"
  404. //
  405. // The top of the stack is the sting we're completing.
  406. // Wrap it in its delimiters and eval it to process escape sequences.
  407. let string = this._str.substring(this._get(-1).offset + 1, this._lastIdx);
  408. string = eval(this._last + string + this._last);
  409. // Is this an object accessor?
  410. if (this._get(-2).char == "[") { // Are we inside of []?
  411. // Stack:
  412. // [-1]: "...
  413. // [-2]: [...
  414. // [-3]: base statement
  415. // Yes. If the [ starts at the beginning of a logical
  416. // statement, we're in an array literal, and we're done.
  417. if (this._get(-3, 0, "statements") == this._get(-2).offset)
  418. return null;
  419. // Beginning of the statement upto the opening [
  420. let obj = this._getObj(-3, this._get(-2).offset);
  421. return this._complete(obj, this._getKey(), null, string, this._last);
  422. }
  423. // Is this a function call?
  424. if (this._get(-2).char == "(") {
  425. // Stack:
  426. // [-1]: "...
  427. // [-2]: (...
  428. // [-3]: base statement
  429. // Does the opening "(" mark a function call?
  430. if (this._get(-3, 0, "functions") != this._get(-2).offset)
  431. return null; // No. We're done.
  432. let [offset, obj, func] = this._getObjKey(-3);
  433. if (!obj.length)
  434. return null;
  435. obj = obj.slice(0, 1);
  436. try {
  437. var completer = obj[0][0][func].liberatorCompleter;
  438. }
  439. catch (e) {}
  440. if (!completer)
  441. completer = JavaScript.completers[func];
  442. if (!completer)
  443. return null;
  444. // Split up the arguments
  445. let prev = this._get(-2).offset;
  446. let args = [];
  447. for (let [i, idx] in Iterator(this._get(-2).comma)) {
  448. let arg = this._str.substring(prev + 1, idx);
  449. prev = idx;
  450. util.memoize(args, i, function () self.eval(arg));
  451. }
  452. let key = this._getKey();
  453. args.push(key + string);
  454. var compl = function (context, obj) {
  455. let res = completer.call(self, context, func, obj, args);
  456. if (res)
  457. context.completions = res;
  458. };
  459. obj[0][1] += "." + func + "(... [" + args.length + "]";
  460. return this._complete(obj, key, compl, string, this._last);
  461. }
  462. // In a string that's not an obj key or a function arg.
  463. // Nothing to do.
  464. return null;
  465. }
  466. //
  467. // str = "foo.bar.baz"
  468. // obj = "foo.bar"
  469. // key = "baz"
  470. //
  471. // str = "foo"
  472. // obj = [modules, window]
  473. // key = "foo"
  474. //
  475. let [offset, obj, key] = this._getObjKey(-1);
  476. // Wait for a keypress before completing the default objects.
  477. if (!this.context.tabPressed && key == "" && obj.length > 1) {
  478. this.context.waitingForTab = true;
  479. this.context.message = "Waiting for key press";
  480. return null;
  481. }
  482. if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
  483. return null; // Not a word. Forget it. Can this even happen?
  484. try { // FIXME
  485. var o = this._top.offset;
  486. this._top.offset = offset;
  487. return this._complete(obj, key);
  488. }
  489. finally {
  490. this._top.offset = o;
  491. }
  492. return null;
  493. }
  494. }, {
  495. EVAL_TMP: "__liberator_eval_tmp",
  496. /**
  497. * A map of argument completion functions for named methods. The
  498. * signature and specification of the completion function
  499. * are fairly complex and yet undocumented.
  500. *
  501. * @see JavaScript.setCompleter
  502. */
  503. completers: {},
  504. /**
  505. * Installs argument string completers for a set of functions.
  506. * The second argument is an array of functions (or null
  507. * values), each corresponding the argument of the same index.
  508. * Each provided completion function receives as arguments a
  509. * CompletionContext, the 'this' object of the method, and an
  510. * array of values for the preceding arguments.
  511. *
  512. * It is important to note that values in the arguments array
  513. * provided to the completers are lazily evaluated the first
  514. * time they are accessed, so they should be accessed
  515. * judiciously.
  516. *
  517. * @param {function|function[]} funcs The functions for which to
  518. * install the completers.
  519. * @param {function[]} completers An array of completer
  520. * functions.
  521. */
  522. setCompleter: function (funcs, completers) {
  523. funcs = Array.concat(funcs);
  524. for (let [, func] in Iterator(funcs)) {
  525. func.liberatorCompleter = function (context, func, obj, args) {
  526. let completer = completers[args.length - 1];
  527. if (!completer)
  528. return [];
  529. return completer.call(this, context, obj, args);
  530. };
  531. }
  532. }
  533. }, {
  534. completion: function () {
  535. completion.javascript = this.closure.complete;
  536. completion.javascriptCompleter = JavaScript; // Backwards compatibility.
  537. },
  538. options: function () {
  539. options.add(["inspectcontentobjects"],
  540. "Allow completion of JavaScript objects coming from web content. POSSIBLY INSECURE!",
  541. "boolean", false);
  542. }
  543. })