1358 lines
46 KiB
JavaScript
1358 lines
46 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const {
|
|
cssTokenizer,
|
|
cssTokenizerWithLineColumn,
|
|
} = require("resource://devtools/shared/css/parsing-utils.js");
|
|
|
|
/**
|
|
* Here is what this file (+ css-parsing-utils.js) do.
|
|
*
|
|
* The main objective here is to provide as much suggestions to the user editing
|
|
* a stylesheet in Style Editor. The possible things that can be suggested are:
|
|
* - CSS property names
|
|
* - CSS property values
|
|
* - CSS Selectors
|
|
* - Some other known CSS keywords
|
|
*
|
|
* Gecko provides a list of both property names and their corresponding values.
|
|
* We take out a list of matching selectors using the Inspector actor's
|
|
* `getSuggestionsForQuery` method. Now the only thing is to parse the CSS being
|
|
* edited by the user, figure out what token or word is being written and last
|
|
* but the most difficult, what is being edited.
|
|
*
|
|
* The file 'css-parsing-utils' helps to convert the CSS into meaningful tokens,
|
|
* each having a certain type associated with it. These tokens help us to figure
|
|
* out the currently edited word and to write a CSS state machine to figure out
|
|
* what the user is currently editing (e.g. a selector or a property or a value,
|
|
* or even fine grained information like an id in the selector).
|
|
*
|
|
* The `resolveState` method iterated over the tokens spitted out by the
|
|
* tokenizer, using switch cases, follows a state machine logic and finally
|
|
* figures out these informations:
|
|
* - The state of the CSS at the cursor (one out of CSS_STATES)
|
|
* - The current token that is being edited `completing`
|
|
* - If the state is "selector", the selector state (one of SELECTOR_STATES)
|
|
* - If the state is "selector", the current selector till the cursor
|
|
* - If the state is "value", the corresponding property name
|
|
*
|
|
* In case of "value" and "property" states, we simply use the information
|
|
* provided by Gecko to filter out the possible suggestions.
|
|
* For "selector" state, we request the Inspector actor to query the page DOM
|
|
* and filter out the possible suggestions.
|
|
* For "media" and "keyframes" state, the only possible suggestions for now are
|
|
* "media" and "keyframes" respectively, although "media" can have suggestions
|
|
* like "max-width", "orientation" etc. Similarly "value" state can also have
|
|
* much better logical suggestions if we fine grain identify a sub state just
|
|
* like we do for the "selector" state.
|
|
*/
|
|
|
|
class CSSCompleter {
|
|
// Autocompletion types.
|
|
|
|
// These can be read _a lot_ in a hotpath, so keep those as individual constants using
|
|
// a Symbol as a value so the lookup is faster.
|
|
static CSS_STATE_NULL = Symbol("state_null");
|
|
// foo { bar|: … };
|
|
static CSS_STATE_PROPERTY = Symbol("state_property");
|
|
// foo {bar: baz|};
|
|
static CSS_STATE_VALUE = Symbol("state_value");
|
|
// f| {bar: baz};
|
|
static CSS_STATE_SELECTOR = Symbol("state_selector");
|
|
// @med| , or , @media scr| { };
|
|
static CSS_STATE_MEDIA = Symbol("state_media");
|
|
// @keyf|;
|
|
static CSS_STATE_KEYFRAMES = Symbol("state_keyframes");
|
|
// @keyframs foobar { t|;
|
|
static CSS_STATE_FRAME = Symbol("state_frame");
|
|
|
|
static CSS_SELECTOR_STATE_NULL = Symbol("selector_state_null");
|
|
// #f|
|
|
static CSS_SELECTOR_STATE_ID = Symbol("selector_state_id");
|
|
// #foo.b|
|
|
static CSS_SELECTOR_STATE_CLASS = Symbol("selector_state_class");
|
|
// fo|
|
|
static CSS_SELECTOR_STATE_TAG = Symbol("selector_state_tag");
|
|
// foo:|
|
|
static CSS_SELECTOR_STATE_PSEUDO = Symbol("selector_state_pseudo");
|
|
// foo[b|
|
|
static CSS_SELECTOR_STATE_ATTRIBUTE = Symbol("selector_state_attribute");
|
|
// foo[bar=b|
|
|
static CSS_SELECTOR_STATE_VALUE = Symbol("selector_state_value");
|
|
|
|
static SELECTOR_STATE_STRING_BY_SYMBOL = new Map([
|
|
[CSSCompleter.CSS_SELECTOR_STATE_NULL, "null"],
|
|
[CSSCompleter.CSS_SELECTOR_STATE_ID, "id"],
|
|
[CSSCompleter.CSS_SELECTOR_STATE_CLASS, "class"],
|
|
[CSSCompleter.CSS_SELECTOR_STATE_TAG, "tag"],
|
|
[CSSCompleter.CSS_SELECTOR_STATE_PSEUDO, "pseudo"],
|
|
[CSSCompleter.CSS_SELECTOR_STATE_ATTRIBUTE, "attribute"],
|
|
[CSSCompleter.CSS_SELECTOR_STATE_VALUE, "value"],
|
|
]);
|
|
|
|
/**
|
|
* @constructor
|
|
* @param options {Object} An options object containing the following options:
|
|
* - walker {Object} The object used for query selecting from the current
|
|
* target's DOM.
|
|
* - maxEntries {Number} Maximum selectors suggestions to display.
|
|
* - cssProperties {Object} The database of CSS properties.
|
|
*/
|
|
constructor(options = {}) {
|
|
this.walker = options.walker;
|
|
this.maxEntries = options.maxEntries || 15;
|
|
this.cssProperties = options.cssProperties;
|
|
|
|
this.propertyNames = this.cssProperties.getNames().sort();
|
|
|
|
// Array containing the [line, ch, scopeStack] for the locations where the
|
|
// CSS state is "null"
|
|
this.nullStates = [];
|
|
}
|
|
|
|
/**
|
|
* Returns a list of suggestions based on the caret position.
|
|
*
|
|
* @param source {String} String of the source code.
|
|
* @param cursor {Object} Cursor location with line and ch properties.
|
|
*
|
|
* @returns [{object}] A sorted list of objects containing the following
|
|
* peroperties:
|
|
* - label {String} Full keyword for the suggestion
|
|
* - preLabel {String} Already entered part of the label
|
|
*/
|
|
complete(source, cursor) {
|
|
// Getting the context from the caret position.
|
|
if (!this.resolveState({ source, line: cursor.line, column: cursor.ch })) {
|
|
// We couldn't resolve the context, we won't be able to complete.
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
// Properly suggest based on the state.
|
|
switch (this.state) {
|
|
case CSSCompleter.CSS_STATE_PROPERTY:
|
|
return this.completeProperties(this.completing);
|
|
|
|
case CSSCompleter.CSS_STATE_VALUE:
|
|
return this.completeValues(this.propertyName, this.completing);
|
|
|
|
case CSSCompleter.CSS_STATE_SELECTOR:
|
|
return this.suggestSelectors();
|
|
|
|
case CSSCompleter.CSS_STATE_MEDIA:
|
|
case CSSCompleter.CSS_STATE_KEYFRAMES:
|
|
if ("media".startsWith(this.completing)) {
|
|
return Promise.resolve([
|
|
{
|
|
label: "media",
|
|
preLabel: this.completing,
|
|
text: "media",
|
|
},
|
|
]);
|
|
} else if ("keyframes".startsWith(this.completing)) {
|
|
return Promise.resolve([
|
|
{
|
|
label: "keyframes",
|
|
preLabel: this.completing,
|
|
text: "keyframes",
|
|
},
|
|
]);
|
|
}
|
|
}
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
/**
|
|
* Resolves the state of CSS given a source and a cursor location, or an array of tokens.
|
|
* This method implements a custom written CSS state machine. The various switch
|
|
* statements provide the transition rules for the state. It also finds out various
|
|
* information about the nearby CSS like the property name being completed, the complete
|
|
* selector, etc.
|
|
*
|
|
* @param options {Object}
|
|
* @param sourceTokens {Array<InspectorCSSToken>} Optional array of the tokens representing
|
|
* a CSS source. When this is defined, `source`, `line` and `column`
|
|
* shouldn't be passed.
|
|
* @param options.source {String} Optional string of the source code. When this is defined,
|
|
* `sourceTokens` shouldn't be passed.
|
|
* @param options.line {Number} Cursor line. Mandatory when source is passed.
|
|
* @param options.column {Number} Cursor column. Mandatory when source is passed
|
|
*
|
|
* @returns CSS_STATE
|
|
* One of CSS_STATE enum or null if the state cannot be resolved.
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
resolveState({ sourceTokens, source, line, column }) {
|
|
if (sourceTokens && source) {
|
|
throw new Error(
|
|
"This function only accepts sourceTokens or source, not both"
|
|
);
|
|
}
|
|
|
|
// _state can be one of CSS_STATES;
|
|
let _state = CSSCompleter.CSS_STATE_NULL;
|
|
let selector = "";
|
|
let selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
let propertyName = null;
|
|
let scopeStack = [];
|
|
let selectors = [];
|
|
|
|
// If we need to retrieve the tokens, fetch the closest null state line/ch from cached
|
|
// null state locations to save some cycle.
|
|
const matchedStateIndex = !sourceTokens
|
|
? this.findNearestNullState(line)
|
|
: -1;
|
|
if (matchedStateIndex > -1) {
|
|
const state = this.nullStates[matchedStateIndex];
|
|
line -= state[0];
|
|
if (line == 0) {
|
|
column -= state[1];
|
|
}
|
|
source = source.split("\n").slice(state[0]);
|
|
source[0] = source[0].slice(state[1]);
|
|
source = source.join("\n");
|
|
scopeStack = [...state[2]];
|
|
this.nullStates.length = matchedStateIndex + 1;
|
|
} else {
|
|
this.nullStates = [];
|
|
}
|
|
|
|
const tokens = sourceTokens || cssTokenizerWithLineColumn(source);
|
|
const tokIndex = tokens.length - 1;
|
|
|
|
if (
|
|
!sourceTokens &&
|
|
tokIndex >= 0 &&
|
|
(tokens[tokIndex].loc.end.line < line ||
|
|
(tokens[tokIndex].loc.end.line === line &&
|
|
tokens[tokIndex].loc.end.column < column))
|
|
) {
|
|
// If the last token ends before the cursor location, we didn't
|
|
// tokenize it correctly. This special case can happen if the
|
|
// final token is a comment.
|
|
return null;
|
|
}
|
|
|
|
let cursor = 0;
|
|
// This will maintain a stack of paired elements like { & }, @m & }, : & ;
|
|
// etc
|
|
let token = null;
|
|
let selectorBeforeNot = null;
|
|
while (cursor <= tokIndex && (token = tokens[cursor++])) {
|
|
switch (_state) {
|
|
case CSSCompleter.CSS_STATE_PROPERTY:
|
|
// From CSS_STATE_PROPERTY, we can either go to CSS_STATE_VALUE
|
|
// state when we hit the first ':' or CSS_STATE_SELECTOR if "}" is
|
|
// reached.
|
|
if (token.tokenType === "Colon") {
|
|
scopeStack.push(":");
|
|
if (tokens[cursor - 2].tokenType != "WhiteSpace") {
|
|
propertyName = tokens[cursor - 2].text;
|
|
} else {
|
|
propertyName = tokens[cursor - 3].text;
|
|
}
|
|
_state = CSSCompleter.CSS_STATE_VALUE;
|
|
}
|
|
|
|
if (token.tokenType === "CloseCurlyBracket") {
|
|
if (/[{f]/.test(scopeStack.at(-1))) {
|
|
const popped = scopeStack.pop();
|
|
if (popped == "f") {
|
|
_state = CSSCompleter.CSS_STATE_FRAME;
|
|
} else {
|
|
selector = "";
|
|
selectors = [];
|
|
_state = CSSCompleter.CSS_STATE_NULL;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_STATE_VALUE:
|
|
// From CSS_STATE_VALUE, we can go to one of CSS_STATE_PROPERTY,
|
|
// CSS_STATE_FRAME, CSS_STATE_SELECTOR and CSS_STATE_NULL
|
|
if (token.tokenType === "Semicolon") {
|
|
if (/[:]/.test(scopeStack.at(-1))) {
|
|
scopeStack.pop();
|
|
_state = CSSCompleter.CSS_STATE_PROPERTY;
|
|
}
|
|
}
|
|
|
|
if (token.tokenType === "CloseCurlyBracket") {
|
|
if (scopeStack.at(-1) == ":") {
|
|
scopeStack.pop();
|
|
}
|
|
|
|
if (/[{f]/.test(scopeStack.at(-1))) {
|
|
const popped = scopeStack.pop();
|
|
if (popped == "f") {
|
|
_state = CSSCompleter.CSS_STATE_FRAME;
|
|
} else {
|
|
selector = "";
|
|
selectors = [];
|
|
_state = CSSCompleter.CSS_STATE_NULL;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_STATE_SELECTOR:
|
|
// From CSS_STATE_SELECTOR, we can only go to CSS_STATE_PROPERTY
|
|
// when we hit "{"
|
|
if (token.tokenType === "CurlyBracketBlock") {
|
|
scopeStack.push("{");
|
|
_state = CSSCompleter.CSS_STATE_PROPERTY;
|
|
selectors.push(selector);
|
|
selector = "";
|
|
break;
|
|
}
|
|
|
|
switch (selectorState) {
|
|
case CSSCompleter.CSS_SELECTOR_STATE_ID:
|
|
case CSSCompleter.CSS_SELECTOR_STATE_CLASS:
|
|
case CSSCompleter.CSS_SELECTOR_STATE_TAG:
|
|
switch (token.tokenType) {
|
|
case "Hash":
|
|
case "IDHash":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ID;
|
|
selector += token.text;
|
|
break;
|
|
|
|
case "Delim":
|
|
if (token.text == ".") {
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_CLASS;
|
|
selector += ".";
|
|
if (
|
|
cursor <= tokIndex &&
|
|
tokens[cursor].tokenType == "Ident"
|
|
) {
|
|
token = tokens[cursor++];
|
|
selector += token.text;
|
|
}
|
|
} else if (token.text == "#") {
|
|
// Lonely # char, that doesn't produce a Hash nor IDHash
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ID;
|
|
selector += "#";
|
|
} else if (
|
|
token.text == "+" ||
|
|
token.text == "~" ||
|
|
token.text == ">"
|
|
) {
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selector += token.text;
|
|
}
|
|
break;
|
|
|
|
case "Comma":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selectors.push(selector);
|
|
selector = "";
|
|
break;
|
|
|
|
case "Colon":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_PSEUDO;
|
|
selector += ":";
|
|
if (cursor > tokIndex) {
|
|
break;
|
|
}
|
|
|
|
token = tokens[cursor++];
|
|
switch (token.tokenType) {
|
|
case "Function":
|
|
if (token.value == "not") {
|
|
selectorBeforeNot = selector;
|
|
selector = "";
|
|
scopeStack.push("(");
|
|
} else {
|
|
selector += token.text;
|
|
}
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
break;
|
|
|
|
case "Ident":
|
|
selector += token.text;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "SquareBracketBlock":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ATTRIBUTE;
|
|
scopeStack.push("[");
|
|
selector += "[";
|
|
break;
|
|
|
|
case "CloseParenthesis":
|
|
if (scopeStack.at(-1) == "(") {
|
|
scopeStack.pop();
|
|
selector = selectorBeforeNot + "not(" + selector + ")";
|
|
selectorBeforeNot = null;
|
|
} else {
|
|
selector += ")";
|
|
}
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
break;
|
|
|
|
case "WhiteSpace":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selector && (selector += " ");
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_SELECTOR_STATE_NULL:
|
|
// From CSS_SELECTOR_STATE_NULL state, we can go to one of
|
|
// CSS_SELECTOR_STATE_ID, CSS_SELECTOR_STATE_CLASS or
|
|
// CSS_SELECTOR_STATE_TAG
|
|
switch (token.tokenType) {
|
|
case "Hash":
|
|
case "IDHash":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ID;
|
|
selector += token.text;
|
|
break;
|
|
|
|
case "Ident":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_TAG;
|
|
selector += token.text;
|
|
break;
|
|
|
|
case "Delim":
|
|
if (token.text == ".") {
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_CLASS;
|
|
selector += ".";
|
|
if (
|
|
cursor <= tokIndex &&
|
|
tokens[cursor].tokenType == "Ident"
|
|
) {
|
|
token = tokens[cursor++];
|
|
selector += token.text;
|
|
}
|
|
} else if (token.text == "#") {
|
|
// Lonely # char, that doesn't produce a Hash nor IDHash
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ID;
|
|
selector += "#";
|
|
} else if (token.text == "*") {
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_TAG;
|
|
selector += "*";
|
|
} else if (
|
|
token.text == "+" ||
|
|
token.text == "~" ||
|
|
token.text == ">"
|
|
) {
|
|
selector += token.text;
|
|
}
|
|
break;
|
|
|
|
case "Comma":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selectors.push(selector);
|
|
selector = "";
|
|
break;
|
|
|
|
case "Colon":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_PSEUDO;
|
|
selector += ":";
|
|
if (cursor > tokIndex) {
|
|
break;
|
|
}
|
|
|
|
token = tokens[cursor++];
|
|
switch (token.tokenType) {
|
|
case "Function":
|
|
if (token.value == "not") {
|
|
selectorBeforeNot = selector;
|
|
selector = "";
|
|
scopeStack.push("(");
|
|
} else {
|
|
selector += token.text;
|
|
}
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
break;
|
|
|
|
case "Ident":
|
|
selector += token.text;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "SquareBracketBlock":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ATTRIBUTE;
|
|
scopeStack.push("[");
|
|
selector += "[";
|
|
break;
|
|
|
|
case "CloseParenthesis":
|
|
if (scopeStack.at(-1) == "(") {
|
|
scopeStack.pop();
|
|
selector = selectorBeforeNot + "not(" + selector + ")";
|
|
selectorBeforeNot = null;
|
|
} else {
|
|
selector += ")";
|
|
}
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
break;
|
|
|
|
case "WhiteSpace":
|
|
selector && (selector += " ");
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_SELECTOR_STATE_PSEUDO:
|
|
switch (token.tokenType) {
|
|
case "Delim":
|
|
if (
|
|
token.text == "+" ||
|
|
token.text == "~" ||
|
|
token.text == ">"
|
|
) {
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selector += token.text;
|
|
}
|
|
break;
|
|
|
|
case "Comma":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selectors.push(selector);
|
|
selector = "";
|
|
break;
|
|
|
|
case "Colon":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_PSEUDO;
|
|
selector += ":";
|
|
if (cursor > tokIndex) {
|
|
break;
|
|
}
|
|
|
|
token = tokens[cursor++];
|
|
switch (token.tokenType) {
|
|
case "Function":
|
|
if (token.value == "not") {
|
|
selectorBeforeNot = selector;
|
|
selector = "";
|
|
scopeStack.push("(");
|
|
} else {
|
|
selector += token.text;
|
|
}
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
break;
|
|
|
|
case "Ident":
|
|
selector += token.text;
|
|
break;
|
|
}
|
|
break;
|
|
case "SquareBracketBlock":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ATTRIBUTE;
|
|
scopeStack.push("[");
|
|
selector += "[";
|
|
break;
|
|
|
|
case "WhiteSpace":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selector && (selector += " ");
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_SELECTOR_STATE_ATTRIBUTE:
|
|
switch (token.tokenType) {
|
|
case "IncludeMatch":
|
|
case "DashMatch":
|
|
case "PrefixMatch":
|
|
case "IncludeSuffixMatchMatch":
|
|
case "SubstringMatch":
|
|
selector += token.text;
|
|
token = tokens[cursor++];
|
|
break;
|
|
|
|
case "Delim":
|
|
if (token.text == "=") {
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_VALUE;
|
|
selector += token.text;
|
|
}
|
|
break;
|
|
|
|
case "CloseSquareBracket":
|
|
if (scopeStack.at(-1) == "[") {
|
|
scopeStack.pop();
|
|
}
|
|
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selector += "]";
|
|
break;
|
|
|
|
case "Ident":
|
|
selector += token.text;
|
|
break;
|
|
|
|
case "QuotedString":
|
|
selector += token.value;
|
|
break;
|
|
|
|
case "WhiteSpace":
|
|
selector && (selector += " ");
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_SELECTOR_STATE_VALUE:
|
|
switch (token.tokenType) {
|
|
case "Ident":
|
|
selector += token.text;
|
|
break;
|
|
|
|
case "QuotedString":
|
|
selector += token.value;
|
|
break;
|
|
|
|
case "CloseSquareBracket":
|
|
if (scopeStack.at(-1) == "[") {
|
|
scopeStack.pop();
|
|
}
|
|
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
selector += "]";
|
|
break;
|
|
|
|
case "WhiteSpace":
|
|
selector && (selector += " ");
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_STATE_NULL:
|
|
// From CSS_STATE_NULL state, we can go to either CSS_STATE_MEDIA or
|
|
// CSS_STATE_SELECTOR.
|
|
switch (token.tokenType) {
|
|
case "Hash":
|
|
case "IDHash":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ID;
|
|
selector = token.text;
|
|
_state = CSSCompleter.CSS_STATE_SELECTOR;
|
|
break;
|
|
|
|
case "Ident":
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_TAG;
|
|
selector = token.text;
|
|
_state = CSSCompleter.CSS_STATE_SELECTOR;
|
|
break;
|
|
|
|
case "Delim":
|
|
if (token.text == ".") {
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_CLASS;
|
|
selector = ".";
|
|
_state = CSSCompleter.CSS_STATE_SELECTOR;
|
|
if (cursor <= tokIndex && tokens[cursor].tokenType == "Ident") {
|
|
token = tokens[cursor++];
|
|
selector += token.text;
|
|
}
|
|
} else if (token.text == "#") {
|
|
// Lonely # char, that doesn't produce a Hash nor IDHash
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ID;
|
|
selector = "#";
|
|
_state = CSSCompleter.CSS_STATE_SELECTOR;
|
|
} else if (token.text == "*") {
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_TAG;
|
|
selector = "*";
|
|
_state = CSSCompleter.CSS_STATE_SELECTOR;
|
|
}
|
|
break;
|
|
|
|
case "Colon":
|
|
_state = CSSCompleter.CSS_STATE_SELECTOR;
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_PSEUDO;
|
|
selector += ":";
|
|
if (cursor > tokIndex) {
|
|
break;
|
|
}
|
|
|
|
token = tokens[cursor++];
|
|
switch (token.tokenType) {
|
|
case "Function":
|
|
if (token.value == "not") {
|
|
selectorBeforeNot = selector;
|
|
selector = "";
|
|
scopeStack.push("(");
|
|
} else {
|
|
selector += token.text;
|
|
}
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_NULL;
|
|
break;
|
|
|
|
case "Ident":
|
|
selector += token.text;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "CloseSquareBracket":
|
|
_state = CSSCompleter.CSS_STATE_SELECTOR;
|
|
selectorState = CSSCompleter.CSS_SELECTOR_STATE_ATTRIBUTE;
|
|
scopeStack.push("[");
|
|
selector += "[";
|
|
break;
|
|
|
|
case "CurlyBracketBlock":
|
|
if (scopeStack.at(-1) == "@m") {
|
|
scopeStack.pop();
|
|
}
|
|
break;
|
|
|
|
case "AtKeyword":
|
|
// XXX: We should probably handle other at-rules (@container, @property, …)
|
|
_state = token.value.startsWith("m")
|
|
? CSSCompleter.CSS_STATE_MEDIA
|
|
: CSSCompleter.CSS_STATE_KEYFRAMES;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_STATE_MEDIA:
|
|
// From CSS_STATE_MEDIA, we can only go to CSS_STATE_NULL state when
|
|
// we hit the first '{'
|
|
if (token.tokenType == "CurlyBracketBlock") {
|
|
scopeStack.push("@m");
|
|
_state = CSSCompleter.CSS_STATE_NULL;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_STATE_KEYFRAMES:
|
|
// From CSS_STATE_KEYFRAMES, we can only go to CSS_STATE_FRAME state
|
|
// when we hit the first '{'
|
|
if (token.tokenType == "CurlyBracketBlock") {
|
|
scopeStack.push("@k");
|
|
_state = CSSCompleter.CSS_STATE_FRAME;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_STATE_FRAME:
|
|
// From CSS_STATE_FRAME, we can either go to CSS_STATE_PROPERTY
|
|
// state when we hit the first '{' or to CSS_STATE_SELECTOR when we
|
|
// hit '}'
|
|
if (token.tokenType == "CurlyBracketBlock") {
|
|
scopeStack.push("f");
|
|
_state = CSSCompleter.CSS_STATE_PROPERTY;
|
|
} else if (token.tokenType == "CloseCurlyBracket") {
|
|
if (scopeStack.at(-1) == "@k") {
|
|
scopeStack.pop();
|
|
}
|
|
|
|
_state = CSSCompleter.CSS_STATE_NULL;
|
|
}
|
|
break;
|
|
}
|
|
if (_state == CSSCompleter.CSS_STATE_NULL) {
|
|
if (!this.nullStates.length) {
|
|
this.nullStates.push([
|
|
token.loc.end.line,
|
|
token.loc.end.column,
|
|
[...scopeStack],
|
|
]);
|
|
continue;
|
|
}
|
|
let tokenLine = token.loc.end.line;
|
|
const tokenCh = token.loc.end.column;
|
|
if (tokenLine == 0) {
|
|
continue;
|
|
}
|
|
if (matchedStateIndex > -1) {
|
|
tokenLine += this.nullStates[matchedStateIndex][0];
|
|
}
|
|
this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]);
|
|
}
|
|
}
|
|
// ^ while loop end
|
|
|
|
this.state = _state;
|
|
this.propertyName =
|
|
_state == CSSCompleter.CSS_STATE_VALUE ? propertyName : null;
|
|
this.selectorState =
|
|
_state == CSSCompleter.CSS_STATE_SELECTOR ? selectorState : null;
|
|
this.selectorBeforeNot =
|
|
selectorBeforeNot == null ? null : selectorBeforeNot;
|
|
if (token) {
|
|
// If the source text is passed, we need to remove the part of the computed selector
|
|
// after the caret (when sourceTokens are passed, the last token is already sliced,
|
|
// so we'll get the expected value)
|
|
if (!sourceTokens) {
|
|
selector = selector.slice(
|
|
0,
|
|
selector.length + token.loc.end.column - column
|
|
);
|
|
}
|
|
this.selector = selector;
|
|
} else {
|
|
this.selector = "";
|
|
}
|
|
this.selectors = selectors;
|
|
|
|
if (token && token.tokenType != "WhiteSpace") {
|
|
let text;
|
|
if (
|
|
token.tokenType === "IDHash" ||
|
|
token.tokenType === "Hash" ||
|
|
token.tokenType === "AtKeyword" ||
|
|
token.tokenType === "Function" ||
|
|
token.tokenType === "QuotedString"
|
|
) {
|
|
text = token.value;
|
|
} else {
|
|
text = token.text;
|
|
}
|
|
this.completing = (
|
|
sourceTokens
|
|
? text
|
|
: // If the source text is passed, we need to remove the text after the caret
|
|
// (when sourceTokens are passed, the last token is already sliced, so we'll
|
|
// get the expected value)
|
|
text.slice(0, column - token.loc.start.column)
|
|
).replace(/^[.#]$/, "");
|
|
} else {
|
|
this.completing = "";
|
|
}
|
|
// Special case the situation when the user just entered ":" after typing a
|
|
// property name.
|
|
if (this.completing == ":" && _state == CSSCompleter.CSS_STATE_VALUE) {
|
|
this.completing = "";
|
|
}
|
|
|
|
// Special check for !important; case.
|
|
if (
|
|
token &&
|
|
tokens[cursor - 2] &&
|
|
tokens[cursor - 2].text == "!" &&
|
|
this.completing == "important".slice(0, this.completing.length)
|
|
) {
|
|
this.completing = "!" + this.completing;
|
|
}
|
|
return _state;
|
|
}
|
|
|
|
/**
|
|
* Queries the DOM Walker actor for suggestions regarding the selector being
|
|
* completed
|
|
*/
|
|
suggestSelectors() {
|
|
const walker = this.walker;
|
|
if (!walker) {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
let query = this.selector;
|
|
// Even though the selector matched atleast one node, there is still
|
|
// possibility of suggestions.
|
|
switch (this.selectorState) {
|
|
case CSSCompleter.CSS_SELECTOR_STATE_NULL:
|
|
if (this.completing === ",") {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
query += "*";
|
|
break;
|
|
|
|
case CSSCompleter.CSS_SELECTOR_STATE_TAG:
|
|
query = query.slice(0, query.length - this.completing.length);
|
|
break;
|
|
|
|
case CSSCompleter.CSS_SELECTOR_STATE_ID:
|
|
case CSSCompleter.CSS_SELECTOR_STATE_CLASS:
|
|
case CSSCompleter.CSS_SELECTOR_STATE_PSEUDO:
|
|
if (/^[.:#]$/.test(this.completing)) {
|
|
query = query.slice(0, query.length - this.completing.length);
|
|
this.completing = "";
|
|
} else {
|
|
query = query.slice(0, query.length - this.completing.length - 1);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (
|
|
/[\s+>~]$/.test(query) &&
|
|
this.selectorState != CSSCompleter.CSS_SELECTOR_STATE_ATTRIBUTE &&
|
|
this.selectorState != CSSCompleter.CSS_SELECTOR_STATE_VALUE
|
|
) {
|
|
query += "*";
|
|
}
|
|
|
|
// Set the values that this request was supposed to suggest to.
|
|
this._currentQuery = query;
|
|
return walker
|
|
.getSuggestionsForQuery(
|
|
query,
|
|
this.completing,
|
|
CSSCompleter.SELECTOR_STATE_STRING_BY_SYMBOL.get(this.selectorState)
|
|
)
|
|
.then(result => this.prepareSelectorResults(result));
|
|
}
|
|
|
|
/**
|
|
* Prepares the selector suggestions returned by the walker actor.
|
|
*/
|
|
prepareSelectorResults(result) {
|
|
if (this._currentQuery != result.query) {
|
|
return [];
|
|
}
|
|
|
|
const { suggestions } = result;
|
|
const query = this.selector;
|
|
const completion = [];
|
|
|
|
// @backward-compat { version 140 } The shape of the returned value from getSuggestionsForQuery
|
|
// changed in 140. This variable should be removed and considered as true when 140 hits release
|
|
const suggestionNewShape =
|
|
this.walker.traits.getSuggestionsForQueryWithoutCount;
|
|
|
|
for (const suggestion of suggestions) {
|
|
let value = suggestion[0];
|
|
const state = suggestionNewShape ? suggestion[1] : suggestion[2];
|
|
|
|
switch (this.selectorState) {
|
|
case CSSCompleter.CSS_SELECTOR_STATE_ID:
|
|
case CSSCompleter.CSS_SELECTOR_STATE_CLASS:
|
|
case CSSCompleter.CSS_SELECTOR_STATE_PSEUDO:
|
|
if (/^[.:#]$/.test(this.completing)) {
|
|
value =
|
|
query.slice(0, query.length - this.completing.length) + value;
|
|
} else {
|
|
value =
|
|
query.slice(0, query.length - this.completing.length - 1) + value;
|
|
}
|
|
break;
|
|
|
|
case CSSCompleter.CSS_SELECTOR_STATE_TAG:
|
|
value = query.slice(0, query.length - this.completing.length) + value;
|
|
break;
|
|
|
|
case CSSCompleter.CSS_SELECTOR_STATE_NULL:
|
|
value = query + value;
|
|
break;
|
|
|
|
default:
|
|
value = query.slice(0, query.length - this.completing.length) + value;
|
|
}
|
|
|
|
const item = {
|
|
label: value,
|
|
preLabel: query,
|
|
text: value,
|
|
};
|
|
|
|
// In case the query's state is tag and the item's state is id or class
|
|
// adjust the preLabel
|
|
if (
|
|
this.selectorState === CSSCompleter.CSS_SELECTOR_STATE_TAG &&
|
|
state === CSSCompleter.CSS_SELECTOR_STATE_CLASS
|
|
) {
|
|
item.preLabel = "." + item.preLabel;
|
|
}
|
|
if (
|
|
this.selectorState === CSSCompleter.CSS_SELECTOR_STATE_TAG &&
|
|
state === CSSCompleter.CSS_SELECTOR_STATE_ID
|
|
) {
|
|
item.preLabel = "#" + item.preLabel;
|
|
}
|
|
|
|
completion.push(item);
|
|
|
|
if (completion.length > this.maxEntries - 1) {
|
|
break;
|
|
}
|
|
}
|
|
return completion;
|
|
}
|
|
|
|
/**
|
|
* Returns CSS property name suggestions based on the input.
|
|
*
|
|
* @param startProp {String} Initial part of the property being completed.
|
|
*/
|
|
completeProperties(startProp) {
|
|
const finalList = [];
|
|
if (!startProp) {
|
|
return Promise.resolve(finalList);
|
|
}
|
|
|
|
const length = this.propertyNames.length;
|
|
let i = 0,
|
|
count = 0;
|
|
for (; i < length && count < this.maxEntries; i++) {
|
|
if (this.propertyNames[i].startsWith(startProp)) {
|
|
count++;
|
|
const propName = this.propertyNames[i];
|
|
finalList.push({
|
|
preLabel: startProp,
|
|
label: propName,
|
|
text: propName + ": ",
|
|
});
|
|
} else if (this.propertyNames[i] > startProp) {
|
|
// We have crossed all possible matches alphabetically.
|
|
break;
|
|
}
|
|
}
|
|
return Promise.resolve(finalList);
|
|
}
|
|
|
|
/**
|
|
* Returns CSS value suggestions based on the corresponding property.
|
|
*
|
|
* @param propName {String} The property to which the value being completed
|
|
* belongs.
|
|
* @param startValue {String} Initial part of the value being completed.
|
|
*/
|
|
completeValues(propName, startValue) {
|
|
const finalList = [];
|
|
const list = ["!important;", ...this.cssProperties.getValues(propName)];
|
|
// If there is no character being completed, we are showing an initial list
|
|
// of possible values. Skipping '!important' in this case.
|
|
if (!startValue) {
|
|
list.splice(0, 1);
|
|
}
|
|
|
|
const length = list.length;
|
|
let i = 0,
|
|
count = 0;
|
|
for (; i < length && count < this.maxEntries; i++) {
|
|
if (list[i].startsWith(startValue)) {
|
|
count++;
|
|
const value = list[i];
|
|
finalList.push({
|
|
preLabel: startValue,
|
|
label: value,
|
|
text: value,
|
|
});
|
|
} else if (list[i] > startValue) {
|
|
// We have crossed all possible matches alphabetically.
|
|
break;
|
|
}
|
|
}
|
|
return Promise.resolve(finalList);
|
|
}
|
|
|
|
/**
|
|
* A biased binary search in a sorted array where the middle element is
|
|
* calculated based on the values at the lower and the upper index in each
|
|
* iteration.
|
|
*
|
|
* This method returns the index of the closest null state from the passed
|
|
* `line` argument. Once we have the closest null state, we can start applying
|
|
* the state machine logic from that location instead of the absolute starting
|
|
* of the CSS source. This speeds up the tokenizing and the state machine a
|
|
* lot while using autocompletion at high line numbers in a CSS source.
|
|
*/
|
|
findNearestNullState(line) {
|
|
const arr = this.nullStates;
|
|
let high = arr.length - 1;
|
|
let low = 0;
|
|
let target = 0;
|
|
|
|
if (high < 0) {
|
|
return -1;
|
|
}
|
|
if (arr[high][0] <= line) {
|
|
return high;
|
|
}
|
|
if (arr[low][0] > line) {
|
|
return -1;
|
|
}
|
|
|
|
while (high > low) {
|
|
if (arr[low][0] <= line && arr[low[0] + 1] > line) {
|
|
return low;
|
|
}
|
|
if (arr[high][0] > line && arr[high - 1][0] <= line) {
|
|
return high - 1;
|
|
}
|
|
|
|
target =
|
|
(((line - arr[low][0]) / (arr[high][0] - arr[low][0])) * (high - low)) |
|
|
0;
|
|
|
|
if (arr[target][0] <= line && arr[target + 1][0] > line) {
|
|
return target;
|
|
} else if (line > arr[target][0]) {
|
|
low = target + 1;
|
|
high--;
|
|
} else {
|
|
high = target - 1;
|
|
low++;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Invalidates the state cache for and above the line.
|
|
*/
|
|
invalidateCache(line) {
|
|
this.nullStates.length = this.findNearestNullState(line) + 1;
|
|
}
|
|
|
|
/**
|
|
* Get the state information about a token surrounding the {line, ch} position
|
|
*
|
|
* @param {string} source
|
|
* The complete source of the CSS file. Unlike resolve state method,
|
|
* this method requires the full source.
|
|
* @param {object} caret
|
|
* The line, ch position of the caret.
|
|
*
|
|
* @returns {object}
|
|
* An object containing the state of token covered by the caret.
|
|
* The object has following properties when the the state is
|
|
* "selector", "value" or "property", null otherwise:
|
|
* - state {string} one of CSS_STATES - "selector", "value" etc.
|
|
* - selector {string} The selector at the caret when `state` is
|
|
* selector. OR
|
|
* - selectors {[string]} Array of selector strings in case when
|
|
* `state` is "value" or "property"
|
|
* - propertyName {string} The property name at the current caret or
|
|
* the property name corresponding to the value at
|
|
* the caret.
|
|
* - value {string} The css value at the current caret.
|
|
* - loc {object} An object containing the starting and the ending
|
|
* caret position of the whole selector, value or property.
|
|
* - { start: {line, ch}, end: {line, ch}}
|
|
*/
|
|
getInfoAt(source, caret) {
|
|
const { line, ch } = caret;
|
|
const sourceArray = source.split("\n");
|
|
|
|
// Limits the input source till the {line, ch} caret position
|
|
const limit = function () {
|
|
// `line` is 0-based
|
|
if (sourceArray.length <= line) {
|
|
return source;
|
|
}
|
|
const list = sourceArray.slice(0, line + 1);
|
|
list[line] = list[line].slice(0, ch);
|
|
return list.join("\n");
|
|
};
|
|
|
|
const limitedSource = limit(source);
|
|
|
|
// Ideally we should be using `cssTokenizer`, which parse incrementaly and returns a generator.
|
|
// `cssTokenizerWithLineColumn` parses the whole `limitedSource` content right away
|
|
// and returns an array of tokens. This can be a performance bottleneck,
|
|
// but `resolveState` would go through all the tokens anyway, as well as `traverseBackward`,
|
|
// which starts from the last token.
|
|
const limitedSourceTokens = cssTokenizerWithLineColumn(limitedSource);
|
|
const state = this.resolveState({
|
|
sourceTokens: limitedSourceTokens,
|
|
});
|
|
const propertyName = this.propertyName;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
const traverseForward = check => {
|
|
let forwardCurrentLine = line;
|
|
let forwardCurrentSource = limitedSource;
|
|
|
|
// loop to determine the end location of the property name/value/selector.
|
|
do {
|
|
let lineText = sourceArray[forwardCurrentLine];
|
|
if (forwardCurrentLine == line) {
|
|
lineText = lineText.substring(ch);
|
|
}
|
|
|
|
let prevToken = undefined;
|
|
const tokensIterator = cssTokenizer(lineText);
|
|
|
|
const ech = forwardCurrentLine == line ? ch : 0;
|
|
for (let token of tokensIterator) {
|
|
forwardCurrentSource += sourceArray[forwardCurrentLine].substring(
|
|
ech + token.startOffset,
|
|
ech + token.endOffset
|
|
);
|
|
|
|
// WhiteSpace cannot change state.
|
|
if (token.tokenType == "WhiteSpace") {
|
|
prevToken = token;
|
|
continue;
|
|
}
|
|
|
|
const forwState = this.resolveState({
|
|
source: forwardCurrentSource,
|
|
line: forwardCurrentLine,
|
|
column: token.endOffset + ech,
|
|
});
|
|
if (check(forwState)) {
|
|
if (prevToken && prevToken.tokenType == "WhiteSpace") {
|
|
token = prevToken;
|
|
}
|
|
return {
|
|
line: forwardCurrentLine,
|
|
ch: token.startOffset + ech,
|
|
};
|
|
}
|
|
prevToken = token;
|
|
}
|
|
forwardCurrentSource += "\n";
|
|
} while (++forwardCurrentLine < sourceArray.length);
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
const traverseBackwards = (check, isValue) => {
|
|
let token;
|
|
let previousToken;
|
|
const remainingTokens = Array.from(limitedSourceTokens);
|
|
|
|
// Backward loop to determine the beginning location of the selector.
|
|
while (((previousToken = token), (token = remainingTokens.pop()))) {
|
|
// WhiteSpace cannot change state.
|
|
if (token.tokenType == "WhiteSpace") {
|
|
continue;
|
|
}
|
|
|
|
const backState = this.resolveState({
|
|
sourceTokens: remainingTokens,
|
|
});
|
|
if (check(backState)) {
|
|
if (previousToken?.tokenType == "WhiteSpace") {
|
|
token = previousToken;
|
|
}
|
|
|
|
const loc = isValue ? token.loc.end : token.loc.start;
|
|
return {
|
|
line: loc.line,
|
|
ch: loc.column,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
if (state == CSSCompleter.CSS_STATE_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.
|
|
const start = traverseBackwards(backState => {
|
|
return (
|
|
backState != CSSCompleter.CSS_STATE_SELECTOR ||
|
|
(this.selector == "" && this.selectorBeforeNot == null)
|
|
);
|
|
});
|
|
|
|
// Forward loop to determine the ending location of the selector.
|
|
const end = traverseForward(forwState => {
|
|
return (
|
|
forwState != CSSCompleter.CSS_STATE_SELECTOR ||
|
|
(this.selector == "" && this.selectorBeforeNot == null)
|
|
);
|
|
});
|
|
|
|
// Since we have start and end positions, figure out the whole selector.
|
|
let selector = sourceArray.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,
|
|
selector,
|
|
loc: {
|
|
start,
|
|
end,
|
|
},
|
|
};
|
|
} else if (state == CSSCompleter.CSS_STATE_PROPERTY) {
|
|
// A property can only be a single word and thus very easy to calculate.
|
|
const tokensIterator = cssTokenizer(sourceArray[line]);
|
|
for (const token of tokensIterator) {
|
|
// 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,
|
|
propertyName: token.text,
|
|
selectors: this.selectors,
|
|
loc: {
|
|
start: {
|
|
line,
|
|
ch: token.startOffset,
|
|
},
|
|
end: {
|
|
line,
|
|
ch: token.endOffset,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
}
|
|
} else if (state == CSSCompleter.CSS_STATE_VALUE) {
|
|
// CSS value can be multiline too, so we go forward and backwards to
|
|
// determine the bounds of the value at caret
|
|
const start = traverseBackwards(
|
|
backState => backState != CSSCompleter.CSS_STATE_VALUE,
|
|
true
|
|
);
|
|
|
|
// Find the end of the value using a simple forward scan.
|
|
const remainingSource = source.substring(limitedSource.length);
|
|
const parser = new InspectorCSSParser(remainingSource);
|
|
let end;
|
|
while (true) {
|
|
const token = parser.nextToken();
|
|
if (
|
|
!token ||
|
|
token.tokenType === "Semicolon" ||
|
|
token.tokenType === "CloseCurlyBracket"
|
|
) {
|
|
// Done. We're guaranteed to exit the loop once we reach
|
|
// the end of the string.
|
|
end = {
|
|
line: parser.lineNumber + line,
|
|
ch: parser.columnNumber,
|
|
};
|
|
if (end.line === line) {
|
|
end.ch = end.ch + ch;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
let value = sourceArray.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,
|
|
propertyName,
|
|
selectors: this.selectors,
|
|
value,
|
|
loc: {
|
|
start,
|
|
end,
|
|
},
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = CSSCompleter;
|