1 /* See license.txt for terms of usage */
  2 
  3 var FBL = fbXPCOMUtils;
  4 
  5 try { /*@explore*/
  6 
  7 (function() {
  8 
  9 // ************************************************************************************************
 10 // Modules
 11 
 12 Components.utils.import("resource://gre/modules/PluralForm.jsm");
 13 
 14 // ************************************************************************************************
 15 // Constants
 16 
 17 const Cc = Components.classes;
 18 const Ci = Components.interfaces;
 19 
 20 this.fbs = Cc["@joehewitt.com/firebug;1"].getService().wrappedJSObject;
 21 this.httpObserver = this.CCSV("@joehewitt.com/firebug-http-observer;1", "nsIObserverService");
 22 this.jsd = this.CCSV("@mozilla.org/js/jsd/debugger-service;1", "jsdIDebuggerService");
 23 
 24 const finder = this.finder = this.CCIN("@mozilla.org/embedcomp/rangefind;1", "nsIFind");
 25 const wm = this.CCSV("@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator");
 26 const ioService = this.CCSV("@mozilla.org/network/io-service;1", "nsIIOService");
 27 const consoleService = Components.classes["@mozilla.org/consoleservice;1"].
 28     getService(Components.interfaces["nsIConsoleService"]);
 29 
 30 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 31 
 32 const reNotWhitespace = /[^\s]/;
 33 const reSplitFile = /:\/{1,3}(.*?)\/([^\/]*?)\/?($|\?.*)/;
 34 const reURL = /(([^:]+:)\/{1,2}[^\/]*)(.*?)$/;  // This RE and the previous one should changed to be consistent
 35 const reChromeCase = /chrome:\/\/([^/]*)\/(.*?)$/;
 36 // Globals
 37 this.reDataURL = /data:text\/javascript;fileName=([^;]*);baseLineNumber=(\d*?),((?:.*?%0A)|(?:.*))/g;
 38 this.reJavascript = /\s*javascript:\s*(.*)/;
 39 this.reChrome = /chrome:\/\/([^\/]*)\//;
 40 this.reCSS = /\.css$/;
 41 this.reFile = /file:\/\/([^\/]*)\//;
 42 this.reUpperCase = /[A-Z]/;
 43 
 44 const reSplitLines = /\r\n|\r|\n/;
 45 const reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/;
 46 const reGuessFunction = /['"]?([$0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/;
 47 const reWord = /([A-Za-z_$][A-Za-z_$0-9]*)(\.([A-Za-z_$][A-Za-z_$0-9]*))*/;
 48 
 49 const overrideDefaultsWithPersistedValuesTimeout = 500;
 50 
 51 const NS_SEEK_SET = Ci.nsISeekableStream.NS_SEEK_SET;
 52 
 53 // ************************************************************************************************
 54 // Namespaces
 55 
 56 var namespaces = [];
 57 
 58 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 59 
 60 this.ns = function(fn)
 61 {
 62     var ns = {};
 63     namespaces.push(fn, ns);
 64     return ns;
 65 };
 66 
 67 this.initialize = function()
 68 {
 69     if (FBTrace.DBG_INITIALIZE)
 70         FBTrace.sysout("FBL.initialize BEGIN "+namespaces.length+" namespaces\n");
 71 
 72     for (var i = 0; i < namespaces.length; i += 2)
 73     {
 74         var fn = namespaces[i];
 75         var ns = namespaces[i+1];
 76         fn.apply(ns);
 77     }
 78 
 79     if (FBTrace.DBG_INITIALIZE)
 80         FBTrace.sysout("FBL.initialize END "+namespaces.length+" namespaces\n");
 81 };
 82 
 83 // ************************************************************************************************
 84 // Basics
 85 
 86 this.bind = function()  // fn, thisObject, args => thisObject.fn(args, arguments);
 87 {
 88    var args = cloneArray(arguments), fn = args.shift(), object = args.shift();
 89    return function() { return fn.apply(object, arrayInsert(cloneArray(args), 0, arguments)); }
 90 };
 91 
 92 this.bindFixed = function() // fn, thisObject, args => thisObject.fn(args);
 93 {
 94     var args = cloneArray(arguments), fn = args.shift(), object = args.shift();
 95     return function() { return fn.apply(object, args); }
 96 };
 97 
 98 this.extend = function(l, r)
 99 {
100     var newOb = {};
101     for (var n in l)
102         newOb[n] = l[n];
103     for (var n in r)
104         newOb[n] = r[n];
105     return newOb;
106 };
107 
108 this.descend = function(prototypeParent, childProperties)
109 {
110     function protoSetter() {};
111     protoSetter.prototype = prototypeParent;
112     var newOb = new protoSetter();
113     for (var n in childProperties)
114         newOb[n] = childProperties[n];
115     return newOb;
116 };
117 
118 // ************************************************************************************************
119 // Arrays
120 
121 this.keys = function(map)  // At least sometimes the keys will be on user-level window objects
122 {
123     var keys = [];
124     try
125     {
126         for (var name in map)  // enumeration is safe
127             keys.push(name);   // name is string, safe
128     }
129     catch (exc)
130     {
131         // Sometimes we get exceptions trying to iterate properties
132     }
133 
134     return keys;  // return is safe
135 };
136 
137 this.values = function(map)
138 {
139     var values = [];
140     try
141     {
142         for (var name in map)
143         {
144             try
145             {
146                 values.push(map[name]);
147             }
148             catch (exc)
149             {
150                 // Sometimes we get exceptions trying to access properties
151                 if (FBTrace.DBG_ERRORS)
152                     FBTrace.dumpPropreties("lib.values FAILED ", exc);
153             }
154 
155         }
156     }
157     catch (exc)
158     {
159         // Sometimes we get exceptions trying to iterate properties
160         if (FBTrace.DBG_ERRORS)
161             FBTrace.dumpPropreties("lib.values FAILED ", exc);
162     }
163 
164     return values;
165 };
166 
167 this.remove = function(list, item)
168 {
169     for (var i = 0; i < list.length; ++i)
170     {
171         if (list[i] == item)
172         {
173             list.splice(i, 1);
174             break;
175         }
176     }
177 };
178 
179 this.sliceArray = function(array, index)
180 {
181     var slice = [];
182     for (var i = index; i < array.length; ++i)
183         slice.push(array[i]);
184 
185     return slice;
186 };
187 
188 function cloneArray(array, fn)
189 {
190    var newArray = [];
191 
192    if (fn)
193        for (var i = 0; i < array.length; ++i)
194            newArray.push(fn(array[i]));
195    else
196        for (var i = 0; i < array.length; ++i)
197            newArray.push(array[i]);
198 
199    return newArray;
200 }
201 
202 function extendArray(array, array2)
203 {
204    var newArray = [];
205    newArray.push.apply(newArray, array);
206    newArray.push.apply(newArray, array2);
207    return newArray;
208 }
209 
210 this.extendArray = extendArray;
211 this.cloneArray = cloneArray;
212 
213 function arrayInsert(array, index, other)
214 {
215    for (var i = 0; i < other.length; ++i)
216        array.splice(i+index, 0, other[i]);
217 
218    return array;
219 }
220 
221 this.arrayInsert = arrayInsert;
222 
223 // ************************************************************************************************
224 
225 this.safeToString = function(ob)
226 {
227     try
228     {
229         if (!ob)
230         {
231             if (ob == undefined)
232                 return 'undefined';
233             if (ob == null)
234                 return 'null';
235             if (ob == false)
236                 return 'false';
237             return "";
238         }
239         if (ob && (typeof (ob['toString']) == "function") )
240             return ob.toString();
241         if (ob && typeof (ob['toSource']) == 'function')
242             return ob.toSource();
243        /* https://bugzilla.mozilla.org/show_bug.cgi?id=522590 */
244         var str = "[";
245         for (var p in ob)
246             str += p+',';
247         return str + ']';
248 
249     }
250     catch (exc)
251     {
252         if (FBTrace.DBG_ERRORS)
253             FBTrace.sysout("safeToString FAILS "+exc, exc);
254     }
255     return "[unsupported: no toString() function in type "+typeof(ob)+"]";
256 };
257 
258 // ************************************************************************************************
259 
260 this.hasProperties = function(ob)
261 {
262     try
263     {
264         for (var name in ob)
265             return true;
266     } catch (exc) {}
267     return false;
268 };
269 
270 // ************************************************************************************************
271 
272 this.convertToUnicode = function(text, charset)
273 {
274     if (!text)
275         return "";
276 
277     try
278     {
279         var conv = this.CCSV("@mozilla.org/intl/scriptableunicodeconverter", "nsIScriptableUnicodeConverter");
280         conv.charset = charset ? charset : "UTF-8";
281         return conv.ConvertToUnicode(text);
282     }
283     catch (exc)
284     {
285         if (FBTrace.DBG_ERRORS)
286             FBTrace.sysout("lib.convertToUnicode: fails: for charset "+charset+" conv.charset:"+conv.charset+" exc: "+exc, exc);
287         // the exception is worthless, make up a new one
288         throw new Error("Firebug failed to convert to unicode using charset: "+conv.charset+" in @mozilla.org/intl/scriptableunicodeconverter");
289     }
290 };
291 
292 this.convertFromUnicode = function(text, charset)
293 {
294     if (!text)
295         return "";
296 
297     try
298     {
299         var conv = this.CCSV("@mozilla.org/intl/scriptableunicodeconverter", "nsIScriptableUnicodeConverter");
300         conv.charset = charset ? charset : "UTF-8";
301         return conv.ConvertFromUnicode(text);
302     }
303     catch (exc)
304     {
305         if (FBTrace.DBG_ERRORS)
306             FBTrace.sysout("lib.convertFromUnicode: fails: for charset "+charset+" conv.charset:"+conv.charset+" exc: "+exc, exc);
307     }
308 };
309 
310 this.getPlatformName = function()
311 {
312     return this.CCSV("@mozilla.org/xre/app-info;1", "nsIXULRuntime").OS;
313 };
314 
315 this.beep = function()
316 {
317     var sounder = this.CCSV("@mozilla.org/sound;1", "nsISound");
318     sounder.beep();
319 };
320 
321 this.getUniqueId = function() {
322     return this.getRandomInt(0,65536);
323 }
324 
325 this.getRandomInt = function(min, max) {
326   return Math.floor(Math.random() * (max - min + 1) + min);
327 }
328 
329 // ************************************************************************************************
330 
331 this.createStyleSheet = function(doc, url)
332 {
333     var style = doc.createElementNS("http://www.w3.org/1999/xhtml", "style");
334     style.setAttribute("charset","utf-8");
335     style.setAttribute("type", "text/css");
336     style.innerHTML = this.getResource(url);
337     FBL.unwrapObject(style).firebugIgnore = true;
338     return style;
339 }
340 
341 this.addStyleSheet = function(doc, style)
342 {
343     var heads = doc.getElementsByTagName("head");
344     if (heads.length)
345         heads[0].appendChild(style);
346     else
347         doc.documentElement.appendChild(style);
348 };
349 
350 this.addScript = function(doc, id, src)
351 {
352     var element = doc.createElementNS("http://www.w3.org/1999/xhtml", "html:script");
353     element.setAttribute("type", "text/javascript");
354     element.setAttribute("id", id);
355     if (!FBTrace.DBG_CONSOLE)
356         FBL.unwrapObject(element).firebugIgnore = true;
357 
358     element.innerHTML = src;
359     if (doc.documentElement)
360         doc.documentElement.appendChild(element);
361     else
362     {
363         // See issue 1079, the svg test case gives this error
364         if (FBTrace.DBG_ERRORS)
365             FBTrace.sysout("lib.addScript doc has no documentElement:", doc);
366     }
367     return element;
368 }
369 
370 // ************************************************************************************************
371 
372 this.isAncestorIgnored = function(node)
373 {
374     for (var parent = node; parent; parent = parent.parentNode)
375     {
376         if (FBL.unwrapObject(parent).firebugIgnore)
377             return true;
378     }
379 
380     return false;
381 }
382 
383 // ************************************************************************************************
384 // Localization
385 
386 /*
387  * $STR - intended for localization of a static string.
388  * $STRF - intended for localization of a string with dynamically inserted values.
389  * $STRP - intended for localization of a string with dynamically plural forms.
390  *
391  * Notes:
392  * 1) Name with _ in place of spaces is the key in the firebug.properties file.
393  * 2) If the specified key isn't localized for particular language, both methods use
394  *    the part after the last dot (in the specified name) as the return value.
395  *
396  * Examples:
397  * $STR("Label"); - search for key "Label" within the firebug.properties file
398  *                 and returns its value. If the key doesn't exist returns "Label".
399  *
400  * $STR("Button Label"); - search for key "Button_Label" withing the firebug.properties
401  *                        file. If the key doesn't exist returns "Button Label".
402  *
403  * $STR("net.Response Header"); - search for key "net.Response_Header". If the key doesn't
404  *                               exist returns "Response Header".
405  *
406  * firebug.properties:
407  * net.timing.Request_Time=Request Time: %S [%S]
408  *
409  * var param1 = 10;
410  * var param2 = "ms";
411  * $STRF("net.timing.Request Time", param1, param2);  -> "Request Time: 10 [ms]"
412  *
413  * - search for key "net.timing.Request_Time" within the firebug.properties file. Parameters
414  *   are inserted at specified places (%S) in the same order as they are passed. If the
415  *   key doesn't exist the method returns "Request Time".
416  */
417 function $STR(name, bundle)
418 {
419     try
420     {
421         if (typeof bundle == "string")
422             bundle = document.getElementById(bundle);
423 
424         if (bundle)
425             return bundle.getString(name.replace(' ', '_', "g"));
426 
427         if (Firebug)
428             return Firebug.getStringBundle().GetStringFromName(name.replace(' ', '_', "g"));
429     }
430     catch (err)
431     {
432         if (FBTrace.DBG_LOCALE)
433         {
434             FBTrace.sysout("lib.getString: " + name + "\n");
435             FBTrace.sysout("lib.getString FAILS ", err);
436         }
437     }
438 
439     // XXXjjb apparently we get to this code if we get an exception above...is that best we can do?
440 
441     // Use only the label after last dot.
442     var index = name.lastIndexOf(".");
443     if (index > 0 && name.charAt(index-1) != "\\")
444         name = name.substr(index + 1);
445     name = name.replace("_", " ");
446 
447     return name;
448 }
449 
450 function $STRF(name, args, bundle)
451 {
452     try
453     {
454         // xxxHonza: Workaround for #485511
455         if (!bundle)
456             bundle = "strings_firebug";
457 
458         if (typeof bundle == "string")
459             bundle = document.getElementById(bundle);
460 
461         if (bundle)
462             return bundle.getFormattedString(name.replace(' ', '_', "g"), args);
463         else
464             return Firebug.getStringBundle().formatStringFromName(name.replace(' ', '_', "g"), args, args.length);
465     }
466     catch (err)
467     {
468         if (FBTrace.DBG_LOCALE)
469         {
470             FBTrace.sysout("lib.getString: " + name + "\n");
471             FBTrace.sysout("lib.getString FAILS ", err);
472         }
473     }
474 
475     // Use only the label after last dot.
476     var index = name.lastIndexOf(".");
477     if (index > 0)
478         name = name.substr(index + 1);
479 
480     return name;
481 }
482 
483 function $STRP(name, args, index, bundle)
484 {
485     // xxxHonza:
486     // pluralRule from chrome://global/locale/intl.properties for Chinese is 1,
487     // which is wrong, it should be 0.
488 
489     var getPluralForm = PluralForm.get;
490     var getNumForms = PluralForm.numForms;
491 
492     // Get custom plural rule; otherwise the rule from chrome://global/locale/intl.properties
493     // (depends on the current locale) is used.
494     var pluralRule = Firebug.getPluralRule();
495     if (!isNaN(parseInt(pluralRule, 10)))
496         [getPluralForm, getNumForms] = PluralForm.makeGetter(pluralRule);
497 
498     // Index of the argument with plural form (there must be only one arg that needs plural form).
499     if (!index)
500         index = 0;
501 
502     var margs = [];
503     var numForms = getNumForms();
504 
505     // Repeat the args for numForms time(s)
506     for (var i = 0; i < numForms; i++)
507         margs = margs.concat(args);
508 
509     // Get proper plural form from the string (depends on the current Firefox locale).
510     var translatedString = $STRF(name, margs, bundle);
511     if (translatedString.search(";") > 0)
512         return getPluralForm(args[index], translatedString);
513 
514     // translatedString contains no ";", either rule 0 or getString fails
515     return translatedString;
516 }
517 
518 this.$STR = $STR;
519 this.$STRF = $STRF;
520 this.$STRP = $STRP;
521 
522 /*
523  * Use the current value of the attribute as a key to look up the localized value.
524  */
525 this.internationalize = function(element, attr, args)
526 {
527     if (typeof element == "string")
528         element = document.getElementById(element);
529 
530     if (element)
531     {
532         var xulString = element.getAttribute(attr);
533         if (xulString)
534         {
535             var localized = args ? $STRF(xulString, args) : $STR(xulString);
536 
537             // Set localized value of the attribute.
538             element.setAttribute(attr, localized);
539         }
540     }
541     else
542     {
543         if (FBTrace.DBG_LOCALE)
544             FBTrace.sysout("Failed to internationalize element with attr "+attr+' args:'+args);
545     }
546 }
547 
548 // ************************************************************************************************
549 // Visibility
550 
551 this.isVisible = function(elt)
552 {
553     if (isElementXUL(elt))
554     {
555         //FBTrace.sysout("isVisible elt.offsetWidth: "+elt.offsetWidth+" offsetHeight:"+ elt.offsetHeight+" localName:"+ elt.localName+" nameSpace:"+elt.nameSpaceURI+"\n");
556         return (!elt.hidden && !elt.collapsed);
557     }
558     return elt.offsetWidth > 0 || elt.offsetHeight > 0 || elt.localName in invisibleTags || isElementSVG(elt) || isElementMathML(elt);
559 };
560 
561 this.collapse = function(elt, collapsed)
562 {
563     elt.setAttribute("collapsed", collapsed ? "true" : "false");
564 };
565 
566 this.obscure = function(elt, obscured)
567 {
568     if (obscured)
569         this.setClass(elt, "obscured");
570     else
571         this.removeClass(elt, "obscured");
572 };
573 
574 this.hide = function(elt, hidden)
575 {
576     elt.style.visibility = hidden ? "hidden" : "visible";
577 };
578 
579 this.clearNode = function(node)
580 {
581     this.clearDomplate(node);
582     node.innerHTML = "";
583 };
584 
585 this.eraseNode = function(node)
586 {
587     this.clearDomplate(node);
588     while (node.lastChild)
589         node.removeChild(node.lastChild);
590 };
591 
592 this.clearDomplate = function(node)
593 {
594     if (!Firebug.clearDomplate)
595         return;
596 
597     var walker = node.ownerDocument.createTreeWalker(node,
598         Ci.nsIDOMNodeFilter.SHOW_ALL, null, true);
599 
600     while (node)
601     {
602         if (node.repObject)
603             node.repObject = null;
604 
605         if (node.stackTrace)
606             node.stackTrace = null;
607 
608         if (node.checked)
609             node.checked = null;
610 
611         if (node.domObject)
612             node.domObject = null;
613 
614         if (node.toggles)
615             node.toggles = null;
616 
617         if (node.domPanel)
618             node.domPanel = null;
619 
620         node = walker.nextNode();
621     }
622 }
623 
624 // ************************************************************************************************
625 // Window iteration
626 
627 this.iterateWindows = function(win, handler)
628 {
629     if (!win || !win.document)
630         return;
631 
632     handler(win);
633 
634     if (win == top || !win.frames) return; // XXXjjb hack for chromeBug
635 
636     for (var i = 0; i < win.frames.length; ++i)
637     {
638         var subWin = win.frames[i];
639         if (subWin != win)
640             this.iterateWindows(subWin, handler);
641     }
642 };
643 
644 this.getRootWindow = function(win)
645 {
646     for (; win; win = win.parent)
647     {
648         if (!win.parent || win == win.parent || !(win.parent instanceof Window) )
649             return win;
650     }
651     return null;
652 };
653 
654 // ************************************************************************************************
655 // CSS classes
656 
657 var classNameReCache={};
658 this.hasClass = function(node, name)
659 {
660     if (!node || node.nodeType != 1 || !node.className || name == '')
661         return false;
662 
663     if (name.indexOf(" ") != -1)
664     {
665         var classes = name.split(" "), len = classes.length, found=false;
666         for (var i = 0; i < len; i++)
667         {
668             var cls = classes[i].trim();
669             if (cls != "")
670             {
671                 if (this.hasClass(node, cls) == false)
672                     return false;
673                 found = true;
674             }
675         }
676         return found;
677     }
678 
679     var re;
680     if (name.indexOf("-") == -1)
681         re = classNameReCache[name] = classNameReCache[name] || new RegExp('(^|\\s)' + name + '(\\s|$)', "g");
682     else // XXXsroussey don't cache these, they are often setting values. Should be using setUserData/getUserData???
683         re = new RegExp('(^|\\s)' + name + '(\\s|$)', "g")
684     return node.className.search(re) != -1;
685 };
686 
687 this.setClass = function(node, name)
688 {
689     if (!node || node.nodeType != 1 || name == '')
690         return;
691 
692     if (name.indexOf(" ") != -1)
693     {
694         var classes = name.split(" "), len = classes.length;
695         for (var i = 0; i < len; i++)
696         {
697             var cls = classes[i].trim();
698             if (cls != "")
699             {
700                 this.setClass(node, cls);
701             }
702         }
703         return;
704     }
705     if (!this.hasClass(node, name))
706         node.className = node.className.trim() + " " + name;
707 };
708 
709 this.getClassValue = function(node, name)
710 {
711     var re = new RegExp(name+"-([^ ]+)");
712     var m = re.exec(node.className);
713     return m ? m[1] : "";
714 };
715 
716 this.removeClass = function(node, name)
717 {
718     if (!node || node.nodeType != 1 || node.className == '' || name == '')
719         return;
720 
721     if (name.indexOf(" ") != -1)
722     {
723         var classes = name.split(" "), len = classes.length;
724         for (var i = 0; i < len; i++)
725         {
726             var cls = classes[i].trim();
727             if (cls != "")
728             {
729                 if (this.hasClass(node, cls) == false)
730                     this.removeClass(node, cls);
731             }
732         }
733         return;
734     }
735 
736     var re;
737     if (name.indexOf("-") == -1)
738         re = classNameReCache[name] = classNameReCache[name] || new RegExp('(^|\\s)' + name + '(\\s|$)', "g");
739     else // XXXsroussey don't cache these, they are often setting values. Should be using setUserData/getUserData???
740         re = new RegExp('(^|\\s)' + name + '(\\s|$)', "g")
741 
742     node.className = node.className.replace(re, " ");
743 
744 };
745 
746 this.toggleClass = function(elt, name)
747 {
748     if (this.hasClass(elt, name))
749         this.removeClass(elt, name);
750     else
751         this.setClass(elt, name);
752 };
753 
754 this.setClassTimed = function(elt, name, context, timeout)
755 {
756     if (!timeout)
757         timeout = 1300;
758 
759     if (elt.__setClassTimeout)
760         context.clearTimeout(elt.__setClassTimeout);
761     else
762         this.setClass(elt, name);
763 
764     if (!this.isVisible(elt))
765     {
766         if (elt.__invisibleAtSetPoint)
767             elt.__invisibleAtSetPoint--;
768         else
769             elt.__invisibleAtSetPoint = 5;
770     }
771     else
772     {
773         delete elt.__invisibleAtSetPoint;
774     }
775 
776     elt.__setClassTimeout = context.setTimeout(function()
777     {
778         delete elt.__setClassTimeout;
779 
780         if (elt.__invisibleAtSetPoint)
781             FBL.setClassTimed(elt, name, context, timeout);
782         else
783         {
784             delete elt.__invisibleAtSetPoint;
785             FBL.removeClass(elt, name);
786         }
787     }, timeout);
788 };
789 
790 this.cancelClassTimed = function(elt, name, context)
791 {
792     if (elt.__setClassTimeout)
793     {
794         FBL.removeClass(elt, name);
795         context.clearTimeout(elt.__setClassTimeout);
796         delete elt.__setClassTimeout;
797     }
798 };
799 
800 // ************************************************************************************************
801 // DOM queries
802 
803 this.$ = function(id, doc)
804 {
805     if (doc)
806         return doc.getElementById(id);
807     else
808         return document.getElementById(id);
809 };
810 
811 this.getChildByClass = function(node) // ,classname, classname, classname...
812 {
813     for (var i = 1; i < arguments.length; ++i)
814     {
815         var className = arguments[i];
816         var child = node.firstChild;
817         node = null;
818         for (; child; child = child.nextSibling)
819         {
820             if (this.hasClass(child, className))
821             {
822                 node = child;
823                 break;
824             }
825         }
826     }
827 
828     return node;
829 };
830 
831 this.getAncestorByClass = function(node, className)
832 {
833     for (var parent = node; parent; parent = parent.parentNode)
834     {
835         if (this.hasClass(parent, className))
836             return parent;
837     }
838 
839     return null;
840 };
841 
842 /* @Deprecated  Use native Firefox: node.getElementsByClassName(names).item(0) */
843 this.getElementByClass = function(node, className)  // className, className, ...
844 {
845     return FBL.getElementsByClass.apply(this,arguments).item(0);
846 };
847 
848 /* @Deprecated  Use native Firefox: node.getElementsByClassName(names) */
849 this.getElementsByClass = function(node, className)  // className, className, ...
850 {
851     var args = cloneArray(arguments); args.splice(0, 1);
852     var className = args.join(" ");
853     return node.getElementsByClassName(className);
854 };
855 
856 this.getElementsByAttribute = function(node, attrName, attrValue)
857 {
858     function iteratorHelper(node, attrName, attrValue, result)
859     {
860         for (var child = node.firstChild; child; child = child.nextSibling)
861         {
862             if (child.getAttribute(attrName) == attrValue)
863                 result.push(child);
864 
865             iteratorHelper(child, attrName, attrValue, result);
866         }
867     }
868 
869     var result = [];
870     iteratorHelper(node, attrName, attrValue, result);
871     return result;
872 }
873 
874 this.isAncestor = function(node, potentialAncestor)
875 {
876     for (var parent = node; parent; parent = parent.parentNode)
877     {
878         if (parent == potentialAncestor)
879             return true;
880     }
881 
882     return false;
883 };
884 
885 this.getNextElement = function(node)
886 {
887     while (node && node.nodeType != 1)
888         node = node.nextSibling;
889 
890     return node;
891 };
892 
893 this.getPreviousElement = function(node)
894 {
895     while (node && node.nodeType != 1)
896         node = node.previousSibling;
897 
898     return node;
899 };
900 
901 this.getBody = function(doc)
902 {
903     if (doc.body)
904         return doc.body;
905 
906     var body = doc.getElementsByTagName("body")[0];
907     if (body)
908         return body;
909 
910     return doc.documentElement;  // For non-HTML docs
911 };
912 
913 this.findNextDown = function(node, criteria)
914 {
915     if (!node)
916         return null;
917 
918     for (var child = node.firstChild; child; child = child.nextSibling)
919     {
920         if (criteria(child))
921             return child;
922 
923         var next = this.findNextDown(child, criteria);
924         if (next)
925             return next;
926     }
927 };
928 
929 this.findPreviousUp = function(node, criteria)
930 {
931     if (!node)
932         return null;
933 
934     for (var child = node.lastChild; child; child = child.previousSibling)
935     {
936         var next = this.findPreviousUp(child, criteria);
937         if (next)
938             return next;
939 
940         if (criteria(child))
941             return child;
942     }
943 };
944 
945 this.findNext = function(node, criteria, upOnly, maxRoot)
946 {
947     if (!node)
948         return null;
949 
950     if (!upOnly)
951     {
952         var next = this.findNextDown(node, criteria);
953         if (next)
954             return next;
955     }
956 
957     for (var sib = node.nextSibling; sib; sib = sib.nextSibling)
958     {
959         if (criteria(sib))
960             return sib;
961 
962         var next = this.findNextDown(sib, criteria);
963         if (next)
964             return next;
965     }
966 
967     if (node.parentNode && node.parentNode != maxRoot)
968         return this.findNext(node.parentNode, criteria, true);
969 };
970 
971 this.findPrevious = function(node, criteria, downOnly, maxRoot)
972 {
973     if (!node)
974         return null;
975 
976     for (var sib = node.previousSibling; sib; sib = sib.previousSibling)
977     {
978         var prev = this.findPreviousUp(sib, criteria);
979         if (prev)
980             return prev;
981 
982         if (criteria(sib))
983             return sib;
984     }
985 
986     if (!downOnly)
987     {
988         var next = this.findPreviousUp(node, criteria);
989         if (next)
990             return next;
991     }
992 
993     if (node.parentNode && node.parentNode != maxRoot)
994     {
995         if (criteria(node.parentNode))
996             return node.parentNode;
997 
998         return this.findPrevious(node.parentNode, criteria, true);
999     }
1000 };
1001 
1002 this.getNextByClass = function(root, state)
1003 {
1004     function iter(node) { return node.nodeType == 1 && FBL.hasClass(node, state); }
1005     return this.findNext(root, iter);
1006 };
1007 
1008 this.getPreviousByClass = function(root, state)
1009 {
1010     function iter(node) { return node.nodeType == 1 && FBL.hasClass(node, state); }
1011     return this.findPrevious(root, iter);
1012 };
1013 
1014 this.hasChildElements = function(node)
1015 {
1016     if (node.contentDocument) // iframes
1017         return true;
1018 
1019     for (var child = node.firstChild; child; child = child.nextSibling)
1020     {
1021         if (child.nodeType == 1)
1022             return true;
1023     }
1024 
1025     return false;
1026 };
1027 
1028 this.isElement = function(o)
1029 {
1030     try {
1031         return o && o instanceof Element;
1032     }
1033     catch (ex) {
1034         return false;
1035     }
1036 };
1037 
1038 this.isNode = function(o)
1039 {
1040     try {
1041         return o && o instanceof Node;
1042     }
1043     catch (ex) {
1044         return false;
1045     }
1046 };
1047 
1048 this.XW_instanceof = function(obj, type) // Cross Window instanceof; type is local to this window
1049 {
1050     if (obj instanceof type)
1051         return true;  // within-window test
1052 
1053     if (!type)
1054         return false;
1055     if (!obj)
1056         return (type == "undefined");
1057 
1058     // compare strings: obj constructor.name to type.name.
1059     // This is not perfect, we should compare type.prototype to object.__proto__, but mostly code does not change the constructor object.
1060     do
1061     {
1062         if (obj.constructor && obj.constructor.name == type.name)  // then the function that constructed us is the argument
1063             return true;
1064     }
1065     while(obj = obj.__proto__);  // walk the prototype chain.
1066     return false;
1067     // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Property_Inheritance_Revisited/Determining_Instance_Relationships
1068 }
1069 
1070 // ************************************************************************************************
1071 // DOM Modification
1072 
1073 this.setOuterHTML = function(element, html)
1074 {
1075     var doc = element.ownerDocument;
1076     var range = doc.createRange();
1077     range.selectNode(element || doc.documentElement);
1078     try
1079     {
1080         var fragment = range.createContextualFragment(html);
1081         var first = fragment.firstChild;
1082         var last = fragment.lastChild;
1083         element.parentNode.replaceChild(fragment, element);
1084         return [first, last];
1085     } catch (e)
1086     {
1087         return [element,element]
1088     }
1089 };
1090 
1091 this.appendInnerHTML = function(element, html, referenceElement)
1092 {
1093     var doc = element.ownerDocument;
1094     var range = doc.createRange();  // a helper object
1095     range.selectNodeContents(element); // the environment to interpret the html
1096 
1097     var fragment = range.createContextualFragment(html);  // parse
1098     var firstChild = fragment.firstChild;
1099     element.insertBefore(fragment, referenceElement);
1100     return firstChild;
1101 };
1102 
1103 this.insertTextIntoElement = function(element, text)
1104 {
1105     var command = "cmd_insertText";
1106 
1107     var controller = element.controllers.getControllerForCommand(command);
1108     if (!controller || !controller.isCommandEnabled(command))
1109         return;
1110 
1111     var params = this.CCIN("@mozilla.org/embedcomp/command-params;1", "nsICommandParams");
1112     params.setStringValue("state_data", text);
1113 
1114     controller = this.QI(controller, Ci.nsICommandController);
1115     controller.doCommandWithParams(command, params);
1116 };
1117 
1118 
1119 // ************************************************************************************************
1120 // XPath
1121 
1122 /**
1123  * Gets an XPath for an element which describes its hierarchical location.
1124  */
1125 this.getElementXPath = function(element)
1126 {
1127     if (element && element.id)
1128         return '//*[@id="' + element.id + '"]';
1129     else
1130         return this.getElementTreeXPath(element);
1131 };
1132 
1133 this.getElementTreeXPath = function(element)
1134 {
1135     var paths = [];
1136 
1137     for (; element && element.nodeType == 1; element = element.parentNode)
1138     {
1139         var index = 0;
1140         for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling)
1141         {
1142             if (sibling.localName == element.localName)
1143                 ++index;
1144         }
1145 
1146         var tagName = element.localName.toLowerCase();
1147         var pathIndex = (index ? "[" + (index+1) + "]" : "");
1148         paths.splice(0, 0, tagName + pathIndex);
1149     }
1150 
1151     return paths.length ? "/" + paths.join("/") : null;
1152 };
1153 
1154 this.cssToXPath = function(rule)
1155 {
1156     var regElement = /^([#.]?)([a-z0-9\\*_-]*)((\|)([a-z0-9\\*_-]*))?/i;
1157     var regAttr1 = /^\[([^\]]*)\]/i;
1158     var regAttr2 = /^\[\s*([^~=\s]+)\s*(~?=)\s*"([^"]+)"\s*\]/i;
1159     var regPseudo = /^:([a-z_-])+/i;
1160     var regCombinator = /^(\s*[>+\s])?/i;
1161     var regComma = /^\s*,/i;
1162 
1163     var index = 1;
1164     var parts = ["//", "*"];
1165     var lastRule = null;
1166 
1167     while (rule.length && rule != lastRule)
1168     {
1169         lastRule = rule;
1170 
1171         // Trim leading whitespace
1172         rule = this.trim(rule);
1173         if (!rule.length)
1174             break;
1175 
1176         // Match the element identifier
1177         var m = regElement.exec(rule);
1178         if (m)
1179         {
1180             if (!m[1])
1181             {
1182                 // XXXjoe Namespace ignored for now
1183                 if (m[5])
1184                     parts[index] = m[5];
1185                 else
1186                     parts[index] = m[2];
1187             }
1188             else if (m[1] == '#')
1189                 parts.push("[@id='" + m[2] + "']");
1190             else if (m[1] == '.')
1191                 parts.push("[contains(@class, '" + m[2] + "')]");
1192 
1193             rule = rule.substr(m[0].length);
1194         }
1195 
1196         // Match attribute selectors
1197         m = regAttr2.exec(rule);
1198         if (m)
1199         {
1200             if (m[2] == "~=")
1201                 parts.push("[contains(@" + m[1] + ", '" + m[3] + "')]");
1202             else
1203                 parts.push("[@" + m[1] + "='" + m[3] + "']");
1204 
1205             rule = rule.substr(m[0].length);
1206         }
1207         else
1208         {
1209             m = regAttr1.exec(rule);
1210             if (m)
1211             {
1212                 parts.push("[@" + m[1] + "]");
1213                 rule = rule.substr(m[0].length);
1214             }
1215         }
1216 
1217         // Skip over pseudo-classes and pseudo-elements, which are of no use to us
1218         m = regPseudo.exec(rule);
1219         while (m)
1220         {
1221             rule = rule.substr(m[0].length);
1222             m = regPseudo.exec(rule);
1223         }
1224 
1225         // Match combinators
1226         m = regCombinator.exec(rule);
1227         if (m && m[0].length)
1228         {
1229             if (m[0].indexOf(">") != -1)
1230                 parts.push("/");
1231             else if (m[0].indexOf("+") != -1)
1232                 parts.push("/following-sibling::");
1233             else
1234                 parts.push("//");
1235 
1236             index = parts.length;
1237             parts.push("*");
1238             rule = rule.substr(m[0].length);
1239         }
1240 
1241         m = regComma.exec(rule);
1242         if (m)
1243         {
1244             parts.push(" | ", "//", "*");
1245             index = parts.length-1;
1246             rule = rule.substr(m[0].length);
1247         }
1248     }
1249 
1250     var xpath = parts.join("");
1251     return xpath;
1252 };
1253 
1254 this.getElementsBySelector = function(doc, css)
1255 {
1256     var xpath = this.cssToXPath(css);
1257     return this.getElementsByXPath(doc, xpath);
1258 };
1259 
1260 this.getElementsByXPath = function(doc, xpath)
1261 {
1262     var nodes = [];
1263 
1264     try {
1265         var result = doc.evaluate(xpath, doc, null, XPathResult.ANY_TYPE, null);
1266         for (var item = result.iterateNext(); item; item = result.iterateNext())
1267             nodes.push(item);
1268     }
1269     catch (exc)
1270     {
1271         // Invalid xpath expressions make their way here sometimes.  If that happens,
1272         // we still want to return an empty set without an exception.
1273     }
1274 
1275     return nodes;
1276 };
1277 
1278 this.getRuleMatchingElements = function(rule, doc)
1279 {
1280     var css = rule.selectorText;
1281     var xpath = this.cssToXPath(css);
1282     return this.getElementsByXPath(doc, xpath);
1283 };
1284 
1285 // ************************************************************************************************
1286 // Clipboard
1287 
1288 this.copyToClipboard = function(string)
1289 {
1290     var clipboard = this.CCSV("@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
1291     clipboard.copyString(string);
1292 };
1293 
1294 // ************************************************************************************************
1295 // Graphics
1296 
1297 this.getClientOffset = function(elt)
1298 {
1299     function addOffset(elt, coords, view)
1300     {
1301         var p = elt.offsetParent;
1302 
1303         var style = view.getComputedStyle(elt, "");
1304 
1305         if (elt.offsetLeft)
1306             coords.x += elt.offsetLeft + parseInt(style.borderLeftWidth);
1307         if (elt.offsetTop)
1308             coords.y += elt.offsetTop + parseInt(style.borderTopWidth);
1309 
1310         if (p)
1311         {
1312             if (p.nodeType == 1)
1313                 addOffset(p, coords, view);
1314         }
1315         else if (elt.ownerDocument.defaultView.frameElement)
1316             addOffset(elt.ownerDocument.defaultView.frameElement, coords, elt.ownerDocument.defaultView);
1317     }
1318 
1319     var coords = {x: 0, y: 0};
1320     if (elt)
1321     {
1322         var view = elt.ownerDocument.defaultView;
1323         addOffset(elt, coords, view);
1324     }
1325 
1326     return coords;
1327 };
1328 
1329 this.getLTRBWH = function(elt)
1330 {
1331     var bcrect,
1332         dims = {"left": 0, "top": 0, "right": 0, "bottom": 0, "width": 0, "height": 0};
1333 
1334     if (elt)
1335     {
1336         bcrect = elt.getBoundingClientRect();
1337         dims.left = bcrect.left;
1338         dims.top = bcrect.top;
1339         dims.right = bcrect.right;
1340         dims.bottom = bcrect.bottom;
1341 
1342         if(bcrect.width)
1343         {
1344             dims.width = bcrect.width;
1345             dims.height = bcrect.height;
1346         }
1347         else
1348         {
1349             dims.width = dims.right - dims.left;
1350             dims.height = dims.bottom - dims.top;
1351         }
1352     }
1353     return dims;
1354 };
1355 
1356 this.applyBodyOffsets = function(elt, clientRect)
1357 {
1358     var od = elt.ownerDocument;
1359     if (!od.body)
1360         return clientRect;
1361 
1362     var style = od.defaultView.getComputedStyle(od.body, null);
1363 
1364     var pos = style.getPropertyValue('position');
1365     if(pos === 'absolute' || pos === 'relative')
1366     {
1367         var borderLeft = parseInt(style.getPropertyValue('border-left-width').replace('px', ''),10) || 0;
1368         var borderTop = parseInt(style.getPropertyValue('border-top-width').replace('px', ''),10) || 0;
1369         var paddingLeft = parseInt(style.getPropertyValue('padding-left').replace('px', ''),10) || 0;
1370         var paddingTop = parseInt(style.getPropertyValue('padding-top').replace('px', ''),10) || 0;
1371         var marginLeft = parseInt(style.getPropertyValue('margin-left').replace('px', ''),10) || 0;
1372         var marginTop = parseInt(style.getPropertyValue('margin-top').replace('px', ''),10) || 0;
1373 
1374         var offsetX = borderLeft + paddingLeft + marginLeft;
1375         var offsetY = borderTop + paddingTop + marginTop;
1376 
1377         clientRect.left -= offsetX;
1378         clientRect.top -= offsetY;
1379         clientRect.right -= offsetX;
1380         clientRect.bottom -= offsetY;
1381     }
1382 
1383     return clientRect;
1384 };
1385 
1386 this.getOffsetSize = function(elt)
1387 {
1388     return {width: elt.offsetWidth, height: elt.offsetHeight};
1389 };
1390 
1391 this.getOverflowParent = function(element)
1392 {
1393     for (var scrollParent = element.parentNode; scrollParent; scrollParent = scrollParent.offsetParent)
1394     {
1395         if (scrollParent.scrollHeight > scrollParent.offsetHeight)
1396             return scrollParent;
1397     }
1398 };
1399 
1400 this.isScrolledToBottom = function(element)
1401 {
1402     var onBottom = (element.scrollTop + element.offsetHeight) == element.scrollHeight;
1403     if (FBTrace.DBG_CONSOLE)
1404         FBTrace.sysout("isScrolledToBottom offsetHeight: " + element.offsetHeight +
1405             ", scrollTop: " + element.scrollTop + ", scrollHeight: " + element.scrollHeight +
1406             ", onBottom: " + onBottom);
1407     return onBottom;
1408 };
1409 
1410 this.scrollToBottom = function(element)
1411 {
1412     element.scrollTop = element.scrollHeight;
1413 
1414     if (FBTrace.DBG_CONSOLE)
1415     {
1416         FBTrace.sysout("scrollToBottom reset scrollTop "+element.scrollTop+" = "+element.scrollHeight);
1417         if (element.scrollHeight == element.offsetHeight)
1418             FBTrace.sysout("scrollToBottom attempt to scroll non-scrollable element "+element, element);
1419     }
1420 
1421     return (element.scrollTop == element.scrollHeight);
1422 };
1423 
1424 this.move = function(element, x, y)
1425 {
1426     element.style.left = x + "px";
1427     element.style.top = y + "px";
1428 };
1429 
1430 this.resize = function(element, w, h)
1431 {
1432     element.style.width = w + "px";
1433     element.style.height = h + "px";
1434 };
1435 
1436 this.linesIntoCenterView = function(element, scrollBox)  // {before: int, after: int}
1437 {
1438     if (!scrollBox)
1439         scrollBox = this.getOverflowParent(element);
1440 
1441     if (!scrollBox)
1442         return;
1443 
1444     var offset = this.getClientOffset(element);
1445 
1446     var topSpace = offset.y - scrollBox.scrollTop;
1447     var bottomSpace = (scrollBox.scrollTop + scrollBox.clientHeight)
1448             - (offset.y + element.offsetHeight);
1449 
1450     if (topSpace < 0 || bottomSpace < 0)
1451     {
1452         var split = (scrollBox.clientHeight/2);
1453         var centerY = offset.y - split;
1454         scrollBox.scrollTop = centerY;
1455         topSpace = split;
1456         bottomSpace = split -  element.offsetHeight;
1457     }
1458 
1459     return {before: Math.round((topSpace/element.offsetHeight) + 0.5),
1460             after: Math.round((bottomSpace/element.offsetHeight) + 0.5) }
1461 };
1462 
1463 this.scrollIntoCenterView = function(element, scrollBox, notX, notY)
1464 {
1465     if (!element)
1466         return;
1467 
1468     if (!scrollBox)
1469         scrollBox = this.getOverflowParent(element);
1470 
1471     if (!scrollBox)
1472         return;
1473 
1474     var offset = this.getClientOffset(element);
1475 
1476     if (!notY)
1477     {
1478         var topSpace = offset.y - scrollBox.scrollTop;
1479         var bottomSpace = (scrollBox.scrollTop + scrollBox.clientHeight)
1480             - (offset.y + element.offsetHeight);
1481 
1482         if (topSpace < 0 || bottomSpace < 0)
1483         {
1484             var centerY = offset.y - (scrollBox.clientHeight/2);
1485             scrollBox.scrollTop = centerY;
1486         }
1487     }
1488 
1489     if (!notX)
1490     {
1491         var leftSpace = offset.x - scrollBox.scrollLeft;
1492         var rightSpace = (scrollBox.scrollLeft + scrollBox.clientWidth)
1493             - (offset.x + element.clientWidth);
1494 
1495         if (leftSpace < 0 || rightSpace < 0)
1496         {
1497             var centerX = offset.x - (scrollBox.clientWidth/2);
1498             scrollBox.scrollLeft = centerX;
1499         }
1500     }
1501     if (FBTrace.DBG_SOURCEFILES)
1502         FBTrace.sysout("lib.scrollIntoCenterView ","Element:"+element.innerHTML);
1503 };
1504 
1505 // ************************************************************************************************
1506 // CSS
1507 
1508 var cssKeywordMap = null;
1509 var cssPropNames = null;
1510 var cssColorNames = null;
1511 var imageRules = null;
1512 
1513 this.getCSSKeywordsByProperty = function(propName)
1514 {
1515     if (!cssKeywordMap)
1516     {
1517         cssKeywordMap = {};
1518 
1519         for (var name in this.cssInfo)
1520         {
1521             var list = [];
1522 
1523             var types = this.cssInfo[name];
1524             for (var i = 0; i < types.length; ++i)
1525             {
1526                 var keywords = this.cssKeywords[types[i]];
1527                 if (keywords)
1528                     list.push.apply(list, keywords);
1529             }
1530 
1531             cssKeywordMap[name] = list;
1532         }
1533     }
1534 
1535     return propName in cssKeywordMap ? cssKeywordMap[propName] : [];
1536 };
1537 
1538 this.getCSSPropertyNames = function()
1539 {
1540     if (!cssPropNames)
1541     {
1542         cssPropNames = [];
1543 
1544         for (var name in this.cssInfo)
1545             cssPropNames.push(name);
1546     }
1547 
1548     return cssPropNames;
1549 };
1550 
1551 this.isColorKeyword = function(keyword)
1552 {
1553     if (keyword == "transparent")
1554         return false;
1555 
1556     if (!cssColorNames)
1557     {
1558         cssColorNames = [];
1559 
1560         var colors = this.cssKeywords["color"];
1561         for (var i = 0; i < colors.length; ++i)
1562             cssColorNames.push(colors[i].toLowerCase());
1563 
1564         var systemColors = this.cssKeywords["systemColor"];
1565         for (var i = 0; i < systemColors.length; ++i)
1566             cssColorNames.push(systemColors[i].toLowerCase());
1567     }
1568 
1569     return cssColorNames.indexOf(keyword.toLowerCase()) != -1;
1570 };
1571 
1572 this.isImageRule = function(rule)
1573 {
1574     if (!imageRules)
1575     {
1576         imageRules = [];
1577 
1578         for (var i in this.cssInfo)
1579         {
1580             var r = i.toLowerCase();
1581             var suffix = "image";
1582             if (r.match(suffix + "$") == suffix || r == "background")
1583                 imageRules.push(r);
1584         }
1585     }
1586 
1587     return imageRules.indexOf(rule.toLowerCase()) != -1;
1588 };
1589 
1590 this.copyTextStyles = function(fromNode, toNode, style)
1591 {
1592     var view = fromNode.ownerDocument.defaultView;
1593     if (view)
1594     {
1595         if (!style)
1596             style = view.getComputedStyle(fromNode, "");
1597 
1598         toNode.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
1599         toNode.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
1600         toNode.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
1601         toNode.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
1602 
1603         return style;
1604     }
1605 };
1606 
1607 this.copyBoxStyles = function(fromNode, toNode, style)
1608 {
1609     var view = fromNode.ownerDocument.defaultView;
1610     if (view)
1611     {
1612         if (!style)
1613             style = view.getComputedStyle(fromNode, "");
1614 
1615         toNode.style.marginTop = style.getPropertyCSSValue("margin-top").cssText;
1616         toNode.style.marginRight = style.getPropertyCSSValue("margin-right").cssText;
1617         toNode.style.marginBottom = style.getPropertyCSSValue("margin-bottom").cssText;
1618         toNode.style.marginLeft = style.getPropertyCSSValue("margin-left").cssText;
1619         toNode.style.borderTopWidth = style.getPropertyCSSValue("border-top-width").cssText;
1620         toNode.style.borderRightWidth = style.getPropertyCSSValue("border-right-width").cssText;
1621         toNode.style.borderBottomWidth = style.getPropertyCSSValue("border-bottom-width").cssText;
1622         toNode.style.borderLeftWidth = style.getPropertyCSSValue("border-left-width").cssText;
1623 
1624         return style;
1625     }
1626 };
1627 
1628 this.readBoxStyles = function(style)
1629 {
1630     const styleNames = {
1631         "margin-top": "marginTop", "margin-right": "marginRight",
1632         "margin-left": "marginLeft", "margin-bottom": "marginBottom",
1633         "border-top-width": "borderTop", "border-right-width": "borderRight",
1634         "border-left-width": "borderLeft", "border-bottom-width": "borderBottom",
1635         "padding-top": "paddingTop", "padding-right": "paddingRight",
1636         "padding-left": "paddingLeft", "padding-bottom": "paddingBottom",
1637         "z-index": "zIndex",
1638     };
1639 
1640     var styles = {};
1641     for (var styleName in styleNames)
1642         styles[styleNames[styleName]] = parseInt(style.getPropertyCSSValue(styleName).cssText) || 0;
1643     if (FBTrace.DBG_INSPECT)
1644         FBTrace.sysout("readBoxStyles ", styles);
1645     return styles;
1646 };
1647 
1648 this.getBoxFromStyles = function(style, element)
1649 {
1650     var args = this.readBoxStyles(style);
1651     args.width = element.offsetWidth
1652         - (args.paddingLeft+args.paddingRight+args.borderLeft+args.borderRight);
1653     args.height = element.offsetHeight
1654         - (args.paddingTop+args.paddingBottom+args.borderTop+args.borderBottom);
1655     return args;
1656 };
1657 
1658 this.getElementCSSSelector = function(element)
1659 {
1660     var label = element.localName.toLowerCase();
1661     if (element.id)
1662         label += "#" + element.id;
1663     if (element.hasAttribute("class"))
1664         label += "." + element.getAttribute("class").split(" ")[0];
1665 
1666     return label;
1667 };
1668 
1669 this.getURLForStyleSheet= function(styleSheet)
1670 {
1671     //http://www.w3.org/TR/DOM-Level-2-Style/stylesheets.html#StyleSheets-StyleSheet. For inline style sheets, the value of this attribute is null.
1672     return (styleSheet.href ? styleSheet.href : styleSheet.ownerNode.ownerDocument.URL);
1673 };
1674 
1675 this.getDocumentForStyleSheet = function(styleSheet)
1676 {
1677     while (styleSheet.parentStyleSheet && !styleSheet.ownerNode)
1678     {
1679         styleSheet = styleSheet.parentStyleSheet;
1680     }
1681     if (styleSheet.ownerNode)
1682       return styleSheet.ownerNode.ownerDocument;
1683 };
1684 
1685 /**
1686  * Retrieves the instance number for a given style sheet. The instance number
1687  * is sheet's index within the set of all other sheets whose URL is the same.
1688  */
1689 this.getInstanceForStyleSheet = function(styleSheet, ownerDocument)
1690 {
1691     // System URLs are always unique (or at least we are making this assumption)
1692     if (FBL.isSystemStyleSheet(styleSheet))
1693         return 0;
1694 
1695     // ownerDocument is an optional hint for performance
1696     if (FBTrace.DBG_CSS) FBTrace.sysout("getInstanceForStyleSheet: " + styleSheet.href + " " + styleSheet.media.mediaText + " " + (styleSheet.ownerNode && FBL.getElementXPath(styleSheet.ownerNode)), ownerDocument);
1697     ownerDocument = ownerDocument || FBL.getDocumentForStyleSheet(styleSheet);
1698 
1699     var ret = 0,
1700         styleSheets = ownerDocument.styleSheets,
1701         href = styleSheet.href;
1702     for (var i = 0; i < styleSheets.length; i++)
1703     {
1704         var curSheet = styleSheets[i];
1705         if (FBTrace.DBG_CSS) FBTrace.sysout("getInstanceForStyleSheet: compare href " + i + " " + curSheet.href + " " + curSheet.media.mediaText + " " + (curSheet.ownerNode && FBL.getElementXPath(curSheet.ownerNode)));
1706         if (curSheet == styleSheet)
1707             break;
1708         if (curSheet.href == href)
1709             ret++;
1710     }
1711     return ret;
1712 };
1713 
1714 // ************************************************************************************************
1715 // HTML and XML Serialization
1716 
1717 
1718 var getElementType = this.getElementType = function(node)
1719 {
1720     if (isElementXUL(node))
1721         return 'xul';
1722     else if (isElementSVG(node))
1723         return 'svg';
1724     else if (isElementMathML(node))
1725         return 'mathml';
1726     else if (isElementXHTML(node))
1727         return 'xhtml';
1728     else if (isElementHTML(node))
1729         return 'html';
1730 }
1731 
1732 var isElementHTML = this.isElementHTML = function(node)
1733 {
1734     return node.nodeName == node.nodeName.toUpperCase();
1735 }
1736 
1737 var isElementXHTML = this.isElementXHTML = function(node)
1738 {
1739     return node.nodeName == node.nodeName.toLowerCase();
1740 }
1741 
1742 var isElementMathML = this.isElementMathML = function(node)
1743 {
1744     return node.namespaceURI == 'http://www.w3.org/1998/Math/MathML';
1745 }
1746 
1747 var isElementSVG = this.isElementSVG = function(node)
1748 {
1749     return node.namespaceURI == 'http://www.w3.org/2000/svg';
1750 }
1751 
1752 var isElementXUL = this.isElementXUL = function(node)
1753 {
1754     return node instanceof XULElement;
1755 }
1756 
1757 this.isSelfClosing = function(element)
1758 {
1759     if (isElementSVG(element) || isElementMathML(element))
1760         return true;
1761     var tag = element.localName.toLowerCase();
1762     return (this.selfClosingTags.hasOwnProperty(tag));
1763 };
1764 
1765 this.getElementHTML = function(element)
1766 {
1767     var self=this;
1768     function toHTML(elt)
1769     {
1770         if (elt.nodeType == Node.ELEMENT_NODE)
1771         {
1772             if (unwrapObject(elt).firebugIgnore)
1773                 return;
1774 
1775             html.push('<', elt.nodeName.toLowerCase());
1776 
1777             for (var i = 0; i < elt.attributes.length; ++i)
1778             {
1779                 var attr = elt.attributes[i];
1780 
1781                 // Hide attributes set by Firebug
1782                 if (attr.localName.indexOf("firebug-") == 0)
1783                     continue;
1784 
1785                 // MathML
1786                 if (attr.localName.indexOf("-moz-math") == 0)
1787                 {
1788                     // just hide for now
1789                     continue;
1790                 }
1791 
1792                 html.push(' ', attr.nodeName, '="', escapeForElementAttribute(attr.nodeValue),'"');
1793             }
1794 
1795             if (elt.firstChild)
1796             {
1797                 html.push('>');
1798 
1799                 var pureText=true;
1800                 for (var child = element.firstChild; child; child = child.nextSibling)
1801                     pureText=pureText && (child.nodeType == Node.TEXT_NODE);
1802 
1803                 if (pureText)
1804                     html.push(escapeForHtmlEditor(elt.textContent));
1805                 else {
1806                     for (var child = elt.firstChild; child; child = child.nextSibling)
1807                         toHTML(child);
1808                 }
1809 
1810                 html.push('</', elt.nodeName.toLowerCase(), '>');
1811             }
1812             else if (isElementSVG(elt) || isElementMathML(elt))
1813             {
1814                 html.push('/>');
1815             }
1816             else if (self.isSelfClosing(elt))
1817             {
1818                 html.push((isElementXHTML(elt))?'/>':'>');
1819             }
1820             else
1821             {
1822                 html.push('></', elt.nodeName.toLowerCase(), '>');
1823             }
1824         }
1825         else if (elt.nodeType == Node.TEXT_NODE)
1826             html.push(escapeForTextNode(elt.textContent));
1827         else if (elt.nodeType == Node.CDATA_SECTION_NODE)
1828             html.push('<![CDATA[', elt.nodeValue, ']]>');
1829         else if (elt.nodeType == Node.COMMENT_NODE)
1830             html.push('<!--', elt.nodeValue, '-->');
1831     }
1832 
1833     var html = [];
1834     toHTML(element);
1835     return html.join("");
1836 };
1837 
1838 this.getElementXML = function(element)
1839 {
1840     function toXML(elt)
1841     {
1842         if (elt.nodeType == Node.ELEMENT_NODE)
1843         {
1844             if (unwrapObject(elt).firebugIgnore)
1845                 return;
1846 
1847             xml.push('<', elt.nodeName.toLowerCase());
1848 
1849             for (var i = 0; i < elt.attributes.length; ++i)
1850             {
1851                 var attr = elt.attributes[i];
1852 
1853                 // Hide attributes set by Firebug
1854                 if (attr.localName.indexOf("firebug-") == 0)
1855                     continue;
1856 
1857                 // MathML
1858                 if (attr.localName.indexOf("-moz-math") == 0)
1859                 {
1860                     // just hide for now
1861                     continue;
1862                 }
1863 
1864                 xml.push(' ', attr.nodeName, '="', escapeForElementAttribute(attr.nodeValue),'"');
1865             }
1866 
1867             if (elt.firstChild)
1868             {
1869                 xml.push('>');
1870 
1871                 for (var child = elt.firstChild; child; child = child.nextSibling)
1872                     toXML(child);
1873 
1874                 xml.push('</', elt.nodeName.toLowerCase(), '>');
1875             }
1876             else
1877                 xml.push('/>');
1878         }
1879         else if (elt.nodeType == Node.TEXT_NODE)
1880             xml.push(elt.nodeValue);
1881         else if (elt.nodeType == Node.CDATA_SECTION_NODE)
1882             xml.push('<![CDATA[', elt.nodeValue, ']]>');
1883         else if (elt.nodeType == Node.COMMENT_NODE)
1884             xml.push('<!--', elt.nodeValue, '-->');
1885     }
1886 
1887     var xml = [];
1888     toXML(element);
1889     return xml.join("");
1890 };
1891 
1892 // ************************************************************************************************
1893 // Whitespace and Entity conversions
1894 
1895 var entityConversionLists = this.entityConversionLists = {
1896     normal : {
1897         whitespace : {
1898             '\t' : '\u200c\u2192',
1899             '\n' : '\u200c\u00b6',
1900             '\r' : '\u200c\u00ac',
1901             ' '  : '\u200c\u00b7'
1902         }
1903     },
1904     reverse : {
1905         whitespace : {
1906             '	' : '\t',
1907             '
' : '\n',
1908             '\u200c\u2192' : '\t',
1909             '\u200c\u00b6' : '\n',
1910             '\u200c\u00ac' : '\r',
1911             '\u200c\u00b7' : ' '
1912         }
1913     }
1914 };
1915 
1916 var normal = entityConversionLists.normal,
1917     reverse = entityConversionLists.reverse;
1918 
1919 function addEntityMapToList(ccode, entity)
1920 {
1921     var lists = Array.slice(arguments, 2),
1922         len = lists.length,
1923         ch = String.fromCharCode(ccode);
1924     for (var i = 0; i < len; i++)
1925     {
1926         var list = lists[i];
1927         normal[list]=normal[list] || {};
1928         normal[list][ch] = '&' + entity + ';';
1929         reverse[list]=reverse[list] || {};
1930         reverse[list]['&' + entity + ';'] = ch;
1931     }
1932 }
1933 
1934 var e = addEntityMapToList,
1935     white = 'whitespace',
1936     text = 'text',
1937     attr = 'attributes',
1938     css = 'css',
1939     editor = 'editor';
1940 
1941 e(0x0022, 'quot', attr, css);
1942 e(0x0026, 'amp', attr, text, css);
1943 e(0x0027, 'apos', css);
1944 e(0x003c, 'lt', attr, text, css);
1945 e(0x003e, 'gt', attr, text, css);
1946 e(0xa9, 'copy', text, editor);
1947 e(0xae, 'reg', text, editor);
1948 e(0x2122, 'trade', text, editor);
1949 
1950 // See http://en.wikipedia.org/wiki/Dash
1951 e(0x2012, '#8210', attr, text, editor); // figure dash
1952 e(0x2013, 'ndash', attr, text, editor); // en dash
1953 e(0x2014, 'mdash', attr, text, editor); // em dash
1954 e(0x2015, '#8213', attr, text, editor); // horizontal bar
1955 
1956 e(0x00a0, 'nbsp', attr, text, white, editor);
1957 e(0x2002, 'ensp', attr, text, white, editor);
1958 e(0x2003, 'emsp', attr, text, white, editor);
1959 e(0x2009, 'thinsp', attr, text, white, editor);
1960 e(0x200c, 'zwnj', attr, text, white, editor);
1961 e(0x200d, 'zwj', attr, text, white, editor);
1962 e(0x200e, 'lrm', attr, text, white, editor);
1963 e(0x200f, 'rlm', attr, text, white, editor);
1964 e(0x200b, '#8203', attr, text, white, editor); // zero-width space (ZWSP)
1965 
1966 //************************************************************************************************
1967 // Entity escaping
1968 
1969 var entityConversionRegexes = {
1970         normal : {},
1971         reverse : {}
1972     };
1973 
1974 var escapeEntitiesRegEx = {
1975     normal : function(list)
1976     {
1977         var chars = [];
1978         for ( var ch in list)
1979         {
1980             chars.push(ch);
1981         }
1982         return new RegExp('([' + chars.join('') + '])', 'gm');
1983     },
1984     reverse : function(list)
1985     {
1986         var chars = [];
1987         for ( var ch in list)
1988         {
1989             chars.push(ch);
1990         }
1991         return new RegExp('(' + chars.join('|') + ')', 'gm');
1992     }
1993 };
1994 
1995 function getEscapeRegexp(direction, lists)
1996 {
1997     var name = '', re;
1998     var groups = [].concat(lists);
1999     for (i = 0; i < groups.length; i++)
2000     {
2001         name += groups[i].group;
2002     }
2003     re = entityConversionRegexes[direction][name];
2004     if (!re)
2005     {
2006         var list = {};
2007         if (groups.length > 1)
2008         {
2009             for ( var i = 0; i < groups.length; i++)
2010             {
2011                 var aList = entityConversionLists[direction][groups[i].group];
2012                 for ( var item in aList)
2013                     list[item] = aList[item];
2014             }
2015         } else if (groups.length==1)
2016         {
2017             list = entityConversionLists[direction][groups[0].group]; // faster for special case
2018         } else {
2019             list = {}; // perhaps should print out an error here?
2020         }
2021         re = entityConversionRegexes[direction][name] = escapeEntitiesRegEx[direction](list);
2022     }
2023     return re;
2024 }
2025 
2026 function createSimpleEscape(name, direction)
2027 {
2028     return function(value)
2029     {
2030         var list = entityConversionLists[direction][name];
2031         return String(value).replace(
2032                 getEscapeRegexp(direction, {
2033                     group : name,
2034                     list : list
2035                 }),
2036                 function(ch)
2037                 {
2038                     return list[ch];
2039                 }
2040                );
2041     }
2042 }
2043 
2044 function escapeGroupsForEntities(str, lists)
2045 {
2046     lists = [].concat(lists);
2047     var re = getEscapeRegexp('normal', lists),
2048         split = String(str).split(re),
2049         len = split.length,
2050         results = [],
2051         cur, r, i, ri = 0, l, list, last = '';
2052     if (!len)
2053         return [ {
2054             str : String(str),
2055             group : '',
2056             name : ''
2057         } ];
2058     for (i = 0; i < len; i++)
2059     {
2060         cur = split[i];
2061         if (cur == '')
2062             continue;
2063         for (l = 0; l < lists.length; l++)
2064         {
2065             list = lists[l];
2066             r = entityConversionLists.normal[list.group][cur];
2067             // if (cur == ' ' && list.group == 'whitespace' && last == ' ') // only show for runs of more than one space
2068             //     r = ' ';
2069             if (r)
2070             {
2071                 results[ri] = {
2072                     'str' : r,
2073                     'class' : list['class'],
2074                     'extra' : list.extra[cur] ? list['class']
2075                             + list.extra[cur] : ''
2076                 };
2077                 break;
2078             }
2079         }
2080         // last=cur;
2081         if (!r)
2082             results[ri] = {
2083                 'str' : cur,
2084                 'class' : '',
2085                 'extra' : ''
2086             };
2087         ri++;
2088     }
2089     return results;
2090 }
2091 
2092 this.escapeGroupsForEntities = escapeGroupsForEntities;
2093 
2094 
2095 function unescapeEntities(str, lists)
2096 {
2097     var re = getEscapeRegexp('reverse', lists),
2098         split = String(str).split(re),
2099         len = split.length,
2100         results = [],
2101         cur, r, i, ri = 0, l, list;
2102     if (!len)
2103         return str;
2104     lists = [].concat(lists);
2105     for (i = 0; i < len; i++)
2106     {
2107         cur = split[i];
2108         if (cur == '')
2109             continue;
2110         for (l = 0; l < lists.length; l++)
2111         {
2112             list = lists[l];
2113             r = entityConversionLists.reverse[list.group][cur];
2114             if (r)
2115             {
2116                 results[ri] = r;
2117                 break;
2118             }
2119         }
2120         if (!r)
2121             results[ri] = cur;
2122         ri++;
2123     }
2124     return results.join('') || '';
2125 }
2126 
2127 
2128 // ************************************************************************************************
2129 // String escaping
2130 
2131 var escapeForTextNode = this.escapeForTextNode = createSimpleEscape('text', 'normal');
2132 var escapeForHtmlEditor = this.escapeForHtmlEditor = createSimpleEscape('editor', 'normal');
2133 var escapeForElementAttribute = this.escapeForElementAttribute = createSimpleEscape('attributes', 'normal');
2134 var escapeForCss = this.escapeForCss = createSimpleEscape('css', 'normal');
2135 
2136 // deprecated compatibility functions
2137 this.deprecateEscapeHTML = createSimpleEscape('text', 'normal');
2138 this.deprecatedUnescapeHTML = createSimpleEscape('text', 'reverse');
2139 this.escapeHTML = deprecated("use appropriate escapeFor... function", this.deprecateEscapeHTML);
2140 this.unescapeHTML = deprecated("use appropriate unescapeFor... function", this.deprecatedUnescapeHTML);
2141 
2142 var escapeForSourceLine = this.escapeForSourceLine = createSimpleEscape('text', 'normal'