Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
3 <html onclick="keepFocusInTextbox(event)">
4 <head>
5 <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
6 <title>JavaScript Shell 1.4</title>
8 <script type="text/javascript">
9 var
10 histList = [""],
11 histPos = 0,
12 _scope = {},
13 _win, // a top-level context
14 question, // {String} the input command that's being evaluated. Accessed via
15 // |Shell.question| from the target window for evaluation.
16 _in, // {HTMLTextAreaElement} the textarea containing the input
17 _out, // {HTMLDivElement} the output is printed to this element
18 tooManyMatches = null,
19 lastError = null,
20 _jsVer = 0 // determines the way to execute the commands, see run()
21 ;
23 function refocus()
24 {
25 _in.blur(); // Needed for Mozilla to scroll correctly.
26 _in.focus();
27 }
29 function init()
30 {
31 _in = document.getElementById("input");
32 _out = document.getElementById("output");
34 _win = window;
36 if (opener && !opener.closed)
37 {
38 println("Using bookmarklet version of shell: commands will run in opener's context.", "message");
39 _win = opener;
40 }
42 /* Run a series of (potentially async, but quick) tests to determine the
43 * way to run code in this browser (for run()). Sets window._jsVer based
44 * on the tests. */
45 _jsVer = 0;
46 for (var jsVerToTry=19; jsVerToTry>=15; jsVerToTry--) {
47 run(window, "if(_jsVer < " + jsVerToTry + ") { " +
48 "_jsVer=" + jsVerToTry + "; }", jsVerToTry);
49 }
51 initTarget();
53 recalculateInputHeight();
54 refocus();
55 }
57 /**
58 * Runs |code| in |_win|'s context.
59 * @param overridenJSVer {int} (optional) overrides the default (specified by _jsVer)
60 * way to run the code.
61 */
62 function run(_win, code, overridenJSVer) {
63 var jsVerToUse = overridenJSVer ? overridenJSVer : _jsVer;
64 if (jsVerToUse <= 15) {
65 _win.location.href = "javascript:" + code + "; void 0";
66 } else {
67 var sc = _win.document.createElement("script");
68 sc.type="application/javascript;version=" + jsVerToUse/10;
69 sc.src="data:application/x-javascript," + code;
70 _win.document.body.appendChild(sc); // runs the script asynchronously
71 }
72 }
74 function initTarget()
75 {
76 _win.Shell = window;
77 _win.print = shellCommands.print;
78 }
81 // Unless the user is selected something, refocus the textbox.
82 // (requested by caillon, brendan, asa)
83 function keepFocusInTextbox(e)
84 {
85 var g = e.srcElement ? e.srcElement : e.target; // IE vs. standard
87 while (!g.tagName)
88 g = g.parentNode;
89 var t = g.tagName.toUpperCase();
90 if (t=="A" || t=="INPUT")
91 return;
93 if (window.getSelection) {
94 // Mozilla
95 if (String(window.getSelection()))
96 return;
97 }
98 else {
99 // IE
100 if ( document.selection.createRange().text )
101 return;
102 }
104 refocus();
105 }
107 function inputKeydown(e) {
108 // Use onkeydown because IE doesn't support onkeypress for arrow keys
110 //alert(e.keyCode + " ^ " + e.keycode);
112 if (e.shiftKey && e.keyCode == 13) { // shift-enter
113 // don't do anything; allow the shift-enter to insert a line break as normal
114 } else if (e.keyCode == 13) { // enter
115 // execute the input on enter
116 try { go(); } catch(er) { alert(er); };
117 setTimeout(function() { _in.value = ""; }, 0); // can't preventDefault on input, so clear it later
118 } else if (e.keyCode == 38) { // up
119 // go up in history if at top or ctrl-up
120 if (e.ctrlKey || caretInFirstLine(_in))
121 hist(true);
122 } else if (e.keyCode == 40) { // down
123 // go down in history if at end or ctrl-down
124 if (e.ctrlKey || caretInLastLine(_in))
125 hist(false);
126 } else if (e.keyCode == 9) { // tab
127 tabcomplete();
128 setTimeout(function() { refocus(); }, 0); // refocus because tab was hit
129 } else { }
131 setTimeout(recalculateInputHeight, 0);
133 //return true;
134 };
136 function caretInFirstLine(textbox)
137 {
138 // IE doesn't support selectionStart/selectionEnd
139 if (textbox.selectionStart == undefined)
140 return true;
142 var firstLineBreak = textbox.value.indexOf("\n");
144 return ((firstLineBreak == -1) || (textbox.selectionStart <= firstLineBreak));
145 }
147 function caretInLastLine(textbox)
148 {
149 // IE doesn't support selectionStart/selectionEnd
150 if (textbox.selectionEnd == undefined)
151 return true;
153 var lastLineBreak = textbox.value.lastIndexOf("\n");
155 return (textbox.selectionEnd > lastLineBreak);
156 }
158 function recalculateInputHeight()
159 {
160 var rows = _in.value.split(/\n/).length
161 + 1 // prevent scrollbar flickering in Mozilla
162 + (window.opera ? 1 : 0); // leave room for scrollbar in Opera
164 if (_in.rows != rows) // without this check, it is impossible to select text in Opera 7.60 or Opera 8.0.
165 _in.rows = rows;
166 }
168 function println(s, type)
169 {
170 if((s=String(s)))
171 {
172 var newdiv = document.createElement("div");
173 newdiv.appendChild(document.createTextNode(s));
174 newdiv.className = type;
175 _out.appendChild(newdiv);
176 return newdiv;
177 }
178 return false;
179 }
181 function printWithRunin(h, s, type)
182 {
183 var div = println(s, type);
184 if (div) {
185 var head = document.createElement("strong");
186 head.appendChild(document.createTextNode(h + ": "));
187 div.insertBefore(head, div.firstChild);
188 }
189 }
192 var shellCommands =
193 {
194 load : function load(url)
195 {
196 var s = _win.document.createElement("script");
197 s.type = "text/javascript";
198 s.src = url;
199 _win.document.getElementsByTagName("head")[0].appendChild(s);
200 println("Loading " + url + "...", "message");
201 },
203 clear : function clear()
204 {
205 var CHILDREN_TO_PRESERVE = 3;
206 while (_out.childNodes[CHILDREN_TO_PRESERVE])
207 _out.removeChild(_out.childNodes[CHILDREN_TO_PRESERVE]);
208 },
210 print : function print(s) { println(s, "print"); },
212 // the normal function, "print", shouldn't return a value
213 // (suggested by brendan; later noticed it was a problem when showing others)
214 pr : function pr(s)
215 {
216 shellCommands.print(s); // need to specify shellCommands so it doesn't try window.print()!
217 return s;
218 },
220 props : function props(e, onePerLine)
221 {
222 if (e === null) {
223 println("props called with null argument", "error");
224 return;
225 }
227 if (e === undefined) {
228 println("props called with undefined argument", "error");
229 return;
230 }
232 var ns = ["Methods", "Fields", "Unreachables"];
233 var as = [[], [], []]; // array of (empty) arrays of arrays!
234 var p, j, i; // loop variables, several used multiple times
236 var protoLevels = 0;
238 for (p = e; p; p = p.__proto__)
239 {
240 for (i=0; i<ns.length; ++i)
241 as[i][protoLevels] = [];
242 ++protoLevels;
243 }
245 for(var a in e)
246 {
247 // Shortcoming: doesn't check that VALUES are the same in object and prototype.
249 var protoLevel = -1;
250 try
251 {
252 for (p = e; p && (a in p); p = p.__proto__)
253 ++protoLevel;
254 }
255 catch(er) { protoLevel = 0; } // "in" operator throws when param to props() is a string
257 var type = 1;
258 try
259 {
260 if ((typeof e[a]) == "function")
261 type = 0;
262 }
263 catch (er) { type = 2; }
265 as[type][protoLevel].push(a);
266 }
268 function times(s, n) { return n ? s + times(s, n-1) : ""; }
270 for (j=0; j<protoLevels; ++j)
271 for (i=0;i<ns.length;++i)
272 if (as[i][j].length)
273 printWithRunin(
274 ns[i] + times(" of prototype", j),
275 (onePerLine ? "\n\n" : "") + as[i][j].sort().join(onePerLine ? "\n" : ", ") + (onePerLine ? "\n\n" : ""),
276 "propList"
277 );
278 },
280 blink : function blink(node)
281 {
282 if (!node) throw("blink: argument is null or undefined.");
283 if (node.nodeType == null) throw("blink: argument must be a node.");
284 if (node.nodeType == 3) throw("blink: argument must not be a text node");
285 if (node.documentElement) throw("blink: argument must not be the document object");
287 function setOutline(o) {
288 return function() {
289 if (node.style.outline != node.style.bogusProperty) {
290 // browser supports outline (Firefox 1.1 and newer, CSS3, Opera 8).
291 node.style.outline = o;
292 }
293 else if (node.style.MozOutline != node.style.bogusProperty) {
294 // browser supports MozOutline (Firefox 1.0.x and older)
295 node.style.MozOutline = o;
296 }
297 else {
298 // browser only supports border (IE). border is a fallback because it moves things around.
299 node.style.border = o;
300 }
301 }
302 }
304 function focusIt(a) {
305 return function() {
306 a.focus();
307 }
308 }
310 if (node.ownerDocument) {
311 var windowToFocusNow = (node.ownerDocument.defaultView || node.ownerDocument.parentWindow); // Moz vs. IE
312 if (windowToFocusNow)
313 setTimeout(focusIt(windowToFocusNow.top), 0);
314 }
316 for(var i=1;i<7;++i)
317 setTimeout(setOutline((i%2)?'3px solid red':'none'), i*100);
319 setTimeout(focusIt(window), 800);
320 setTimeout(focusIt(_in), 810);
321 },
323 scope : function scope(sc)
324 {
325 if (!sc) sc = {};
326 _scope = sc;
327 println("Scope is now " + sc + ". If a variable is not found in this scope, window will also be searched. New variables will still go on window.", "message");
328 },
330 mathHelp : function mathHelp()
331 {
332 printWithRunin("Math constants", "E, LN2, LN10, LOG2E, LOG10E, PI, SQRT1_2, SQRT2", "propList");
333 printWithRunin("Math methods", "abs, acos, asin, atan, atan2, ceil, cos, exp, floor, log, max, min, pow, random, round, sin, sqrt, tan", "propList");
334 },
336 ans : undefined
337 };
340 function hist(up)
341 {
342 // histList[0] = first command entered, [1] = second, etc.
343 // type something, press up --> thing typed is now in "limbo"
344 // (last item in histList) and should be reachable by pressing
345 // down again.
347 var L = histList.length;
349 if (L == 1)
350 return;
352 if (up)
353 {
354 if (histPos == L-1)
355 {
356 // Save this entry in case the user hits the down key.
357 histList[histPos] = _in.value;
358 }
360 if (histPos > 0)
361 {
362 histPos--;
363 // Use a timeout to prevent up from moving cursor within new text
364 // Set to nothing first for the same reason
365 setTimeout(
366 function() {
367 _in.value = '';
368 _in.value = histList[histPos];
369 var caretPos = _in.value.length;
370 if (_in.setSelectionRange)
371 _in.setSelectionRange(caretPos, caretPos);
372 },
373 0
374 );
375 }
376 }
377 else // down
378 {
379 if (histPos < L-1)
380 {
381 histPos++;
382 _in.value = histList[histPos];
383 }
384 else if (histPos == L-1)
385 {
386 // Already on the current entry: clear but save
387 if (_in.value)
388 {
389 histList[histPos] = _in.value;
390 ++histPos;
391 _in.value = "";
392 }
393 }
394 }
395 }
397 function tabcomplete()
398 {
399 /*
400 * Working backwards from s[from], find the spot
401 * where this expression starts. It will scan
402 * until it hits a mismatched ( or a space,
403 * but it skips over quoted strings.
404 * If stopAtDot is true, stop at a '.'
405 */
406 function findbeginning(s, from, stopAtDot)
407 {
408 /*
409 * Complicated function.
410 *
411 * Return true if s[i] == q BUT ONLY IF
412 * s[i-1] is not a backslash.
413 */
414 function equalButNotEscaped(s,i,q)
415 {
416 if(s.charAt(i) != q) // not equal go no further
417 return false;
419 if(i==0) // beginning of string
420 return true;
422 if(s.charAt(i-1) == '\\') // escaped?
423 return false;
425 return true;
426 }
428 var nparens = 0;
429 var i;
430 for(i=from; i>=0; i--)
431 {
432 if(s.charAt(i) == ' ')
433 break;
435 if(stopAtDot && s.charAt(i) == '.')
436 break;
438 if(s.charAt(i) == ')')
439 nparens++;
440 else if(s.charAt(i) == '(')
441 nparens--;
443 if(nparens < 0)
444 break;
446 // skip quoted strings
447 if(s.charAt(i) == '\'' || s.charAt(i) == '\"')
448 {
449 //dump("skipping quoted chars: ");
450 var quot = s.charAt(i);
451 i--;
452 while(i >= 0 && !equalButNotEscaped(s,i,quot)) {
453 //dump(s.charAt(i));
454 i--;
455 }
456 //dump("\n");
457 }
458 }
459 return i;
460 }
462 // XXX should be used more consistently (instead of using selectionStart/selectionEnd throughout code)
463 // XXX doesn't work in IE, even though it contains IE-specific code
464 function getcaretpos(inp)
465 {
466 if(inp.selectionEnd != null)
467 return inp.selectionEnd;
469 if(inp.createTextRange)
470 {
471 var docrange = _win.Shell.document.selection.createRange();
472 var inprange = inp.createTextRange();
473 if (inprange.setEndPoint)
474 {
475 inprange.setEndPoint('EndToStart', docrange);
476 return inprange.text.length;
477 }
478 }
480 return inp.value.length; // sucks, punt
481 }
483 function setselectionto(inp,pos)
484 {
485 if(inp.selectionStart) {
486 inp.selectionStart = inp.selectionEnd = pos;
487 }
488 else if(inp.createTextRange) {
489 var docrange = _win.Shell.document.selection.createRange();
490 var inprange = inp.createTextRange();
491 inprange.move('character',pos);
492 inprange.select();
493 }
494 else { // err...
495 /*
496 inp.select();
497 if(_win.Shell.document.getSelection())
498 _win.Shell.document.getSelection() = "";
499 */
500 }
501 }
502 // get position of cursor within the input box
503 var caret = getcaretpos(_in);
505 if(caret) {
506 //dump("----\n");
507 var dotpos, spacepos, complete, obj;
508 //dump("caret pos: " + caret + "\n");
509 // see if there's a dot before here
510 dotpos = findbeginning(_in.value, caret-1, true);
511 //dump("dot pos: " + dotpos + "\n");
512 if(dotpos == -1 || _in.value.charAt(dotpos) != '.') {
513 dotpos = caret;
514 //dump("changed dot pos: " + dotpos + "\n");
515 }
517 // look backwards for a non-variable-name character
518 spacepos = findbeginning(_in.value, dotpos-1, false);
519 //dump("space pos: " + spacepos + "\n");
520 // get the object we're trying to complete on
521 if(spacepos == dotpos || spacepos+1 == dotpos || dotpos == caret)
522 {
523 // try completing function args
524 if(_in.value.charAt(dotpos) == '(' ||
525 (_in.value.charAt(spacepos) == '(' && (spacepos+1) == dotpos))
526 {
527 var fn,fname;
528 var from = (_in.value.charAt(dotpos) == '(') ? dotpos : spacepos;
529 spacepos = findbeginning(_in.value, from-1, false);
531 fname = _in.value.substr(spacepos+1,from-(spacepos+1));
532 //dump("fname: " + fname + "\n");
533 try {
534 with(_win.Shell._scope)
535 with(_win)
536 with(Shell.shellCommands)
537 fn = eval(fname);
538 }
539 catch(er) {
540 //dump('fn is not a valid object\n');
541 return;
542 }
543 if(fn == undefined) {
544 //dump('fn is undefined');
545 return;
546 }
547 if(fn instanceof Function)
548 {
549 // Print function definition, including argument names, but not function body
550 if(!fn.toString().match(/function .+?\(\) +\{\n +\[native code\]\n\}/))
551 println(fn.toString().match(/function .+?\(.*?\)/), "tabcomplete");
552 }
554 return;
555 }
556 else
557 obj = _win;
558 }
559 else
560 {
561 var objname = _in.value.substr(spacepos+1,dotpos-(spacepos+1));
562 //dump("objname: |" + objname + "|\n");
563 try {
564 with(_win.Shell._scope)
565 with(_win)
566 obj = eval(objname);
567 }
568 catch(er) {
569 printError(er);
570 return;
571 }
572 if(obj == undefined) {
573 // sometimes this is tabcomplete's fault, so don't print it :(
574 // e.g. completing from "print(document.getElements"
575 // println("Can't complete from null or undefined expression " + objname, "error");
576 return;
577 }
578 }
579 //dump("obj: " + obj + "\n");
580 // get the thing we're trying to complete
581 if(dotpos == caret)
582 {
583 if(spacepos+1 == dotpos || spacepos == dotpos)
584 {
585 // nothing to complete
586 //dump("nothing to complete\n");
587 return;
588 }
590 complete = _in.value.substr(spacepos+1,dotpos-(spacepos+1));
591 }
592 else {
593 complete = _in.value.substr(dotpos+1,caret-(dotpos+1));
594 }
595 //dump("complete: " + complete + "\n");
596 // ok, now look at all the props/methods of this obj
597 // and find ones starting with 'complete'
598 var matches = [];
599 var bestmatch = null;
600 for(var a in obj)
601 {
602 //a = a.toString();
603 //XXX: making it lowercase could help some cases,
604 // but screws up my general logic.
605 if(a.substr(0,complete.length) == complete) {
606 matches.push(a);
607 ////dump("match: " + a + "\n");
608 // if no best match, this is the best match
609 if(bestmatch == null)
610 {
611 bestmatch = a;
612 }
613 else {
614 // the best match is the longest common string
615 function min(a,b){ return ((a<b)?a:b); }
616 var i;
617 for(i=0; i< min(bestmatch.length, a.length); i++)
618 {
619 if(bestmatch.charAt(i) != a.charAt(i))
620 break;
621 }
622 bestmatch = bestmatch.substr(0,i);
623 ////dump("bestmatch len: " + i + "\n");
624 }
625 ////dump("bestmatch: " + bestmatch + "\n");
626 }
627 }
628 bestmatch = (bestmatch || "");
629 ////dump("matches: " + matches + "\n");
630 var objAndComplete = (objname || obj) + "." + bestmatch;
631 //dump("matches.length: " + matches.length + ", tooManyMatches: " + tooManyMatches + ", objAndComplete: " + objAndComplete + "\n");
632 if(matches.length > 1 && (tooManyMatches == objAndComplete || matches.length <= 10)) {
634 printWithRunin("Matches: ", matches.join(', '), "tabcomplete");
635 tooManyMatches = null;
636 }
637 else if(matches.length > 10)
638 {
639 println(matches.length + " matches. Press tab again to see them all", "tabcomplete");
640 tooManyMatches = objAndComplete;
641 }
642 else {
643 tooManyMatches = null;
644 }
645 if(bestmatch != "")
646 {
647 var sstart;
648 if(dotpos == caret) {
649 sstart = spacepos+1;
650 }
651 else {
652 sstart = dotpos+1;
653 }
654 _in.value = _in.value.substr(0, sstart)
655 + bestmatch
656 + _in.value.substr(caret);
657 setselectionto(_in,caret + (bestmatch.length - complete.length));
658 }
659 }
660 }
662 function printQuestion(q)
663 {
664 println(q, "input");
665 }
667 function printAnswer(a)
668 {
669 if (a !== undefined) {
670 println(a, "normalOutput");
671 shellCommands.ans = a;
672 }
673 }
675 function printError(er)
676 {
677 var lineNumberString;
679 lastError = er; // for debugging the shell
680 if (er.name)
681 {
682 // lineNumberString should not be "", to avoid a very wacky bug in IE 6.
683 lineNumberString = (er.lineNumber != undefined) ? (" on line " + er.lineNumber + ": ") : ": ";
684 println(er.name + lineNumberString + er.message, "error"); // Because IE doesn't have error.toString.
685 }
686 else
687 println(er, "error"); // Because security errors in Moz /only/ have toString.
688 }
690 /**
691 * Evaluates |s| or current input (_in.value) in the previously set up context.
692 * @param {String} s - (optional) command to evaluate.
693 */
694 function go(s)
695 {
696 // save the command to eval in |question|, so that the target window can access
697 // it when evaluating.
698 _in.value = question = s ? s : _in.value;
700 if (question == "")
701 return;
703 histList[histList.length-1] = question;
704 histList[histList.length] = "";
705 histPos = histList.length - 1;
707 // Unfortunately, this has to happen *before* the JavaScript is run, so that
708 // print() output will go in the right place.
709 _in.value='';
710 recalculateInputHeight();
711 printQuestion(question);
713 if (_win.closed) {
714 printError("Target window has been closed.");
715 return;
716 }
718 try { ("Shell" in _win) }
719 catch(er) {
720 printError("The JavaScript Shell cannot access variables in the target window. The most likely reason is that the target window now has a different page loaded and that page has a different hostname than the original page.");
721 return;
722 }
724 if (!("Shell" in _win))
725 initTarget(); // silent
727 // Evaluate Shell.question using _win's eval (this is why eval isn't in the |with|, IIRC).
728 run(_win, "try{ Shell.printAnswer(eval('with(Shell._scope) with(Shell.shellCommands) {' + Shell.question + String.fromCharCode(10) + '}')); } catch(er) { Shell.printError(er); }; setTimeout(Shell.refocus, 0);");
729 }
731 </script>
733 <!-- for http://ted.mielczarek.org/code/mozilla/extensiondev/ -->
734 <script type="text/javascript" src="chrome://extensiondev/content/chromeShellExtras.js"></script>
736 <style type="text/css">
737 body { background: white; color: black; }
739 #output {
740 /* Preserve line breaks, but wrap too if browser supports it */
741 white-space: pre;
742 white-space: -moz-pre-wrap;
743 white-space: pre-wrap;
744 }
746 h3 { margin-top: 0; margin-bottom: 0em; }
747 h3 + div { margin: 0; }
749 form { margin: 0; padding: 0; }
750 #input { width: 100%; border: none; padding: 0; overflow: auto; }
752 .input { color: blue; background: white; font: inherit; font-weight: bold; margin-top: .5em; /* background: #E6E6FF; */ }
753 .normalOutput { color: black; background: white; }
754 .print { color: brown; background: white; }
755 .error { color: red; background: white; }
756 .propList { color: green; background: white; }
757 .message { color: green; background: white; }
758 .tabcomplete { color: purple; background: white; }
759 </style>
760 </head>
762 <body onload="init()">
764 <div id="output"><h3>JavaScript Shell 1.4</h3><div>Features: autocompletion of property names with Tab, multiline input with Shift+Enter, input history with (Ctrl+) Up/Down, <a accesskey="M" href="javascript:go('scope(Math); mathHelp();');" title="Accesskey: M">Math</a>, <a accesskey="H" href="http://www.squarefree.com/shell/?ignoreReferrerFrom=shell1.4" title="Accesskey: H">help</a></div><div>Values and functions: ans, print(string), <a accesskey="P" href="javascript:go('props(ans)')" title="Accesskey: P">props(object)</a>, <a accesskey="B" href="javascript:go('blink(ans)')" title="Accesskey: B">blink(node)</a>, <a accesskey="C" href="javascript:go('clear()')" title="Accesskey: C">clear()</a>, load(scriptURL), scope(object)</div></div>
766 <div><textarea id="input" class="input" wrap="off" spellcheck="false" onkeydown="inputKeydown(event)" rows="1"></textarea></div>
768 </body>
770 </html>