]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/xul/staff_client/chrome/content/util/shell.js
XMLENT was killing the Javascript Shell
[working/Evergreen.git] / Open-ILS / xul / staff_client / chrome / content / util / shell.js
1 var 
2 histList = [""], 
3 histPos = 0, 
4 _scope = {}, 
5 _win, // a top-level context
6 question,
7 _in,
8 _out,
9 tooManyMatches = null,
10 lastError = null;
11
12 function refocus()
13 {
14   _in.blur(); // Needed for Mozilla to scroll correctly.
15   _in.focus();
16 }
17
18 function init()
19 {
20   _in = document.getElementById("input");
21   _out = document.getElementById("output");
22
23   _win = window;
24
25   if (opener && !opener.closed)
26   {
27     println("Using bookmarklet version of shell: commands will run in opener's context.", "message");
28     _win = opener;
29   }
30
31   initTarget();
32
33   recalculateInputHeight();
34   refocus();
35 }
36
37 function initTarget()
38 {
39   _win.Shell = window;
40   _win.print = shellCommands.print;
41 }
42
43
44 // Unless the user is selected something, refocus the textbox.
45 // (requested by caillon, brendan, asa)
46 function keepFocusInTextbox(e) 
47 {
48   var g = e.srcElement ? e.srcElement : e.target; // IE vs. standard
49   
50   while (!g.tagName)
51     g = g.parentNode;
52   var t = g.tagName.toUpperCase();
53   if (t=="A" || t=="INPUT")
54     return;
55     
56   if (window.getSelection) {
57     // Mozilla
58     if (String(window.getSelection()))
59       return;
60   }
61   else if (document.getSelection) {
62     // Opera? Netscape 4?
63     if (document.getSelection())
64       return;
65   }
66   else {
67     // IE
68     if ( document.selection.createRange().text )
69       return;
70   }
71   
72   refocus();
73 }
74
75 function inputKeydown(e) {
76   // Use onkeydown because IE doesn't support onkeypress for arrow keys
77
78   //alert(e.keyCode + " ^ " + e.keycode);
79
80   if (e.shiftKey && e.keyCode == 13) { // shift-enter
81     // don't do anything; allow the shift-enter to insert a line break as normal
82   } else if (e.keyCode == 13) { // enter
83     // execute the input on enter
84     try { go(); } catch(er) { alert(er); };
85     setTimeout(function() { _in.value = ""; }, 0); // can't preventDefault on input, so clear it later
86   } else if (e.keyCode == 38) { // up
87     // go up in history if at top or ctrl-up
88     if (e.ctrlKey || caretInFirstLine(_in))
89       hist(true);
90   } else if (e.keyCode == 40) { // down
91     // go down in history if at end or ctrl-down
92     if (e.ctrlKey || caretInLastLine(_in))
93       hist(false);
94   } else if (e.keyCode == 9) { // tab
95     tabcomplete();
96     setTimeout(function() { refocus(); }, 0); // refocus because tab was hit
97   } else { }
98
99   setTimeout(recalculateInputHeight, 0);
100   
101   //return true;
102 };
103
104 function caretInFirstLine(textbox)
105 {
106   // IE doesn't support selectionStart/selectionEnd
107   if (textbox.selectionStart == undefined)
108     return true;
109
110   var firstLineBreak = textbox.value.indexOf("\n");
111   
112   return ((firstLineBreak == -1) || (textbox.selectionStart <= firstLineBreak));
113 }
114
115 function caretInLastLine(textbox)
116 {
117   // IE doesn't support selectionStart/selectionEnd
118   if (textbox.selectionEnd == undefined)
119     return true;
120
121   var lastLineBreak = textbox.value.lastIndexOf("\n");
122   
123   return (textbox.selectionEnd > lastLineBreak);
124 }
125
126 function recalculateInputHeight()
127 {
128   var rows = _in.value.split(/\n/).length
129     + 1 // prevent scrollbar flickering in Mozilla
130     + (window.opera ? 1 : 0); // leave room for scrollbar in Opera
131   
132   if (_in.rows != rows) // without this check, it is impossible to select text in Opera 7.60 or Opera 8.0.
133     _in.rows = rows;
134 }
135
136 function println(s, type)
137 {
138   if((s=String(s)))
139   {
140     var newdiv = document.createElement("div");
141     newdiv.appendChild(document.createTextNode(s));
142     newdiv.className = type;
143     _out.appendChild(newdiv);
144     return newdiv;
145   }
146 }
147
148 function printWithRunin(h, s, type)
149 {
150   var div = println(s, type);
151   var head = document.createElement("strong");
152   head.appendChild(document.createTextNode(h + ": "));
153   div.insertBefore(head, div.firstChild);
154 }
155
156
157 var shellCommands = 
158 {
159 load : function load(url)
160 {
161   var s = _win.document.createElement("script");
162   s.type = "text/javascript";
163   s.src = url;
164   _win.document.getElementsByTagName("head")[0].appendChild(s);
165   println("Loading " + url + "...", "message");
166 },
167
168 clear : function clear()
169 {
170   var CHILDREN_TO_PRESERVE = 3;
171   while (_out.childNodes[CHILDREN_TO_PRESERVE]) 
172     _out.removeChild(_out.childNodes[CHILDREN_TO_PRESERVE]);
173 },
174
175 print : function print(s) { println(s, "print"); },
176
177 // the normal function, "print", shouldn't return a value
178 // (suggested by brendan; later noticed it was a problem when showing others)
179 pr : function pr(s) 
180
181   shellCommands.print(s); // need to specify shellCommands so it doesn't try window.print()!
182   return s;
183 },
184
185 props : function props(e, onePerLine)
186 {
187   if (e === null) {
188     println("props called with null argument", "error");
189     return;
190   }
191
192   if (e === undefined) {
193     println("props called with undefined argument", "error");
194     return;
195   }
196
197   var ns = ["Methods", "Fields", "Unreachables"];
198   var as = [[], [], []]; // array of (empty) arrays of arrays!
199   var p, j, i; // loop variables, several used multiple times
200
201   var protoLevels = 0;
202
203   for (p = e; p; p = p.__proto__)
204   {
205     for (i=0; i<ns.length; ++i)
206       as[i][protoLevels] = [];
207     ++protoLevels;
208   }
209
210   for(var a in e)
211   {
212     // Shortcoming: doesn't check that VALUES are the same in object and prototype.
213
214     var protoLevel = -1;
215     try
216     {
217       for (p = e; p && (a in p); p = p.__proto__)
218         ++protoLevel;
219     }
220     catch(er) { protoLevel = 0; } // "in" operator throws when param to props() is a string
221
222     var type = 1;
223     try
224     {
225       if ((typeof e[a]) == "function")
226         type = 0;
227     }
228     catch (er) { type = 2; }
229
230     as[type][protoLevel].push(a);
231   }
232
233   function times(s, n) { return n ? s + times(s, n-1) : ""; }
234
235   for (j=0; j<protoLevels; ++j)
236     for (i=0;i<ns.length;++i)
237       if (as[i][j].length) 
238         printWithRunin(
239           ns[i] + times(" of prototype", j), 
240           (onePerLine ? "\n\n" : "") + as[i][j].sort().join(onePerLine ? "\n" : ", ") + (onePerLine ? "\n\n" : ""), 
241           "propList"
242         );
243 },
244
245 blink : function blink(node)
246 {
247   if (!node)                     throw("blink: argument is null or undefined.");
248   if (node.nodeType == null)     throw("blink: argument must be a node.");
249   if (node.nodeType == 3)        throw("blink: argument must not be a text node");
250   if (node.documentElement)      throw("blink: argument must not be the document object");
251
252   function setOutline(o) { 
253     return function() {
254       if (node.style.outline != node.style.bogusProperty) {
255         // browser supports outline (Firefox 1.1 and newer, CSS3, Opera 8).
256         node.style.outline = o;
257       }
258       else if (node.style.MozOutline != node.style.bogusProperty) {
259         // browser supports MozOutline (Firefox 1.0.x and older)
260         node.style.MozOutline = o;
261       }
262       else {
263         // browser only supports border (IE). border is a fallback because it moves things around.
264         node.style.border = o;
265       }
266     }
267   } 
268   
269   function focusIt(a) {
270     return function() {
271       a.focus(); 
272     }
273   }
274
275   if (node.ownerDocument) {
276     var windowToFocusNow = (node.ownerDocument.defaultView || node.ownerDocument.parentWindow); // Moz vs. IE
277     if (windowToFocusNow)
278       setTimeout(focusIt(windowToFocusNow.top), 0);
279   }
280
281   for(var i=1;i<7;++i)
282     setTimeout(setOutline((i%2)?'3px solid red':'none'), i*100);
283
284   setTimeout(focusIt(window), 800);
285   setTimeout(focusIt(_in), 810);
286 },
287
288 scope : function scope(sc)
289 {
290   if (!sc) sc = {};
291   _scope = sc;
292   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");
293 },
294
295 mathHelp : function mathHelp()
296 {
297   printWithRunin("Math constants", "E, LN2, LN10, LOG2E, LOG10E, PI, SQRT1_2, SQRT2", "propList");
298   printWithRunin("Math methods", "abs, acos, asin, atan, atan2, ceil, cos, exp, floor, log, max, min, pow, random, round, sin, sqrt, tan", "propList");
299 },
300
301 ans : undefined
302 };
303
304
305 function hist(up)
306 {
307   // histList[0] = first command entered, [1] = second, etc.
308   // type something, press up --> thing typed is now in "limbo"
309   // (last item in histList) and should be reachable by pressing 
310   // down again.
311
312   var L = histList.length;
313
314   if (L == 1)
315     return;
316
317   if (up)
318   {
319     if (histPos == L-1)
320     {
321       // Save this entry in case the user hits the down key.
322       histList[histPos] = _in.value;
323     }
324
325     if (histPos > 0)
326     {
327       histPos--;
328       // Use a timeout to prevent up from moving cursor within new text
329       // Set to nothing first for the same reason
330       setTimeout(
331         function() {
332           _in.value = ''; 
333           _in.value = histList[histPos];
334           var caretPos = _in.value.length;
335           if (_in.setSelectionRange) 
336             _in.setSelectionRange(caretPos, caretPos);
337         },
338         0
339       );
340     }
341   } 
342   else // down
343   {
344     if (histPos < L-1)
345     {
346       histPos++;
347       _in.value = histList[histPos];
348     }
349     else if (histPos == L-1)
350     {
351       // Already on the current entry: clear but save
352       if (_in.value)
353       {
354         histList[histPos] = _in.value;
355         ++histPos;
356         _in.value = "";
357       }
358     }
359   }
360 }
361
362 function tabcomplete()
363 {
364   /*
365    * Working backwards from s[from], find the spot
366    * where this expression starts.  It will scan
367    * until it hits a mismatched ( or a space,
368    * but it skips over quoted strings.
369    * If stopAtDot is true, stop at a '.'
370    */
371   function findbeginning(s, from, stopAtDot)
372   {
373     /*
374      *  Complicated function.
375      *
376      *  Return true if s[i] == q BUT ONLY IF
377      *  s[i-1] is not a backslash.
378      */
379     function equalButNotEscaped(s,i,q)
380     {
381       if(s.charAt(i) != q) // not equal go no further
382         return false;
383
384       if(i==0) // beginning of string
385         return true;
386
387       if(s.charAt(i-1) == '\\') // escaped?
388         return false;
389
390       return true;
391     }
392
393     var nparens = 0;
394     var i;
395     for(i=from; i>=0; i--)
396     {
397       if(s.charAt(i) == ' ')
398         break;
399
400       if(stopAtDot && s.charAt(i) == '.')
401         break;
402         
403       if(s.charAt(i) == ')')
404         nparens++;
405       else if(s.charAt(i) == '(')
406         nparens--;
407
408       if(nparens < 0)
409         break;
410
411       // skip quoted strings
412       if(s.charAt(i) == '\'' || s.charAt(i) == '\"')
413       {
414         //dump("skipping quoted chars: ");
415         var quot = s.charAt(i);
416         i--;
417         while(i >= 0 && !equalButNotEscaped(s,i,quot)) {
418           //dump(s.charAt(i));
419           i--;
420         }
421         //dump("\n");
422       }
423     }
424     return i;
425   }
426
427   // XXX should be used more consistently (instead of using selectionStart/selectionEnd throughout code)
428   // XXX doesn't work in IE, even though it contains IE-specific code
429   function getcaretpos(inp)
430   {
431     if(inp.selectionEnd != null)
432       return inp.selectionEnd;
433       
434     if(inp.createTextRange)
435     {
436       var docrange = _win.Shell.document.selection.createRange();
437       var inprange = inp.createTextRange();
438       if (inprange.setEndPoint)
439       {
440         inprange.setEndPoint('EndToStart', docrange);
441         return inprange.text.length;
442       }
443     }
444
445     return inp.value.length; // sucks, punt
446   }
447
448   function setselectionto(inp,pos)
449   {
450     if(inp.selectionStart) {
451       inp.selectionStart = inp.selectionEnd = pos;
452     }
453     else if(inp.createTextRange) {
454       var docrange = _win.Shell.document.selection.createRange();
455       var inprange = inp.createTextRange();
456       inprange.move('character',pos);
457       inprange.select();
458     }
459     else { // err...
460     /*
461       inp.select();
462       if(_win.Shell.document.getSelection())
463         _win.Shell.document.getSelection() = "";
464         */
465     }
466   }
467     // get position of cursor within the input box
468     var caret = getcaretpos(_in);
469
470     if(caret) {
471       //dump("----\n");
472       var dotpos, spacepos, complete, obj;
473       //dump("caret pos: " + caret + "\n");
474       // see if there's a dot before here
475       dotpos = findbeginning(_in.value, caret-1, true);
476       //dump("dot pos: " + dotpos + "\n");
477       if(dotpos == -1 || _in.value.charAt(dotpos) != '.') {
478         dotpos = caret;
479 //dump("changed dot pos: " + dotpos + "\n");
480       }
481
482       // look backwards for a non-variable-name character
483       spacepos = findbeginning(_in.value, dotpos-1, false);
484       //dump("space pos: " + spacepos + "\n");
485       // get the object we're trying to complete on
486       if(spacepos == dotpos || spacepos+1 == dotpos || dotpos == caret)
487       {
488         // try completing function args
489         if(_in.value.charAt(dotpos) == '(' ||
490  (_in.value.charAt(spacepos) == '(' && (spacepos+1) == dotpos))
491         {
492           var fn,fname;
493   var from = (_in.value.charAt(dotpos) == '(') ? dotpos : spacepos;
494           spacepos = findbeginning(_in.value, from-1, false);
495
496           fname = _in.value.substr(spacepos+1,from-(spacepos+1));
497   //dump("fname: " + fname + "\n");
498           try {
499             with(_win.Shell._scope)
500               with(_win)
501                 with(Shell.shellCommands)
502                   fn = eval(fname);
503           }
504           catch(er) {
505             //dump('fn is not a valid object\n');
506             return;
507           }
508           if(fn == undefined) {
509              //dump('fn is undefined');
510              return;
511           }
512           if(fn instanceof Function)
513           {
514             // Print function definition, including argument names, but not function body
515             if(!fn.toString().match(/function .+?\(\) +\{\n +\[native code\]\n\}/))
516               println(fn.toString().match(/function .+?\(.*?\)/), "tabcomplete");
517           }
518
519           return;
520         }
521         else
522           obj = _win;
523       }
524       else
525       {
526         var objname = _in.value.substr(spacepos+1,dotpos-(spacepos+1));
527         //dump("objname: |" + objname + "|\n");
528         try {
529           with(_win.Shell._scope)
530             with(_win)
531                 obj = eval(objname);
532         }
533         catch(er) {
534           printError(er); 
535           return;
536         }
537         if(obj == undefined) {
538           // sometimes this is tabcomplete's fault, so don't print it :(
539           // e.g. completing from "print(document.getElements"
540           // println("Can't complete from null or undefined expression " + objname, "error");
541           return;
542         }
543       }
544       //dump("obj: " + obj + "\n");
545       // get the thing we're trying to complete
546       if(dotpos == caret)
547       {
548         if(spacepos+1 == dotpos || spacepos == dotpos)
549         {
550           // nothing to complete
551           //dump("nothing to complete\n");
552           return;
553         }
554
555         complete = _in.value.substr(spacepos+1,dotpos-(spacepos+1));
556       }
557       else {
558         complete = _in.value.substr(dotpos+1,caret-(dotpos+1));
559       }
560       //dump("complete: " + complete + "\n");
561       // ok, now look at all the props/methods of this obj
562       // and find ones starting with 'complete'
563       var matches = [];
564       var bestmatch = null;
565       for(var a in obj)
566       {
567         //a = a.toString();
568         //XXX: making it lowercase could help some cases,
569         // but screws up my general logic.
570         if(a.substr(0,complete.length) == complete) {
571           matches.push(a);
572           ////dump("match: " + a + "\n");
573           // if no best match, this is the best match
574           if(bestmatch == null)
575           {
576             bestmatch = a;
577           }
578           else {
579             // the best match is the longest common string
580             function min(a,b){ return ((a<b)?a:b); }
581             var i;
582             for(i=0; i< min(bestmatch.length, a.length); i++)
583             {
584               if(bestmatch.charAt(i) != a.charAt(i))
585                 break;
586             }
587             bestmatch = bestmatch.substr(0,i);
588             ////dump("bestmatch len: " + i + "\n");
589           }
590           ////dump("bestmatch: " + bestmatch + "\n");
591         }
592       }
593       bestmatch = (bestmatch || "");
594       ////dump("matches: " + matches + "\n");
595       var objAndComplete = (objname || obj) + "." + bestmatch;
596       //dump("matches.length: " + matches.length + ", tooManyMatches: " + tooManyMatches + ", objAndComplete: " + objAndComplete + "\n");
597       if(matches.length > 1 && (tooManyMatches == objAndComplete || matches.length <= 10)) {
598
599         printWithRunin("Matches: ", matches.join(', '), "tabcomplete");
600         tooManyMatches = null;
601       }
602       else if(matches.length > 10)
603       {
604         println(matches.length + " matches.  Press tab again to see them all", "tabcomplete");
605         tooManyMatches = objAndComplete;
606       }
607       else {
608         tooManyMatches = null;
609       }
610       if(bestmatch != "")
611       {
612         var sstart;
613         if(dotpos == caret) {
614           sstart = spacepos+1;
615         }
616         else {
617           sstart = dotpos+1;
618         }
619         _in.value = _in.value.substr(0, sstart)
620                   + bestmatch
621                   + _in.value.substr(caret);
622         setselectionto(_in,caret + (bestmatch.length - complete.length));
623       }
624     }
625 }
626
627 function printQuestion(q)
628 {
629   println(q, "input");
630 }
631
632 function printAnswer(a)
633 {
634   if (a !== undefined) {
635     println(a, "normalOutput");
636     shellCommands.ans = a;
637   }
638 }
639
640 function printError(er)
641
642   var lineNumberString;
643
644   lastError = er; // for debugging the shell
645   if (er.name)
646   {
647     // lineNumberString should not be "", to avoid a very wacky bug in IE 6.
648     lineNumberString = (er.lineNumber != undefined) ? (" on line " + er.lineNumber + ": ") : ": ";
649     println(er.name + lineNumberString + er.message, "error"); // Because IE doesn't have error.toString.
650   }
651   else
652     println(er, "error"); // Because security errors in Moz /only/ have toString.
653 }
654
655 function go(s)
656 {
657   _in.value = question = s ? s : _in.value;
658
659   if (question == "")
660     return;
661
662   histList[histList.length-1] = question;
663   histList[histList.length] = "";
664   histPos = histList.length - 1;
665   
666   // Unfortunately, this has to happen *before* the JavaScript is run, so that 
667   // print() output will go in the right place.
668   _in.value='';
669   recalculateInputHeight();
670   printQuestion(question);
671
672   if (_win.closed) {
673     printError("Target window has been closed.");
674     return;
675   }
676   
677   try { ("Shell" in _win) }
678   catch(er) {
679     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.");
680     return;
681   }
682
683   if (!("Shell" in _win))
684     initTarget(); // silent
685
686   // Evaluate Shell.question using _win's eval (this is why eval isn't in the |with|, IIRC).
687   _win.location.href = "javascript:try{ Shell.printAnswer(eval('with(Shell._scope) with(Shell.shellCommands) {' + Shell.question + String.fromCharCode(10) + '}')); } catch(er) { Shell.printError(er); }; setTimeout(Shell.refocus, 0); void 0";
688 }
689
690