| /* |
| * Copyright (C) 2013 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| (function () { |
| // By default CodeMirror defines syntax highlighting styles based on token |
| // only and shared styles between modes. This limiting and does not match |
| // what we have done in the Web Inspector. So this modifies the XML, CSS |
| // and JavaScript modes to supply two styles for each token. One for the |
| // token and one with the mode name. |
| |
| function tokenizeLinkString(stream, state) |
| { |
| console.assert(state._linkQuoteCharacter !== undefined); |
| |
| // Eat the string until the same quote is found that started the string. |
| // If this is unquoted, then eat until whitespace or common parse errors. |
| if (state._linkQuoteCharacter) |
| stream.eatWhile(new RegExp("[^" + state._linkQuoteCharacter + "]")); |
| else |
| stream.eatWhile(/[^\s\u00a0=<>\"\']/); |
| |
| // If the stream isn't at the end of line then we found the end quote. |
| // In the case, change _linkTokenize to parse the end of the link next. |
| // Otherwise _linkTokenize will stay as-is to parse more of the link. |
| if (!stream.eol()) |
| state._linkTokenize = tokenizeEndOfLinkString; |
| |
| return "link"; |
| } |
| |
| function tokenizeEndOfLinkString(stream, state) |
| { |
| console.assert(state._linkQuoteCharacter !== undefined); |
| console.assert(state._linkBaseStyle); |
| |
| // Eat the quote character to style it with the base style. |
| if (state._linkQuoteCharacter) |
| stream.eat(state._linkQuoteCharacter); |
| |
| var style = state._linkBaseStyle; |
| |
| // Clean up the state. |
| delete state._linkTokenize; |
| delete state._linkQuoteCharacter; |
| delete state._linkBaseStyle; |
| delete state._srcSetTokenizeState; |
| |
| return style; |
| } |
| |
| function tokenizeSrcSetString(stream, state) |
| { |
| console.assert(state._linkQuoteCharacter !== undefined); |
| |
| if (state._srcSetTokenizeState === "link") { |
| // Eat the string until a space, comma, or ending quote. |
| // If this is unquoted, then eat until whitespace or common parse errors. |
| if (state._linkQuoteCharacter) |
| stream.eatWhile(new RegExp("[^\\s," + state._linkQuoteCharacter + "]")); |
| else |
| stream.eatWhile(/[^\s,\u00a0=<>\"\']/); |
| } else { |
| // Eat the string until a comma, or ending quote. |
| // If this is unquoted, then eat until whitespace or common parse errors. |
| stream.eatSpace(); |
| if (state._linkQuoteCharacter) |
| stream.eatWhile(new RegExp("[^," + state._linkQuoteCharacter + "]")); |
| else |
| stream.eatWhile(/[^\s\u00a0=<>\"\']/); |
| stream.eatWhile(/[\s,]/); |
| } |
| |
| // If the stream isn't at the end of line and we found the end quote |
| // change _linkTokenize to parse the end of the link next. Otherwise |
| // _linkTokenize will stay as-is to parse more of the srcset. |
| if (stream.eol() || (!state._linkQuoteCharacter || stream.peek() === state._linkQuoteCharacter)) |
| state._linkTokenize = tokenizeEndOfLinkString; |
| |
| // Link portion. |
| if (state._srcSetTokenizeState === "link") { |
| state._srcSetTokenizeState = "descriptor"; |
| return "link"; |
| } |
| |
| // Descriptor portion. |
| state._srcSetTokenizeState = "link"; |
| return state._linkBaseStyle; |
| } |
| |
| function extendedXMLToken(stream, state) |
| { |
| if (state._linkTokenize) { |
| // Call the link tokenizer instead. |
| var style = state._linkTokenize(stream, state); |
| return style && (style + " m-" + this.name); |
| } |
| |
| // Remember the start position so we can rewind if needed. |
| var startPosition = stream.pos; |
| var style = this._token(stream, state); |
| if (style === "attribute") { |
| // Look for "href" or "src" attributes. If found then we should |
| // expect a string later that should get the "link" style instead. |
| var text = stream.current().toLowerCase(); |
| if (text === "src" || /\bhref\b/.test(text)) |
| state._expectLink = true; |
| else if (text === "srcset") |
| state._expectSrcSet = true; |
| else { |
| delete state._expectLink; |
| delete state._expectSrcSet; |
| } |
| } else if (state._expectLink && style === "string") { |
| var current = stream.current(); |
| |
| // Unless current token is empty quotes, consume quote character |
| // and tokenize link next. |
| if (current !== "\"\"" && current !== "''") { |
| delete state._expectLink; |
| |
| // This is a link, so setup the state to process it next. |
| state._linkTokenize = tokenizeLinkString; |
| state._linkBaseStyle = style; |
| |
| // The attribute may or may not be quoted. |
| var quote = current[0]; |
| |
| state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null; |
| |
| // Rewind the stream to the start of this token. |
| stream.pos = startPosition; |
| |
| // Eat the open quote of the string so the string style |
| // will be used for the quote character. |
| if (state._linkQuoteCharacter) |
| stream.eat(state._linkQuoteCharacter); |
| } |
| } else if (state._expectSrcSet && style === "string") { |
| var current = stream.current(); |
| |
| // Unless current token is empty quotes, consume quote character |
| // and tokenize link next. |
| if (current !== "\"\"" && current !== "''") { |
| delete state._expectSrcSet; |
| |
| // This is a link, so setup the state to process it next. |
| state._srcSetTokenizeState = "link"; |
| state._linkTokenize = tokenizeSrcSetString; |
| state._linkBaseStyle = style; |
| |
| // The attribute may or may not be quoted. |
| var quote = current[0]; |
| |
| state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null; |
| |
| // Rewind the stream to the start of this token. |
| stream.pos = startPosition; |
| |
| // Eat the open quote of the string so the string style |
| // will be used for the quote character. |
| if (state._linkQuoteCharacter) |
| stream.eat(state._linkQuoteCharacter); |
| } |
| } else if (style) { |
| // We don't expect other tokens between attribute and string since |
| // spaces and the equal character are not tokenized. So if we get |
| // another token before a string then we stop expecting a link. |
| delete state._expectLink; |
| delete state._expectSrcSet; |
| } |
| |
| return style && (style + " m-" + this.name); |
| } |
| |
| function tokenizeCSSURLString(stream, state) |
| { |
| console.assert(state._urlQuoteCharacter); |
| |
| // If we are an unquoted url string, return whitespace blocks as a whitespace token (null). |
| if (state._unquotedURLString && stream.eatSpace()) |
| return null; |
| |
| var ch = null; |
| var escaped = false; |
| var reachedEndOfURL = false; |
| var lastNonWhitespace = stream.pos; |
| var quote = state._urlQuoteCharacter; |
| |
| // Parse characters until the end of the stream/line or a proper end quote character. |
| while ((ch = stream.next()) != null) { |
| if (ch === quote && !escaped) { |
| reachedEndOfURL = true; |
| break; |
| } |
| escaped = !escaped && ch === "\\"; |
| if (!/[\s\u00a0]/.test(ch)) |
| lastNonWhitespace = stream.pos; |
| } |
| |
| // If we are an unquoted url string, do not include trailing whitespace, rewind to the last real character. |
| if (state._unquotedURLString) |
| stream.pos = lastNonWhitespace; |
| |
| // If we have reached the proper the end of the url string, switch to the end tokenizer to reset the state. |
| if (reachedEndOfURL) { |
| if (!state._unquotedURLString) |
| stream.backUp(1); |
| this._urlTokenize = tokenizeEndOfCSSURLString; |
| } |
| |
| return "link"; |
| } |
| |
| function tokenizeEndOfCSSURLString(stream, state) |
| { |
| console.assert(state._urlQuoteCharacter); |
| console.assert(state._urlBaseStyle); |
| |
| // Eat the quote character to style it with the base style. |
| if (!state._unquotedURLString) |
| stream.eat(state._urlQuoteCharacter); |
| |
| var style = state._urlBaseStyle; |
| |
| delete state._urlTokenize; |
| delete state._urlQuoteCharacter; |
| delete state._urlBaseStyle; |
| |
| return style; |
| } |
| |
| function extendedCSSToken(stream, state) |
| { |
| var hexColorRegex = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/g; |
| |
| if (state._urlTokenize) { |
| // Call the link tokenizer instead. |
| var style = state._urlTokenize(stream, state); |
| return style && (style + " m-" + (this.alternateName || this.name)); |
| } |
| |
| // Remember the start position so we can rewind if needed. |
| var startPosition = stream.pos; |
| var style = this._token(stream, state); |
| |
| if (style) { |
| if (style === "atom") { |
| if (stream.current() === "url") { |
| // If the current text is "url" then we should expect the next string token to be a link. |
| state._expectLink = true; |
| } else if (hexColorRegex.test(stream.current())) |
| style = style + " hex-color"; |
| } else if (state._expectLink) { |
| delete state._expectLink; |
| |
| if (style === "string") { |
| // This is a link, so setup the state to process it next. |
| state._urlTokenize = tokenizeCSSURLString; |
| state._urlBaseStyle = style; |
| |
| // The url may or may not be quoted. |
| var quote = stream.current()[0]; |
| state._urlQuoteCharacter = quote === "'" || quote === "\"" ? quote : ")"; |
| state._unquotedURLString = state._urlQuoteCharacter === ")"; |
| |
| // Rewind the stream to the start of this token. |
| stream.pos = startPosition; |
| |
| // Eat the open quote of the string so the string style |
| // will be used for the quote character. |
| if (!state._unquotedURLString) |
| stream.eat(state._urlQuoteCharacter); |
| } |
| } |
| } |
| |
| return style && (style + " m-" + (this.alternateName || this.name)); |
| } |
| |
| function extendedToken(stream, state) |
| { |
| // CodeMirror moves the original token function to _token when we extended it. |
| // So call it to get the style that we will add an additional class name to. |
| var style = this._token(stream, state); |
| return style && (style + " m-" + (this.alternateName || this.name)); |
| } |
| |
| function extendedCSSRuleStartState(base) |
| { |
| // CodeMirror moves the original token function to _startState when we extended it. |
| // So call it to get the original start state that we will modify. |
| var state = this._startState(base); |
| |
| // Start off the state stack like it has already parsed a rule. This causes everything |
| // after to be parsed as properties in a rule. |
| state.state = "block"; |
| state.context.type = "block"; |
| |
| return state; |
| } |
| |
| function scrollCursorIntoView(codeMirror, event) |
| { |
| // We don't want to use the default implementation since it can cause massive jumping |
| // when the editor is contained inside overflow elements. |
| event.preventDefault(); |
| |
| function delayedWork() |
| { |
| // Don't try to scroll unless the editor is focused. |
| if (!codeMirror.getWrapperElement().classList.contains("CodeMirror-focused")) |
| return; |
| |
| // The cursor element can contain multiple cursors. The first one is the blinky cursor, |
| // which is the one we want to scroll into view. It can be missing, so check first. |
| var cursorElement = codeMirror.getScrollerElement().getElementsByClassName("CodeMirror-cursor")[0]; |
| if (cursorElement) |
| cursorElement.scrollIntoViewIfNeeded(false); |
| } |
| |
| // We need to delay this because CodeMirror can fire scrollCursorIntoView as a view is being blurred |
| // and another is being focused. The blurred editor still has the focused state when this event fires. |
| // We don't want to scroll the blurred editor into view, only the focused editor. |
| setTimeout(delayedWork, 0); |
| } |
| |
| CodeMirror.extendMode("css", {token: extendedCSSToken}); |
| CodeMirror.extendMode("xml", {token: extendedXMLToken}); |
| CodeMirror.extendMode("javascript", {token: extendedToken}); |
| |
| CodeMirror.defineMode("css-rule", CodeMirror.modes.css); |
| CodeMirror.extendMode("css-rule", {token: extendedCSSToken, startState: extendedCSSRuleStartState, alternateName: "css"}); |
| |
| CodeMirror.defineInitHook(function(codeMirror) { |
| codeMirror.on("scrollCursorIntoView", scrollCursorIntoView); |
| }); |
| |
| const maximumNeighboringWhitespaceCharacters = 16; |
| CodeMirror.defineOption("showWhitespaceCharacters", false, function(cm, value, old) { |
| if (!value || (old && old !== CodeMirror.Init)) { |
| cm.removeOverlay("whitespace"); |
| return; |
| } |
| |
| cm.addOverlay({ |
| name: "whitespace", |
| token(stream) { |
| if (stream.peek() === " ") { |
| let count = 0; |
| while (count < maximumNeighboringWhitespaceCharacters && stream.peek() === " ") { |
| ++count; |
| stream.next(); |
| } |
| return `whitespace whitespace-${count}`; |
| } |
| |
| while (!stream.eol() && stream.peek() !== " ") |
| stream.next(); |
| |
| return null; |
| } |
| }); |
| }); |
| |
| CodeMirror.defineExtension("hasLineClass", function(line, where, className) { |
| // This matches the arguments to addLineClass and removeLineClass. |
| var classProperty = where === "text" ? "textClass" : (where === "background" ? "bgClass" : "wrapClass"); |
| var lineInfo = this.lineInfo(line); |
| if (!lineInfo) |
| return false; |
| |
| if (!lineInfo[classProperty]) |
| return false; |
| |
| // Test for the simple case. |
| if (lineInfo[classProperty] === className) |
| return true; |
| |
| // Do a quick check for the substring. This is faster than a regex, which requires escaping the input first. |
| var index = lineInfo[classProperty].indexOf(className); |
| if (index === -1) |
| return false; |
| |
| // Check that it is surrounded by spaces. Add padding spaces first to work with beginning and end of string cases. |
| var paddedClass = " " + lineInfo[classProperty] + " "; |
| return paddedClass.indexOf(" " + className + " ", index) !== -1; |
| }); |
| |
| CodeMirror.defineExtension("setUniqueBookmark", function(position, options) { |
| var marks = this.findMarksAt(position); |
| for (var i = 0; i < marks.length; ++i) { |
| if (marks[i].__uniqueBookmark) { |
| marks[i].clear(); |
| break; |
| } |
| } |
| |
| var uniqueBookmark = this.setBookmark(position, options); |
| uniqueBookmark.__uniqueBookmark = true; |
| return uniqueBookmark; |
| }); |
| |
| CodeMirror.defineExtension("toggleLineClass", function(line, where, className) { |
| if (this.hasLineClass(line, where, className)) { |
| this.removeLineClass(line, where, className); |
| return false; |
| } |
| |
| this.addLineClass(line, where, className); |
| return true; |
| }); |
| |
| CodeMirror.defineExtension("alterNumberInRange", function(amount, startPosition, endPosition, updateSelection) { |
| // We don't try if the range is multiline, pass to another key handler. |
| if (startPosition.line !== endPosition.line) |
| return false; |
| |
| if (updateSelection) { |
| // Remember the cursor position/selection. |
| var selectionStart = this.getCursor("start"); |
| var selectionEnd = this.getCursor("end"); |
| } |
| |
| var line = this.getLine(startPosition.line); |
| |
| var foundPeriod = false; |
| |
| var start = NaN; |
| var end = NaN; |
| |
| for (var i = startPosition.ch; i >= 0; --i) { |
| var character = line.charAt(i); |
| |
| if (character === ".") { |
| if (foundPeriod) |
| break; |
| foundPeriod = true; |
| } else if (character !== "-" && character !== "+" && isNaN(parseInt(character))) { |
| // Found the end already, just scan backwards. |
| if (i === startPosition.ch) { |
| end = i; |
| continue; |
| } |
| |
| break; |
| } |
| |
| start = i; |
| } |
| |
| if (isNaN(end)) { |
| for (var i = startPosition.ch + 1; i < line.length; ++i) { |
| var character = line.charAt(i); |
| |
| if (character === ".") { |
| if (foundPeriod) { |
| end = i; |
| break; |
| } |
| |
| foundPeriod = true; |
| } else if (isNaN(parseInt(character))) { |
| end = i; |
| break; |
| } |
| |
| end = i + 1; |
| } |
| } |
| |
| // No number range found, pass to another key handler. |
| if (isNaN(start) || isNaN(end)) |
| return false; |
| |
| var number = parseFloat(line.substring(start, end)); |
| |
| // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns. |
| // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1. |
| var alteredNumber = Number((number + amount).toFixed(6)); |
| var alteredNumberString = alteredNumber.toString(); |
| |
| var from = {line: startPosition.line, ch: start}; |
| var to = {line: startPosition.line, ch: end}; |
| |
| this.replaceRange(alteredNumberString, from, to); |
| |
| if (updateSelection) { |
| var previousLength = to.ch - from.ch; |
| var newLength = alteredNumberString.length; |
| |
| // Fix up the selection so it follows the increase or decrease in the replacement length. |
| // selectionStart/End may the same object if there is no selection. If that is the case |
| // make only one modification to prevent a double adjustment, and keep it a single object |
| // to avoid CodeMirror inadvertently creating an actual selection range. |
| let diff = newLength - previousLength; |
| if (selectionStart === selectionEnd) |
| selectionStart.ch += diff; |
| else { |
| if (selectionStart.ch > from.ch) |
| selectionStart.ch += diff; |
| if (selectionEnd.ch > from.ch) |
| selectionEnd.ch += diff; |
| } |
| |
| this.setSelection(selectionStart, selectionEnd); |
| } |
| |
| return true; |
| }); |
| |
| function alterNumber(amount, codeMirror) |
| { |
| function findNumberToken(position) |
| { |
| // CodeMirror includes the unit in the number token, so searching for |
| // number tokens is the best way to get both the number and unit. |
| var token = codeMirror.getTokenAt(position); |
| if (token && token.type && /\bnumber\b/.test(token.type)) |
| return token; |
| return null; |
| } |
| |
| var position = codeMirror.getCursor("head"); |
| var token = findNumberToken(position); |
| |
| if (!token) { |
| // If the cursor is at the outside beginning of the token, the previous |
| // findNumberToken wont find it. So check the next column for a number too. |
| position.ch += 1; |
| token = findNumberToken(position); |
| } |
| |
| if (!token) |
| return CodeMirror.Pass; |
| |
| var foundNumber = codeMirror.alterNumberInRange(amount, {ch: token.start, line: position.line}, {ch: token.end, line: position.line}, true); |
| if (!foundNumber) |
| return CodeMirror.Pass; |
| } |
| |
| CodeMirror.defineExtension("rectsForRange", function(range) { |
| var lineRects = []; |
| |
| for (var line = range.start.line; line <= range.end.line; ++line) { |
| var lineContent = this.getLine(line); |
| |
| var startChar = line === range.start.line ? range.start.ch : (lineContent.length - lineContent.trimLeft().length); |
| var endChar = line === range.end.line ? range.end.ch : lineContent.length; |
| var firstCharCoords = this.cursorCoords({ch: startChar, line}); |
| var endCharCoords = this.cursorCoords({ch: endChar, line}); |
| |
| // Handle line wrapping. |
| if (firstCharCoords.bottom !== endCharCoords.bottom) { |
| var maxY = -Number.MAX_VALUE; |
| for (var ch = startChar; ch <= endChar; ++ch) { |
| var coords = this.cursorCoords({ch, line}); |
| if (coords.bottom > maxY) { |
| if (ch > startChar) { |
| var maxX = Math.ceil(this.cursorCoords({ch: ch - 1, line}).right); |
| lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY)); |
| } |
| var minX = Math.floor(coords.left); |
| var minY = Math.floor(coords.top); |
| maxY = Math.ceil(coords.bottom); |
| } |
| } |
| maxX = Math.ceil(coords.right); |
| lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY)); |
| } else { |
| var minX = Math.floor(firstCharCoords.left); |
| var minY = Math.floor(firstCharCoords.top); |
| var maxX = Math.ceil(endCharCoords.right); |
| var maxY = Math.ceil(endCharCoords.bottom); |
| lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY)); |
| } |
| } |
| return lineRects; |
| }); |
| |
| let mac = WI.Platform.name === "mac"; |
| |
| CodeMirror.keyMap["default"] = { |
| "Alt-Up": alterNumber.bind(null, 1), |
| "Ctrl-Alt-Up": alterNumber.bind(null, 0.1), |
| "Shift-Alt-Up": alterNumber.bind(null, 10), |
| "Alt-PageUp": alterNumber.bind(null, 10), |
| "Shift-Alt-PageUp": alterNumber.bind(null, 100), |
| "Alt-Down": alterNumber.bind(null, -1), |
| "Ctrl-Alt-Down": alterNumber.bind(null, -0.1), |
| "Shift-Alt-Down": alterNumber.bind(null, -10), |
| "Alt-PageDown": alterNumber.bind(null, -10), |
| "Shift-Alt-PageDown": alterNumber.bind(null, -100), |
| "Cmd-/": "toggleComment", |
| "Cmd-D": "selectNextOccurrence", |
| "Shift-Tab": "indentLess", |
| fallthrough: mac ? "macDefault" : "pcDefault" |
| }; |
| |
| // Register some extra MIME-types for CodeMirror. These are in addition to the |
| // ones CodeMirror already registers, like text/html, text/javascript, etc. |
| var extraXMLTypes = ["text/xml", "text/xsl"]; |
| extraXMLTypes.forEach(function(type) { |
| CodeMirror.defineMIME(type, "xml"); |
| }); |
| |
| var extraHTMLTypes = ["application/xhtml+xml", "image/svg+xml"]; |
| extraHTMLTypes.forEach(function(type) { |
| CodeMirror.defineMIME(type, "htmlmixed"); |
| }); |
| |
| var extraJavaScriptTypes = ["text/ecmascript", "application/javascript", "application/ecmascript", "application/x-javascript", |
| "text/x-javascript", "text/javascript1.1", "text/javascript1.2", "text/javascript1.3", "text/jscript", "text/livescript"]; |
| extraJavaScriptTypes.forEach(function(type) { |
| CodeMirror.defineMIME(type, "javascript"); |
| }); |
| |
| var extraJSONTypes = ["application/x-json", "text/x-json", "application/vnd.api+json"]; |
| extraJSONTypes.forEach(function(type) { |
| CodeMirror.defineMIME(type, {name: "javascript", json: true}); |
| }); |
| })(); |
| |
| WI.compareCodeMirrorPositions = function(a, b) |
| { |
| var lineCompare = a.line - b.line; |
| if (lineCompare !== 0) |
| return lineCompare; |
| |
| var aColumn = "ch" in a ? a.ch : Number.MAX_VALUE; |
| var bColumn = "ch" in b ? b.ch : Number.MAX_VALUE; |
| return aColumn - bColumn; |
| }; |
| |
| WI.walkTokens = function(cm, mode, initialPosition, callback) |
| { |
| let state = CodeMirror.copyState(mode, cm.getTokenAt(initialPosition).state); |
| if (state.localState) |
| state = state.localState; |
| |
| let lineCount = cm.lineCount(); |
| let abort = false; |
| for (let lineNumber = initialPosition.line; !abort && lineNumber < lineCount; ++lineNumber) { |
| let line = cm.getLine(lineNumber); |
| let stream = new CodeMirror.StringStream(line); |
| if (lineNumber === initialPosition.line) |
| stream.start = stream.pos = initialPosition.ch; |
| |
| while (!stream.eol()) { |
| let tokenType = mode.token(stream, state); |
| if (!callback(tokenType, stream.current())) { |
| abort = true; |
| break; |
| } |
| stream.start = stream.pos; |
| } |
| } |
| |
| if (!abort) |
| callback(null); |
| }; |
| |
| WI.tokenizeCSSValue = function(cssValue) |
| { |
| const rulePrefix = "*{X:"; |
| let cssRule = rulePrefix + cssValue + "}"; |
| let tokens = []; |
| |
| let mode = CodeMirror.getMode({indentUnit: 0}, "text/css"); |
| let state = CodeMirror.startState(mode); |
| let stream = new CodeMirror.StringStream(cssRule); |
| |
| function processToken(token, tokenType, column) { |
| if (column < rulePrefix.length) |
| return; |
| |
| if (token === "}" && !tokenType) |
| return; |
| |
| tokens.push({value: token, type: tokenType}); |
| } |
| |
| while (!stream.eol()) { |
| let style = mode.token(stream, state); |
| let value = stream.current(); |
| processToken(value, style, stream.start); |
| stream.start = stream.pos; |
| } |
| |
| return tokens; |
| }; |