Changed the line highlight transition for an easier animation.
[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     _highlightLineEnds: function(event)
168     {
169         event.target.parentNode.removeStyleClass("webkit-highlighted-line");
170     },
171
172     highlightLine: function(lineNumber)
173     {
174         var sourceRow = this.sourceRow(lineNumber);
175         if (!sourceRow)
176             return;
177         var line = sourceRow.getElementsByClassName('webkit-line-content')[0];
178         // Trick to reset the animation if the user clicks on the same link
179         // Using a timeout to avoid coalesced style updates
180         line.style.setProperty("-webkit-animation-name", "none");
181         setTimeout(function () {
182             line.style.removeProperty("-webkit-animation-name");
183             sourceRow.addStyleClass("webkit-highlighted-line");
184         }, 0);
185     },
186
187     _loaded: function()
188     {
189         WebInspector.addMainEventListeners(this.element.contentDocument);
190         this.element.contentDocument.addEventListener("mousedown", this._documentMouseDown.bind(this), true);
191         this.element.contentDocument.addEventListener("webkitAnimationEnd", this._highlightLineEnds.bind(this), false);
192
193         var headElement = this.element.contentDocument.getElementsByTagName("head")[0];
194         if (!headElement) {
195             headElement = this.element.contentDocument.createElement("head");
196             this.element.contentDocument.documentElement.insertBefore(headElement, this.element.contentDocument.documentElement.firstChild);
197         }
198
199         var styleElement = this.element.contentDocument.createElement("style");
200         headElement.appendChild(styleElement);
201
202         // Add these style rules here since they are specific to the Inspector. They also behave oddly and not
203         // all properties apply if added to view-source.css (becuase it is a user agent sheet.)
204         var styleText = ".webkit-line-number { background-repeat: no-repeat; background-position: right 1px; }\n";
205         styleText += ".webkit-breakpoint .webkit-line-number { color: white; background-image: -webkit-canvas(breakpoint); }\n";
206         styleText += ".webkit-breakpoint-disabled .webkit-line-number { color: white; background-image: -webkit-canvas(breakpoint-disabled); }\n";
207         styleText += ".webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(program-counter); }\n";
208         styleText += ".webkit-breakpoint.webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(breakpoint-program-counter); }\n";
209         styleText += ".webkit-breakpoint-disabled.webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(breakpoint-disabled-program-counter); }\n";
210         styleText += ".webkit-execution-line .webkit-line-content { background-color: rgb(171, 191, 254); outline: 1px solid rgb(64, 115, 244); }\n";
211         styleText += ".webkit-height-sized-to-fit { overflow-y: hidden }\n";
212         styleText += ".webkit-line-content { background-color: white; }\n";
213         styleText += "@-webkit-keyframes fadeout {from {background-color: rgb(255, 255, 120);} to { background-color: white;}}\n";
214         styleText += ".webkit-highlighted-line .webkit-line-content { background-color: rgb(255, 255, 120); -webkit-animation: 'fadeout' 2s 500ms}\n";
215         styleText += ".webkit-javascript-comment { color: rgb(0, 116, 0); }\n";
216         styleText += ".webkit-javascript-keyword { color: rgb(170, 13, 145); }\n";
217         styleText += ".webkit-javascript-number { color: rgb(28, 0, 207); }\n";
218         styleText += ".webkit-javascript-string, .webkit-javascript-regexp { color: rgb(196, 26, 22); }\n";
219
220         styleElement.textContent = styleText;
221
222         this._needsProgramCounterImage = true;
223         this._needsBreakpointImages = true;
224
225         this.element.contentWindow.Element.prototype.addStyleClass = Element.prototype.addStyleClass;
226         this.element.contentWindow.Element.prototype.removeStyleClass = Element.prototype.removeStyleClass;
227         this.element.contentWindow.Element.prototype.hasStyleClass = Element.prototype.hasStyleClass;
228         this.element.contentWindow.Node.prototype.enclosingNodeOrSelfWithNodeName = Node.prototype.enclosingNodeOrSelfWithNodeName;
229
230         this._addExistingMessagesToSource();
231         this._addExistingBreakpointsToSource();
232         this._updateExecutionLine();
233
234         if (this.autoSizesToFitContentHeight)
235             this.sizeToFitContentHeight();
236     },
237
238     _windowResized: function(event)
239     {
240         if (!this._autoSizesToFitContentHeight)
241             return;
242         this.sizeToFitContentHeight();
243     },
244
245     _documentMouseDown: function(event)
246     {
247         if (!event.target.hasStyleClass("webkit-line-number"))
248             return;
249
250         var sourceRow = event.target.enclosingNodeOrSelfWithNodeName("tr");
251         if (sourceRow._breakpointObject)
252             sourceRow._breakpointObject.enabled = !sourceRow._breakpointObject.enabled;
253         else if (this.addBreakpointDelegate)
254             this.addBreakpointDelegate(this.lineNumberForSourceRow(sourceRow));
255     },
256
257     _breakpointEnableChanged: function(event)
258     {
259         var breakpoint = event.target;
260         var sourceRow = this.sourceRow(breakpoint.line);
261         if (!sourceRow)
262             return;
263
264         sourceRow.addStyleClass("webkit-breakpoint");
265
266         if (breakpoint.enabled)
267             sourceRow.removeStyleClass("webkit-breakpoint-disabled");
268         else
269             sourceRow.addStyleClass("webkit-breakpoint-disabled");
270     },
271
272     _updateExecutionLine: function(previousLine)
273     {
274         if (previousLine) {
275             var sourceRow = this.sourceRow(previousLine);
276             if (sourceRow)
277                 sourceRow.removeStyleClass("webkit-execution-line");
278         }
279
280         if (!this._executionLine)
281             return;
282
283         this._drawProgramCounterImageIfNeeded();
284
285         var sourceRow = this.sourceRow(this._executionLine);
286         if (sourceRow)
287             sourceRow.addStyleClass("webkit-execution-line");
288     },
289
290     _addExistingBreakpointsToSource: function()
291     {
292         var length = this.breakpoints.length;
293         for (var i = 0; i < length; ++i)
294             this._addBreakpointToSource(this.breakpoints[i]);
295     },
296
297     _addBreakpointToSource: function(breakpoint)
298     {
299         var sourceRow = this.sourceRow(breakpoint.line);
300         if (!sourceRow)
301             return;
302
303         this._drawBreakpointImagesIfNeeded();
304
305         sourceRow._breakpointObject = breakpoint;
306
307         sourceRow.addStyleClass("webkit-breakpoint");
308         if (!breakpoint.enabled)
309             sourceRow.addStyleClass("webkit-breakpoint-disabled");
310     },
311
312     _removeBreakpointFromSource: function(breakpoint)
313     {
314         var sourceRow = this.sourceRow(breakpoint.line);
315         if (!sourceRow)
316             return;
317
318         delete sourceRow._breakpointObject;
319
320         sourceRow.removeStyleClass("webkit-breakpoint");
321         sourceRow.removeStyleClass("webkit-breakpoint-disabled");
322     },
323
324     _addExistingMessagesToSource: function()
325     {
326         var length = this.messages.length;
327         for (var i = 0; i < length; ++i)
328             this._addMessageToSource(this.messages[i]);
329     },
330
331     _addMessageToSource: function(msg)
332     {
333         var row = this.sourceRow(msg.line);
334         if (!row)
335             return;
336
337         var cell = row.cells[1];
338         if (!cell)
339             return;
340
341         var errorDiv = cell.lastChild;
342         if (!errorDiv || errorDiv.nodeType !== Node.ELEMENT_NODE || !errorDiv.hasStyleClass("webkit-html-message-bubble")) {
343             errorDiv = this.element.contentDocument.createElement("div");
344             errorDiv.className = "webkit-html-message-bubble";
345             cell.appendChild(errorDiv);
346         }
347
348         var imageURL;
349         switch (msg.level) {
350             case WebInspector.ConsoleMessage.MessageLevel.Error:
351                 errorDiv.addStyleClass("webkit-html-error-message");
352                 imageURL = "Images/errorIcon.png";
353                 break;
354             case WebInspector.ConsoleMessage.MessageLevel.Warning:
355                 errorDiv.addStyleClass("webkit-html-warning-message");
356                 imageURL = "Images/warningIcon.png";
357                 break;
358         }
359
360         var lineDiv = this.element.contentDocument.createElement("div");
361         lineDiv.className = "webkit-html-message-line";
362         errorDiv.appendChild(lineDiv);
363
364         // Create the image element in the Inspector's document so we can use relative image URLs.
365         var image = document.createElement("img");
366         image.src = imageURL;
367         image.className = "webkit-html-message-icon";
368
369         // Adopt the image element since it wasn't created in element's contentDocument.
370         image = this.element.contentDocument.adoptNode(image);
371         lineDiv.appendChild(image);
372
373         lineDiv.appendChild(this.element.contentDocument.createTextNode(msg.message));
374     },
375
376     _drawProgramCounterInContext: function(ctx, glow)
377     {
378         if (glow)
379             ctx.save();
380
381         ctx.beginPath();
382         ctx.moveTo(17, 2);
383         ctx.lineTo(19, 2);
384         ctx.lineTo(19, 0);
385         ctx.lineTo(21, 0);
386         ctx.lineTo(26, 5.5);
387         ctx.lineTo(21, 11);
388         ctx.lineTo(19, 11);
389         ctx.lineTo(19, 9);
390         ctx.lineTo(17, 9);
391         ctx.closePath();
392         ctx.fillStyle = "rgb(142, 5, 4)";
393
394         if (glow) {
395             ctx.shadowBlur = 4;
396             ctx.shadowColor = "rgb(255, 255, 255)";
397             ctx.shadowOffsetX = -1;
398             ctx.shadowOffsetY = 0;
399         }
400
401         ctx.fill();
402         ctx.fill(); // Fill twice to get a good shadow and darker anti-aliased pixels.
403
404         if (glow)
405             ctx.restore();
406     },
407
408     _drawProgramCounterImageIfNeeded: function()
409     {
410         if (!this._needsProgramCounterImage || !this.element.contentDocument)
411             return;
412
413         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "program-counter", 26, 11);
414         ctx.clearRect(0, 0, 26, 11);
415         this._drawProgramCounterInContext(ctx, true);
416
417         delete this._needsProgramCounterImage;
418     },
419
420     _drawBreakpointImagesIfNeeded: function()
421     {
422         if (!this._needsBreakpointImages || !this.element.contentDocument)
423             return;
424
425         function drawBreakpoint(ctx, disabled)
426         {
427             ctx.beginPath();
428             ctx.moveTo(0, 2);
429             ctx.lineTo(2, 0);
430             ctx.lineTo(21, 0);
431             ctx.lineTo(26, 5.5);
432             ctx.lineTo(21, 11);
433             ctx.lineTo(2, 11);
434             ctx.lineTo(0, 9);
435             ctx.closePath();
436             ctx.fillStyle = "rgb(1, 142, 217)";
437             ctx.strokeStyle = "rgb(0, 103, 205)";
438             ctx.lineWidth = 3;
439             ctx.fill();
440             ctx.save();
441             ctx.clip();
442             ctx.stroke();
443             ctx.restore();
444
445             if (!disabled)
446                 return;
447
448             ctx.save();
449             ctx.globalCompositeOperation = "destination-out";
450             ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
451             ctx.fillRect(0, 0, 26, 11);
452             ctx.restore();
453         }
454
455         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint", 26, 11);
456         ctx.clearRect(0, 0, 26, 11);
457         drawBreakpoint(ctx);
458
459         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-program-counter", 26, 11);
460         ctx.clearRect(0, 0, 26, 11);
461         drawBreakpoint(ctx);
462         ctx.clearRect(20, 0, 6, 11);
463         this._drawProgramCounterInContext(ctx, true);
464
465         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-disabled", 26, 11);
466         ctx.clearRect(0, 0, 26, 11);
467         drawBreakpoint(ctx, true);
468
469         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-disabled-program-counter", 26, 11);
470         ctx.clearRect(0, 0, 26, 11);
471         drawBreakpoint(ctx, true);
472         ctx.clearRect(20, 0, 6, 11);
473         this._drawProgramCounterInContext(ctx, true);
474
475         delete this._needsBreakpointImages;
476     },
477
478     syntaxHighlightJavascript: function()
479     {
480         var table = this.element.contentDocument.getElementsByTagName("table")[0];
481         if (!table)
482             return;
483
484         function deleteContinueFlags(cell)
485         {
486             if (!cell)
487                 return;
488             delete cell._commentContinues;
489             delete cell._singleQuoteStringContinues;
490             delete cell._doubleQuoteStringContinues;
491             delete cell._regexpContinues;
492         }
493
494         function createSpan(content, className)
495         {
496             var span = document.createElement("span");
497             span.className = className;
498             span.appendChild(document.createTextNode(content));
499             return span;
500         }
501
502         function generateFinder(regex, matchNumber, className)
503         {
504             return function(str) {
505                 var match = regex.exec(str);
506                 if (!match)
507                     return null;
508                 previousMatchLength = match[matchNumber].length;
509                 return createSpan(match[matchNumber], className);
510             };
511         }
512
513         var findNumber = generateFinder(/^(-?(\d+\.?\d*([eE][+-]\d+)?|0[xX]\h+|Infinity)|NaN)(?:\W|$)/, 1, "webkit-javascript-number");
514         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");
515         var findSingleLineString = generateFinder(/^"(?:[^"\\]|\\.)*"|^'([^'\\]|\\.)*'/, 0, "webkit-javascript-string"); // " this quote keeps Xcode happy
516         var findMultilineCommentStart = generateFinder(/^\/\*.*$/, 0, "webkit-javascript-comment");
517         var findMultilineCommentEnd = generateFinder(/^.*?\*\//, 0, "webkit-javascript-comment");
518         var findMultilineSingleQuoteStringStart = generateFinder(/^'(?:[^'\\]|\\.)*\\$/, 0, "webkit-javascript-string");
519         var findMultilineSingleQuoteStringEnd = generateFinder(/^(?:[^'\\]|\\.)*?'/, 0, "webkit-javascript-string");
520         var findMultilineDoubleQuoteStringStart = generateFinder(/^"(?:[^"\\]|\\.)*\\$/, 0, "webkit-javascript-string");
521         var findMultilineDoubleQuoteStringEnd = generateFinder(/^(?:[^"\\]|\\.)*?"/, 0, "webkit-javascript-string");
522         var findMultilineRegExpEnd = generateFinder(/^(?:[^\/\\]|\\.)*?\/([gim]{0,3})/, 0, "webkit-javascript-regexp");
523         var findSingleLineComment = generateFinder(/^\/\/.*|^\/\*.*?\*\//, 0, "webkit-javascript-comment");
524
525         function findMultilineRegExpStart(str)
526         {
527             var match = /^\/(?:[^\/\\]|\\.)*\\$/.exec(str);
528             if (!match || !/\\|\$|\.[\?\*\+]|[^\|]\|[^\|]/.test(match[0]))
529                 return null;
530             var node = createSpan(match[0], "webkit-javascript-regexp");
531             previousMatchLength = match[0].length;
532             return node;
533         }
534
535         function findSingleLineRegExp(str)
536         {
537             var match = /^(\/(?:[^\/\\]|\\.)*\/([gim]{0,3}))(.?)/.exec(str);
538             if (!match || !(match[2].length > 0 || /\\|\$|\.[\?\*\+]|[^\|]\|[^\|]/.test(match[1]) || /\.|;|,/.test(match[3])))
539                 return null;
540             var node = createSpan(match[1], "webkit-javascript-regexp");
541             previousMatchLength = match[1].length;
542             return node;
543         }
544
545         function syntaxHighlightJavascriptLine(line, prevLine)
546         {
547             var messageBubble = line.lastChild;
548             if (messageBubble && messageBubble.nodeType === Node.ELEMENT_NODE && messageBubble.hasStyleClass("webkit-html-message-bubble"))
549                 line.removeChild(messageBubble);
550             else
551                 messageBubble = null;
552
553             var code = line.textContent;
554
555             while (line.firstChild)
556                 line.removeChild(line.firstChild);
557
558             var token;
559             var tmp = 0;
560             var i = 0;
561
562             if (prevLine) {
563                 if (prevLine._commentContinues) {
564                     if (!(token = findMultilineCommentEnd(code))) {
565                         token = createSpan(code, "webkit-javascript-comment");
566                         line._commentContinues = true;
567                     }
568                 } else if (prevLine._singleQuoteStringContinues) {
569                     if (!(token = findMultilineSingleQuoteStringEnd(code))) {
570                         token = createSpan(code, "webkit-javascript-string");
571                         line._singleQuoteStringContinues = true;
572                     }
573                 } else if (prevLine._doubleQuoteStringContinues) {
574                     if (!(token = findMultilineDoubleQuoteStringEnd(code))) {
575                         token = createSpan(code, "webkit-javascript-string");
576                         line._doubleQuoteStringContinues = true;
577                     }
578                 } else if (prevLine._regexpContinues) {
579                     if (!(token = findMultilineRegExpEnd(code))) {
580                         token = createSpan(code, "webkit-javascript-regexp");
581                         line._regexpContinues = true;
582                     }
583                 }
584                 if (token) {
585                     i += previousMatchLength ? previousMatchLength : code.length;
586                     tmp = i;
587                     line.appendChild(token);
588                 }
589             }
590
591             for ( ; i < code.length; ++i) {
592                 var codeFragment = code.substr(i);
593                 var prevChar = code[i - 1];
594                 token = findSingleLineComment(codeFragment);
595                 if (!token) {
596                     if ((token = findMultilineCommentStart(codeFragment)))
597                         line._commentContinues = true;
598                     else if (!prevChar || /^\W/.test(prevChar)) {
599                         token = findNumber(codeFragment, code[i - 1]) ||
600                                 findKeyword(codeFragment, code[i - 1]) ||
601                                 findSingleLineString(codeFragment) ||
602                                 findSingleLineRegExp(codeFragment);
603                         if (!token) {
604                             if (token = findMultilineSingleQuoteStringStart(codeFragment))
605                                 line._singleQuoteStringContinues = true;
606                             else if (token = findMultilineDoubleQuoteStringStart(codeFragment))
607                                 line._doubleQuoteStringContinues = true;
608                             else if (token = findMultilineRegExpStart(codeFragment))
609                                 line._regexpContinues = true;
610                         }
611                     }
612                 }
613
614                 if (token) {
615                     if (tmp !== i)
616                         line.appendChild(document.createTextNode(code.substring(tmp, i)));
617                     line.appendChild(token);
618                     i += previousMatchLength - 1;
619                     tmp = i + 1;
620                 }
621             }
622
623             if (tmp < code.length)
624                 line.appendChild(document.createTextNode(code.substring(tmp, i)));
625
626             if (messageBubble)
627                 line.appendChild(messageBubble);
628         }
629
630         var i = 0;
631         var rows = table.rows;
632         var rowsLength = rows.length;
633         var previousCell = null;
634         var previousMatchLength = 0;
635
636         // Split up the work into chunks so we don't block the
637         // UI thread while processing.
638
639         function processChunk()
640         {
641             for (var end = Math.min(i + 10, rowsLength); i < end; ++i) {
642                 var row = rows[i];
643                 if (!row)
644                     continue;
645                 var cell = row.cells[1];
646                 if (!cell)
647                     continue;
648                 syntaxHighlightJavascriptLine(cell, previousCell);
649                 if (i < (end - 1))
650                     deleteContinueFlags(previousCell);
651                 previousCell = cell;
652             }
653
654             if (i >= rowsLength && processChunkInterval) {
655                 deleteContinueFlags(previousCell);
656                 clearInterval(processChunkInterval);
657             }
658         }
659
660         processChunk();
661
662         var processChunkInterval = setInterval(processChunk, 25);
663     }
664 }