Set of utilities for simple parsing of CSS text.
List of all bracket pairs that is keyed by opening brackets, and the inverted list that is keyed by closing brackets.
var _bracketPairs = { "{": "}",
"[": "]",
"(": ")" },
_invertedBracketPairs = _.invert(_bracketPairs);
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();
}
Converts the results of _findAllMatchingSelectorsInText() into a simpler bag of data and appends those new objects to the given 'resultSelectors' Array.
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
});
});
}
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.
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;
}
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();
}
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);
});
}
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;
}
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);
}
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;
}
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);
}
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;
}
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;
}
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);
}
Converts the given selector array into the actual CSS selectors similar to those generated by a CSS preprocessor.
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(", ");
}
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;
}
function _hasNonWhitespace(text) {
return (/\S/.test(text));
}
function _isInAtRule(ctx) {
var state = _getContextState(ctx);
if (!state || !state.context) {
return false;
}
return (state.context.type === "atBlock_parens");
}
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");
}
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));
}
removes CSS comments from the content
function _removeComments(content) {
return content.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\//g, "");
}
removes strings from the 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, "");
}
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 @.
function _stripAtRules(selector) {
selector = selector.trim();
if (selector.indexOf("@") === 0) {
return "";
}
return selector;
}
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).
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}
};
}
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;
}
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;
}
Extracts all named flow instances
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;
}
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
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;
}
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 {}