2ee4e5b495d7356701e974e93f98e85bf7dff805
[WebKit-https.git] / WebCore / page / inspector / SourceFrame.js
1 /*
2  * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.SourceFrame = function(element, addBreakpointDelegate)
27 {
28     this.messages = [];
29     this.breakpoints = [];
30
31     this.addBreakpointDelegate = addBreakpointDelegate;
32
33     this.element = element || document.createElement("iframe");
34     this.element.addStyleClass("source-view-frame");
35     this.element.setAttribute("viewsource", "true");
36
37     this.element.addEventListener("load", this._loaded.bind(this), false);
38 }
39
40 WebInspector.SourceFrame.prototype = {
41     get executionLine()
42     {
43         return this._executionLine;
44     },
45
46     set executionLine(x)
47     {
48         if (this._executionLine === x)
49             return;
50
51         var previousLine = this._executionLine;
52         this._executionLine = x;
53
54         this._updateExecutionLine(previousLine);
55     },
56
57     get autoSizesToFitContentHeight()
58     {
59         return this._autoSizesToFitContentHeight;
60     },
61
62     set autoSizesToFitContentHeight(x)
63     {
64         if (this._autoSizesToFitContentHeight === x)
65             return;
66
67         this._autoSizesToFitContentHeight = x;
68
69         if (this._autoSizesToFitContentHeight) {
70             this._windowResizeListener = this._windowResized.bind(this);
71             window.addEventListener("resize", this._windowResizeListener, false);
72             this.sizeToFitContentHeight();
73         } else {
74             this.element.style.removeProperty("height");
75             if (this.element.contentDocument)
76                 this.element.contentDocument.body.removeStyleClass("webkit-height-sized-to-fit");
77             window.removeEventListener("resize", this._windowResizeListener, false);
78             delete this._windowResizeListener;
79         }
80     },
81
82     sourceRow: function(lineNumber)
83     {
84         if (!lineNumber || !this.element.contentDocument)
85             return;
86
87         var table = this.element.contentDocument.getElementsByTagName("table")[0];
88         if (!table)
89             return;
90
91         var rows = table.rows;
92
93         // Line numbers are a 1-based index, but the rows collection is 0-based.
94         --lineNumber;
95         if (lineNumber >= rows.length)
96             lineNumber = rows.length - 1;
97
98         return rows[lineNumber];
99     },
100
101     lineNumberForSourceRow: function(sourceRow)
102     {
103         // Line numbers are a 1-based index, but the rows collection is 0-based.
104         var lineNumber = 0;
105         while (sourceRow) {
106             ++lineNumber;
107             sourceRow = sourceRow.previousSibling;
108         }
109
110         return lineNumber;
111     },
112
113     revealLine: function(lineNumber)
114     {
115         var row = this.sourceRow(lineNumber);
116         if (row)
117             row.scrollIntoViewIfNeeded(true);
118     },
119
120     addBreakpoint: function(breakpoint)
121     {
122         this.breakpoints.push(breakpoint);
123         breakpoint.addEventListener("enabled", this._breakpointEnableChanged, this);
124         breakpoint.addEventListener("disabled", this._breakpointEnableChanged, this);
125         this._addBreakpointToSource(breakpoint);
126     },
127
128     removeBreakpoint: function(breakpoint)
129     {
130         this.breakpoints.remove(breakpoint);
131         breakpoint.removeEventListener("enabled", null, this);
132         breakpoint.removeEventListener("disabled", null, this);
133         this._removeBreakpointFromSource(breakpoint);
134     },
135
136     addMessage: function(msg)
137     {
138         this.messages.push(msg);
139         this._addMessageToSource(msg);
140     },
141
142     clearMessages: function()
143     {
144         this.messages = [];
145
146         if (!this.element.contentDocument)
147             return;
148
149         var bubbles = this.element.contentDocument.querySelectorAll(".webkit-html-message-bubble");
150         if (!bubbles)
151             return;
152
153         for (var i = 0; i < bubbles.length; ++i) {
154             var bubble = bubbles[i];
155             bubble.parentNode.removeChild(bubble);
156         }
157     },
158
159     sizeToFitContentHeight: function()
160     {
161         if (this.element.contentDocument) {
162             this.element.style.setProperty("height", this.element.contentDocument.body.offsetHeight + "px");
163             this.element.contentDocument.body.addStyleClass("webkit-height-sized-to-fit");
164         }
165     },
166
167     highlightLine: function(lineNumber)
168     {
169         var sourceRow = this.sourceRow(lineNumber);
170         if (!sourceRow)
171             return;
172         sourceRow.addStyleClass("webkit-highlighted-line");
173         setTimeout(function() {
174             var line = sourceRow.getElementsByClassName('webkit-line-content')[0];
175             line.addStyleClass("webkit-fade-out-effect");
176             // FIXME Replace this timeout when ontransitionend is implemented
177             sourceRow.removeStyleClass("webkit-highlighted-line");
178             setTimeout(function () { line.removeStyleClass("webkit-fade-out-effect"); }, 2000);
179         }, 500);
180     },
181
182     _loaded: function()
183     {
184         WebInspector.addMainEventListeners(this.element.contentDocument);
185         this.element.contentDocument.addEventListener("mousedown", this._documentMouseDown.bind(this), true);
186
187         var headElement = this.element.contentDocument.getElementsByTagName("head")[0];
188         if (!headElement) {
189             headElement = this.element.contentDocument.createElement("head");
190             this.element.contentDocument.documentElement.insertBefore(headElement, this.element.contentDocument.documentElement.firstChild);
191         }
192
193         var styleElement = this.element.contentDocument.createElement("style");
194         headElement.appendChild(styleElement);
195
196         // Add these style rules here since they are specific to the Inspector. They also behave oddly and not
197         // all properties apply if added to view-source.css (becuase it is a user agent sheet.)
198         var styleText = ".webkit-line-number { background-repeat: no-repeat; background-position: right 1px; }\n";
199         styleText += ".webkit-breakpoint .webkit-line-number { color: white; background-image: -webkit-canvas(breakpoint); }\n";
200         styleText += ".webkit-breakpoint-disabled .webkit-line-number { color: white; background-image: -webkit-canvas(breakpoint-disabled); }\n";
201         styleText += ".webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(program-counter); }\n";
202         styleText += ".webkit-breakpoint.webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(breakpoint-program-counter); }\n";
203         styleText += ".webkit-breakpoint-disabled.webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(breakpoint-disabled-program-counter); }\n";
204         styleText += ".webkit-execution-line .webkit-line-content { background-color: rgb(171, 191, 254); outline: 1px solid rgb(64, 115, 244); }\n";
205         styleText += ".webkit-height-sized-to-fit { overflow-y: hidden }\n";
206         styleText += ".webkit-line-content { background-color: white; }\n";
207         styleText += ".webkit-highlighted-line .webkit-line-content { background-color: rgb(255, 255, 120); }\n";
208         styleText += ".webkit-fade-out-effect { -webkit-transition-property: background-color; -webkit-transition-duration: 2s; }"
209         styleText += ".webkit-javascript-comment { color: rgb(0, 116, 0); }\n";
210         styleText += ".webkit-javascript-keyword { color: rgb(170, 13, 145); }\n";
211         styleText += ".webkit-javascript-number { color: rgb(28, 0, 207); }\n";
212         styleText += ".webkit-javascript-string, .webkit-javascript-regexp { color: rgb(196, 26, 22); }\n";
213
214         styleElement.textContent = styleText;
215
216         this._needsProgramCounterImage = true;
217         this._needsBreakpointImages = true;
218
219         this.element.contentWindow.Element.prototype.addStyleClass = Element.prototype.addStyleClass;
220         this.element.contentWindow.Element.prototype.removeStyleClass = Element.prototype.removeStyleClass;
221         this.element.contentWindow.Element.prototype.hasStyleClass = Element.prototype.hasStyleClass;
222         this.element.contentWindow.Node.prototype.enclosingNodeOrSelfWithNodeName = Node.prototype.enclosingNodeOrSelfWithNodeName;
223
224         this._addExistingMessagesToSource();
225         this._addExistingBreakpointsToSource();
226         this._updateExecutionLine();
227
228         if (this.autoSizesToFitContentHeight)
229             this.sizeToFitContentHeight();
230     },
231
232     _windowResized: function(event)
233     {
234         if (!this._autoSizesToFitContentHeight)
235             return;
236         this.sizeToFitContentHeight();
237     },
238
239     _documentMouseDown: function(event)
240     {
241         if (!event.target.hasStyleClass("webkit-line-number"))
242             return;
243
244         var sourceRow = event.target.enclosingNodeOrSelfWithNodeName("tr");
245         if (sourceRow._breakpointObject)
246             sourceRow._breakpointObject.enabled = !sourceRow._breakpointObject.enabled;
247         else if (this.addBreakpointDelegate)
248             this.addBreakpointDelegate(this.lineNumberForSourceRow(sourceRow));
249     },
250
251     _breakpointEnableChanged: function(event)
252     {
253         var breakpoint = event.target;
254         var sourceRow = this.sourceRow(breakpoint.line);
255         if (!sourceRow)
256             return;
257
258         sourceRow.addStyleClass("webkit-breakpoint");
259
260         if (breakpoint.enabled)
261             sourceRow.removeStyleClass("webkit-breakpoint-disabled");
262         else
263             sourceRow.addStyleClass("webkit-breakpoint-disabled");
264     },
265
266     _updateExecutionLine: function(previousLine)
267     {
268         if (previousLine) {
269             var sourceRow = this.sourceRow(previousLine);
270             if (sourceRow)
271                 sourceRow.removeStyleClass("webkit-execution-line");
272         }
273
274         if (!this._executionLine)
275             return;
276
277         this._drawProgramCounterImageIfNeeded();
278
279         var sourceRow = this.sourceRow(this._executionLine);
280         if (sourceRow)
281             sourceRow.addStyleClass("webkit-execution-line");
282     },
283
284     _addExistingBreakpointsToSource: function()
285     {
286         var length = this.breakpoints.length;
287         for (var i = 0; i < length; ++i)
288             this._addBreakpointToSource(this.breakpoints[i]);
289     },
290
291     _addBreakpointToSource: function(breakpoint)
292     {
293         var sourceRow = this.sourceRow(breakpoint.line);
294         if (!sourceRow)
295             return;
296
297         this._drawBreakpointImagesIfNeeded();
298
299         sourceRow._breakpointObject = breakpoint;
300
301         sourceRow.addStyleClass("webkit-breakpoint");
302         if (!breakpoint.enabled)
303             sourceRow.addStyleClass("webkit-breakpoint-disabled");
304     },
305
306     _removeBreakpointFromSource: function(breakpoint)
307     {
308         var sourceRow = this.sourceRow(breakpoint.line);
309         if (!sourceRow)
310             return;
311
312         delete sourceRow._breakpointObject;
313
314         sourceRow.removeStyleClass("webkit-breakpoint");
315         sourceRow.removeStyleClass("webkit-breakpoint-disabled");
316     },
317
318     _addExistingMessagesToSource: function()
319     {
320         var length = this.messages.length;
321         for (var i = 0; i < length; ++i)
322             this._addMessageToSource(this.messages[i]);
323     },
324
325     _addMessageToSource: function(msg)
326     {
327         var row = this.sourceRow(msg.line);
328         if (!row)
329             return;
330
331         var cell = row.cells[1];
332         if (!cell)
333             return;
334
335         var errorDiv = cell.lastChild;
336         if (!errorDiv || errorDiv.nodeType !== Node.ELEMENT_NODE || !errorDiv.hasStyleClass("webkit-html-message-bubble")) {
337             errorDiv = this.element.contentDocument.createElement("div");
338             errorDiv.className = "webkit-html-message-bubble";
339             cell.appendChild(errorDiv);
340         }
341
342         var imageURL;
343         switch (msg.level) {
344             case WebInspector.ConsoleMessage.MessageLevel.Error:
345                 errorDiv.addStyleClass("webkit-html-error-message");
346                 imageURL = "Images/errorIcon.png";
347                 break;
348             case WebInspector.ConsoleMessage.MessageLevel.Warning:
349                 errorDiv.addStyleClass("webkit-html-warning-message");
350                 imageURL = "Images/warningIcon.png";
351                 break;
352         }
353
354         var lineDiv = this.element.contentDocument.createElement("div");
355         lineDiv.className = "webkit-html-message-line";
356         errorDiv.appendChild(lineDiv);
357
358         // Create the image element in the Inspector's document so we can use relative image URLs.
359         var image = document.createElement("img");
360         image.src = imageURL;
361         image.className = "webkit-html-message-icon";
362
363         // Adopt the image element since it wasn't created in element's contentDocument.
364         image = this.element.contentDocument.adoptNode(image);
365         lineDiv.appendChild(image);
366
367         lineDiv.appendChild(this.element.contentDocument.createTextNode(msg.message));
368     },
369
370     _drawProgramCounterInContext: function(ctx, glow)
371     {
372         if (glow)
373             ctx.save();
374
375         ctx.beginPath();
376         ctx.moveTo(17, 2);
377         ctx.lineTo(19, 2);
378         ctx.lineTo(19, 0);
379         ctx.lineTo(21, 0);
380         ctx.lineTo(26, 5.5);
381         ctx.lineTo(21, 11);
382         ctx.lineTo(19, 11);
383         ctx.lineTo(19, 9);
384         ctx.lineTo(17, 9);
385         ctx.closePath();
386         ctx.fillStyle = "rgb(142, 5, 4)";
387
388         if (glow) {
389             ctx.shadowBlur = 4;
390             ctx.shadowColor = "rgb(255, 255, 255)";
391             ctx.shadowOffsetX = -1;
392             ctx.shadowOffsetY = 0;
393         }
394
395         ctx.fill();
396         ctx.fill(); // Fill twice to get a good shadow and darker anti-aliased pixels.
397
398         if (glow)
399             ctx.restore();
400     },
401
402     _drawProgramCounterImageIfNeeded: function()
403     {
404         if (!this._needsProgramCounterImage || !this.element.contentDocument)
405             return;
406
407         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "program-counter", 26, 11);
408         ctx.clearRect(0, 0, 26, 11);
409         this._drawProgramCounterInContext(ctx, true);
410
411         delete this._needsProgramCounterImage;
412     },
413
414     _drawBreakpointImagesIfNeeded: function()
415     {
416         if (!this._needsBreakpointImages || !this.element.contentDocument)
417             return;
418
419         function drawBreakpoint(ctx, disabled)
420         {
421             ctx.beginPath();
422             ctx.moveTo(0, 2);
423             ctx.lineTo(2, 0);
424             ctx.lineTo(21, 0);
425             ctx.lineTo(26, 5.5);
426             ctx.lineTo(21, 11);
427             ctx.lineTo(2, 11);
428             ctx.lineTo(0, 9);
429             ctx.closePath();
430             ctx.fillStyle = "rgb(1, 142, 217)";
431             ctx.strokeStyle = "rgb(0, 103, 205)";
432             ctx.lineWidth = 3;
433             ctx.fill();
434             ctx.save();
435             ctx.clip();
436             ctx.stroke();
437             ctx.restore();
438
439             if (!disabled)
440                 return;
441
442             ctx.save();
443             ctx.globalCompositeOperation = "destination-out";
444             ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
445             ctx.fillRect(0, 0, 26, 11);
446             ctx.restore();
447         }
448
449         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint", 26, 11);
450         ctx.clearRect(0, 0, 26, 11);
451         drawBreakpoint(ctx);
452
453         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-program-counter", 26, 11);
454         ctx.clearRect(0, 0, 26, 11);
455         drawBreakpoint(ctx);
456         ctx.clearRect(20, 0, 6, 11);
457         this._drawProgramCounterInContext(ctx, true);
458
459         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-disabled", 26, 11);
460         ctx.clearRect(0, 0, 26, 11);
461         drawBreakpoint(ctx, true);
462
463         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-disabled-program-counter", 26, 11);
464         ctx.clearRect(0, 0, 26, 11);
465         drawBreakpoint(ctx, true);
466         ctx.clearRect(20, 0, 6, 11);
467         this._drawProgramCounterInContext(ctx, true);
468
469         delete this._needsBreakpointImages;
470     },
471
472     syntaxHighlightJavascript: function()
473     {
474         var table = this.element.contentDocument.getElementsByTagName("table")[0];
475         if (!table)
476             return;
477
478         function deleteContinueFlags(cell)
479         {
480             if (!cell)
481                 return;
482             delete cell._commentContinues;
483             delete cell._singleQuoteStringContinues;
484             delete cell._doubleQuoteStringContinues;
485             delete cell._regexpContinues;
486         }
487
488         function createSpan(content, className)
489         {
490             var span = document.createElement("span");
491             span.className = className;
492             span.appendChild(document.createTextNode(content));
493             return span;
494         }
495
496         function generateFinder(regex, matchNumber, className)
497         {
498             return function(str) {
499                 var match = regex.exec(str);
500                 if (!match)
501                     return null;
502                 previousMatchLength = match[matchNumber].length;
503                 return createSpan(match[matchNumber], className);
504             };
505         }
506
507         var findNumber = generateFinder(/^(-?(\d+\.?\d*([eE][+-]\d+)?|0[xX]\h+|Infinity)|NaN)(?:\W|$)/, 1, "webkit-javascript-number");
508         var findKeyword = generateFinder(/^(null|true|false|break|case|catch|const|default|finally|for|instanceof|new|var|continue|function|return|void|delete|if|this|do|while|else|in|switch|throw|try|typeof|with|debugger|class|enum|export|extends|import|super|get|set)(?:\W|$)/, 1, "webkit-javascript-keyword");
509         var findSingleLineString = generateFinder(/^"(?:[^"\\]|\\.)*"|^'([^'\\]|\\.)*'/, 0, "webkit-javascript-string"); // " this quote keeps Xcode happy
510         var findMultilineCommentStart = generateFinder(/^\/\*.*$/, 0, "webkit-javascript-comment");
511         var findMultilineCommentEnd = generateFinder(/^.*?\*\//, 0, "webkit-javascript-comment");
512         var findMultilineSingleQuoteStringStart = generateFinder(/^'(?:[^'\\]|\\.)*\\$/, 0, "webkit-javascript-string");
513         var findMultilineSingleQuoteStringEnd = generateFinder(/^(?:[^'\\]|\\.)*?'/, 0, "webkit-javascript-string");
514         var findMultilineDoubleQuoteStringStart = generateFinder(/^"(?:[^"\\]|\\.)*\\$/, 0, "webkit-javascript-string");
515         var findMultilineDoubleQuoteStringEnd = generateFinder(/^(?:[^"\\]|\\.)*?"/, 0, "webkit-javascript-string");
516         var findMultilineRegExpEnd = generateFinder(/^(?:[^\/\\]|\\.)*?\/([gim]{0,3})/, 0, "webkit-javascript-regexp");
517         var findSingleLineComment = generateFinder(/^\/\/.*|^\/\*.*?\*\//, 0, "webkit-javascript-comment");
518
519         function findMultilineRegExpStart(str)
520         {
521             var match = /^\/(?:[^\/\\]|\\.)*\\$/.exec(str);
522             if (!match || !/\\|\$|\.[\?\*\+]|[^\|]\|[^\|]/.test(match[0]))
523                 return null;
524             var node = createSpan(match[0], "webkit-javascript-regexp");
525             previousMatchLength = match[0].length;
526             return node;
527         }
528
529         function findSingleLineRegExp(str)
530         {
531             var match = /^(\/(?:[^\/\\]|\\.)*\/([gim]{0,3}))(.?)/.exec(str);
532             if (!match || !(match[2].length > 0 || /\\|\$|\.[\?\*\+]|[^\|]\|[^\|]/.test(match[1]) || /\.|;|,/.test(match[3])))
533                 return null;
534             var node = createSpan(match[1], "webkit-javascript-regexp");
535             previousMatchLength = match[1].length;
536             return node;
537         }
538
539         function syntaxHighlightJavascriptLine(line, prevLine)
540         {
541             var messageBubble = line.lastChild;
542             if (messageBubble && messageBubble.nodeType === Node.ELEMENT_NODE && messageBubble.hasStyleClass("webkit-html-message-bubble"))
543                 line.removeChild(messageBubble);
544             else
545                 messageBubble = null;
546
547             var code = line.textContent;
548
549             while (line.firstChild)
550                 line.removeChild(line.firstChild);
551
552             var token;
553             var tmp = 0;
554             var i = 0;
555
556             if (prevLine) {
557                 if (prevLine._commentContinues) {
558                     if (!(token = findMultilineCommentEnd(code))) {
559                         token = createSpan(code, "webkit-javascript-comment");
560                         line._commentContinues = true;
561                     }
562                 } else if (prevLine._singleQuoteStringContinues) {
563                     if (!(token = findMultilineSingleQuoteStringEnd(code))) {
564                         token = createSpan(code, "webkit-javascript-string");
565                         line._singleQuoteStringContinues = true;
566                     }
567                 } else if (prevLine._doubleQuoteStringContinues) {
568                     if (!(token = findMultilineDoubleQuoteStringEnd(code))) {
569                         token = createSpan(code, "webkit-javascript-string");
570                         line._doubleQuoteStringContinues = true;
571                     }
572                 } else if (prevLine._regexpContinues) {
573                     if (!(token = findMultilineRegExpEnd(code))) {
574                         token = createSpan(code, "webkit-javascript-regexp");
575                         line._regexpContinues = true;
576                     }
577                 }
578                 if (token) {
579                     i += previousMatchLength ? previousMatchLength : code.length;
580                     tmp = i;
581                     line.appendChild(token);
582                 }
583             }
584
585             for ( ; i < code.length; ++i) {
586                 var codeFragment = code.substr(i);
587                 var prevChar = code[i - 1];
588                 token = findSingleLineComment(codeFragment);
589                 if (!token) {
590                     if ((token = findMultilineCommentStart(codeFragment)))
591                         line._commentContinues = true;
592                     else if (!prevChar || /^\W/.test(prevChar)) {
593                         token = findNumber(codeFragment, code[i - 1]) ||
594                                 findKeyword(codeFragment, code[i - 1]) ||
595                                 findSingleLineString(codeFragment) ||
596                                 findSingleLineRegExp(codeFragment);
597                         if (!token) {
598                             if (token = findMultilineSingleQuoteStringStart(codeFragment))
599                                 line._singleQuoteStringContinues = true;
600                             else if (token = findMultilineDoubleQuoteStringStart(codeFragment))
601                                 line._doubleQuoteStringContinues = true;
602                             else if (token = findMultilineRegExpStart(codeFragment))
603                                 line._regexpContinues = true;
604                         }
605                     }
606                 }
607
608                 if (token) {
609                     if (tmp !== i)
610                         line.appendChild(document.createTextNode(code.substring(tmp, i)));
611                     line.appendChild(token);
612                     i += previousMatchLength - 1;
613                     tmp = i + 1;
614                 }
615             }
616
617             if (tmp < code.length)
618                 line.appendChild(document.createTextNode(code.substring(tmp, i)));
619
620             if (messageBubble)
621                 line.appendChild(messageBubble);
622         }
623
624         var i = 0;
625         var rows = table.rows;
626         var rowsLength = rows.length;
627         var previousCell = null;
628         var previousMatchLength = 0;
629
630         // Split up the work into chunks so we don't block the
631         // UI thread while processing.
632
633         function processChunk()
634         {
635             for (var end = Math.min(i + 10, rowsLength); i < end; ++i) {
636                 var row = rows[i];
637                 if (!row)
638                     continue;
639                 var cell = row.cells[1];
640                 if (!cell)
641                     continue;
642                 syntaxHighlightJavascriptLine(cell, previousCell);
643                 if (i < (end - 1))
644                     deleteContinueFlags(previousCell);
645                 previousCell = cell;
646             }
647
648             if (i >= rowsLength && processChunkInterval) {
649                 deleteContinueFlags(previousCell);
650                 clearInterval(processChunkInterval);
651             }
652         }
653
654         processChunk();
655
656         var processChunkInterval = setInterval(processChunk, 25);
657     }
658 }