Web Inspector: Add application/vnd.api+json as a valid JSON MIME-type
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CodeMirrorAdditions.js
1 /*
2  * Copyright (C) 2013 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. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 (function () {
27     // By default CodeMirror defines syntax highlighting styles based on token
28     // only and shared styles between modes. This limiting and does not match
29     // what we have done in the Web Inspector. So this modifies the XML, CSS
30     // and JavaScript modes to supply two styles for each token. One for the
31     // token and one with the mode name.
32
33     function tokenizeLinkString(stream, state)
34     {
35         console.assert(state._linkQuoteCharacter !== undefined);
36
37         // Eat the string until the same quote is found that started the string.
38         // If this is unquoted, then eat until whitespace or common parse errors.
39         if (state._linkQuoteCharacter)
40             stream.eatWhile(new RegExp("[^" + state._linkQuoteCharacter + "]"));
41         else
42             stream.eatWhile(/[^\s\u00a0=<>\"\']/);
43
44         // If the stream isn't at the end of line then we found the end quote.
45         // In the case, change _linkTokenize to parse the end of the link next.
46         // Otherwise _linkTokenize will stay as-is to parse more of the link.
47         if (!stream.eol())
48             state._linkTokenize = tokenizeEndOfLinkString;
49
50         return "link";
51     }
52
53     function tokenizeEndOfLinkString(stream, state)
54     {
55         console.assert(state._linkQuoteCharacter !== undefined);
56         console.assert(state._linkBaseStyle);
57
58         // Eat the quote character to style it with the base style.
59         if (state._linkQuoteCharacter)
60             stream.eat(state._linkQuoteCharacter);
61
62         var style = state._linkBaseStyle;
63
64         // Clean up the state.
65         delete state._linkTokenize;
66         delete state._linkQuoteCharacter;
67         delete state._linkBaseStyle;
68         delete state._srcSetTokenizeState;
69
70         return style;
71     }
72
73     function tokenizeSrcSetString(stream, state)
74     {
75         console.assert(state._linkQuoteCharacter !== undefined);
76
77         if (state._srcSetTokenizeState === "link") {
78             // Eat the string until a space, comma, or ending quote.
79             // If this is unquoted, then eat until whitespace or common parse errors.
80             if (state._linkQuoteCharacter)
81                 stream.eatWhile(new RegExp("[^\\s," + state._linkQuoteCharacter + "]"));
82             else
83                 stream.eatWhile(/[^\s,\u00a0=<>\"\']/);
84         } else {
85             // Eat the string until a comma, or ending quote.
86             // If this is unquoted, then eat until whitespace or common parse errors.
87             stream.eatSpace();
88             if (state._linkQuoteCharacter)
89                 stream.eatWhile(new RegExp("[^," + state._linkQuoteCharacter + "]"));
90             else
91                 stream.eatWhile(/[^\s\u00a0=<>\"\']/);
92             stream.eatWhile(/[\s,]/);
93         }
94
95         // If the stream isn't at the end of line and we found the end quote
96         // change _linkTokenize to parse the end of the link next. Otherwise
97         // _linkTokenize will stay as-is to parse more of the srcset.
98         if (stream.eol() || (!state._linkQuoteCharacter || stream.peek() === state._linkQuoteCharacter))
99             state._linkTokenize = tokenizeEndOfLinkString;
100
101         // Link portion.
102         if (state._srcSetTokenizeState === "link") {
103             state._srcSetTokenizeState = "descriptor";
104             return "link";
105         }
106
107         // Descriptor portion.
108         state._srcSetTokenizeState = "link";
109         return state._linkBaseStyle;
110     }
111
112     function extendedXMLToken(stream, state)
113     {
114         if (state._linkTokenize) {
115             // Call the link tokenizer instead.
116             var style = state._linkTokenize(stream, state);
117             return style && (style + " m-" + this.name);
118         }
119
120         // Remember the start position so we can rewind if needed.
121         var startPosition = stream.pos;
122         var style = this._token(stream, state);
123         if (style === "attribute") {
124             // Look for "href" or "src" attributes. If found then we should
125             // expect a string later that should get the "link" style instead.
126             var text = stream.current().toLowerCase();
127             if (text === "src" || /\bhref\b/.test(text))
128                 state._expectLink = true;
129             else if (text === "srcset")
130                 state._expectSrcSet = true;
131             else {
132                 delete state._expectLink;
133                 delete state._expectSrcSet;
134             }
135         } else if (state._expectLink && style === "string") {
136             var current = stream.current();
137
138             // Unless current token is empty quotes, consume quote character
139             // and tokenize link next.
140             if (current !== "\"\"" && current !== "''") {
141                 delete state._expectLink;
142
143                 // This is a link, so setup the state to process it next.
144                 state._linkTokenize = tokenizeLinkString;
145                 state._linkBaseStyle = style;
146
147                 // The attribute may or may not be quoted.
148                 var quote = current[0];
149
150                 state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null;
151
152                 // Rewind the stream to the start of this token.
153                 stream.pos = startPosition;
154
155                 // Eat the open quote of the string so the string style
156                 // will be used for the quote character.
157                 if (state._linkQuoteCharacter)
158                     stream.eat(state._linkQuoteCharacter);
159             }
160         } else if (state._expectSrcSet && style === "string") {
161             var current = stream.current();
162
163             // Unless current token is empty quotes, consume quote character
164             // and tokenize link next.
165             if (current !== "\"\"" && current !== "''") {
166                 delete state._expectSrcSet;
167
168                 // This is a link, so setup the state to process it next.
169                 state._srcSetTokenizeState = "link";
170                 state._linkTokenize = tokenizeSrcSetString;
171                 state._linkBaseStyle = style;
172
173                 // The attribute may or may not be quoted.
174                 var quote = current[0];
175
176                 state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null;
177
178                 // Rewind the stream to the start of this token.
179                 stream.pos = startPosition;
180
181                 // Eat the open quote of the string so the string style
182                 // will be used for the quote character.
183                 if (state._linkQuoteCharacter)
184                     stream.eat(state._linkQuoteCharacter);
185             }
186         } else if (style) {
187             // We don't expect other tokens between attribute and string since
188             // spaces and the equal character are not tokenized. So if we get
189             // another token before a string then we stop expecting a link.
190             delete state._expectLink;
191             delete state._expectSrcSet;
192         }
193
194         return style && (style + " m-" + this.name);
195     }
196
197     function tokenizeCSSURLString(stream, state)
198     {
199         console.assert(state._urlQuoteCharacter);
200
201         // If we are an unquoted url string, return whitespace blocks as a whitespace token (null).
202         if (state._unquotedURLString && stream.eatSpace())
203             return null;
204
205         var ch = null;
206         var escaped = false;
207         var reachedEndOfURL = false;
208         var lastNonWhitespace = stream.pos;
209         var quote = state._urlQuoteCharacter;
210
211         // Parse characters until the end of the stream/line or a proper end quote character.
212         while ((ch = stream.next()) != null) {
213             if (ch === quote && !escaped) {
214                 reachedEndOfURL = true;
215                 break;
216             }
217             escaped = !escaped && ch === "\\";
218             if (!/[\s\u00a0]/.test(ch))
219                 lastNonWhitespace = stream.pos;
220         }
221
222         // If we are an unquoted url string, do not include trailing whitespace, rewind to the last real character.
223         if (state._unquotedURLString)
224             stream.pos = lastNonWhitespace;
225
226         // If we have reached the proper the end of the url string, switch to the end tokenizer to reset the state.
227         if (reachedEndOfURL) {
228             if (!state._unquotedURLString)
229                 stream.backUp(1);
230             this._urlTokenize = tokenizeEndOfCSSURLString;
231         }
232
233         return "link";
234     }
235
236     function tokenizeEndOfCSSURLString(stream, state)
237     {
238         console.assert(state._urlQuoteCharacter);
239         console.assert(state._urlBaseStyle);
240
241         // Eat the quote character to style it with the base style.
242         if (!state._unquotedURLString)
243             stream.eat(state._urlQuoteCharacter);
244
245         var style = state._urlBaseStyle;
246
247         delete state._urlTokenize;
248         delete state._urlQuoteCharacter;
249         delete state._urlBaseStyle;
250
251         return style;
252     }
253
254     function extendedCSSToken(stream, state)
255     {
256         var hexColorRegex = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/g;
257
258         if (state._urlTokenize) {
259             // Call the link tokenizer instead.
260             var style = state._urlTokenize(stream, state);
261             return style && (style + " m-" + (this.alternateName || this.name));
262         }
263
264         // Remember the start position so we can rewind if needed.
265         var startPosition = stream.pos;
266         var style = this._token(stream, state);
267
268         if (style) {
269             if (style === "atom") {
270                 if (stream.current() === "url") {
271                     // If the current text is "url" then we should expect the next string token to be a link.
272                     state._expectLink = true;
273                 } else if (hexColorRegex.test(stream.current()))
274                     style = style + " hex-color";
275             } else if (state._expectLink) {
276                 delete state._expectLink;
277
278                 if (style === "string") {
279                     // This is a link, so setup the state to process it next.
280                     state._urlTokenize = tokenizeCSSURLString;
281                     state._urlBaseStyle = style;
282
283                     // The url may or may not be quoted.
284                     var quote = stream.current()[0];
285                     state._urlQuoteCharacter = quote === "'" || quote === "\"" ? quote : ")";
286                     state._unquotedURLString = state._urlQuoteCharacter === ")";
287
288                     // Rewind the stream to the start of this token.
289                     stream.pos = startPosition;
290
291                     // Eat the open quote of the string so the string style
292                     // will be used for the quote character.
293                     if (!state._unquotedURLString)
294                         stream.eat(state._urlQuoteCharacter);
295                 }
296             }
297         }
298
299         return style && (style + " m-" + (this.alternateName || this.name));
300     }
301
302     function extendedToken(stream, state)
303     {
304         // CodeMirror moves the original token function to _token when we extended it.
305         // So call it to get the style that we will add an additional class name to.
306         var style = this._token(stream, state);
307         return style && (style + " m-" + (this.alternateName || this.name));
308     }
309
310     function extendedCSSRuleStartState(base)
311     {
312         // CodeMirror moves the original token function to _startState when we extended it.
313         // So call it to get the original start state that we will modify.
314         var state = this._startState(base);
315
316         // Start off the state stack like it has already parsed a rule. This causes everything
317         // after to be parsed as properties in a rule.
318         state.state = "block";
319         state.context.type = "block";
320
321         return state;
322     }
323
324     function scrollCursorIntoView(codeMirror, event)
325     {
326         // We don't want to use the default implementation since it can cause massive jumping
327         // when the editor is contained inside overflow elements.
328         event.preventDefault();
329
330         function delayedWork()
331         {
332             // Don't try to scroll unless the editor is focused.
333             if (!codeMirror.getWrapperElement().classList.contains("CodeMirror-focused"))
334                 return;
335
336             // The cursor element can contain multiple cursors. The first one is the blinky cursor,
337             // which is the one we want to scroll into view. It can be missing, so check first.
338             var cursorElement = codeMirror.getScrollerElement().getElementsByClassName("CodeMirror-cursor")[0];
339             if (cursorElement)
340                 cursorElement.scrollIntoViewIfNeeded(false);
341         }
342
343         // We need to delay this because CodeMirror can fire scrollCursorIntoView as a view is being blurred
344         // and another is being focused. The blurred editor still has the focused state when this event fires.
345         // We don't want to scroll the blurred editor into view, only the focused editor.
346         setTimeout(delayedWork, 0);
347     }
348
349     CodeMirror.extendMode("css", {token: extendedCSSToken});
350     CodeMirror.extendMode("xml", {token: extendedXMLToken});
351     CodeMirror.extendMode("javascript", {token: extendedToken});
352
353     CodeMirror.defineMode("css-rule", CodeMirror.modes.css);
354     CodeMirror.extendMode("css-rule", {token: extendedCSSToken, startState: extendedCSSRuleStartState, alternateName: "css"});
355
356     CodeMirror.defineInitHook(function(codeMirror) {
357         codeMirror.on("scrollCursorIntoView", scrollCursorIntoView);
358     });
359
360     CodeMirror.defineExtension("hasLineClass", function(line, where, className) {
361         // This matches the arguments to addLineClass and removeLineClass.
362         var classProperty = (where === "text" ? "textClass" : (where === "background" ? "bgClass" : "wrapClass"));
363         var lineInfo = this.lineInfo(line);
364         if (!lineInfo)
365             return false;
366
367         if (!lineInfo[classProperty])
368             return false;
369
370         // Test for the simple case.
371         if (lineInfo[classProperty] === className)
372             return true;
373
374         // Do a quick check for the substring. This is faster than a regex, which requires escaping the input first.
375         var index = lineInfo[classProperty].indexOf(className);
376         if (index === -1)
377             return false;
378
379         // Check that it is surrounded by spaces. Add padding spaces first to work with beginning and end of string cases.
380         var paddedClass = " " + lineInfo[classProperty] + " ";
381         return paddedClass.indexOf(" " + className + " ", index) !== -1;
382     });
383
384     CodeMirror.defineExtension("setUniqueBookmark", function(position, options) {
385         var marks = this.findMarksAt(position);
386         for (var i = 0; i < marks.length; ++i) {
387             if (marks[i].__uniqueBookmark) {
388                 marks[i].clear();
389                 break;
390             }
391         }
392
393         var uniqueBookmark = this.setBookmark(position, options);
394         uniqueBookmark.__uniqueBookmark = true;
395         return uniqueBookmark;
396     });
397
398     CodeMirror.defineExtension("toggleLineClass", function(line, where, className) {
399         if (this.hasLineClass(line, where, className)) {
400             this.removeLineClass(line, where, className);
401             return false;
402         }
403
404         this.addLineClass(line, where, className);
405         return true;
406     });
407
408     CodeMirror.defineExtension("alterNumberInRange", function(amount, startPosition, endPosition, updateSelection) {
409         // We don't try if the range is multiline, pass to another key handler.
410         if (startPosition.line !== endPosition.line)
411             return false;
412
413         if (updateSelection) {
414             // Remember the cursor position/selection.
415             var selectionStart = this.getCursor("start");
416             var selectionEnd = this.getCursor("end");
417         }
418
419         var line = this.getLine(startPosition.line);
420
421         var foundPeriod = false;
422
423         var start = NaN;
424         var end = NaN;
425
426         for (var i = startPosition.ch; i >= 0; --i) {
427             var character = line.charAt(i);
428
429             if (character === ".") {
430                 if (foundPeriod)
431                     break;
432                 foundPeriod = true;
433             } else if (character !== "-" && character !== "+" && isNaN(parseInt(character))) {
434                 // Found the end already, just scan backwards.
435                 if (i === startPosition.ch) {
436                     end = i;
437                     continue;
438                 }
439
440                 break;
441             }
442
443             start = i;
444         }
445
446         if (isNaN(end)) {
447             for (var i = startPosition.ch + 1; i < line.length; ++i) {
448                 var character = line.charAt(i);
449
450                 if (character === ".") {
451                     if (foundPeriod) {
452                         end = i;
453                         break;
454                     }
455
456                     foundPeriod = true;
457                 } else if (isNaN(parseInt(character))) {
458                     end = i;
459                     break;
460                 }
461
462                 end = i + 1;
463             }
464         }
465
466         // No number range found, pass to another key handler.
467         if (isNaN(start) || isNaN(end))
468             return false;
469
470         var number = parseFloat(line.substring(start, end));
471
472         // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
473         // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
474         var alteredNumber = Number((number + amount).toFixed(6));
475         var alteredNumberString = alteredNumber.toString();
476
477         var from = {line: startPosition.line, ch: start};
478         var to = {line: startPosition.line, ch: end};
479
480         this.replaceRange(alteredNumberString, from, to);
481
482         if (updateSelection) {
483             var previousLength = to.ch - from.ch;
484             var newLength = alteredNumberString.length;
485
486             // Fix up the selection so it follows the increase or decrease in the replacement length.
487             // selectionStart/End may the same object if there is no selection. If that is the case
488             // make only one modification to prevent a double adjustment, and keep it a single object
489             // to avoid CodeMirror inadvertently creating an actual selection range.
490             let diff = (newLength - previousLength);
491             if (selectionStart === selectionEnd)
492                 selectionStart.ch += diff;
493             else {
494                 if (selectionStart.ch > from.ch)
495                     selectionStart.ch += diff;
496                 if (selectionEnd.ch > from.ch)
497                     selectionEnd.ch += diff;
498             }
499
500             this.setSelection(selectionStart, selectionEnd);
501         }
502
503         return true;
504     });
505
506     function alterNumber(amount, codeMirror)
507     {
508         function findNumberToken(position)
509         {
510             // CodeMirror includes the unit in the number token, so searching for
511             // number tokens is the best way to get both the number and unit.
512             var token = codeMirror.getTokenAt(position);
513             if (token && token.type && /\bnumber\b/.test(token.type))
514                 return token;
515             return null;
516         }
517
518         var position = codeMirror.getCursor("head");
519         var token = findNumberToken(position);
520
521         if (!token) {
522             // If the cursor is at the outside beginning of the token, the previous
523             // findNumberToken wont find it. So check the next column for a number too.
524             position.ch += 1;
525             token = findNumberToken(position);
526         }
527
528         if (!token)
529             return CodeMirror.Pass;
530
531         var foundNumber = codeMirror.alterNumberInRange(amount, {ch: token.start, line: position.line}, {ch: token.end, line: position.line}, true);
532         if (!foundNumber)
533             return CodeMirror.Pass;
534     }
535
536     CodeMirror.defineExtension("rectsForRange", function(range) {
537         var lineRects = [];
538
539         for (var line = range.start.line; line <= range.end.line; ++line) {
540             var lineContent = this.getLine(line);
541
542             var startChar = line === range.start.line ? range.start.ch : (lineContent.length - lineContent.trimLeft().length);
543             var endChar = line === range.end.line ? range.end.ch : lineContent.length;
544             var firstCharCoords = this.cursorCoords({ch: startChar, line});
545             var endCharCoords = this.cursorCoords({ch: endChar, line});
546
547             // Handle line wrapping.
548             if (firstCharCoords.bottom !== endCharCoords.bottom) {
549                 var maxY = -Number.MAX_VALUE;
550                 for (var ch = startChar; ch <= endChar; ++ch) {
551                     var coords = this.cursorCoords({ch, line});
552                     if (coords.bottom > maxY) {
553                         if (ch > startChar) {
554                             var maxX = Math.ceil(this.cursorCoords({ch: ch - 1, line}).right);
555                             lineRects.push(new WebInspector.Rect(minX, minY, maxX - minX, maxY - minY));
556                         }
557                         var minX = Math.floor(coords.left);
558                         var minY = Math.floor(coords.top);
559                         maxY = Math.ceil(coords.bottom);
560                     }
561                 }
562                 maxX = Math.ceil(coords.right);
563                 lineRects.push(new WebInspector.Rect(minX, minY, maxX - minX, maxY - minY));
564             } else {
565                 var minX = Math.floor(firstCharCoords.left);
566                 var minY = Math.floor(firstCharCoords.top);
567                 var maxX = Math.ceil(endCharCoords.right);
568                 var maxY = Math.ceil(endCharCoords.bottom);
569                 lineRects.push(new WebInspector.Rect(minX, minY, maxX - minX, maxY - minY));
570             }
571         }
572         return lineRects;
573     });
574
575     function ignoreKey(codeMirror)
576     {
577         // Do nothing to ignore the key.
578     }
579
580     CodeMirror.keyMap["default"] = {
581         "Alt-Up": alterNumber.bind(null, 1),
582         "Ctrl-Alt-Up": alterNumber.bind(null, 0.1),
583         "Shift-Alt-Up": alterNumber.bind(null, 10),
584         "Alt-PageUp": alterNumber.bind(null, 10),
585         "Shift-Alt-PageUp": alterNumber.bind(null, 100),
586         "Alt-Down": alterNumber.bind(null, -1),
587         "Ctrl-Alt-Down": alterNumber.bind(null, -0.1),
588         "Shift-Alt-Down": alterNumber.bind(null, -10),
589         "Alt-PageDown": alterNumber.bind(null, -10),
590         "Shift-Alt-PageDown": alterNumber.bind(null, -100),
591         "Cmd-/": "toggleComment",
592         "Shift-Tab": ignoreKey,
593         fallthrough: "macDefault"
594     };
595
596     // Register some extra MIME-types for CodeMirror. These are in addition to the
597     // ones CodeMirror already registers, like text/html, text/javascript, etc.
598     var extraXMLTypes = ["text/xml", "text/xsl"];
599     extraXMLTypes.forEach(function(type) {
600         CodeMirror.defineMIME(type, "xml");
601     });
602
603     var extraHTMLTypes = ["application/xhtml+xml", "image/svg+xml"];
604     extraHTMLTypes.forEach(function(type) {
605         CodeMirror.defineMIME(type, "htmlmixed");
606     });
607
608     var extraJavaScriptTypes = ["text/ecmascript", "application/javascript", "application/ecmascript", "application/x-javascript",
609         "text/x-javascript", "text/javascript1.1", "text/javascript1.2", "text/javascript1.3", "text/jscript", "text/livescript"];
610     extraJavaScriptTypes.forEach(function(type) {
611         CodeMirror.defineMIME(type, "javascript");
612     });
613
614     var extraJSONTypes = ["application/x-json", "text/x-json", "application/vnd.api+json"];
615     extraJSONTypes.forEach(function(type) {
616         CodeMirror.defineMIME(type, {name: "javascript", json: true});
617     });
618 })();
619
620 WebInspector.compareCodeMirrorPositions = function(a, b)
621 {
622     var lineCompare = a.line - b.line;
623     if (lineCompare !== 0)
624         return lineCompare;
625
626     var aColumn = "ch" in a ? a.ch : Number.MAX_VALUE;
627     var bColumn = "ch" in b ? b.ch : Number.MAX_VALUE;
628     return aColumn - bColumn;
629 };
630
631 WebInspector.walkTokens = function(cm, mode, initialPosition, callback)
632 {
633     let state = CodeMirror.copyState(mode, cm.getTokenAt(initialPosition).state);
634     if (state.localState)
635         state = state.localState;
636
637     let lineCount = cm.lineCount();
638     let abort = false;
639     for (lineNumber = initialPosition.line; !abort && lineNumber < lineCount; ++lineNumber) {
640         let line = cm.getLine(lineNumber);
641         let stream = new CodeMirror.StringStream(line);
642         if (lineNumber === initialPosition.line)
643             stream.start = stream.pos = initialPosition.ch;
644
645         while (!stream.eol()) {
646             let innerMode = CodeMirror.innerMode(mode, state);
647             let tokenType = mode.token(stream, state);
648             if (!callback(tokenType, stream.current())) {
649                 abort = true;
650                 break;
651             }
652             stream.start = stream.pos;
653         }
654     }
655
656     if (!abort)
657         callback(null);
658 }