parsing-utils.js (27641B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // This file holds various CSS parsing and rewriting utilities. 6 // Some entry points of note are: 7 // parseDeclarations - parse a CSS rule into declarations 8 // parsePseudoClassesAndAttributes - parse selector and extract 9 // pseudo-classes 10 // parseSingleValue - parse a single CSS property value 11 12 "use strict"; 13 14 const { 15 InspectorCSSParserWrapper, 16 } = require("resource://devtools/shared/css/lexer.js"); 17 18 loader.lazyRequireGetter( 19 this, 20 "CSS_ANGLEUNIT", 21 "resource://devtools/shared/css/constants.js", 22 true 23 ); 24 25 const SELECTOR_ATTRIBUTE = (exports.SELECTOR_ATTRIBUTE = 1); 26 const SELECTOR_ELEMENT = (exports.SELECTOR_ELEMENT = 2); 27 const SELECTOR_PSEUDO_CLASS = (exports.SELECTOR_PSEUDO_CLASS = 3); 28 29 // When commenting out a declaration, we put this character into the 30 // comment opener so that future parses of the commented text know to 31 // bypass the property name validity heuristic. 32 const COMMENT_PARSING_HEURISTIC_BYPASS_CHAR = 33 (exports.COMMENT_PARSING_HEURISTIC_BYPASS_CHAR = "!"); 34 35 /** 36 * A generator function that lexes a CSS source string, yielding the 37 * CSS tokens. Comment tokens are dropped. 38 * 39 * @param {string} string - CSS source string 40 * @yields {CSSToken} The next CSSToken that is lexed 41 * @see CSSToken for details about the returned tokens 42 */ 43 function* cssTokenizer(string) { 44 const lexer = new InspectorCSSParserWrapper(string); 45 while (true) { 46 const token = lexer.nextToken(); 47 if (!token) { 48 break; 49 } 50 // None of the existing consumers want comments. 51 if (token.tokenType !== "Comment") { 52 yield token; 53 } 54 } 55 } 56 57 /** 58 * Pass |string| to the CSS lexer and return an array of all the 59 * returned tokens. Comment tokens are not included. In addition to 60 * the usual information, each token will have starting and ending 61 * line and column information attached. Specifically, each token 62 * has an additional "loc" attribute. This attribute is an object 63 * of the form {line: L, column: C}. Lines and columns are both zero 64 * based. 65 * 66 * It's best not to add new uses of this function. In general it is 67 * simpler and better to use the CSSToken offsets, rather than line 68 * and column. Also, this function lexes the entire input string at 69 * once, rather than lazily yielding a token stream. Use 70 * |cssTokenizer| or |getCSSLexer| instead. 71 * 72 * @param {string} string The input string. 73 * @returns {CSSToken[]} An array of tokens (@see CSSToken) that have 74 * line and column information. 75 */ 76 function cssTokenizerWithLineColumn(string) { 77 const lexer = new InspectorCSSParserWrapper(string); 78 const result = []; 79 let prevToken = undefined; 80 while (true) { 81 const token = lexer.nextToken(); 82 const lineNumber = lexer.lineNumber; 83 const columnNumber = lexer.columnNumber; 84 85 if (prevToken) { 86 prevToken.loc.end = { 87 line: lineNumber, 88 column: columnNumber, 89 }; 90 } 91 92 if (!token) { 93 break; 94 } 95 96 if (token.tokenType === "Comment") { 97 // We've already dealt with the previous token's location. 98 prevToken = undefined; 99 } else { 100 const startLoc = { 101 line: lineNumber, 102 column: columnNumber, 103 }; 104 token.loc = { start: startLoc }; 105 106 result.push(token); 107 prevToken = token; 108 } 109 } 110 111 return result; 112 } 113 114 /** 115 * Escape a comment body. Find the comment start and end strings in a 116 * string and inserts backslashes so that the resulting text can 117 * itself be put inside a comment. 118 * 119 * @param {string} inputString 120 * input string 121 * @returns {string} the escaped result 122 */ 123 function escapeCSSComment(inputString) { 124 const result = inputString.replace(/\/(\\*)\*/g, "/\\$1*"); 125 return result.replace(/\*(\\*)\//g, "*\\$1/"); 126 } 127 128 /** 129 * Un-escape a comment body. This undoes any comment escaping that 130 * was done by escapeCSSComment. That is, given input like "/\* 131 * comment *\/", it will strip the backslashes. 132 * 133 * @param {string} inputString 134 * input string 135 * @returns {string} the un-escaped result 136 */ 137 function unescapeCSSComment(inputString) { 138 const result = inputString.replace(/\/\\(\\*)\*/g, "/$1*"); 139 return result.replace(/\*\\(\\*)\//g, "*$1/"); 140 } 141 142 /** 143 * Parsed CSS declaration 144 * 145 * @typedef {object} ParsedDeclaration 146 * @property {string} name - The name of the declaration 147 * @property {string} value - The (authored) value of the declaration (i.e. not the computed value) 148 * @property {string} priority - "important" if the declaration ends with `!important`, 149 * empty string otherwise. 150 * @property {string} terminator - String to use to terminate the declaration, usually "" 151 * to mean no additional termination is needed. 152 * @property {number[]} offsets - Holds the offsets of the start and end of the declaration 153 * text, in a form suitable for use with String.substring. 154 * @property {number[]} colonOffsets - Holds the start and end locations of the colon (":") 155 * that separates the property name from the value. 156 * @property {number[]} [commentOffsets] - If the declaration appears in a comment, holds the 157 * offsets of the start and end of the enclosing comment. 158 * @property {boolean} [isCustomProperty] - Is this a CSS custom property (aka CSS variable) 159 * declaration. Only set when the declaration is a custom property so we save 160 * some cycles for non custom property declaration when sending them to the client. 161 */ 162 163 /** 164 * A helper function for @see parseDeclarations that handles parsing 165 * of comment text. This wraps a recursive call to parseDeclarations 166 * with the processing needed to ensure that offsets in the result 167 * refer back to the original, unescaped, input string. 168 * 169 * @param {Function} isCssPropertyKnown 170 * A function to check if the CSS property is known. This is either an 171 * internal server function or from the CssPropertiesFront. 172 * @param {string} commentText The text of the comment, without the 173 * delimiters. 174 * @param {number} startOffset The offset of the comment opener 175 * in the original text. 176 * @param {number} endOffset The offset of the comment closer 177 * in the original text. 178 * @returns {ParsedDeclaration[]} Array of parsed declarations. 179 */ 180 function parseCommentDeclarations( 181 isCssPropertyKnown, 182 commentText, 183 startOffset, 184 endOffset 185 ) { 186 let commentOverride = false; 187 if (commentText === "") { 188 return []; 189 } else if (commentText[0] === COMMENT_PARSING_HEURISTIC_BYPASS_CHAR) { 190 // This is the special sign that the comment was written by 191 // rewriteDeclarations and so we should bypass the usual 192 // heuristic. 193 commentOverride = true; 194 commentText = commentText.substring(1); 195 } 196 197 const rewrittenText = unescapeCSSComment(commentText); 198 199 // We might have rewritten an embedded comment. For example 200 // /\* ... *\/ would turn into /* ... */. 201 // This rewriting is necessary for proper lexing, but it means 202 // that the offsets we get back can be off. So now we compute 203 // a map so that we can rewrite offsets later. The map is the same 204 // length as |rewrittenText| and tells us how to map an index 205 // into |rewrittenText| to an index into |commentText|. 206 // 207 // First, we find the location of each comment starter or closer in 208 // |rewrittenText|. At these spots we put a 1 into |rewrites|. 209 // Then we walk the array again, using the elements to compute a 210 // delta, which we use to make the final mapping. 211 // 212 // Note we allocate one extra entry because we can see an ending 213 // offset that is equal to the length. 214 const rewrites = new Array(rewrittenText.length + 1).fill(0); 215 216 const commentRe = /\/\\*\*|\*\\*\//g; 217 while (true) { 218 const matchData = commentRe.exec(rewrittenText); 219 if (!matchData) { 220 break; 221 } 222 rewrites[matchData.index] = 1; 223 } 224 225 let delta = 0; 226 for (let i = 0; i <= rewrittenText.length; ++i) { 227 delta += rewrites[i]; 228 // |startOffset| to add the offset from the comment starter, |+2| 229 // for the length of the "/*", then |i| and |delta| as described 230 // above. 231 rewrites[i] = startOffset + 2 + i + delta; 232 if (commentOverride) { 233 ++rewrites[i]; 234 } 235 } 236 237 // Note that we pass "false" for parseComments here. It doesn't 238 // seem worthwhile to support declarations in comments-in-comments 239 // here, as there's no way to generate those using the tools, and 240 // users would be crazy to write such things. 241 const newDecls = parseDeclarationsInternal( 242 isCssPropertyKnown, 243 rewrittenText, 244 false, 245 true, 246 commentOverride 247 ); 248 for (const decl of newDecls) { 249 decl.offsets[0] = rewrites[decl.offsets[0]]; 250 decl.offsets[1] = rewrites[decl.offsets[1]]; 251 decl.colonOffsets[0] = rewrites[decl.colonOffsets[0]]; 252 decl.colonOffsets[1] = rewrites[decl.colonOffsets[1]]; 253 decl.commentOffsets = [startOffset, endOffset]; 254 } 255 return newDecls; 256 } 257 258 /** 259 * A helper function for parseDeclarationsInternal that creates a new 260 * empty declaration. 261 * 262 * @returns {ParsedDeclaration} an empty declaration that matches what is returned by parseDeclarations 263 */ 264 function getEmptyDeclaration() { 265 return { 266 name: "", 267 value: "", 268 priority: "", 269 terminator: "", 270 offsets: [undefined, undefined], 271 colonOffsets: false, 272 }; 273 } 274 275 /** 276 * Like trim, but only trims CSS-allowed whitespace. 277 * 278 * @param {string} str - The string to trim 279 */ 280 function cssTrim(str) { 281 const match = /^[ \t\r\n\f]*(.*?)[ \t\r\n\f]*$/.exec(str); 282 if (match) { 283 return match[1]; 284 } 285 return str; 286 } 287 288 /** 289 * A helper function that does all the parsing work for 290 * parseDeclarations. This is separate because it has some arguments 291 * that don't make sense in isolation. 292 * 293 * The return value and arguments are like parseDeclarations, with 294 * these additional arguments. 295 * 296 * @param {Function} isCssPropertyKnown 297 * Function to check if the CSS property is known. 298 * @param {string} inputString 299 * An input string of CSS 300 * @param {boolean} parseComments 301 * If true, try to parse the contents of comments as well. 302 * A comment will only be parsed if it occurs outside of 303 * the body of some other declaration. 304 * @param {boolean} inComment 305 * If true, assume that this call is parsing some text 306 * which came from a comment in another declaration. 307 * In this case some heuristics are used to avoid parsing 308 * text which isn't obviously a series of declarations. 309 * @param {boolean} commentOverride 310 * This only makes sense when inComment=true. 311 * When true, assume that the comment was generated by 312 * rewriteDeclarations, and skip the usual name-checking 313 * heuristic. 314 * @returns {ParsedDeclaration[]} Array of parsed declarations. 315 */ 316 // eslint-disable-next-line complexity 317 function parseDeclarationsInternal( 318 isCssPropertyKnown, 319 inputString, 320 parseComments, 321 inComment, 322 commentOverride 323 ) { 324 if (inputString === null || inputString === undefined) { 325 throw new Error("empty input string"); 326 } 327 328 const lexer = new InspectorCSSParserWrapper(inputString, { 329 trackEOFChars: true, 330 }); 331 332 let declarations = [getEmptyDeclaration()]; 333 let lastProp = declarations[0]; 334 335 // This tracks the various CSS blocks the current token is in currently. 336 // This is a stack we push to when a block is opened, and we pop from when a block is 337 // closed. Within a block, colons and semicolons don't advance the way they do outside 338 // of blocks. 339 let currentBlocks = []; 340 341 // This tracks the "!important" parsing state. The states are: 342 // 0 - haven't seen anything 343 // 1 - have seen "!", looking for "important" next (possibly after 344 // whitespace). 345 // 2 - have seen "!important" 346 let importantState = 0; 347 // This is true if we saw whitespace or comments between the "!" and 348 // the "important". 349 let importantWS = false; 350 351 // This tracks the nesting parsing state 352 let isInNested = false; 353 let nestingLevel = 0; 354 355 let current = ""; 356 357 const resetStateForNextDeclaration = () => { 358 current = ""; 359 currentBlocks = []; 360 importantState = 0; 361 importantWS = false; 362 declarations.push(getEmptyDeclaration()); 363 lastProp = declarations.at(-1); 364 }; 365 366 while (true) { 367 const token = lexer.nextToken(); 368 if (!token) { 369 break; 370 } 371 372 // Update the start and end offsets of the declaration, but only 373 // when we see a significant token. 374 if (token.tokenType !== "WhiteSpace" && token.tokenType !== "Comment") { 375 if (lastProp.offsets[0] === undefined) { 376 lastProp.offsets[0] = token.startOffset; 377 } 378 lastProp.offsets[1] = token.endOffset; 379 } else if ( 380 lastProp.name && 381 !current && 382 !importantState && 383 !lastProp.priority && 384 lastProp.colonOffsets[1] 385 ) { 386 // Whitespace appearing after the ":" is attributed to it. 387 lastProp.colonOffsets[1] = token.endOffset; 388 } else if (importantState === 1) { 389 importantWS = true; 390 } 391 392 if ( 393 token.tokenType === "Ident" && 394 token.text[0] == "-" && 395 token.text[1] == "-" && 396 token.text.length > 2 397 ) { 398 if (!lastProp.name) { 399 lastProp.isCustomProperty = true; 400 } 401 } 402 403 if ( 404 // If we're not already in a nested rule 405 !isInNested && 406 // and there's an opening curly bracket 407 token.tokenType === "CurlyBracketBlock" && 408 // and we're not inside a function or an attribute 409 !currentBlocks.length 410 ) { 411 // Assume we're encountering a nested rule. 412 413 if (inComment) { 414 // If we're in a comment, we still want to retrieve all the "top" level declarations, 415 // e.g. for `/* color: red; & > span { color: blue; } color: yellow; */`, we do want 416 // to get the red and yellow declarations. 417 isInNested = true; 418 nestingLevel = 1; 419 continue; 420 } 421 422 // If we're not in a comment, once we encounter a nested rule, we can stop; 423 // even if there are declarations after the nested rules, they will be retrieved in 424 // a different (CSSNestedDeclaration) rule. 425 declarations.pop(); 426 break; 427 } else if (isInNested) { 428 if (token.tokenType == "CurlyBracketBlock") { 429 nestingLevel++; 430 } else if (token.tokenType == "CloseCurlyBracket") { 431 nestingLevel--; 432 } 433 434 // If we were in a nested rule, and we saw the last closing curly bracket, 435 // reset the state to parse possible declarations declared after the nested rule. 436 if (nestingLevel === 0) { 437 isInNested = false; 438 // We need to remove the previous pending declaration and reset the state 439 declarations.pop(); 440 resetStateForNextDeclaration(); 441 } 442 continue; 443 } else if ( 444 token.tokenType === "CloseParenthesis" || 445 token.tokenType === "CloseSquareBracket" 446 ) { 447 // Closing the last block that was opened. 448 currentBlocks.pop(); 449 current += token.text; 450 } else if ( 451 token.tokenType === "ParenthesisBlock" || 452 token.tokenType === "SquareBracketBlock" 453 ) { 454 // Opening a new block. 455 currentBlocks.push(token.text); 456 current += token.text; 457 } else if (token.tokenType === "Function") { 458 // Opening a function is like opening a new block, so push one to the stack. 459 currentBlocks.push("("); 460 current += token.text; 461 } else if (token.tokenType === "Colon") { 462 // Either way, a "!important" we've seen is no longer valid now. 463 importantState = 0; 464 importantWS = false; 465 if (!lastProp.name) { 466 // Set the current declaration name if there's no name yet 467 lastProp.name = cssTrim(current); 468 lastProp.colonOffsets = [token.startOffset, token.endOffset]; 469 current = ""; 470 currentBlocks = []; 471 472 // When parsing a comment body, if the left-hand-side is not a 473 // valid property name, then drop it and stop parsing. 474 if ( 475 inComment && 476 !commentOverride && 477 !isCssPropertyKnown(lastProp.name) 478 ) { 479 lastProp.name = null; 480 break; 481 } 482 } else { 483 // Otherwise, just append ':' to the current value (declaration value 484 // with colons) 485 current += ":"; 486 } 487 } else if (token.tokenType === "Semicolon" && !currentBlocks.length) { 488 lastProp.terminator = ""; 489 // When parsing a comment, if the name hasn't been set, then we 490 // have probably just seen an ordinary semicolon used in text, 491 // so drop this and stop parsing. 492 if (inComment && !lastProp.name) { 493 current = ""; 494 currentBlocks = []; 495 break; 496 } 497 if (importantState === 2) { 498 lastProp.priority = "important"; 499 } else if (importantState === 1) { 500 current += "!"; 501 if (importantWS) { 502 current += " "; 503 } 504 } 505 lastProp.value = cssTrim(current); 506 resetStateForNextDeclaration(); 507 } else if (token.tokenType === "Ident") { 508 if (token.text === "important" && importantState === 1) { 509 importantState = 2; 510 } else { 511 if (importantState > 0) { 512 current += "!"; 513 if (importantWS) { 514 current += " "; 515 } 516 if (importantState === 2) { 517 current += "important "; 518 } 519 importantState = 0; 520 importantWS = false; 521 } 522 current += token.text; 523 } 524 } else if (token.tokenType === "Delim" && token.text === "!") { 525 importantState = 1; 526 } else if (token.tokenType === "WhiteSpace") { 527 if (current !== "") { 528 current = current.trimEnd() + " "; 529 } 530 } else if (token.tokenType === "Comment") { 531 if (parseComments && !lastProp.name && !lastProp.value) { 532 const commentText = inputString.substring( 533 token.startOffset + 2, 534 token.endOffset - 2 535 ); 536 const newDecls = parseCommentDeclarations( 537 isCssPropertyKnown, 538 commentText, 539 token.startOffset, 540 token.endOffset 541 ); 542 543 // Insert the new declarations just before the final element. 544 const lastDecl = declarations.pop(); 545 declarations = [...declarations, ...newDecls, lastDecl]; 546 } else { 547 current = current.trimEnd() + " "; 548 } 549 } else { 550 if (importantState > 0) { 551 current += "!"; 552 if (importantWS) { 553 current += " "; 554 } 555 if (importantState === 2) { 556 current += "important "; 557 } 558 importantState = 0; 559 importantWS = false; 560 } 561 current += inputString.substring(token.startOffset, token.endOffset); 562 } 563 } 564 565 // Handle whatever trailing properties or values might still be there 566 if (current) { 567 // If nested rule doesn't have closing bracket 568 if (isInNested && nestingLevel > 0) { 569 // We need to remove the previous (nested) pending declaration 570 declarations.pop(); 571 } else if (!lastProp.name) { 572 // Ignore this case in comments. 573 if (!inComment) { 574 // Trailing property found, e.g. p1:v1;p2:v2;p3 575 lastProp.name = cssTrim(current); 576 } 577 } else { 578 // Trailing value found, i.e. value without an ending ; 579 if (importantState === 2) { 580 lastProp.priority = "important"; 581 } else if (importantState === 1) { 582 current += "!"; 583 } 584 lastProp.value = cssTrim(current); 585 const terminator = lexer.performEOFFixup(""); 586 lastProp.terminator = terminator + ";"; 587 // If the input was unterminated, attribute the remainder to 588 // this property. This avoids some bad behavior when rewriting 589 // an unterminated comment. 590 if (terminator) { 591 lastProp.offsets[1] = inputString.length; 592 } 593 } 594 } 595 596 // Remove declarations that have neither a name nor a value 597 declarations = declarations.filter(prop => prop.name || prop.value); 598 599 return declarations; 600 } 601 602 /** 603 * Returns an array of CSS declarations given a string. 604 * For example, `parseDeclarations(isCssPropertyKnown, "--h: 1px; width: 1px !important; height: var(--h);")` 605 * would return: 606 * [{ 607 * name: "--h", 608 * value: "1px", 609 * priority: "", 610 * terminator: "", 611 * offsets: [0,9], 612 * colonOffsets: [3,5], 613 * isCustomProperty: true 614 * }, { 615 * name: "width", 616 * value: "1px", 617 * priority: "important", 618 * terminator: "", 619 * offsets: [10,32], 620 * colonOffsets: [15,17] 621 * }, { 622 * name: "height", 623 * value: "var(--h)", 624 * priority: "", 625 * terminator: "", 626 * offsets: [33,50], 627 * colonOffsets: [39,41], 628 * }] 629 * 630 * The input string is assumed to only contain declarations so `{` and `}` 631 * characters will be treated as part of either the property or value, 632 * depending where it's found. 633 * 634 * @param {Function} isCssPropertyKnown 635 * A function to check if the CSS property is known. This is either an 636 * internal server function or from the CssPropertiesFront. 637 * that are supported by the server. 638 * @param {string} inputString 639 * An input string of CSS 640 * @param {boolean} parseComments 641 * If true, try to parse the contents of comments as well. 642 * A comment will only be parsed if it occurs outside of 643 * the body of some other declaration. 644 * @returns {ParsedDeclaration[]} Array of parsed declarations. 645 */ 646 function parseDeclarations( 647 isCssPropertyKnown, 648 inputString, 649 parseComments = false 650 ) { 651 return parseDeclarationsInternal( 652 isCssPropertyKnown, 653 inputString, 654 parseComments, 655 false, 656 false 657 ); 658 } 659 660 /** 661 * Like @see parseDeclarations, but removes properties that do not have a name. 662 * 663 * @param {Function} isCssPropertyKnown 664 * A function to check if the CSS property is known. This is either an 665 * internal server function or from the CssPropertiesFront. 666 * that are supported by the server. 667 * @param {string} inputString 668 * An input string of CSS 669 * @param {boolean} parseComments 670 * If true, try to parse the contents of comments as well. 671 * A comment will only be parsed if it occurs outside of 672 * the body of some other declaration. 673 * @returns {ParsedDeclaration[]} Array of parsed declarations. 674 */ 675 function parseNamedDeclarations( 676 isCssPropertyKnown, 677 inputString, 678 parseComments = false 679 ) { 680 return parseDeclarations( 681 isCssPropertyKnown, 682 inputString, 683 parseComments 684 ).filter(item => !!item.name); 685 } 686 687 /** 688 * Returns an array of the parsed CSS selector value and type given a string. 689 * 690 * The components making up the CSS selector can be extracted into 3 different 691 * types: element, attribute and pseudoclass. The object that is appended to 692 * the returned array contains the value related to one of the 3 types described 693 * along with the actual type. 694 * 695 * The following are the 3 types that can be returned in the object signature: 696 * (1) SELECTOR_ATTRIBUTE 697 * (2) SELECTOR_ELEMENT 698 * (3) SELECTOR_PSEUDO_CLASS 699 * 700 * @param {string} value 701 * The CSS selector text. 702 * @returns {Array} an array of objects with the following signature: 703 * [{ "value": string, "type": integer }, ...] 704 */ 705 // eslint-disable-next-line complexity 706 function parsePseudoClassesAndAttributes(value) { 707 if (!value) { 708 throw new Error("empty input string"); 709 } 710 711 // See InspectorCSSToken dictionnary in InspectorUtils.webidl for more information 712 // about the tokens. 713 const tokensIterator = cssTokenizer(value); 714 const result = []; 715 let current = ""; 716 let functionCount = 0; 717 let hasAttribute = false; 718 let hasColon = false; 719 720 for (const token of tokensIterator) { 721 if (token.tokenType === "Ident") { 722 current += value.substring(token.startOffset, token.endOffset); 723 724 if (hasColon && !functionCount) { 725 if (current) { 726 result.push({ value: current, type: SELECTOR_PSEUDO_CLASS }); 727 } 728 729 current = ""; 730 hasColon = false; 731 } 732 } else if (token.tokenType === "Colon") { 733 if (!hasColon) { 734 if (current) { 735 result.push({ value: current, type: SELECTOR_ELEMENT }); 736 } 737 738 current = ""; 739 hasColon = true; 740 } 741 742 current += token.text; 743 } else if (token.tokenType === "Function") { 744 current += value.substring(token.startOffset, token.endOffset); 745 functionCount++; 746 } else if (token.tokenType === "CloseParenthesis") { 747 current += token.text; 748 749 if (hasColon && functionCount == 1) { 750 if (current) { 751 result.push({ value: current, type: SELECTOR_PSEUDO_CLASS }); 752 } 753 754 current = ""; 755 functionCount--; 756 hasColon = false; 757 } else { 758 functionCount--; 759 } 760 } else if (token.tokenType === "SquareBracketBlock") { 761 if (!hasAttribute && !functionCount) { 762 if (current) { 763 result.push({ value: current, type: SELECTOR_ELEMENT }); 764 } 765 766 current = ""; 767 hasAttribute = true; 768 } 769 770 current += token.text; 771 } else if (token.tokenType === "CloseSquareBracket") { 772 current += token.text; 773 774 if (hasAttribute && !functionCount) { 775 if (current) { 776 result.push({ value: current, type: SELECTOR_ATTRIBUTE }); 777 } 778 779 current = ""; 780 hasAttribute = false; 781 } 782 } else { 783 current += value.substring(token.startOffset, token.endOffset); 784 } 785 } 786 787 if (current) { 788 result.push({ value: current, type: SELECTOR_ELEMENT }); 789 } 790 791 return result; 792 } 793 794 /** 795 * Expects a single CSS value to be passed as the input and parses the value 796 * and priority. 797 * 798 * @param {Function} isCssPropertyKnown 799 * A function to check if the CSS property is known. This is either an 800 * internal server function or from the CssPropertiesFront. 801 * that are supported by the server. 802 * @param {string} value 803 * The value from the text editor. 804 * @returns {object} an object with 'value' and 'priority' properties. 805 */ 806 function parseSingleValue(isCssPropertyKnown, value) { 807 const declaration = parseDeclarations( 808 isCssPropertyKnown, 809 "a: " + value + ";" 810 )[0]; 811 return { 812 value: declaration ? declaration.value : "", 813 priority: declaration ? declaration.priority : "", 814 }; 815 } 816 817 /** 818 * Convert an angle value to degree. 819 * 820 * @param {number} angleValue The angle value. 821 * @param {CSS_ANGLEUNIT} angleUnit The angleValue's angle unit. 822 * @returns {number} An angle value in degree. 823 */ 824 function getAngleValueInDegrees(angleValue, angleUnit) { 825 switch (angleUnit) { 826 case CSS_ANGLEUNIT.deg: 827 return angleValue; 828 case CSS_ANGLEUNIT.grad: 829 return angleValue * 0.9; 830 case CSS_ANGLEUNIT.rad: 831 return (angleValue * 180) / Math.PI; 832 case CSS_ANGLEUNIT.turn: 833 return angleValue * 360; 834 default: 835 throw new Error("No matched angle unit."); 836 } 837 } 838 839 exports.cssTokenizer = cssTokenizer; 840 exports.cssTokenizerWithLineColumn = cssTokenizerWithLineColumn; 841 exports.escapeCSSComment = escapeCSSComment; 842 exports.unescapeCSSComment = unescapeCSSComment; 843 exports.parseDeclarations = parseDeclarations; 844 exports.parseNamedDeclarations = parseNamedDeclarations; 845 // parseCommentDeclarations is exported for testing. 846 exports._parseCommentDeclarations = parseCommentDeclarations; 847 exports.parsePseudoClassesAndAttributes = parsePseudoClassesAndAttributes; 848 exports.parseSingleValue = parseSingleValue; 849 exports.getAngleValueInDegrees = getAngleValueInDegrees;