PageRenderTime 52ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/common/content/javascript.js

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