Modules (188)

CSSUtils

Description

Set of utilities for simple parsing of CSS text.

Dependencies

Variables

Private

_bracketPairs

List of all bracket pairs that is keyed by opening brackets, and the inverted list that is keyed by closing brackets.

Type
{string: string}
    var _bracketPairs = { "{": "}",
                          "[": "]",
                          "(": ")" },
        _invertedBracketPairs = _.invert(_bracketPairs);
Private

_contextCM

We will use this CM to cook css context in case of style attribute value as CM in htmlmixed mode doesn't yet identify this as css context. We provide a no-op display function to run CM without a DOM head.

            var _contextCM = new CodeMirror(function () {}, {
                value: "{" + tagInfo.attr.value.replace(/(^")|("$)/g, ""),
                mode:  "css"
            });

            ctx = TokenUtils.getInitialContext(_contextCM, {line: 0, ch: offset + 1});
        }
        
        if (_isInPropName(ctx)) {
            return _getPropNameInfo(ctx);
        }
        
        if (_isInPropValue(ctx)) {
            return _getRuleInfoStartingFromPropValue(ctx, ctx.editor);
        }
        
        if (_isInAtRule(ctx)) {
            return _getImportUrlInfo(ctx, editor);
        }

        return createInfo();
    }

Functions

Private

_addSelectorsToResults

Converts the results of _findAllMatchingSelectorsInText() into a simpler bag of data and appends those new objects to the given 'resultSelectors' Array.

resultSelectors Array.<{document:Document,lineStart:number,lineEnd:number}>
selectorsToAdd Array.<SelectorInfo>
sourceDoc non-nullable Document
lineOffset non-nullable number
Amount to offset all line number info by. Used if the first line of the parsed CSS text is not the first line of the sourceDoc.
    function _addSelectorsToResults(resultSelectors, selectorsToAdd, sourceDoc, lineOffset) {
        selectorsToAdd.forEach(function (selectorInfo) {
            resultSelectors.push({
                name: getCompleteSelectors(selectorInfo),
                document: sourceDoc,
                lineStart: selectorInfo.ruleStartLine + lineOffset,
                lineEnd: selectorInfo.declListEndLine + lineOffset,
                selectorGroup: selectorInfo.selectorGroup
            });
        });
    }
Private Public API

_findAllMatchingSelectorsInText

Finds all instances of the specified selector in "text". Returns an Array of Objects with start and end properties.

For now, we only support simple selectors. This function will need to change dramatically to support full selectors.

FUTURE: (JRB) It would be nice to eventually use the browser/jquery to do the selector evaluation. One way to do this would be to take the user's HTML, add a special attribute to every tag with a UID, and then construct a DOM (using the commented out code above). Then, give this DOM and the selector to jquery and ask what matches. If the node that the user's cursor is in comes back from jquery, then we know the selector applies.

text non-nullable string
CSS text to search
selector non-nullable string
selector to search for
mode non-nullable string
language mode of the document that text belongs to
Returns: Array.<{selectorGroupStartLine:number,declListEndLine:number,selector:string}>
Array of objects containing the start and end line numbers (0-based, inclusive range) for each matched selector.
    function _findAllMatchingSelectorsInText(text, selector, mode) {
        var allSelectors = extractAllSelectors(text, mode);
        var result = [];

        // For now, we only match the rightmost simple selector, and ignore
        // attribute selectors and pseudo selectors
        var classOrIdSelector = selector[0] === "." || selector[0] === "#";

        // Escape initial "." in selector, if present.
        if (selector[0] === ".") {
            selector = "\\" + selector;
        }

        if (!classOrIdSelector) {
            // Tag selectors must have nothing, whitespace, or a combinator before it.
            selector = "(^|[\\s>+~])" + selector;
        }

        var re = new RegExp(selector + "(\\[[^\\]]*\\]|:{1,2}[\\w-()]+|\\.[\\w-]+|#[\\w-]+)*\\s*$", classOrIdSelector ? "" : "i");
        allSelectors.forEach(function (entry) {
            var actualSelector = entry.selector;
            if (entry.selector.indexOf("&") !== -1 && entry.parentSelectors) {
                var selectorArray = entry.parentSelectors.split(" / ");
                selectorArray.push(entry.selector);
                actualSelector = _getSelectorInFinalCSSForm(selectorArray);
            }
            if (actualSelector.search(re) !== -1) {
                result.push(entry);
            } else if (!classOrIdSelector) {
                // Special case for tag selectors - match "*" as the rightmost character
                if (/\*\s*$/.test(actualSelector)) {
                    result.push(entry);
                }
            }
        });

        return result;
    }
Private

_findMatchingRulesInCSSFiles

Finds matching selectors in CSS files; adds them to 'resultSelectors'

    function _findMatchingRulesInCSSFiles(selector, resultSelectors) {
        var result          = new $.Deferred();

        // Load one CSS file and search its contents
        function _loadFileAndScan(fullPath, selector) {
            var oneFileResult = new $.Deferred();

            DocumentManager.getDocumentForPath(fullPath)
                .done(function (doc) {
                    // Find all matching rules for the given CSS file's content, and add them to the
                    // overall search result
                    var oneCSSFileMatches = _findAllMatchingSelectorsInText(doc.getText(), selector, doc.getLanguage().getMode());
                    _addSelectorsToResults(resultSelectors, oneCSSFileMatches, doc, 0);

                    oneFileResult.resolve();
                })
                .fail(function (error) {
                    console.warn("Unable to read " + fullPath + " during CSS rule search:", error);
                    oneFileResult.resolve();  // still resolve, so the overall result doesn't reject
                });

            return oneFileResult.promise();
        }

        ProjectManager.getAllFiles(ProjectManager.getLanguageFilter(["css", "less", "scss"]))
            .done(function (cssFiles) {
                // Load index of all CSS files; then process each CSS file in turn (see above)
                Async.doInParallel(cssFiles, function (fileInfo, number) {
                    return _loadFileAndScan(fileInfo.fullPath, selector);
                })
                    .then(result.resolve, result.reject);
            });

        return result.promise();
    }
Private

_findMatchingRulesInStyleBlocks

Finds matching selectors in the <style> block of a single HTML file; adds them to 'resultSelectors'

    function _findMatchingRulesInStyleBlocks(htmlDocument, selector, resultSelectors) {
        // HTMLUtils requires a real CodeMirror instance; make sure we can give it the right Editor
        var htmlEditor = EditorManager.getCurrentFullEditor();
        if (htmlEditor.document !== htmlDocument) {
            console.error("Cannot search for <style> blocks in HTML file other than current editor");
            return;
        }

        // Find all <style> blocks in the HTML file
        var styleBlocks = HTMLUtils.findStyleBlocks(htmlEditor);

        styleBlocks.forEach(function (styleBlockInfo) {
            // Search this one <style> block's content, appending results to 'resultSelectors'
            var oneStyleBlockMatches = _findAllMatchingSelectorsInText(styleBlockInfo.text, selector);
            _addSelectorsToResults(resultSelectors, oneStyleBlockMatches, htmlDocument, styleBlockInfo.start.line);
        });
    }
Private

_getContextState

ctx {editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}}
Returns: {tokenize:function,state:string,stateArg:string,context:Object}
    function _getContextState(ctx) {
        if (!ctx || !ctx.token) {
            return null;
        }
        var state = ctx.token.state.localState || ctx.token.state;
        // if state contains a valid html inner state use that first
        if (state.htmlState) {
            state = ctx.token.state.htmlState;
        } else {
            if (!state.context && ctx.token.state.html && ctx.token.state.html.localState) {
                state = ctx.token.state.html.localState;
            }
        }
        return state;
    }
Private

_getImportUrlInfo

context editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
editor non-nullable Editor
Returns: {context: string,offset: number,name: string,index: number,values: Array.<string>,isNewItem: boolean,range: {start: {line: number,ch: number},end: {line: number,ch: number}}}
A CSS context info object.
    function _getImportUrlInfo(ctx, editor) {
        var backwardPos = $.extend({}, ctx.pos),
            forwardPos  = $.extend({}, ctx.pos),
            backwardCtx,
            forwardCtx,
            index = 0,
            propValues = [],
            offset = TokenUtils.offsetInToken(ctx);

        // Currently only support url. May be null if starting to type
        if (ctx.token.type && ctx.token.type !== "string") {
            return createInfo();
        }

        // Move backward to @import and collect data as we go. We return propValues
        // array, but we can only have 1 value, so put all data in first item
        backwardCtx = TokenUtils.getInitialContext(editor._codeMirror, backwardPos);
        propValues[0] = backwardCtx.token.string;

        while (TokenUtils.movePrevToken(backwardCtx)) {
            if (backwardCtx.token.type === "def" && backwardCtx.token.string === "@import") {
                break;
            }

            if (backwardCtx.token.type && backwardCtx.token.type !== "atom" && backwardCtx.token.string !== "url") {
                // Previous token may be white-space
                // Otherwise, previous token may only be "url("
                break;
            }

            propValues[0] = backwardCtx.token.string + propValues[0];
            offset += backwardCtx.token.string.length;
        }

        if (backwardCtx.token.type !== "def" || backwardCtx.token.string !== "@import") {
            // Not in url
            return createInfo();
        }

        // Get value after cursor up until closing paren or newline
        forwardCtx = TokenUtils.getInitialContext(editor._codeMirror, forwardPos);
        do {
            if (!TokenUtils.moveNextToken(forwardCtx)) {
                if (forwardCtx.token.string === "(") {
                    break;
                } else {
                    return createInfo();
                }
            }
            propValues[0] += forwardCtx.token.string;
        } while (forwardCtx.token.string !== ")" && forwardCtx.token.string !== "");

        return createInfo(IMPORT_URL, offset, "", index, propValues, false);
    }
Private

_getPrecedingPropValues

context editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
Returns: ?Array.<string>
An array of all the space/comma seperated tokens before the current cursor position
    function _getPrecedingPropValues(ctx) {
        var lastValue = "",
            curValue,
            propValues = [];
        while (ctx.token.string !== ":" && TokenUtils.movePrevToken(ctx)) {
            if (ctx.token.string === ":" || !_isInPropValue(ctx)) {
                break;
            }

            curValue = ctx.token.string;
            if (lastValue !== "") {
                curValue += lastValue;
            }

            if ((ctx.token.string.length > 0 && !ctx.token.string.match(/\S/)) ||
                    ctx.token.string === ",") {
                lastValue = curValue;
            } else {
                lastValue = "";
                if (propValues.length === 0 || curValue.match(/,\s*$/)) {
                    // stack is empty, or current value ends with a comma
                    // (and optional whitespace), so push it on the stack
                    propValues.push(curValue);
                } else {
                    // current value does not end with a comma (and optional ws) so prepend
                    // to last stack item (e.g. "rgba(50" get broken into 2 tokens)
                    propValues[propValues.length - 1] = curValue + propValues[propValues.length - 1];
                }
            }
        }
        if (propValues.length > 0) {
            propValues.reverse();
        }

        return propValues;
    }
Private

_getPropNameInfo

ctx editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
context
Returns: {context: string,offset: number,name: string,index: number,values: Array.<string>,isNewItem: boolean,range: {start: {line: number,ch: number},end: {line: number,ch: number}}}
A CSS context info object.
    function _getPropNameInfo(ctx) {
        var propName = "",
            offset = TokenUtils.offsetInToken(ctx),
            tokenString = ctx.token.string,
            excludedCharacters = [";", "{", "}"];

        if (ctx.token.type === "property" || ctx.token.type === "property error" ||
                ctx.token.type === "tag") {
            propName = tokenString;
            if (TokenUtils.movePrevToken(ctx) && _hasNonWhitespace(ctx.token.string) &&
                    excludedCharacters.indexOf(ctx.token.string) === -1) {
                propName = ctx.token.string + tokenString;
                offset += ctx.token.string.length;
            }
        } else if (ctx.token.type === "meta" || tokenString === "-") {
            propName = tokenString;
            if (TokenUtils.moveNextToken(ctx) &&
                    (ctx.token.type === "property" || ctx.token.type === "property error" ||
                    ctx.token.type === "tag")) {
                propName += ctx.token.string;
            }
        } else if (_hasNonWhitespace(tokenString) && excludedCharacters.indexOf(tokenString) === -1) {
            // We're not inside the property name context.
            return createInfo();
        } else {
            var testPos = {ch: ctx.pos.ch + 1, line: ctx.pos.line},
                testToken = ctx.editor.getTokenAt(testPos, true);

            if (testToken.type === "property" || testToken.type === "property error" ||
                    testToken.type === "tag") {
                propName = testToken.string;
                offset = 0;
            } else if (testToken.type === "meta" || testToken.string === "-") {
                ctx.pos = testPos;
                ctx.token = testToken;
                return _getPropNameInfo(ctx);
            }
        }

        // If we're in the property name context but not in an existing property name,
        // then reset offset to zero.
        if (propName === "") {
            offset = 0;
        }

        return createInfo(PROP_NAME, offset, propName);
    }
Private

_getPropNameStartingFromPropValue

context editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
Returns: string
the property name of the current rule.
    function _getPropNameStartingFromPropValue(ctx) {
        var ctxClone = $.extend({}, ctx),
            propName = "";
        do {
            // If we're no longer in the property value before seeing a colon, then we don't
            // have a valid property name. Just return an empty string.
            if (ctxClone.token.string !== ":" && !_isInPropValue(ctxClone)) {
                return "";
            }
        } while (ctxClone.token.string !== ":" && TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctxClone));

        if (ctxClone.token.string === ":" && TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctxClone) &&
                (ctxClone.token.type === "property" || ctxClone.token.type === "property error")) {
            propName = ctxClone.token.string;
            if (TokenUtils.movePrevToken(ctxClone) && ctxClone.token.type === "meta") {
                propName = ctxClone.token.string + propName;
            }
        }

        return propName;
    }
Private

_getRangeForPropValue

startCtx editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
context
endCtx editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
context
Returns: {start: {line: number,ch: number},end: {line: number,ch: number}}
A range object.
    function _getRangeForPropValue(startCtx, endCtx) {
        var range = { "start": {},
                      "end": {} };

        // Skip the ":" and any leading whitespace
        while (TokenUtils.moveNextToken(startCtx)) {
            if (_hasNonWhitespace(startCtx.token.string)) {
                break;
            }
        }

        // Skip the trailing whitespace and property separators.
        while (endCtx.token.string === ";" || endCtx.token.string === "}" ||
                !_hasNonWhitespace(endCtx.token.string)) {
            TokenUtils.movePrevToken(endCtx);
        }

        range.start = _.clone(startCtx.pos);
        range.start.ch = startCtx.token.start;

        range.end = _.clone(endCtx.pos);
        range.end.ch = endCtx.token.end;

        return range;
    }
Private

_getRuleInfoStartingFromPropValue

context editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
editor non-nullable Editor
Returns: {context: string,offset: number,name: string,index: number,values: Array.<string>,isNewItem: boolean,range: {start: {line: number,ch: number},end: {line: number,ch: number}}}
A CSS context info object.
    function _getRuleInfoStartingFromPropValue(ctx, editorParam) {
        var editor      = editorParam._codeMirror || editorParam,
            contextDoc  = editor.document || editor.doc,
            propNamePos = $.extend({}, ctx.pos),
            backwardPos = $.extend({}, ctx.pos),
            forwardPos  = $.extend({}, ctx.pos),
            propNameCtx = TokenUtils.getInitialContext(editor, propNamePos),
            backwardCtx,
            forwardCtx,
            lastValue = "",
            propValues = [],
            index = -1,
            offset = TokenUtils.offsetInToken(ctx),
            canAddNewOne = false,
            testPos = {ch: ctx.pos.ch + 1, line: ctx.pos.line},
            testToken = editor.getTokenAt(testPos, true),
            propName,
            range;

        // Get property name first. If we don't have a valid property name, then
        // return a default rule info.
        propName = _getPropNameStartingFromPropValue(propNameCtx);
        if (!propName) {
            return createInfo();
        }

        // Scan backward to collect all preceding property values
        backwardCtx = TokenUtils.getInitialContext(editor, backwardPos);
        propValues = _getPrecedingPropValues(backwardCtx);

        lastValue = "";
        if (ctx.token.string === ":") {
            index = 0;
            canAddNewOne = true;
        } else {
            index = propValues.length - 1;
            if (ctx.token.string === ",") {
                propValues[index] += ctx.token.string;
                index++;
                canAddNewOne = true;
            } else {
                index = (index < 0) ? 0 : index + 1;
                if (ctx.token.string.match(/\S/)) {
                    lastValue = ctx.token.string;
                } else {
                    // Last token is all whitespace
                    canAddNewOne = true;
                    if (index > 0) {
                        // Append all spaces before the cursor to the previous value in values array
                        propValues[index - 1] += ctx.token.string.substr(0, offset);
                    }
                }
            }
        }

        if (canAddNewOne) {
            offset = 0;

            // If pos is at EOL, then there's implied whitespace (newline).
            if (contextDoc.getLine(ctx.pos.line).length > ctx.pos.ch  &&
                    (testToken.string.length === 0 || testToken.string.match(/\S/))) {
                canAddNewOne = false;
            }
        }

        // Scan forward to collect all succeeding property values and append to all propValues.
        forwardCtx = TokenUtils.getInitialContext(editor, forwardPos);
        propValues = propValues.concat(_getSucceedingPropValues(forwardCtx, lastValue));

        if (propValues.length) {
            range = _getRangeForPropValue(backwardCtx, forwardCtx);
        } else {
            // No property value, so just return the cursor pos as range
            range = { "start": _.clone(ctx.pos),
                      "end": _.clone(ctx.pos) };
        }

        // If current index is more than the propValues size, then the cursor is
        // at the end of the existing property values and is ready for adding another one.
        if (index === propValues.length) {
            canAddNewOne = true;
        }

        return createInfo(PROP_VALUE, offset, propName, index, propValues, canAddNewOne, range);
    }
Private

_getSelectorInFinalCSSForm

Converts the given selector array into the actual CSS selectors similar to those generated by a CSS preprocessor.

selectorArray Array.<string>
Returns: string
    function _getSelectorInFinalCSSForm(selectorArray) {
        var finalSelectorArray = [""],
            parentSelectorArray = [],
            group = [];
        _.forEach(selectorArray, function (selector) {
            selector = _stripAtRules(selector);
            group = selector.split(",");
            parentSelectorArray = [];
            _.forEach(group, function (cs) {
                var ampersandIndex = cs.indexOf("&");
                _.forEach(finalSelectorArray, function (ps) {
                    if (ampersandIndex === -1) {
                        cs = _stripAtRules(cs);
                        if (ps.length && cs.length) {
                            ps += " ";
                        }
                        ps += cs;
                    } else {
                        // Replace all instances of & with regexp
                        ps = _stripAtRules(cs.replace(/&/g, ps));
                    }
                    parentSelectorArray.push(ps);
                });
            });
            finalSelectorArray = parentSelectorArray;
        });
        return finalSelectorArray.join(", ");
    }
Private

_getSucceedingPropValues

context editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
currentValue string
The token string at the current cursor position
Returns: ?Array.<string>
An array of all the space/comma seperated tokens after the current cursor position
    function _getSucceedingPropValues(ctx, currentValue) {
        var lastValue = currentValue,
            propValues = [];

        while (ctx.token.string !== ";" && ctx.token.string !== "}" && TokenUtils.moveNextToken(ctx)) {
            if (ctx.token.string === ";" || ctx.token.string === "}") {
                break;
            }
            if (!_isInPropValue(ctx)) {
                lastValue = "";
                break;
            }

            if (lastValue === "") {
                lastValue = ctx.token.string.trim();
            } else if (lastValue.length > 0) {
                if (ctx.token.string.length > 0 && !ctx.token.string.match(/\S/)) {
                    lastValue += ctx.token.string;
                    propValues.push(lastValue);
                    lastValue = "";
                } else if (ctx.token.string === ",") {
                    lastValue += ctx.token.string;
                } else if (lastValue && lastValue.match(/,$/)) {
                    propValues.push(lastValue);
                    if (ctx.token.string.length > 0) {
                        lastValue = ctx.token.string;
                    } else {
                        lastValue = "";
                    }
                } else {
                    // e.g. "rgba(50" gets broken into 2 tokens
                    lastValue += ctx.token.string;
                }
            }
        }
        if (lastValue.length > 0) {
            propValues.push(lastValue);
        }

        return propValues;
    }
Private

_hasNonWhitespace

text non-nullable string
Returns: boolean
true if text has any non whitespace character
    function _hasNonWhitespace(text) {
        return (/\S/.test(text));
    }
Private

_isInAtRule

context editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
Returns: boolean
true if the context is in property value
    function _isInAtRule(ctx) {
        var state = _getContextState(ctx);
        if (!state || !state.context) {
            return false;
        }
        return (state.context.type === "atBlock_parens");
    }
Private

_isInPropName

context editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
Returns: boolean
true if the context is in property name
    function _isInPropName(ctx) {
        var state = _getContextState(ctx),
            lastToken;
        if (!state || !state.context || ctx.token.type === "comment") {
            return false;
        }

        lastToken = state.context.type;
        return (lastToken === "{" || lastToken === "rule" || lastToken === "block");
    }
Private

_isInPropValue

context editor:{CodeMirror},pos:{ch:{string},line:{number}},token:{object}
Returns: boolean
true if the context is in property value
    function _isInPropValue(ctx) {

        function isInsideParens(context) {
            if (context.type !== "parens" || !context.prev) {
                return false;
            }

            if (context.prev.type === "prop") {
                return true;
            }

            return isInsideParens(context.prev);
        }

        var state = _getContextState(ctx);
        if (!state || !state.context || !state.context.prev || ctx.token.type === "comment") {
            return false;
        }

        return ((state.context.type === "prop" &&
                    (state.context.prev.type === "rule" || state.context.prev.type === "block")) ||
                    isInsideParens(state.context));
    }
Private

_removeComments

removes CSS comments from the content

content non-nullable string
to reduce
Returns: string
reduced content
    function _removeComments(content) {
        return content.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\//g, "");
    }
Private

_removeStrings

removes strings from the content

content non-nullable string
to reduce
Returns: string
reduced content
    function _removeStrings(content) {
        // First remove escaped quotes so we can balance unescaped quotes
        // since JavaScript doesn't support negative lookbehind
        var s = content.replace(/\\\"|\\\'/g, "");

        // Now remove strings
        return s.replace(/\"(.*?)\"|\'(.*?)\'/g, "");
    }
Private

_stripAtRules

Helper function to remove whitespaces before and after a selector Returns trimmed selector if it is not an at-rule, or null if it starts with @.

selector string
Returns: string
    function _stripAtRules(selector) {
        selector = selector.trim();
        if (selector.indexOf("@") === 0) {
            return "";
        }
        return selector;
    }
Public API

addRuleToDocument

Adds a new rule to the end of the given document, and returns the range of the added rule and the position of the cursor on the indented blank line within it. Note that the range will not include all the inserted text (we insert extra newlines before and after the rule).

doc Document
The document to insert the rule into.
selector string
The selector to use for the given rule.
useTabChar boolean
Whether to indent with a tab.
indentUnit number
If useTabChar is false, how many spaces to indent with.
Returns: {range: {from: {line: number,ch: number},to: {line: number,ch: number}},pos: {line: number,ch: number}}
The range of the inserted rule and the location where the cursor should be placed.
    function addRuleToDocument(doc, selector, useTabChar, indentUnit) {
        var newRule = "\n" + selector + " {\n",
            blankLineOffset;
        if (useTabChar) {
            newRule += "\t";
            blankLineOffset = 1;
        } else {
            var i;
            for (i = 0; i < indentUnit; i++) {
                newRule += " ";
            }
            blankLineOffset = indentUnit;
        }
        newRule += "\n}\n";

        var docLines = doc.getText().split("\n"),
            lastDocLine = docLines.length - 1,
            lastDocChar = docLines[docLines.length - 1].length;
        doc.replaceRange(newRule, {line: lastDocLine, ch: lastDocChar});
        return {
            range: {
                from: {line: lastDocLine + 1, ch: 0},
                to: {line: lastDocLine + 3, ch: 1}
            },
            pos: {line: lastDocLine + 2, ch: blankLineOffset}
        };
    }
Public API

consolidateRules

In the given rule array (as returned by findMatchingRules()), if multiple rules in a row refer to the same rule (because there were multiple matching selectors), eliminate the redundant rules. Also, always use the selector group if available instead of the original matching selector.

    function consolidateRules(rules) {
        var newRules = [], lastRule;
        rules.forEach(function (rule) {
            if (rule.selectorGroup) {
                rule.name = rule.selectorGroup;
            }
            // Push the entry unless it refers to the same rule as the previous entry.
            if (!(lastRule &&
                     rule.document === lastRule.document &&
                     rule.lineStart === lastRule.lineStart &&
                     rule.lineEnd === lastRule.lineEnd &&
                     rule.selectorGroup === lastRule.selectorGroup)) {
                newRules.push(rule);
            }
            lastRule = rule;
        });
        return newRules;
    }
Private Public API

createInfo

context optional string
A constant string
offset optional number
The offset of the token for a given cursor position
name optional string
Property name of the context
index optional number
The index of the property value for a given cursor position
values optional Array.<string>
An array of property values
isNewItem optional boolean
If this is true, then the value in index refers to the index at which a new item is going to be inserted and should not be used for accessing an existing value in values array.
range optional {start: {line: number, ch: number}, end: {line: number, ch: number}}
A range object with a start position and an end position
Returns: {context: string,offset: number,name: string,index: number,values: Array.<string>,isNewItem: boolean,range: {start: {line: number,ch: number},end: {line: number,ch: number}}}
A CSS context info object.
    function createInfo(context, offset, name, index, values, isNewItem, range) {
        var ruleInfo = { context: context || "",
                         offset: offset || 0,
                         name: name || "",
                         index: -1,
                         values: [],
                         isNewItem: (isNewItem === true),
                         range: range };

        if (context === PROP_VALUE || context === SELECTOR || context === IMPORT_URL) {
            ruleInfo.index = index;
            ruleInfo.values = values;
        }

        return ruleInfo;
    }
Public API

extractAllNamedFlows

Extracts all named flow instances

text non-nullable string
to extract from
Returns: Array.<string>
array of unique flow names found in the content (empty if none)
    function extractAllNamedFlows(text) {
        var namedFlowRegEx = /(?:flow\-(into|from)\:\s*)([\w\-]+)(?:\s*;)/gi,
            result = [],
            names = {},
            thisMatch;

        // Reduce the content so that matches
        // inside strings and comments are ignored
        text = reduceStyleSheetForRegExParsing(text);

        // Find the first match
        thisMatch = namedFlowRegEx.exec(text);

        // Iterate over the matches and add them to result
        while (thisMatch) {
            var thisName = thisMatch[2];

            if (IGNORED_FLOW_NAMES.indexOf(thisName) === -1 && !names.hasOwnProperty(thisName)) {
                names[thisName] = result.push(thisName);
            }
            thisMatch = namedFlowRegEx.exec(text);
        }

        return result;
    }
Public API

extractAllSelectors

Extracts all CSS selectors from the given text Returns an array of SelectorInfo. Each SelectorInfo is an object with the following properties: selector: the text of the selector (note: comma separated selector groups like "h1, h2" are broken into separate selectors) ruleStartLine: line in the text where the rule (including preceding comment) appears ruleStartChar: column in the line where the rule (including preceding comment) starts selectorStartLine: line in the text where the selector appears selectorStartChar: column in the line where the selector starts selectorEndLine: line where the selector ends selectorEndChar: column where the selector ends selectorGroupStartLine: line where the comma-separated selector group (e.g. .foo, .bar, .baz) starts that this selector (e.g. .baz) is part of. Particularly relevant for groups that are on multiple lines. selectorGroupStartChar: column in line where the selector group starts. selectorGroup: the entire selector group containing this selector, or undefined if there is only one selector in the rule. declListStartLine: line where the declaration list for the rule starts declListStartChar: column in line where the declaration list for the rule starts declListEndLine: line where the declaration list for the rule ends declListEndChar: column in the line where the declaration list for the rule ends level: the level of the current selector including any containing @media block in the nesting level count. Use this property with caution since it is primarily for internal parsing use. For example, two sibling selectors may have different levels if one of them is nested inside an @media block and it should not be used for sibling info. parentSelectors: all ancestor selectors separated with '/' if the current selector is a nested one

text non-nullable string
CSS text to extract from
documentMode nullable string
language mode of the document that text belongs to, default to css if undefined.
Returns: Array.<SelectorInfo>
Array with objects specifying selectors.
    function extractAllSelectors(text, documentMode) {
        var state, lines, lineCount,
            token, style, stream, line,
            selectors              = [],
            mode                   = CodeMirror.getMode({indentUnit: 2}, documentMode || "css"),
            currentSelector        = "",
            currentLevel           = 0,
            ruleStartChar          = -1,
            ruleStartLine          = -1,
            selectorStartChar      = -1,
            selectorStartLine      = -1,
            selectorGroupStartLine = -1,
            selectorGroupStartChar = -1,
            declListStartLine      = -1,
            declListStartChar      = -1,
            escapePattern          = new RegExp("\\\\[^\\\\]+", "g"),
            validationPattern      = new RegExp("\\\\([a-f0-9]{6}|[a-f0-9]{4}(\\s|\\\\|$)|[a-f0-9]{2}(\\s|\\\\|$)|.)", "i"),
            _parseRuleList;

        // implement _firstToken()/_nextToken() methods to
        // provide a single stream of tokens

        function _hasStream() {
            while (stream.eol()) {
                line++;
                if (line >= lineCount) {
                    return false;
                }
                if (_hasNonWhitespace(currentSelector)) {
                    // If we are in a current selector and starting a newline,
                    // make sure there is whitespace in the selector
                    currentSelector += " ";
                }
                stream = new CodeMirror.StringStream(lines[line]);
            }
            return true;
        }

        function _firstToken() {
            state = CodeMirror.startState(mode);
            lines = CodeMirror.splitLines(text);
            lineCount = lines.length;
            if (lineCount === 0) {
                return false;
            }
            line = 0;
            stream = new CodeMirror.StringStream(lines[line]);
            if (!_hasStream()) {
                return false;
            }
            style = mode.token(stream, state);
            token = stream.current();
            return true;
        }

        function _nextToken() {
            // advance the stream past this token
            stream.start = stream.pos;
            if (!_hasStream()) {
                return false;
            }
            style = mode.token(stream, state);
            token = stream.current();
            return true;
        }

        function _firstTokenSkippingWhitespace() {
            if (!_firstToken()) {
                return false;
            }
            while (!_hasNonWhitespace(token)) {
                if (!_nextToken()) {
                    return false;
                }
            }
            return true;
        }

        function _nextTokenSkippingWhitespace() {
            if (!_nextToken()) {
                return false;
            }
            while (!_hasNonWhitespace(token)) {
                if (!_nextToken()) {
                    return false;
                }
            }
            return true;
        }

        function _isStartComment() {
            // Also check for line comments used in LESS and SASS.
            return (/^\/[\/\*]/.test(token));
        }

        function _parseComment() {
            // If it is a line comment, then do nothing and just return. Unlike block
            // comment, a line comment is just one single token and the caller always
            // has to find the next token by skipping the current token. So leaving
            // it for the caller to skip the current token.
            if (/^\/\//.test(token)) {
                return;
            }
            while (!/\*\/$/.test(token)) {
                if (!_nextToken()) {
                    break;
                }
            }
        }

        function _nextTokenSkippingComments() {
            if (!_nextToken()) {
                return false;
            }
            while (_isStartComment()) {
                _parseComment();
                if (!_nextToken()) {
                    return false;
                }
            }
            return true;
        }

        function _skipToClosingBracket(startChar) {
            var skippedText = "",
                unmatchedBraces = 0;
            if (!startChar) {
                startChar = "{";
            }
            while (true) {
                if (token.indexOf(startChar) !== -1 && token.indexOf(_bracketPairs[startChar]) === -1) {
                    // Found an opening bracket but not the matching closing bracket in the same token
                    unmatchedBraces++;
                } else if (token === _bracketPairs[startChar]) {
                    unmatchedBraces--;
                    if (unmatchedBraces <= 0) {
                        skippedText += token;
                        return skippedText;
                    }
                }
                skippedText += token;

                if (!_nextTokenSkippingComments()) {
                    return skippedText; // eof
                }
            }
        }

        function _maybeProperty() {
            return (/^-(moz|ms|o|webkit)-$/.test(token) ||
                    (state.state !== "top" && state.state !== "block" && state.state !== "pseudo" &&
                    // Has a semicolon as in "rgb(0,0,0);", but not one of those after a LESS
                    // mixin parameter variable as in ".size(@width; @height)"
                    stream.string.indexOf(";") !== -1 && !/\([^)]+;/.test(stream.string)));
        }

        function _skipProperty() {
            var prevToken = "";
            while (token !== ";") {
                // Skip tokens until the closing brace if we find an interpolated variable.
                if (/[#@]\{$/.test(token) || (token === "{" && /[#@]$/.test(prevToken))) {
                    _skipToClosingBracket("{");
                    if (token === "}") {
                        _nextToken();   // Skip the closing brace
                    }
                    if (token === ";") {
                        break;
                    }
                }
                // If there is a '{' or '}' before the ';',
                // then stop skipping.
                if (token === "{" || token === "}") {
                    return false;   // can't tell if the entire property is skipped
                }
                prevToken = token;
                if (!_nextTokenSkippingComments()) {
                    break;
                }
            }
            return true;    // skip the entire property
        }

        function _getParentSelectors() {
            var j;
            for (j = selectors.length - 1; j >= 0; j--) {
                if (selectors[j].declListEndLine === -1 && selectors[j].level < currentLevel) {
                    return getCompleteSelectors(selectors[j], true);
                }
            }
            return "";
        }

        function _parseSelector(start, level) {

            currentSelector = "";
            selectorStartChar = start;
            selectorStartLine = line;

            // Everything until the next ',' or '{' is part of the current selector
            while ((token !== "," && token !== "{") ||
                    (token === "{" && /[#@]$/.test(currentSelector)) ||
                    (token === "," && !_hasNonWhitespace(currentSelector))) {
                if (token === "{") {
                    // Append the interpolated variable to selector
                    currentSelector += _skipToClosingBracket("{");
                    _nextToken();  // skip the closing brace
                } else if (token === "}" &&
                        (!currentSelector || /:\s*\S/.test(currentSelector) || !/[#@]\{.+/.test(currentSelector))) {
                    // Either empty currentSelector or currentSelector is a CSS property
                    // but not a selector that is in the form of #{$class} or @{class}
                    return false;
                }
                // Clear currentSelector if we're in a property, but make sure we don't treat
                // the semicolors inside a parameter as a property separators.
                if ((token === ";" && state.state !== "parens") ||
                        // Make sure that something like `> li > a {` is not identified as a property
                        (state.state === "prop" && !/\{/.test(stream.string))) {
                    currentSelector = "";
                } else if (token === "(") {
                    // Collect everything inside the parentheses as a whole chunk so that
                    // commas inside the parentheses won't be identified as selector separators
                    // by while loop.
                    if (_hasNonWhitespace(currentSelector)) {
                        currentSelector += _skipToClosingBracket("(");
                    } else {
                        // Nothing in currentSelector yet. Skip to the closing parenthesis
                        // without collecting the selector since a selector cannot start with
                        // an opening parenthesis.
                        _skipToClosingBracket("(");
                    }
                } else if (_hasNonWhitespace(token) || _hasNonWhitespace(currentSelector)) {
                    currentSelector += token;
                }
                if (!_nextTokenSkippingComments()) {
                    return false; // eof
                }
            }

            if (!currentSelector) {
                return false;
            }

            // Unicode character replacement as defined in http://www.w3.org/TR/CSS21/syndata.html#characters
            if (/\\/.test(currentSelector)) {
                // Double replace in case of pattern overlapping (regex improvement?)
                currentSelector = currentSelector.replace(escapePattern, function (escapedToken) {
                    return escapedToken.replace(validationPattern, function (unicodeChar) {
                        unicodeChar = unicodeChar.substr(1);
                        if (unicodeChar.length === 1) {
                            return unicodeChar;
                        } else {
                            if (parseInt(unicodeChar, 16) < 0x10FFFF) {
                                return String.fromCharCode(parseInt(unicodeChar, 16));
                            } else { return String.fromCharCode(0xFFFD); }
                        }
                    });
                });
            }

            currentSelector = currentSelector.trim();
            var startChar = (selectorGroupStartLine === -1) ? selectorStartChar : selectorStartChar + 1;
            var selectorStart = (stream.string.indexOf(currentSelector, selectorStartChar) !== -1) ? stream.string.indexOf(currentSelector, selectorStartChar - currentSelector.length) : startChar;

            if (currentSelector !== "") {
                if (currentLevel < level) {
                    currentLevel++;
                }
                if (ruleStartLine === -1) {
                    ruleStartLine = line;
                    ruleStartChar = stream.start - currentSelector.length;
                }
                var parentSelectors = _getParentSelectors();
                selectors.push({selector: currentSelector,
                                ruleStartLine: ruleStartLine,
                                ruleStartChar: ruleStartChar,
                                selectorStartLine: selectorStartLine,
                                selectorStartChar: selectorStart,
                                declListEndLine: -1,
                                selectorEndLine: line,
                                selectorEndChar: selectorStart + currentSelector.length,
                                selectorGroupStartLine: selectorGroupStartLine,
                                selectorGroupStartChar: selectorGroupStartChar,
                                level: currentLevel,
                                parentSelectors: parentSelectors
                               });
                currentSelector = "";
            }
            selectorStartChar = -1;

            return true;
        }

        function _parseSelectorList(level) {
            selectorGroupStartLine = (stream.string.indexOf(",") !== -1) ? line : -1;
            selectorGroupStartChar = stream.start;

            if (!_parseSelector(stream.start, level)) {
                return false;
            }

            while (token === ",") {
                if (!_nextTokenSkippingComments()) {
                    return false; // eof
                }
                if (!_parseSelector(stream.start, level)) {
                    return false;
                }
            }

            return true;
        }

        function _parseDeclarationList(level) {

            var j;
            declListStartLine = Math.min(line, lineCount - 1);
            declListStartChar = stream.start;

            // Extract the entire selector group we just saw.
            var selectorGroup, sgLine;
            if (selectorGroupStartLine !== -1) {
                selectorGroup = "";
                for (sgLine = selectorGroupStartLine; sgLine <= declListStartLine; sgLine++) {
                    var startChar = 0, endChar = lines[sgLine].length;
                    if (sgLine === selectorGroupStartLine) {
                        startChar = selectorGroupStartChar;
                    } else {
                        selectorGroup += " "; // replace the newline with a single space
                    }
                    if (sgLine === declListStartLine) {
                        endChar = declListStartChar;
                    }
                    selectorGroup += lines[sgLine].substring(startChar, endChar).trim();
                }
                selectorGroup = selectorGroup.trim();
            }

            // assign this declaration list position and selector group to every selector on the stack
            // that doesn't have a declaration list start and end line
            for (j = selectors.length - 1; j >= 0; j--) {
                if (selectors[j].level === level) {
                    if (selectors[j].declListEndLine !== -1) {
                        break;
                    } else {
                        selectors[j].declListStartLine = declListStartLine;
                        selectors[j].declListStartChar = declListStartChar;
                        if (selectorGroup) {
                            selectors[j].selectorGroup = selectorGroup;
                        }
                    }
                }
            }

            var nested = true;
            do {
                // Since we're now in a declaration list, that means we also finished
                // parsing the whole selector group. Therefore, reset selectorGroupStartLine
                // so that next time we parse a selector we know it's a new group
                selectorGroupStartLine = -1;
                selectorGroupStartChar = -1;
                ruleStartLine = -1;
                ruleStartChar = -1;

                if (!nested) {
                    if (currentLevel > 0 && currentLevel === level) {
                        currentLevel--;
                        // Skip past '}'
                        if (token === "}") {
                            _nextTokenSkippingWhitespace();
                        }
                    }
                }
                // Skip past '{' before parsing nested rule list.
                if (token === "{") {
                    _nextTokenSkippingWhitespace();
                }
                nested = _parseRuleList(undefined, currentLevel + 1);

                // assign this declaration list position to every selector on the stack
                // that doesn't have a declaration list end line
                for (j = selectors.length - 1; j >= 0; j--) {
                    if (selectors[j].level < currentLevel) {
                        break;
                    }
                    if (selectors[j].declListEndLine === -1) {
                        selectors[j].declListEndLine = line;
                        selectors[j].declListEndChar = stream.pos - 1; // stream.pos actually points to the char after the }
                    }
                }
            } while (currentLevel > 0 && currentLevel === level);
        }

        function includeCommentInNextRule() {
            if (ruleStartChar !== -1) {
                return false;       // already included
            }
            if (stream.start > 0 && lines[line].substr(0, stream.start).indexOf("}") !== -1) {
                return false;       // on same line as '}', so it's for previous rule
            }
            return true;
        }

        function _isStartAtRule() {
            // Exclude @mixin from at-rule so that we can parse it like a normal rule list
            return (/^@/.test(token) && !/^@mixin/i.test(token) && token !== "@");
        }

        function _followedByPseudoSelector() {
            return (/\}:(enabled|disabled|checked|indeterminate|link|visited|hover|active|focus|target|lang|root|nth-|first-|last-|only-|empty|not)/.test(stream.string));
        }

        function _isVariableInterpolatedProperty() {
            return (/[@#]\{\S+\}(\s*:|.*;)/.test(stream.string) && !_followedByPseudoSelector());
        }

        function _parseAtRule(level) {

            // reset these fields to ignore comments preceding @rules
            ruleStartLine = -1;
            ruleStartChar = -1;
            selectorStartLine = -1;
            selectorStartChar = -1;
            selectorGroupStartLine = -1;
            selectorGroupStartChar = -1;

            if (/@media/i.test(token)) {
                // @media rule holds a rule list

                // Skip everything until the opening '{'
                while (token !== "{") {
                    if (!_nextTokenSkippingComments()) {
                        return; // eof
                    }
                }

                // skip past '{', to next non-ws token
                if (!_nextTokenSkippingWhitespace()) {
                    return; // eof
                }

                if (currentLevel <= level) {
                    currentLevel++;
                }

                // Parse rules until we see '}'
                // Treat media rule as one nested level by
                // calling _parseRuleList with next level.
                _parseRuleList("}", currentLevel + 1);

                if (currentLevel > 0) {
                    currentLevel--;
                }

            } else {
                // This code handles @rules in this format:
                //   @rule ... ;
                // Or any less variable that starts with @var ... ;
                // Skip everything until the next ';'
                while (token !== ";") {
                    // This code handle @rules that use this format:
                    //    @rule ... { ... }
                    // such as @page, @keyframes (also -webkit-keyframes, etc.), and @font-face.
                    // Skip everything including nested braces until the next matching '}'
                    if (token === "{") {
                        _skipToClosingBracket("{");
                        return;
                    }
                    if (!_nextTokenSkippingComments()) {
                        return; // eof
                    }
                }
            }
        }

        // parse a style rule
        function _parseRule(level) {
            if (!_parseSelectorList(level)) {
                return false;
            }

            _parseDeclarationList(level);
            return true;
        }

        _parseRuleList = function (escapeToken, level) {
            while ((!escapeToken) || token !== escapeToken) {
                if (_isVariableInterpolatedProperty()) {
                    if (!_skipProperty()) {
                        // We found a "{" or "}" while skipping a property. Return false to handle the
                        // opening or closing of a block properly.
                        return false;
                    }
                } else if (_isStartAtRule()) {
                    // @rule
                    _parseAtRule(level);
                } else if (_isStartComment()) {
                    // comment - make this part of style rule
                    if (includeCommentInNextRule()) {
                        ruleStartChar = stream.start;
                        ruleStartLine = line;
                    }
                    _parseComment();
                } else if (_maybeProperty()) {
                    // Skip the property.
                    if (!_skipProperty()) {
                        // We found a "{" or "}" while skipping a property. Return false to handle the
                        // opening or closing of a block properly.
                        return false;
                    }
                } else {
                    // Otherwise, it's style rule
                    if (!_parseRule(level === undefined ? 0 : level) && level > 0) {
                        return false;
                    }
                    if (level > 0) {
                        return true;
                    }
                    // Clear ruleStartChar and ruleStartLine in case we have a comment
                    // at the end of previous rule in level 0.
                    ruleStartChar = -1;
                    ruleStartLine = -1;
                }

                if (!_nextTokenSkippingWhitespace()) {
                    break;
                }
            }

            return true;
        };

        // Do parsing

        if (_firstTokenSkippingWhitespace()) {

            // Style sheet is a rule list
            _parseRuleList();
        }

        return selectors;
    }
Public API

findMatchingRules

Return all rules matching the specified selector. For now, we only look at the rightmost simple selector. For example, searching for ".foo" will match these rules: .foo {} div .foo {} div.foo {} div .foo[bar="42"] {} div .foo:hovered {} div .foo::first-child but will not match these rules: .foobar {} .foo .bar {} div .foo .bar {} .foo.bar {}

selector non-nullable string
The selector to match. This can be a tag selector, class selector or id selector
htmlDocument nullable Document
An HTML file for context (so we can search