let traverseForward = check => { let location; // Backward loop to determine the beginning location of the selector. do { let lineText = sourceArray[line]; if (line == caret.line) { lineText = lineText.substring(caret.ch); } let prevToken = undefined; let tokens = cssTokenizer(lineText); let found = false; let ech = line == caret.line ? caret.ch : 0; for (let token of tokens) { // If the line is completely spaces, handle it differently if (lineText.trim() == "") { limitedSource += lineText; } else { limitedSource += sourceArray[line] .substring(ech + token.startOffset, ech + token.endOffset); } // Whitespace cannot change state. if (token.tokenType == "whitespace") { prevToken = token; continue; } let state = this.resolveState(limitedSource, { line: line, ch: token.endOffset + ech }); if (check(state)) { if (prevToken && prevToken.tokenType == "whitespace") { token = prevToken; } location = { line: line, ch: token.startOffset + ech }; found = true; break; } prevToken = token; } limitedSource += "\n"; if (found) { break; } } while (line++ < sourceArray.length); return location; };
/** * Tokenizes a CSS Filter value and returns an array of {name, value} pairs. * * @param {String} css CSS Filter value to be parsed * @return {Array} An array of {name, value} pairs */ function tokenizeFilterValue(css) { let filters = []; let depth = 0; if (SPECIAL_VALUES.has(css)) { return filters; } let state = "initial"; let name; let contents; for (let token of cssTokenizer(css)) { switch (state) { case "initial": if (token.tokenType === "function") { name = token.text; contents = ""; state = "function"; depth = 1; } else if (token.tokenType === "url" || token.tokenType === "bad_url") { // Extract the quoting style from the url. let originalText = css.substring(token.startOffset, token.endOffset); let [, quote] = /^url\([ \t\r\n\f]*(["']?)/i.exec(originalText); filters.push({name: "url", value: token.text.trim(), quote: quote}); // Leave state as "initial" because the URL token includes // the trailing close paren. } break; case "function": if (token.tokenType === "symbol" && token.text === ")") { --depth; if (depth === 0) { filters.push({name: name, value: contents.trim()}); state = "initial"; break; } } contents += css.substring(token.startOffset, token.endOffset); if (token.tokenType === "function" || (token.tokenType === "symbol" && token.text === "(")) { ++depth; } break; } } return filters; }
let traverseBackwards = (check, isValue) => { let location; // Backward loop to determine the beginning location of the selector. do { let lineText = sourceArray[line]; if (line == caret.line) { lineText = lineText.substring(0, caret.ch); } let tokens = Array.from(cssTokenizer(lineText)); let found = false; for (let i = tokens.length - 1; i >= 0; i--) { let token = tokens[i]; // If the line is completely spaces, handle it differently if (lineText.trim() == "") { limitedSource = limitedSource.slice(0, -1 * lineText.length); } else { let length = token.endOffset - token.startOffset; limitedSource = limitedSource.slice(0, -1 * length); } // Whitespace cannot change state. if (token.tokenType == "whitespace") { continue; } let state = this.resolveState(limitedSource, { line: line, ch: token.startOffset }); if (check(state)) { if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") { token = tokens[i + 1]; } location = { line: line, ch: isValue ? token.endOffset : token.startOffset }; found = true; break; } } limitedSource = limitedSource.slice(0, -1); if (found) { break; } } while (line-- >= 0); return location; };
/** * Tokenizes a CSS Filter value and returns an array of {name, value} pairs. * * @param {String} css CSS Filter value to be parsed * @return {Array} An array of {name, value} pairs */ function tokenizeFilterValue(css) { let filters = []; let depth = 0; if (css === "none") { return filters; } let state = "initial"; let name; let contents; for (let token of cssTokenizer(css)) { switch (state) { case "initial": if (token.tokenType === "function") { name = token.text; contents = ""; state = "function"; depth = 1; } else if (token.tokenType === "url" || token.tokenType === "bad_url") { filters.push({name: "url", value: token.text.trim()}); // Leave state as "initial" because the URL token includes // the trailing close paren. } break; case "function": if (token.tokenType === "symbol" && token.text === ")") { --depth; if (depth === 0) { filters.push({name: name, value: contents.trim()}); state = "initial"; break; } } contents += css.substring(token.startOffset, token.endOffset); if (token.tokenType === "function" || (token.tokenType === "symbol" && token.text === "(")) { ++depth; } break; } } return filters; }
getInfoAt: function (source, caret) { // Limits the input source till the {line, ch} caret position function limit(source, {line, ch}) { line++; let list = source.split("\n"); if (list.length < line) { return source; } if (line == 1) { return list[0].slice(0, ch); } return [...list.slice(0, line - 1), list[line - 1].slice(0, ch)].join("\n"); } // Get the state at the given line, ch let state = this.resolveState(limit(source, caret), caret); let propertyName = this.propertyName; let {line, ch} = caret; let sourceArray = source.split("\n"); let limitedSource = limit(source, caret); /** * Method to traverse forwards from the caret location to figure out the * ending point of a selector or css value. * * @param {function} check * A method which takes the current state as an input and determines * whether the state changed or not. */ let traverseForward = check => { let location; // Backward loop to determine the beginning location of the selector. do { let lineText = sourceArray[line]; if (line == caret.line) { lineText = lineText.substring(caret.ch); } let prevToken = undefined; let tokens = cssTokenizer(lineText); let found = false; let ech = line == caret.line ? caret.ch : 0; for (let token of tokens) { // If the line is completely spaces, handle it differently if (lineText.trim() == "") { limitedSource += lineText; } else { limitedSource += sourceArray[line] .substring(ech + token.startOffset, ech + token.endOffset); } // Whitespace cannot change state. if (token.tokenType == "whitespace") { prevToken = token; continue; } let state = this.resolveState(limitedSource, { line: line, ch: token.endOffset + ech }); if (check(state)) { if (prevToken && prevToken.tokenType == "whitespace") { token = prevToken; } location = { line: line, ch: token.startOffset + ech }; found = true; break; } prevToken = token; } limitedSource += "\n"; if (found) { break; } } while (line++ < sourceArray.length); return location; }; /** * Method to traverse backwards from the caret location to figure out the * starting point of a selector or css value. * * @param {function} check * A method which takes the current state as an input and determines * whether the state changed or not. * @param {boolean} isValue * true if the traversal is being done for a css value state. */ let traverseBackwards = (check, isValue) => { let location; // Backward loop to determine the beginning location of the selector. do { let lineText = sourceArray[line]; if (line == caret.line) { lineText = lineText.substring(0, caret.ch); } let tokens = Array.from(cssTokenizer(lineText)); let found = false; for (let i = tokens.length - 1; i >= 0; i--) { let token = tokens[i]; // If the line is completely spaces, handle it differently if (lineText.trim() == "") { limitedSource = limitedSource.slice(0, -1 * lineText.length); } else { let length = token.endOffset - token.startOffset; limitedSource = limitedSource.slice(0, -1 * length); } // Whitespace cannot change state. if (token.tokenType == "whitespace") { continue; } let state = this.resolveState(limitedSource, { line: line, ch: token.startOffset }); if (check(state)) { if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") { token = tokens[i + 1]; } location = { line: line, ch: isValue ? token.endOffset : token.startOffset }; found = true; break; } } limitedSource = limitedSource.slice(0, -1); if (found) { break; } } while (line-- >= 0); return location; }; if (state == CSS_STATES.selector) { // For selector state, the ending and starting point of the selector is // either when the state changes or the selector becomes empty and a // single selector can span multiple lines. // Backward loop to determine the beginning location of the selector. let start = traverseBackwards(state => { return (state != CSS_STATES.selector || (this.selector == "" && this.selectorBeforeNot == null)); }); line = caret.line; limitedSource = limit(source, caret); // Forward loop to determine the ending location of the selector. let end = traverseForward(state => { return (state != CSS_STATES.selector || (this.selector == "" && this.selectorBeforeNot == null)); }); // Since we have start and end positions, figure out the whole selector. let selector = source.split("\n").slice(start.line, end.line + 1); selector[selector.length - 1] = selector[selector.length - 1].substring(0, end.ch); selector[0] = selector[0].substring(start.ch); selector = selector.join("\n"); return { state: state, selector: selector, loc: { start: start, end: end } }; } else if (state == CSS_STATES.property) { // A property can only be a single word and thus very easy to calculate. let tokens = cssTokenizer(sourceArray[line]); for (let token of tokens) { // Note that, because we're tokenizing a single line, the // token's offset is also the column number. if (token.startOffset <= ch && token.endOffset >= ch) { return { state: state, propertyName: token.text, selectors: this.selectors, loc: { start: { line: line, ch: token.startOffset }, end: { line: line, ch: token.endOffset } } }; } } } else if (state == CSS_STATES.value) { // CSS value can be multiline too, so we go forward and backwards to // determine the bounds of the value at caret let start = traverseBackwards(state => state != CSS_STATES.value, true); line = caret.line; limitedSource = limit(source, caret); let end = traverseForward(state => state != CSS_STATES.value); let value = source.split("\n").slice(start.line, end.line + 1); value[value.length - 1] = value[value.length - 1].substring(0, end.ch); value[0] = value[0].substring(start.ch); value = value.join("\n"); return { state: state, propertyName: propertyName, selectors: this.selectors, value: value, loc: { start: start, end: end } }; } return null; }