browser/devtools/sourceeditor/css-autocompleter.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 const { Cc, Ci, Cu } = require('chrome');
michael@0 6 const cssTokenizer = require("devtools/sourceeditor/css-tokenizer");
michael@0 7 const promise = Cu.import("resource://gre/modules/Promise.jsm");
michael@0 8
michael@0 9 /**
michael@0 10 * Here is what this file (+ ./css-tokenizer.js) do.
michael@0 11 *
michael@0 12 * The main objective here is to provide as much suggestions to the user editing
michael@0 13 * a stylesheet in Style Editor. The possible things that can be suggested are:
michael@0 14 * - CSS property names
michael@0 15 * - CSS property values
michael@0 16 * - CSS Selectors
michael@0 17 * - Some other known CSS keywords
michael@0 18 *
michael@0 19 * Gecko provides a list of both property names and their corresponding values.
michael@0 20 * We take out a list of matching selectors using the Inspector actor's
michael@0 21 * `getSuggestionsForQuery` method. Now the only thing is to parse the CSS being
michael@0 22 * edited by the user, figure out what token or word is being written and last
michael@0 23 * but the most difficult, what is being edited.
michael@0 24 *
michael@0 25 * The file 'css-tokenizer' helps in converting the CSS into meaningful tokens,
michael@0 26 * each having a certain type associated with it. These tokens help us to figure
michael@0 27 * out the currently edited word and to write a CSS state machine to figure out
michael@0 28 * what the user is currently editing. By that, I mean, whether he is editing a
michael@0 29 * selector or a property or a value, or even fine grained information like an
michael@0 30 * id in the selector.
michael@0 31 *
michael@0 32 * The `resolveState` method iterated over the tokens spitted out by the
michael@0 33 * tokenizer, using switch cases, follows a state machine logic and finally
michael@0 34 * figures out these informations:
michael@0 35 * - The state of the CSS at the cursor (one out of CSS_STATES)
michael@0 36 * - The current token that is being edited `cmpleting`
michael@0 37 * - If the state is "selector", the selector state (one of SELECTOR_STATES)
michael@0 38 * - If the state is "selector", the current selector till the cursor
michael@0 39 * - If the state is "value", the corresponding property name
michael@0 40 *
michael@0 41 * In case of "value" and "property" states, we simply use the information
michael@0 42 * provided by Gecko to filter out the possible suggestions.
michael@0 43 * For "selector" state, we request the Inspector actor to query the page DOM
michael@0 44 * and filter out the possible suggestions.
michael@0 45 * For "media" and "keyframes" state, the only possible suggestions for now are
michael@0 46 * "media" and "keyframes" respectively, although "media" can have suggestions
michael@0 47 * like "max-width", "orientation" etc. Similarly "value" state can also have
michael@0 48 * much better logical suggestions if we fine grain identify a sub state just
michael@0 49 * like we do for the "selector" state.
michael@0 50 */
michael@0 51
michael@0 52 // Autocompletion types.
michael@0 53
michael@0 54 const CSS_STATES = {
michael@0 55 "null": "null",
michael@0 56 property: "property", // foo { bar|: … }
michael@0 57 value: "value", // foo {bar: baz|}
michael@0 58 selector: "selector", // f| {bar: baz}
michael@0 59 media: "media", // @med| , or , @media scr| { }
michael@0 60 keyframes: "keyframes", // @keyf|
michael@0 61 frame: "frame", // @keyframs foobar { t|
michael@0 62 };
michael@0 63
michael@0 64 const SELECTOR_STATES = {
michael@0 65 "null": "null",
michael@0 66 id: "id", // #f|
michael@0 67 class: "class", // #foo.b|
michael@0 68 tag: "tag", // fo|
michael@0 69 pseudo: "pseudo", // foo:|
michael@0 70 attribute: "attribute", // foo[b|
michael@0 71 value: "value", // foo[bar=b|
michael@0 72 };
michael@0 73
michael@0 74 const { properties, propertyNames } = getCSSKeywords();
michael@0 75
michael@0 76 /**
michael@0 77 * Constructor for the autocompletion object.
michael@0 78 *
michael@0 79 * @param options {Object} An options object containing the following options:
michael@0 80 * - walker {Object} The object used for query selecting from the current
michael@0 81 * target's DOM.
michael@0 82 * - maxEntries {Number} Maximum selectors suggestions to display.
michael@0 83 */
michael@0 84 function CSSCompleter(options = {}) {
michael@0 85 this.walker = options.walker;
michael@0 86 this.maxEntries = options.maxEntries || 15;
michael@0 87
michael@0 88 // Array containing the [line, ch, scopeStack] for the locations where the
michael@0 89 // CSS state is "null"
michael@0 90 this.nullStates = [];
michael@0 91 }
michael@0 92
michael@0 93 CSSCompleter.prototype = {
michael@0 94
michael@0 95 /**
michael@0 96 * Returns a list of suggestions based on the caret position.
michael@0 97 *
michael@0 98 * @param source {String} String of the source code.
michael@0 99 * @param caret {Object} Cursor location with line and ch properties.
michael@0 100 *
michael@0 101 * @returns [{object}] A sorted list of objects containing the following
michael@0 102 * peroperties:
michael@0 103 * - label {String} Full keyword for the suggestion
michael@0 104 * - preLabel {String} Already entered part of the label
michael@0 105 */
michael@0 106 complete: function(source, caret) {
michael@0 107 // Getting the context from the caret position.
michael@0 108 if (!this.resolveState(source, caret)) {
michael@0 109 // We couldn't resolve the context, we won't be able to complete.
michael@0 110 return Promise.resolve([]);
michael@0 111 }
michael@0 112
michael@0 113 // Properly suggest based on the state.
michael@0 114 switch(this.state) {
michael@0 115 case CSS_STATES.property:
michael@0 116 return this.completeProperties(this.completing);
michael@0 117
michael@0 118 case CSS_STATES.value:
michael@0 119 return this.completeValues(this.propertyName, this.completing);
michael@0 120
michael@0 121 case CSS_STATES.selector:
michael@0 122 return this.suggestSelectors();
michael@0 123
michael@0 124 case CSS_STATES.media:
michael@0 125 case CSS_STATES.keyframes:
michael@0 126 if ("media".startsWith(this.completing)) {
michael@0 127 return Promise.resolve([{
michael@0 128 label: "media",
michael@0 129 preLabel: this.completing,
michael@0 130 text: "media"
michael@0 131 }]);
michael@0 132 } else if ("keyframes".startsWith(this.completing)) {
michael@0 133 return Promise.resolve([{
michael@0 134 label: "keyframes",
michael@0 135 preLabel: this.completing,
michael@0 136 text: "keyframes"
michael@0 137 }]);
michael@0 138 }
michael@0 139 }
michael@0 140 return Promise.resolve([]);
michael@0 141 },
michael@0 142
michael@0 143 /**
michael@0 144 * Resolves the state of CSS at the cursor location. This method implements a
michael@0 145 * custom written CSS state machine. The various switch statements provide the
michael@0 146 * transition rules for the state. It also finds out various informatino about
michael@0 147 * the nearby CSS like the property name being completed, the complete
michael@0 148 * selector, etc.
michael@0 149 *
michael@0 150 * @param source {String} String of the source code.
michael@0 151 * @param caret {Object} Cursor location with line and ch properties.
michael@0 152 *
michael@0 153 * @returns CSS_STATE
michael@0 154 * One of CSS_STATE enum or null if the state cannot be resolved.
michael@0 155 */
michael@0 156 resolveState: function(source, {line, ch}) {
michael@0 157 // Function to return the last element of an array
michael@0 158 let peek = arr => arr[arr.length - 1];
michael@0 159 // _state can be one of CSS_STATES;
michael@0 160 let _state = CSS_STATES.null;
michael@0 161 let selector = "";
michael@0 162 let selectorState = SELECTOR_STATES.null;
michael@0 163 let propertyName = null;
michael@0 164 let scopeStack = [];
michael@0 165 let selectors = [];
michael@0 166
michael@0 167 // Fetch the closest null state line, ch from cached null state locations
michael@0 168 let matchedStateIndex = this.findNearestNullState(line);
michael@0 169 if (matchedStateIndex > -1) {
michael@0 170 let state = this.nullStates[matchedStateIndex];
michael@0 171 line -= state[0];
michael@0 172 if (line == 0)
michael@0 173 ch -= state[1];
michael@0 174 source = source.split("\n").slice(state[0]);
michael@0 175 source[0] = source[0].slice(state[1]);
michael@0 176 source = source.join("\n");
michael@0 177 scopeStack = [...state[2]];
michael@0 178 this.nullStates.length = matchedStateIndex + 1;
michael@0 179 }
michael@0 180 else {
michael@0 181 this.nullStates = [];
michael@0 182 }
michael@0 183 let tokens = cssTokenizer(source, {loc:true});
michael@0 184 let tokIndex = tokens.length - 1;
michael@0 185 if (tokens[tokIndex].loc.end.line < line ||
michael@0 186 (tokens[tokIndex].loc.end.line === line &&
michael@0 187 tokens[tokIndex].loc.end.column < ch)) {
michael@0 188 // If the last token is not an EOF, we didn't tokenize it correctly.
michael@0 189 // This special case is handled in case we couldn't tokenize, but the last
michael@0 190 // token that *could be tokenized* was an identifier.
michael@0 191 return null;
michael@0 192 }
michael@0 193 // Since last token is EOF, the cursor token is last - 1
michael@0 194 tokIndex--;
michael@0 195
michael@0 196 let cursor = 0;
michael@0 197 // This will maintain a stack of paired elements like { & }, @m & }, : & ; etc
michael@0 198 let token = null;
michael@0 199 let selectorBeforeNot = null;
michael@0 200 while (cursor <= tokIndex && (token = tokens[cursor++])) {
michael@0 201 switch (_state) {
michael@0 202 case CSS_STATES.property:
michael@0 203 // From CSS_STATES.property, we can either go to CSS_STATES.value state
michael@0 204 // when we hit the first ':' or CSS_STATES.selector if "}" is reached.
michael@0 205 switch(token.tokenType) {
michael@0 206 case ":":
michael@0 207 scopeStack.push(":");
michael@0 208 if (tokens[cursor - 2].tokenType != "WHITESPACE")
michael@0 209 propertyName = tokens[cursor - 2].value;
michael@0 210 else
michael@0 211 propertyName = tokens[cursor - 3].value;
michael@0 212 _state = CSS_STATES.value;
michael@0 213 break;
michael@0 214
michael@0 215 case "}":
michael@0 216 if (/[{f]/.test(peek(scopeStack))) {
michael@0 217 let popped = scopeStack.pop();
michael@0 218 if (popped == "f") {
michael@0 219 _state = CSS_STATES.frame;
michael@0 220 } else {
michael@0 221 selector = "";
michael@0 222 selectors = [];
michael@0 223 _state = CSS_STATES.null;
michael@0 224 }
michael@0 225 }
michael@0 226 break;
michael@0 227 }
michael@0 228 break;
michael@0 229
michael@0 230 case CSS_STATES.value:
michael@0 231 // From CSS_STATES.value, we can go to one of CSS_STATES.property,
michael@0 232 // CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
michael@0 233 switch(token.tokenType) {
michael@0 234 case ";":
michael@0 235 if (/[:]/.test(peek(scopeStack))) {
michael@0 236 scopeStack.pop();
michael@0 237 _state = CSS_STATES.property;
michael@0 238 }
michael@0 239 break;
michael@0 240
michael@0 241 case "}":
michael@0 242 if (peek(scopeStack) == ":")
michael@0 243 scopeStack.pop();
michael@0 244
michael@0 245 if (/[{f]/.test(peek(scopeStack))) {
michael@0 246 let popped = scopeStack.pop();
michael@0 247 if (popped == "f") {
michael@0 248 _state = CSS_STATES.frame;
michael@0 249 } else {
michael@0 250 selector = "";
michael@0 251 selectors = [];
michael@0 252 _state = CSS_STATES.null;
michael@0 253 }
michael@0 254 }
michael@0 255 break;
michael@0 256 }
michael@0 257 break;
michael@0 258
michael@0 259 case CSS_STATES.selector:
michael@0 260 // From CSS_STATES.selector, we can only go to CSS_STATES.property when
michael@0 261 // we hit "{"
michael@0 262 if (token.tokenType == "{") {
michael@0 263 scopeStack.push("{");
michael@0 264 _state = CSS_STATES.property;
michael@0 265 selectors.push(selector);
michael@0 266 selector = "";
michael@0 267 break;
michael@0 268 }
michael@0 269 switch(selectorState) {
michael@0 270 case SELECTOR_STATES.id:
michael@0 271 case SELECTOR_STATES.class:
michael@0 272 case SELECTOR_STATES.tag:
michael@0 273 switch(token.tokenType) {
michael@0 274 case "HASH":
michael@0 275 selectorState = SELECTOR_STATES.id;
michael@0 276 selector += "#" + token.value;
michael@0 277 break;
michael@0 278
michael@0 279 case "DELIM":
michael@0 280 if (token.value == ".") {
michael@0 281 selectorState = SELECTOR_STATES.class;
michael@0 282 selector += ".";
michael@0 283 if (cursor <= tokIndex &&
michael@0 284 tokens[cursor].tokenType == "IDENT") {
michael@0 285 token = tokens[cursor++];
michael@0 286 selector += token.value;
michael@0 287 }
michael@0 288 } else if (token.value == "#") {
michael@0 289 selectorState = SELECTOR_STATES.id;
michael@0 290 selector += "#";
michael@0 291 } else if (/[>~+]/.test(token.value)) {
michael@0 292 selectorState = SELECTOR_STATES.null;
michael@0 293 selector += token.value;
michael@0 294 } else if (token.value == ",") {
michael@0 295 selectorState = SELECTOR_STATES.null;
michael@0 296 selectors.push(selector);
michael@0 297 selector = "";
michael@0 298 }
michael@0 299 break;
michael@0 300
michael@0 301 case ":":
michael@0 302 selectorState = SELECTOR_STATES.pseudo;
michael@0 303 selector += ":";
michael@0 304 if (cursor > tokIndex)
michael@0 305 break;
michael@0 306
michael@0 307 token = tokens[cursor++];
michael@0 308 switch(token.tokenType) {
michael@0 309 case "FUNCTION":
michael@0 310 if (token.value == "not") {
michael@0 311 selectorBeforeNot = selector;
michael@0 312 selector = "";
michael@0 313 scopeStack.push("(");
michael@0 314 } else {
michael@0 315 selector += token.value + "(";
michael@0 316 }
michael@0 317 selectorState = SELECTOR_STATES.null;
michael@0 318 break;
michael@0 319
michael@0 320 case "IDENT":
michael@0 321 selector += token.value;
michael@0 322 break;
michael@0 323 }
michael@0 324 break;
michael@0 325
michael@0 326 case "[":
michael@0 327 selectorState = SELECTOR_STATES.attribute;
michael@0 328 scopeStack.push("[");
michael@0 329 selector += "[";
michael@0 330 break;
michael@0 331
michael@0 332 case ")":
michael@0 333 if (peek(scopeStack) == "(") {
michael@0 334 scopeStack.pop();
michael@0 335 selector = selectorBeforeNot + "not(" + selector + ")";
michael@0 336 selectorBeforeNot = null;
michael@0 337 } else {
michael@0 338 selector += ")";
michael@0 339 }
michael@0 340 selectorState = SELECTOR_STATES.null;
michael@0 341 break;
michael@0 342
michael@0 343 case "WHITESPACE":
michael@0 344 selectorState = SELECTOR_STATES.null;
michael@0 345 selector && (selector += " ");
michael@0 346 break;
michael@0 347 }
michael@0 348 break;
michael@0 349
michael@0 350 case SELECTOR_STATES.null:
michael@0 351 // From SELECTOR_STATES.null state, we can go to one of
michael@0 352 // SELECTOR_STATES.id, SELECTOR_STATES.class or SELECTOR_STATES.tag
michael@0 353 switch(token.tokenType) {
michael@0 354 case "HASH":
michael@0 355 selectorState = SELECTOR_STATES.id;
michael@0 356 selector += "#" + token.value;
michael@0 357 break;
michael@0 358
michael@0 359 case "IDENT":
michael@0 360 selectorState = SELECTOR_STATES.tag;
michael@0 361 selector += token.value;
michael@0 362 break;
michael@0 363
michael@0 364 case "DELIM":
michael@0 365 if (token.value == ".") {
michael@0 366 selectorState = SELECTOR_STATES.class;
michael@0 367 selector += ".";
michael@0 368 if (cursor <= tokIndex &&
michael@0 369 tokens[cursor].tokenType == "IDENT") {
michael@0 370 token = tokens[cursor++];
michael@0 371 selector += token.value;
michael@0 372 }
michael@0 373 } else if (token.value == "#") {
michael@0 374 selectorState = SELECTOR_STATES.id;
michael@0 375 selector += "#";
michael@0 376 } else if (token.value == "*") {
michael@0 377 selectorState = SELECTOR_STATES.tag;
michael@0 378 selector += "*";
michael@0 379 } else if (/[>~+]/.test(token.value)) {
michael@0 380 selector += token.value;
michael@0 381 } else if (token.value == ",") {
michael@0 382 selectorState = SELECTOR_STATES.null;
michael@0 383 selectors.push(selector);
michael@0 384 selector = "";
michael@0 385 }
michael@0 386 break;
michael@0 387
michael@0 388 case ":":
michael@0 389 selectorState = SELECTOR_STATES.pseudo;
michael@0 390 selector += ":";
michael@0 391 if (cursor > tokIndex)
michael@0 392 break;
michael@0 393
michael@0 394 token = tokens[cursor++];
michael@0 395 switch(token.tokenType) {
michael@0 396 case "FUNCTION":
michael@0 397 if (token.value == "not") {
michael@0 398 selectorBeforeNot = selector;
michael@0 399 selector = "";
michael@0 400 scopeStack.push("(");
michael@0 401 } else {
michael@0 402 selector += token.value + "(";
michael@0 403 }
michael@0 404 selectorState = SELECTOR_STATES.null;
michael@0 405 break;
michael@0 406
michael@0 407 case "IDENT":
michael@0 408 selector += token.value;
michael@0 409 break;
michael@0 410 }
michael@0 411 break;
michael@0 412
michael@0 413 case "[":
michael@0 414 selectorState = SELECTOR_STATES.attribute;
michael@0 415 scopeStack.push("[");
michael@0 416 selector += "[";
michael@0 417 break;
michael@0 418
michael@0 419 case ")":
michael@0 420 if (peek(scopeStack) == "(") {
michael@0 421 scopeStack.pop();
michael@0 422 selector = selectorBeforeNot + "not(" + selector + ")";
michael@0 423 selectorBeforeNot = null;
michael@0 424 } else {
michael@0 425 selector += ")";
michael@0 426 }
michael@0 427 selectorState = SELECTOR_STATES.null;
michael@0 428 break;
michael@0 429
michael@0 430 case "WHITESPACE":
michael@0 431 selector && (selector += " ");
michael@0 432 break;
michael@0 433 }
michael@0 434 break;
michael@0 435
michael@0 436 case SELECTOR_STATES.pseudo:
michael@0 437 switch(token.tokenType) {
michael@0 438 case "DELIM":
michael@0 439 if (/[>~+]/.test(token.value)) {
michael@0 440 selectorState = SELECTOR_STATES.null;
michael@0 441 selector += token.value;
michael@0 442 } else if (token.value == ",") {
michael@0 443 selectorState = SELECTOR_STATES.null;
michael@0 444 selectors.push(selector);
michael@0 445 selector = "";
michael@0 446 }
michael@0 447 break;
michael@0 448
michael@0 449 case ":":
michael@0 450 selectorState = SELECTOR_STATES.pseudo;
michael@0 451 selector += ":";
michael@0 452 if (cursor > tokIndex)
michael@0 453 break;
michael@0 454
michael@0 455 token = tokens[cursor++];
michael@0 456 switch(token.tokenType) {
michael@0 457 case "FUNCTION":
michael@0 458 if (token.value == "not") {
michael@0 459 selectorBeforeNot = selector;
michael@0 460 selector = "";
michael@0 461 scopeStack.push("(");
michael@0 462 } else {
michael@0 463 selector += token.value + "(";
michael@0 464 }
michael@0 465 selectorState = SELECTOR_STATES.null;
michael@0 466 break;
michael@0 467
michael@0 468 case "IDENT":
michael@0 469 selector += token.value;
michael@0 470 break;
michael@0 471 }
michael@0 472 break;
michael@0 473
michael@0 474 case "[":
michael@0 475 selectorState = SELECTOR_STATES.attribute;
michael@0 476 scopeStack.push("[");
michael@0 477 selector += "[";
michael@0 478 break;
michael@0 479
michael@0 480 case "WHITESPACE":
michael@0 481 selectorState = SELECTOR_STATES.null;
michael@0 482 selector && (selector += " ");
michael@0 483 break;
michael@0 484 }
michael@0 485 break;
michael@0 486
michael@0 487 case SELECTOR_STATES.attribute:
michael@0 488 switch(token.tokenType) {
michael@0 489 case "DELIM":
michael@0 490 if (/[~|^$*]/.test(token.value)) {
michael@0 491 selector += token.value;
michael@0 492 token = tokens[cursor++];
michael@0 493 }
michael@0 494 if(token.value == "=") {
michael@0 495 selectorState = SELECTOR_STATES.value;
michael@0 496 selector += token.value;
michael@0 497 }
michael@0 498 break;
michael@0 499
michael@0 500 case "IDENT":
michael@0 501 case "STRING":
michael@0 502 selector += token.value;
michael@0 503 break;
michael@0 504
michael@0 505 case "]":
michael@0 506 if (peek(scopeStack) == "[")
michael@0 507 scopeStack.pop();
michael@0 508
michael@0 509 selectorState = SELECTOR_STATES.null;
michael@0 510 selector += "]";
michael@0 511 break;
michael@0 512
michael@0 513 case "WHITESPACE":
michael@0 514 selector && (selector += " ");
michael@0 515 break;
michael@0 516 }
michael@0 517 break;
michael@0 518
michael@0 519 case SELECTOR_STATES.value:
michael@0 520 switch(token.tokenType) {
michael@0 521 case "STRING":
michael@0 522 case "IDENT":
michael@0 523 selector += token.value;
michael@0 524 break;
michael@0 525
michael@0 526 case "]":
michael@0 527 if (peek(scopeStack) == "[")
michael@0 528 scopeStack.pop();
michael@0 529
michael@0 530 selectorState = SELECTOR_STATES.null;
michael@0 531 selector += "]";
michael@0 532 break;
michael@0 533
michael@0 534 case "WHITESPACE":
michael@0 535 selector && (selector += " ");
michael@0 536 break;
michael@0 537 }
michael@0 538 break;
michael@0 539 }
michael@0 540 break;
michael@0 541
michael@0 542 case CSS_STATES.null:
michael@0 543 // From CSS_STATES.null state, we can go to either CSS_STATES.media or
michael@0 544 // CSS_STATES.selector.
michael@0 545 switch(token.tokenType) {
michael@0 546 case "HASH":
michael@0 547 selectorState = SELECTOR_STATES.id;
michael@0 548 selector = "#" + token.value;
michael@0 549 _state = CSS_STATES.selector;
michael@0 550 break;
michael@0 551
michael@0 552 case "IDENT":
michael@0 553 selectorState = SELECTOR_STATES.tag;
michael@0 554 selector = token.value;
michael@0 555 _state = CSS_STATES.selector;
michael@0 556 break;
michael@0 557
michael@0 558 case "DELIM":
michael@0 559 if (token.value == ".") {
michael@0 560 selectorState = SELECTOR_STATES.class;
michael@0 561 selector = ".";
michael@0 562 _state = CSS_STATES.selector;
michael@0 563 if (cursor <= tokIndex &&
michael@0 564 tokens[cursor].tokenType == "IDENT") {
michael@0 565 token = tokens[cursor++];
michael@0 566 selector += token.value;
michael@0 567 }
michael@0 568 } else if (token.value == "#") {
michael@0 569 selectorState = SELECTOR_STATES.id;
michael@0 570 selector = "#";
michael@0 571 _state = CSS_STATES.selector;
michael@0 572 } else if (token.value == "*") {
michael@0 573 selectorState = SELECTOR_STATES.tag;
michael@0 574 selector = "*";
michael@0 575 _state = CSS_STATES.selector;
michael@0 576 }
michael@0 577 break;
michael@0 578
michael@0 579 case ":":
michael@0 580 _state = CSS_STATES.selector;
michael@0 581 selectorState = SELECTOR_STATES.pseudo;
michael@0 582 selector += ":";
michael@0 583 if (cursor > tokIndex)
michael@0 584 break;
michael@0 585
michael@0 586 token = tokens[cursor++];
michael@0 587 switch(token.tokenType) {
michael@0 588 case "FUNCTION":
michael@0 589 if (token.value == "not") {
michael@0 590 selectorBeforeNot = selector;
michael@0 591 selector = "";
michael@0 592 scopeStack.push("(");
michael@0 593 } else {
michael@0 594 selector += token.value + "(";
michael@0 595 }
michael@0 596 selectorState = SELECTOR_STATES.null;
michael@0 597 break;
michael@0 598
michael@0 599 case "IDENT":
michael@0 600 selector += token.value;
michael@0 601 break;
michael@0 602 }
michael@0 603 break;
michael@0 604
michael@0 605 case "[":
michael@0 606 _state = CSS_STATES.selector;
michael@0 607 selectorState = SELECTOR_STATES.attribute;
michael@0 608 scopeStack.push("[");
michael@0 609 selector += "[";
michael@0 610 break;
michael@0 611
michael@0 612 case "AT-KEYWORD":
michael@0 613 _state = token.value.startsWith("m") ? CSS_STATES.media
michael@0 614 : CSS_STATES.keyframes;
michael@0 615 break;
michael@0 616
michael@0 617 case "}":
michael@0 618 if (peek(scopeStack) == "@m")
michael@0 619 scopeStack.pop();
michael@0 620
michael@0 621 break;
michael@0 622 }
michael@0 623 break;
michael@0 624
michael@0 625 case CSS_STATES.media:
michael@0 626 // From CSS_STATES.media, we can only go to CSS_STATES.null state when
michael@0 627 // we hit the first '{'
michael@0 628 if (token.tokenType == "{") {
michael@0 629 scopeStack.push("@m");
michael@0 630 _state = CSS_STATES.null;
michael@0 631 }
michael@0 632 break;
michael@0 633
michael@0 634 case CSS_STATES.keyframes:
michael@0 635 // From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
michael@0 636 // when we hit the first '{'
michael@0 637 if (token.tokenType == "{") {
michael@0 638 scopeStack.push("@k");
michael@0 639 _state = CSS_STATES.frame;
michael@0 640 }
michael@0 641 break;
michael@0 642
michael@0 643 case CSS_STATES.frame:
michael@0 644 // From CSS_STATES.frame, we can either go to CSS_STATES.property state
michael@0 645 // when we hit the first '{' or to CSS_STATES.selector when we hit '}'
michael@0 646 if (token.tokenType == "{") {
michael@0 647 scopeStack.push("f");
michael@0 648 _state = CSS_STATES.property;
michael@0 649 } else if (token.tokenType == "}") {
michael@0 650 if (peek(scopeStack) == "@k")
michael@0 651 scopeStack.pop();
michael@0 652
michael@0 653 _state = CSS_STATES.null;
michael@0 654 }
michael@0 655 break;
michael@0 656 }
michael@0 657 if (_state == CSS_STATES.null) {
michael@0 658 if (this.nullStates.length == 0) {
michael@0 659 this.nullStates.push([token.loc.end.line, token.loc.end.column,
michael@0 660 [...scopeStack]]);
michael@0 661 continue;
michael@0 662 }
michael@0 663 let tokenLine = token.loc.end.line;
michael@0 664 let tokenCh = token.loc.end.column;
michael@0 665 if (tokenLine == 0)
michael@0 666 continue;
michael@0 667 if (matchedStateIndex > -1)
michael@0 668 tokenLine += this.nullStates[matchedStateIndex][0];
michael@0 669 this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]);
michael@0 670 }
michael@0 671 }
michael@0 672 this.state = _state;
michael@0 673 this.propertyName = _state == CSS_STATES.value ? propertyName : null;
michael@0 674 this.selectorState = _state == CSS_STATES.selector ? selectorState : null;
michael@0 675 this.selectorBeforeNot = selectorBeforeNot == null ? null: selectorBeforeNot;
michael@0 676 if (token) {
michael@0 677 selector = selector.slice(0, selector.length + token.loc.end.column - ch);
michael@0 678 this.selector = selector;
michael@0 679 }
michael@0 680 else {
michael@0 681 this.selector = "";
michael@0 682 }
michael@0 683 this.selectors = selectors;
michael@0 684
michael@0 685 if (token && token.tokenType != "WHITESPACE") {
michael@0 686 this.completing = ((token.value || token.repr || token.tokenType) + "")
michael@0 687 .slice(0, ch - token.loc.start.column)
michael@0 688 .replace(/^[.#]$/, "");
michael@0 689 } else {
michael@0 690 this.completing = "";
michael@0 691 }
michael@0 692 // Special case the situation when the user just entered ":" after typing a
michael@0 693 // property name.
michael@0 694 if (this.completing == ":" && _state == CSS_STATES.value)
michael@0 695 this.completing = "";
michael@0 696
michael@0 697 // Special check for !important; case.
michael@0 698 if (token && tokens[cursor - 2] && tokens[cursor - 2].value == "!" &&
michael@0 699 this.completing == "important".slice(0, this.completing.length)) {
michael@0 700 this.completing = "!" + this.completing;
michael@0 701 }
michael@0 702 return _state;
michael@0 703 },
michael@0 704
michael@0 705 /**
michael@0 706 * Queries the DOM Walker actor for suggestions regarding the selector being
michael@0 707 * completed
michael@0 708 */
michael@0 709 suggestSelectors: function () {
michael@0 710 let walker = this.walker;
michael@0 711 if (!walker)
michael@0 712 return Promise.resolve([]);
michael@0 713
michael@0 714 let query = this.selector;
michael@0 715 // Even though the selector matched atleast one node, there is still
michael@0 716 // possibility of suggestions.
michael@0 717 switch(this.selectorState) {
michael@0 718 case SELECTOR_STATES.null:
michael@0 719 query += "*";
michael@0 720 break;
michael@0 721
michael@0 722 case SELECTOR_STATES.tag:
michael@0 723 query = query.slice(0, query.length - this.completing.length);
michael@0 724 break;
michael@0 725
michael@0 726 case SELECTOR_STATES.id:
michael@0 727 case SELECTOR_STATES.class:
michael@0 728 case SELECTOR_STATES.pseudo:
michael@0 729 if (/^[.:#]$/.test(this.completing)) {
michael@0 730 query = query.slice(0, query.length - this.completing.length);
michael@0 731 this.completing = "";
michael@0 732 } else {
michael@0 733 query = query.slice(0, query.length - this.completing.length - 1);
michael@0 734 }
michael@0 735 break;
michael@0 736 }
michael@0 737
michael@0 738 if (/[\s+>~]$/.test(query) &&
michael@0 739 this.selectorState != SELECTOR_STATES.attribute &&
michael@0 740 this.selectorState != SELECTOR_STATES.value) {
michael@0 741 query += "*";
michael@0 742 }
michael@0 743
michael@0 744 // Set the values that this request was supposed to suggest to.
michael@0 745 this._currentQuery = query;
michael@0 746 return walker.getSuggestionsForQuery(query, this.completing, this.selectorState)
michael@0 747 .then(result => this.prepareSelectorResults(result));
michael@0 748 },
michael@0 749
michael@0 750 /**
michael@0 751 * Prepares the selector suggestions returned by the walker actor.
michael@0 752 */
michael@0 753 prepareSelectorResults: function(result) {
michael@0 754 if (this._currentQuery != result.query)
michael@0 755 return [];
michael@0 756
michael@0 757 result = result.suggestions;
michael@0 758 let query = this.selector;
michael@0 759 let completion = [];
michael@0 760 for (let value of result) {
michael@0 761 switch(this.selectorState) {
michael@0 762 case SELECTOR_STATES.id:
michael@0 763 case SELECTOR_STATES.class:
michael@0 764 case SELECTOR_STATES.pseudo:
michael@0 765 if (/^[.:#]$/.test(this.completing)) {
michael@0 766 value[0] = query.slice(0, query.length - this.completing.length) +
michael@0 767 value[0];
michael@0 768 } else {
michael@0 769 value[0] = query.slice(0, query.length - this.completing.length - 1) +
michael@0 770 value[0];
michael@0 771 }
michael@0 772 break;
michael@0 773
michael@0 774 case SELECTOR_STATES.tag:
michael@0 775 value[0] = query.slice(0, query.length - this.completing.length) +
michael@0 776 value[0];
michael@0 777 break;
michael@0 778
michael@0 779 case SELECTOR_STATES.null:
michael@0 780 value[0] = query + value[0];
michael@0 781 break;
michael@0 782
michael@0 783 default:
michael@0 784 value[0] = query.slice(0, query.length - this.completing.length) +
michael@0 785 value[0];
michael@0 786 }
michael@0 787 completion.push({
michael@0 788 label: value[0],
michael@0 789 preLabel: query,
michael@0 790 text: value[0],
michael@0 791 score: value[1]
michael@0 792 });
michael@0 793 if (completion.length > this.maxEntries - 1)
michael@0 794 break;
michael@0 795 }
michael@0 796 return completion;
michael@0 797 },
michael@0 798
michael@0 799 /**
michael@0 800 * Returns CSS property name suggestions based on the input.
michael@0 801 *
michael@0 802 * @param startProp {String} Initial part of the property being completed.
michael@0 803 */
michael@0 804 completeProperties: function(startProp) {
michael@0 805 let finalList = [];
michael@0 806 if (!startProp)
michael@0 807 return Promise.resolve(finalList);
michael@0 808
michael@0 809 let length = propertyNames.length;
michael@0 810 let i = 0, count = 0;
michael@0 811 for (; i < length && count < this.maxEntries; i++) {
michael@0 812 if (propertyNames[i].startsWith(startProp)) {
michael@0 813 count++;
michael@0 814 let propName = propertyNames[i];
michael@0 815 finalList.push({
michael@0 816 preLabel: startProp,
michael@0 817 label: propName,
michael@0 818 text: propName + ": "
michael@0 819 });
michael@0 820 } else if (propertyNames[i] > startProp) {
michael@0 821 // We have crossed all possible matches alphabetically.
michael@0 822 break;
michael@0 823 }
michael@0 824 }
michael@0 825 return Promise.resolve(finalList);
michael@0 826 },
michael@0 827
michael@0 828 /**
michael@0 829 * Returns CSS value suggestions based on the corresponding property.
michael@0 830 *
michael@0 831 * @param propName {String} The property to which the value being completed
michael@0 832 * belongs.
michael@0 833 * @param startValue {String} Initial part of the value being completed.
michael@0 834 */
michael@0 835 completeValues: function(propName, startValue) {
michael@0 836 let finalList = [];
michael@0 837 let list = ["!important;", ...(properties[propName] || [])];
michael@0 838 // If there is no character being completed, we are showing an initial list
michael@0 839 // of possible values. Skipping '!important' in this case.
michael@0 840 if (!startValue)
michael@0 841 list.splice(0, 1);
michael@0 842
michael@0 843 let length = list.length;
michael@0 844 let i = 0, count = 0;
michael@0 845 for (; i < length && count < this.maxEntries; i++) {
michael@0 846 if (list[i].startsWith(startValue)) {
michael@0 847 count++;
michael@0 848 let value = list[i];
michael@0 849 finalList.push({
michael@0 850 preLabel: startValue,
michael@0 851 label: value,
michael@0 852 text: value
michael@0 853 });
michael@0 854 } else if (list[i] > startValue) {
michael@0 855 // We have crossed all possible matches alphabetically.
michael@0 856 break;
michael@0 857 }
michael@0 858 }
michael@0 859 return Promise.resolve(finalList);
michael@0 860 },
michael@0 861
michael@0 862 /**
michael@0 863 * A biased binary search in a sorted array where the middle element is
michael@0 864 * calculated based on the values at the lower and the upper index in each
michael@0 865 * iteration.
michael@0 866 *
michael@0 867 * This method returns the index of the closest null state from the passed
michael@0 868 * `line` argument. Once we have the closest null state, we can start applying
michael@0 869 * the state machine logic from that location instead of the absolute starting
michael@0 870 * of the CSS source. This speeds up the tokenizing and the state machine a
michael@0 871 * lot while using autocompletion at high line numbers in a CSS source.
michael@0 872 */
michael@0 873 findNearestNullState: function(line) {
michael@0 874 let arr = this.nullStates;
michael@0 875 let high = arr.length - 1;
michael@0 876 let low = 0;
michael@0 877 let target = 0;
michael@0 878
michael@0 879 if (high < 0)
michael@0 880 return -1;
michael@0 881 if (arr[high][0] <= line)
michael@0 882 return high;
michael@0 883 if (arr[low][0] > line)
michael@0 884 return -1;
michael@0 885
michael@0 886 while (high > low) {
michael@0 887 if (arr[low][0] <= line && arr[low [0]+ 1] > line)
michael@0 888 return low;
michael@0 889 if (arr[high][0] > line && arr[high - 1][0] <= line)
michael@0 890 return high - 1;
michael@0 891
michael@0 892 target = (((line - arr[low][0]) / (arr[high][0] - arr[low][0])) *
michael@0 893 (high - low)) | 0;
michael@0 894
michael@0 895 if (arr[target][0] <= line && arr[target + 1][0] > line) {
michael@0 896 return target;
michael@0 897 } else if (line > arr[target][0]) {
michael@0 898 low = target + 1;
michael@0 899 high--;
michael@0 900 } else {
michael@0 901 high = target - 1;
michael@0 902 low++;
michael@0 903 }
michael@0 904 }
michael@0 905
michael@0 906 return -1;
michael@0 907 },
michael@0 908
michael@0 909 /**
michael@0 910 * Invalidates the state cache for and above the line.
michael@0 911 */
michael@0 912 invalidateCache: function(line) {
michael@0 913 this.nullStates.length = this.findNearestNullState(line) + 1;
michael@0 914 },
michael@0 915
michael@0 916 /**
michael@0 917 * Get the state information about a token surrounding the {line, ch} position
michael@0 918 *
michael@0 919 * @param {string} source
michael@0 920 * The complete source of the CSS file. Unlike resolve state method,
michael@0 921 * this method requires the full source.
michael@0 922 * @param {object} caret
michael@0 923 * The line, ch position of the caret.
michael@0 924 *
michael@0 925 * @returns {object}
michael@0 926 * An object containing the state of token covered by the caret.
michael@0 927 * The object has following properties when the the state is
michael@0 928 * "selector", "value" or "property", null otherwise:
michael@0 929 * - state {string} one of CSS_STATES - "selector", "value" etc.
michael@0 930 * - selector {string} The selector at the caret when `state` is
michael@0 931 * selector. OR
michael@0 932 * - selectors {[string]} Array of selector strings in case when
michael@0 933 * `state` is "value" or "property"
michael@0 934 * - propertyName {string} The property name at the current caret or
michael@0 935 * the property name corresponding to the value at
michael@0 936 * the caret.
michael@0 937 * - value {string} The css value at the current caret.
michael@0 938 * - loc {object} An object containing the starting and the ending
michael@0 939 * caret position of the whole selector, value or property.
michael@0 940 * - { start: {line, ch}, end: {line, ch}}
michael@0 941 */
michael@0 942 getInfoAt: function(source, caret) {
michael@0 943 // Limits the input source till the {line, ch} caret position
michael@0 944 function limit(source, {line, ch}) {
michael@0 945 line++;
michael@0 946 let list = source.split("\n");
michael@0 947 if (list.length < line)
michael@0 948 return source;
michael@0 949 if (line == 1)
michael@0 950 return list[0].slice(0, ch);
michael@0 951 return [...list.slice(0, line - 1), list[line - 1].slice(0, ch)].join("\n");
michael@0 952 }
michael@0 953
michael@0 954 // Get the state at the given line, ch
michael@0 955 let state = this.resolveState(limit(source, caret), caret);
michael@0 956 let propertyName = this.propertyName;
michael@0 957 let {line, ch} = caret;
michael@0 958 let sourceArray = source.split("\n");
michael@0 959 let limitedSource = limit(source, caret);
michael@0 960
michael@0 961 /**
michael@0 962 * Method to traverse forwards from the caret location to figure out the
michael@0 963 * ending point of a selector or css value.
michael@0 964 *
michael@0 965 * @param {function} check
michael@0 966 * A method which takes the current state as an input and determines
michael@0 967 * whether the state changed or not.
michael@0 968 */
michael@0 969 let traverseForward = check => {
michael@0 970 let location;
michael@0 971 // Backward loop to determine the beginning location of the selector.
michael@0 972 do {
michael@0 973 let lineText = sourceArray[line];
michael@0 974 if (line == caret.line)
michael@0 975 lineText = lineText.substring(caret.ch);
michael@0 976
michael@0 977 let tokens = cssTokenizer(lineText, {loc: true});
michael@0 978 let found = false;
michael@0 979 let ech = line == caret.line ? caret.ch : 0;
michael@0 980 for (let i = 0; i < tokens.length; i++) {
michael@0 981 let token = tokens[i];
michael@0 982 // If the line is completely spaces, handle it differently
michael@0 983 if (lineText.trim() == "") {
michael@0 984 limitedSource += lineText;
michael@0 985 } else {
michael@0 986 limitedSource += sourceArray[line]
michael@0 987 .substring(ech + token.loc.start.column,
michael@0 988 ech + token.loc.end.column);
michael@0 989 }
michael@0 990
michael@0 991 // Whitespace cannot change state.
michael@0 992 if (token.tokenType == "WHITESPACE")
michael@0 993 continue;
michael@0 994
michael@0 995 let state = this.resolveState(limitedSource, {
michael@0 996 line: line,
michael@0 997 ch: token.loc.end.column + ech
michael@0 998 });
michael@0 999 if (check(state)) {
michael@0 1000 if (tokens[i - 1] && tokens[i - 1].tokenType == "WHITESPACE")
michael@0 1001 token = tokens[i - 1];
michael@0 1002 location = {
michael@0 1003 line: line,
michael@0 1004 ch: token.loc.start.column + ech
michael@0 1005 };
michael@0 1006 found = true;
michael@0 1007 break;
michael@0 1008 }
michael@0 1009 }
michael@0 1010 limitedSource += "\n";
michael@0 1011 if (found)
michael@0 1012 break;
michael@0 1013 } while (line++ < sourceArray.length);
michael@0 1014 return location;
michael@0 1015 };
michael@0 1016
michael@0 1017 /**
michael@0 1018 * Method to traverse backwards from the caret location to figure out the
michael@0 1019 * starting point of a selector or css value.
michael@0 1020 *
michael@0 1021 * @param {function} check
michael@0 1022 * A method which takes the current state as an input and determines
michael@0 1023 * whether the state changed or not.
michael@0 1024 * @param {boolean} isValue
michael@0 1025 * true if the traversal is being done for a css value state.
michael@0 1026 */
michael@0 1027 let traverseBackwards = (check, isValue) => {
michael@0 1028 let location;
michael@0 1029 // Backward loop to determine the beginning location of the selector.
michael@0 1030 do {
michael@0 1031 let lineText = sourceArray[line];
michael@0 1032 if (line == caret.line)
michael@0 1033 lineText = lineText.substring(0, caret.ch);
michael@0 1034
michael@0 1035 let tokens = cssTokenizer(lineText, {loc: true});
michael@0 1036 let found = false;
michael@0 1037 let ech = 0;
michael@0 1038 for (let i = tokens.length - 2; i >= 0; i--) {
michael@0 1039 let token = tokens[i];
michael@0 1040 // If the line is completely spaces, handle it differently
michael@0 1041 if (lineText.trim() == "") {
michael@0 1042 limitedSource = limitedSource.slice(0, -1 * lineText.length);
michael@0 1043 } else {
michael@0 1044 let length = token.loc.end.column - token.loc.start.column;
michael@0 1045 limitedSource = limitedSource.slice(0, -1 * length);
michael@0 1046 }
michael@0 1047
michael@0 1048 // Whitespace cannot change state.
michael@0 1049 if (token.tokenType == "WHITESPACE")
michael@0 1050 continue;
michael@0 1051
michael@0 1052 let state = this.resolveState(limitedSource, {
michael@0 1053 line: line,
michael@0 1054 ch: token.loc.start.column
michael@0 1055 });
michael@0 1056 if (check(state)) {
michael@0 1057 if (tokens[i + 1] && tokens[i + 1].tokenType == "WHITESPACE")
michael@0 1058 token = tokens[i + 1];
michael@0 1059 location = {
michael@0 1060 line: line,
michael@0 1061 ch: isValue ? token.loc.end.column: token.loc.start.column
michael@0 1062 };
michael@0 1063 found = true;
michael@0 1064 break;
michael@0 1065 }
michael@0 1066 }
michael@0 1067 limitedSource = limitedSource.slice(0, -1);
michael@0 1068 if (found)
michael@0 1069 break;
michael@0 1070 } while (line-- >= 0);
michael@0 1071 return location;
michael@0 1072 };
michael@0 1073
michael@0 1074 if (state == CSS_STATES.selector) {
michael@0 1075 // For selector state, the ending and starting point of the selector is
michael@0 1076 // either when the state changes or the selector becomes empty and a
michael@0 1077 // single selector can span multiple lines.
michael@0 1078 // Backward loop to determine the beginning location of the selector.
michael@0 1079 let start = traverseBackwards(state => {
michael@0 1080 return (state != CSS_STATES.selector ||
michael@0 1081 (this.selector == "" && this.selectorBeforeNot == null));
michael@0 1082 });
michael@0 1083
michael@0 1084 line = caret.line;
michael@0 1085 limitedSource = limit(source, caret);
michael@0 1086 // Forward loop to determine the ending location of the selector.
michael@0 1087 let end = traverseForward(state => {
michael@0 1088 return (state != CSS_STATES.selector ||
michael@0 1089 (this.selector == "" && this.selectorBeforeNot == null));
michael@0 1090 });
michael@0 1091
michael@0 1092 // Since we have start and end positions, figure out the whole selector.
michael@0 1093 let selector = source.split("\n").slice(start.line, end.line + 1);
michael@0 1094 selector[selector.length - 1] =
michael@0 1095 selector[selector.length - 1].substring(0, end.ch);
michael@0 1096 selector[0] = selector[0].substring(start.ch);
michael@0 1097 selector = selector.join("\n");
michael@0 1098 return {
michael@0 1099 state: state,
michael@0 1100 selector: selector,
michael@0 1101 loc: {
michael@0 1102 start: start,
michael@0 1103 end: end
michael@0 1104 }
michael@0 1105 };
michael@0 1106 }
michael@0 1107 else if (state == CSS_STATES.property) {
michael@0 1108 // A property can only be a single word and thus very easy to calculate.
michael@0 1109 let tokens = cssTokenizer(sourceArray[line], {loc: true});
michael@0 1110 for (let token of tokens) {
michael@0 1111 if (token.loc.start.column <= ch && token.loc.end.column >= ch) {
michael@0 1112 return {
michael@0 1113 state: state,
michael@0 1114 propertyName: token.value,
michael@0 1115 selectors: this.selectors,
michael@0 1116 loc: {
michael@0 1117 start: {
michael@0 1118 line: line,
michael@0 1119 ch: token.loc.start.column
michael@0 1120 },
michael@0 1121 end: {
michael@0 1122 line: line,
michael@0 1123 ch: token.loc.end.column
michael@0 1124 }
michael@0 1125 }
michael@0 1126 };
michael@0 1127 }
michael@0 1128 }
michael@0 1129 }
michael@0 1130 else if (state == CSS_STATES.value) {
michael@0 1131 // CSS value can be multiline too, so we go forward and backwards to
michael@0 1132 // determine the bounds of the value at caret
michael@0 1133 let start = traverseBackwards(state => state != CSS_STATES.value, true);
michael@0 1134
michael@0 1135 line = caret.line;
michael@0 1136 limitedSource = limit(source, caret);
michael@0 1137 let end = traverseForward(state => state != CSS_STATES.value);
michael@0 1138
michael@0 1139 let value = source.split("\n").slice(start.line, end.line + 1);
michael@0 1140 value[value.length - 1] = value[value.length - 1].substring(0, end.ch);
michael@0 1141 value[0] = value[0].substring(start.ch);
michael@0 1142 value = value.join("\n");
michael@0 1143 return {
michael@0 1144 state: state,
michael@0 1145 propertyName: propertyName,
michael@0 1146 selectors: this.selectors,
michael@0 1147 value: value,
michael@0 1148 loc: {
michael@0 1149 start: start,
michael@0 1150 end: end
michael@0 1151 }
michael@0 1152 };
michael@0 1153 }
michael@0 1154 return null;
michael@0 1155 }
michael@0 1156 }
michael@0 1157
michael@0 1158 /**
michael@0 1159 * Returns a list of all property names and a map of property name vs possible
michael@0 1160 * CSS values provided by the Gecko engine.
michael@0 1161 *
michael@0 1162 * @return {Object} An object with following properties:
michael@0 1163 * - propertyNames {Array} Array of string containing all the possible
michael@0 1164 * CSS property names.
michael@0 1165 * - properties {Object|Map} A map where key is the property name and
michael@0 1166 * value is an array of string containing all the possible
michael@0 1167 * CSS values the property can have.
michael@0 1168 */
michael@0 1169 function getCSSKeywords() {
michael@0 1170 let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
michael@0 1171 .getService(Ci.inIDOMUtils);
michael@0 1172 let props = {};
michael@0 1173 let propNames = domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES);
michael@0 1174 propNames.forEach(prop => {
michael@0 1175 props[prop] = domUtils.getCSSValuesForProperty(prop).sort();
michael@0 1176 });
michael@0 1177 return {
michael@0 1178 properties: props,
michael@0 1179 propertyNames: propNames.sort()
michael@0 1180 };
michael@0 1181 }
michael@0 1182
michael@0 1183 module.exports = CSSCompleter;

mercurial