css-logic.js (26708B)
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 "use strict"; 6 7 const LINE_BREAK_RE = /\r\n?|\n|\u2028|\u2029/; 8 const MAX_DATA_URL_LENGTH = 40; 9 /** 10 * Provide access to the style information in a page. 11 * CssLogic uses the standard DOM API, and the Gecko InspectorUtils API to 12 * access styling information in the page, and present this to the user in a way 13 * that helps them understand: 14 * - why their expectations may not have been fulfilled 15 * - how browsers process CSS 16 * 17 * @class 18 */ 19 20 loader.lazyRequireGetter( 21 this, 22 "InspectorCSSParserWrapper", 23 "resource://devtools/shared/css/lexer.js", 24 true 25 ); 26 loader.lazyRequireGetter( 27 this, 28 "getTabPrefs", 29 "resource://devtools/shared/indentation.js", 30 true 31 ); 32 loader.lazyRequireGetter( 33 this, 34 "getNodeDisplayName", 35 "resource://devtools/server/actors/inspector/utils.js", 36 true 37 ); 38 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 39 const styleInspectorL10N = new LocalizationHelper( 40 "devtools/shared/locales/styleinspector.properties" 41 ); 42 43 /** 44 * Special values for filter, in addition to an href these values can be used 45 */ 46 exports.FILTER = { 47 // show properties for all user style sheets. 48 USER: "user", 49 // USER, plus user-agent (i.e. browser) style sheets 50 UA: "ua", 51 }; 52 53 /** 54 * Each rule has a status, the bigger the number, the better placed it is to 55 * provide styling information. 56 * 57 * These statuses are localized inside the styleinspector.properties 58 * string bundle. 59 * 60 * @see csshtmltree.js RuleView._cacheStatusNames() 61 */ 62 exports.STATUS = { 63 BEST: 3, 64 MATCHED: 2, 65 PARENT_MATCH: 1, 66 UNMATCHED: 0, 67 UNKNOWN: -1, 68 }; 69 70 /** 71 * Mapping of CSS at-Rule className to CSSRule type name. 72 */ 73 exports.CSSAtRuleClassNameType = { 74 CSSContainerRule: "container", 75 CSSCounterStyleRule: "counter-style", 76 CSSDocumentRule: "document", 77 CSSFontFaceRule: "font-face", 78 CSSFontFeatureValuesRule: "font-feature-values", 79 CSSImportRule: "import", 80 CSSKeyframeRule: "keyframe", 81 CSSKeyframesRule: "keyframes", 82 CSSLayerBlockRule: "layer", 83 CSSMediaRule: "media", 84 CSSNamespaceRule: "namespace", 85 CSSPageRule: "page", 86 CSSScopeRule: "scope", 87 CSSStartingStyleRule: "starting-style", 88 CSSSupportsRule: "supports", 89 }; 90 91 /** 92 * Get Rule type as human-readable string (ex: "@media", "@container", …) 93 * 94 * @param {CSSRule} cssRule 95 * @returns {string} 96 */ 97 exports.getCSSAtRuleTypeName = function (cssRule) { 98 const ruleClassName = ChromeUtils.getClassName(cssRule); 99 const atRuleTypeName = exports.CSSAtRuleClassNameType[ruleClassName]; 100 if (atRuleTypeName) { 101 return "@" + atRuleTypeName; 102 } 103 104 return ""; 105 }; 106 107 /** 108 * Lookup a l10n string in the shared styleinspector string bundle. 109 * 110 * @param {string} name 111 * The key to lookup. 112 * @returns {string} A localized version of the given key. 113 */ 114 exports.l10n = name => styleInspectorL10N.getStr(name); 115 exports.l10nFormatStr = (name, ...args) => 116 styleInspectorL10N.getFormatStr(name, ...args); 117 118 /** 119 * Is the given property sheet an author stylesheet? 120 * 121 * @param {CSSStyleSheet} sheet a stylesheet 122 * @return {boolean} true if the given stylesheet is an author stylesheet, 123 * false otherwise. 124 */ 125 exports.isAuthorStylesheet = function (sheet) { 126 return sheet.parsingMode === "author"; 127 }; 128 129 /** 130 * Is the given property sheet a user stylesheet? 131 * 132 * @param {CSSStyleSheet} sheet a stylesheet 133 * @return {boolean} true if the given stylesheet is a user stylesheet, 134 * false otherwise. 135 */ 136 exports.isUserStylesheet = function (sheet) { 137 return sheet.parsingMode === "user"; 138 }; 139 140 /** 141 * Is the given property sheet a agent stylesheet? 142 * 143 * @param {CSSStyleSheet} sheet a stylesheet 144 * @return {boolean} true if the given stylesheet is a agent stylesheet, 145 * false otherwise. 146 */ 147 exports.isAgentStylesheet = function (sheet) { 148 return sheet.parsingMode === "agent"; 149 }; 150 151 /** 152 * Return a shortened version of a style sheet's source. 153 * 154 * @param {CSSStyleSheet} sheet the DOM object for the style sheet. 155 */ 156 exports.shortSource = function (sheet) { 157 if (!sheet) { 158 return exports.l10n("rule.sourceInline"); 159 } 160 161 if (!sheet.href) { 162 return exports.l10n( 163 sheet.constructed ? "rule.sourceConstructed" : "rule.sourceInline" 164 ); 165 } 166 167 let name = sheet.href; 168 169 // If the sheet is a data URL, return a trimmed version of it. 170 const dataUrl = sheet.href.trim().match(/^data:.*?,((?:.|\r|\n)*)$/); 171 if (dataUrl) { 172 name = 173 dataUrl[1].length > MAX_DATA_URL_LENGTH 174 ? `${dataUrl[1].substr(0, MAX_DATA_URL_LENGTH - 1)}…` 175 : dataUrl[1]; 176 } else { 177 // We try, in turn, the filename, filePath, query string, whole thing 178 const url = URL.parse(sheet.href); 179 if (url) { 180 if (url.pathname) { 181 const index = url.pathname.lastIndexOf("/"); 182 if (index !== -1 && index < url.pathname.length) { 183 name = url.pathname.slice(index + 1); 184 } else { 185 name = url.pathname; 186 } 187 } else if (url.query) { 188 name = url.query; 189 } 190 } // else some UA-provided stylesheets are not valid URLs. 191 } 192 193 try { 194 name = decodeURIComponent(name); 195 } catch (e) { 196 // This may still fail if the URL contains invalid % numbers (for ex) 197 } 198 199 return name; 200 }; 201 202 /** 203 * Return the style sheet's source, handling element, inline and constructed stylesheets. 204 * 205 * @param {CSSStyleSheet} sheet the DOM object for the style sheet. 206 */ 207 exports.longSource = function (sheet) { 208 if (!sheet) { 209 return exports.l10n("rule.sourceInline"); 210 } 211 212 if (!sheet.href) { 213 return exports.l10n( 214 sheet.constructed ? "rule.sourceConstructed" : "rule.sourceInline" 215 ); 216 } 217 218 return sheet.href; 219 }; 220 221 const TAB_CHARS = "\t"; 222 const SPACE_CHARS = " "; 223 224 function getLineCountInComments(text) { 225 let count = 0; 226 227 for (const comment of text.match(/\/\*(?:.|\n)*?\*\//gm) || []) { 228 count += comment.split("\n").length + 1; 229 } 230 231 return count; 232 } 233 234 /** 235 * Prettify minified CSS text. 236 * This prettifies CSS code where there is no indentation in usual places while 237 * keeping original indentation as-is elsewhere. 238 * 239 * Returns an object with the resulting prettified source and a list of mappings of 240 * token positions between the original and the prettified source. Each single mapping 241 * is an object that looks like this: 242 * 243 * { 244 * original: {line: {number}, column: {number}}, 245 * generated: {line: {number}, column: {number}}, 246 * } 247 * 248 * @param {string} text 249 * The CSS source to prettify. 250 * @param {number} ruleCount 251 * The number of CSS rules expected in the CSS source. 252 * Set to null to force the text to be pretty-printed. 253 * 254 * @return {object} 255 * Object with the prettified source and source mappings. 256 * { 257 * result: {String} // Prettified source 258 * mappings: {Array} // List of objects with mappings for lines and columns 259 * // between the original source and prettified source 260 * } 261 */ 262 // eslint-disable-next-line complexity 263 function prettifyCSS(text, ruleCount) { 264 if (prettifyCSS.LINE_SEPARATOR == null) { 265 const os = Services.appinfo.OS; 266 prettifyCSS.LINE_SEPARATOR = os === "WINNT" ? "\r\n" : "\n"; 267 } 268 269 // Stylesheets may start and end with HTML comment tags (possibly with whitespaces 270 // before and after). Remove those first. Don't do anything if there aren't any. 271 const trimmed = text.trim(); 272 if (trimmed.startsWith("<!--")) { 273 text = trimmed.replace(/^<!--/, "").replace(/-->$/, "").trim(); 274 } 275 276 const originalText = text; 277 text = text.trim(); 278 279 // don't attempt to prettify if there's more than one line per rule, excluding comments. 280 const lineCount = text.split("\n").length - 1 - getLineCountInComments(text); 281 if (ruleCount !== null && lineCount >= ruleCount) { 282 return { result: originalText, mappings: [] }; 283 } 284 285 // We reformat the text using a simple state machine. The 286 // reformatting preserves most of the input text, changing only 287 // whitespace. The rules are: 288 // 289 // * After a "{" or ";" symbol, ensure there is a newline and 290 // indentation before the next non-comment, non-whitespace token. 291 // * Additionally after a "{" symbol, increase the indentation. 292 // * A "}" symbol ensures there is a preceding newline, and 293 // decreases the indentation level. 294 // * Ensure there is whitespace before a "{". 295 // 296 // This approach can be confused sometimes, but should do ok on a 297 // minified file. 298 let indent = ""; 299 let indentLevel = 0; 300 const lexer = new InspectorCSSParserWrapper(text); 301 // List of mappings of token positions from original source to prettified source. 302 const mappings = []; 303 // Line and column offsets used to shift the token positions after prettyfication. 304 let lineOffset = 0; 305 let columnOffset = 0; 306 let indentOffset = 0; 307 let result = ""; 308 let pushbackToken = undefined; 309 310 // A helper function that reads tokens, looking for the next 311 // non-comment, non-whitespace token. Comment and whitespace tokens 312 // are appended to |result|. If this encounters EOF, it returns 313 // null. Otherwise it returns the last whitespace token that was 314 // seen. This function also updates |pushbackToken|. 315 const readUntilSignificantToken = () => { 316 while (true) { 317 const token = lexer.nextToken(); 318 if (!token || token.tokenType !== "WhiteSpace") { 319 pushbackToken = token; 320 return token; 321 } 322 // Saw whitespace. Before committing to it, check the next 323 // token. 324 const nextToken = lexer.nextToken(); 325 if (!nextToken || nextToken.tokenType !== "Comment") { 326 pushbackToken = nextToken; 327 return token; 328 } 329 // Saw whitespace + comment. Update the result and continue. 330 result = result + text.substring(token.startOffset, nextToken.endOffset); 331 } 332 }; 333 334 // State variables for readUntilNewlineNeeded. 335 // 336 // Starting index of the accumulated tokens. 337 let startIndex; 338 // Ending index of the accumulated tokens. 339 let endIndex; 340 // True if any non-whitespace token was seen. 341 let anyNonWS; 342 // True if the terminating token is "}". 343 let isCloseBrace; 344 // True if the terminating token is a new line character. 345 let isNewLine; 346 // True if the token just before the terminating token was 347 // whitespace. 348 let lastWasWS; 349 // True if the current token is inside a CSS selector. 350 let isInSelector = true; 351 // True if the current token is inside an at-rule definition. 352 let isInAtRuleDefinition = false; 353 354 // A helper function that reads tokens until there is a reason to 355 // insert a newline. This updates the state variables as needed. 356 // If this encounters EOF, it returns null. Otherwise it returns 357 // the final token read. Note that if the returned token is "{", 358 // then it will not be included in the computed start/end token 359 // range. This is used to handle whitespace insertion before a "{". 360 const readUntilNewlineNeeded = () => { 361 let token; 362 while (true) { 363 if (pushbackToken) { 364 token = pushbackToken; 365 pushbackToken = undefined; 366 } else { 367 token = lexer.nextToken(); 368 } 369 if (!token) { 370 endIndex = text.length; 371 break; 372 } 373 374 const line = lexer.lineNumber; 375 const column = lexer.columnNumber; 376 mappings.push({ 377 original: { 378 line, 379 column, 380 }, 381 generated: { 382 line: lineOffset + line, 383 column: columnOffset, 384 }, 385 }); 386 // Shift the column offset for the next token by the current token's length. 387 columnOffset += token.endOffset - token.startOffset; 388 389 if (token.tokenType === "AtKeyword") { 390 isInAtRuleDefinition = true; 391 } 392 393 // A "}" symbol must be inserted later, to deal with indentation 394 // and newline. 395 if (token.tokenType === "CloseCurlyBracket") { 396 isInSelector = true; 397 isCloseBrace = true; 398 break; 399 } else if (token.tokenType === "CurlyBracketBlock") { 400 if (isInAtRuleDefinition) { 401 isInAtRuleDefinition = false; 402 } else { 403 isInSelector = false; 404 } 405 break; 406 } 407 408 if (token.tokenType === "WhiteSpace") { 409 if (LINE_BREAK_RE.test(token.text)) { 410 // If we encounter a new line after a significant token, we can 411 // move on to the next significant token. 412 // This avoids messing with declarations with no semi-colon preceding 413 // a closing brace, eg `{\n color: red\n }` 414 isNewLine = true; 415 break; 416 } 417 } else { 418 anyNonWS = true; 419 } 420 421 if (startIndex === undefined) { 422 startIndex = token.startOffset; 423 } 424 endIndex = token.endOffset; 425 426 if (token.tokenType === "Semicolon") { 427 break; 428 } 429 430 if ( 431 token.tokenType === "Comma" && 432 isInSelector && 433 !isInAtRuleDefinition 434 ) { 435 break; 436 } 437 438 lastWasWS = token.tokenType === "WhiteSpace"; 439 } 440 return token; 441 }; 442 443 // Get preference of the user regarding what to use for indentation, 444 // spaces or tabs. 445 const tabPrefs = getTabPrefs(); 446 const baseIndentString = tabPrefs.indentWithTabs 447 ? TAB_CHARS 448 : SPACE_CHARS.repeat(tabPrefs.indentUnit); 449 450 while (true) { 451 // Set the initial state. 452 startIndex = undefined; 453 endIndex = undefined; 454 anyNonWS = false; 455 isCloseBrace = false; 456 isNewLine = false; 457 lastWasWS = false; 458 459 // Read tokens until we see a reason to insert a newline. 460 let token = readUntilNewlineNeeded(); 461 462 // Append any saved up text to the result, applying indentation. 463 if (startIndex !== undefined) { 464 if (isCloseBrace && !anyNonWS) { 465 // If we saw only whitespace followed by a "}", then we don't 466 // need anything here. 467 } else { 468 result = result + indent + text.substring(startIndex, endIndex); 469 if (isNewLine) { 470 lineOffset = lineOffset - 1; 471 } 472 if (isCloseBrace) { 473 result += prettifyCSS.LINE_SEPARATOR; 474 lineOffset = lineOffset + 1; 475 } 476 } 477 } 478 479 if (isCloseBrace) { 480 // Even if the stylesheet contains extra closing braces, the indent level should 481 // remain > 0. 482 indentLevel = Math.max(0, indentLevel - 1); 483 indent = baseIndentString.repeat(indentLevel); 484 485 // FIXME: This is incorrect and should be fixed in Bug 1839297 486 if (tabPrefs.indentWithTabs) { 487 indentOffset = 4 * indentLevel; 488 } else { 489 indentOffset = 1 * indentLevel; 490 } 491 result = result + indent + "}"; 492 } 493 494 if (!token) { 495 break; 496 } 497 498 if (token.tokenType === "CurlyBracketBlock") { 499 if (!lastWasWS) { 500 result += " "; 501 columnOffset++; 502 } 503 result += "{"; 504 indentLevel++; 505 indent = baseIndentString.repeat(indentLevel); 506 indentOffset = indent.length; 507 508 // FIXME: This is incorrect and should be fixed in Bug 1839297 509 if (tabPrefs.indentWithTabs) { 510 indentOffset = 4 * indentLevel; 511 } else { 512 indentOffset = 1 * indentLevel; 513 } 514 } 515 516 // Now it is time to insert a newline. However first we want to 517 // deal with any trailing comments. 518 token = readUntilSignificantToken(); 519 520 // "Early" bail-out if the text does not appear to be minified. 521 // Here we ignore the case where whitespace appears at the end of 522 // the text. 523 if ( 524 ruleCount !== null && 525 pushbackToken && 526 token && 527 token.tokenType === "WhiteSpace" && 528 /\n/g.test(text.substring(token.startOffset, token.endOffset)) 529 ) { 530 return { result: originalText, mappings: [] }; 531 } 532 533 // Finally time for that newline. 534 result = result + prettifyCSS.LINE_SEPARATOR; 535 536 // Update line and column offsets for the new line. 537 lineOffset = lineOffset + 1; 538 columnOffset = 0 + indentOffset; 539 540 // Maybe we hit EOF. 541 if (!pushbackToken) { 542 break; 543 } 544 } 545 546 return { result, mappings }; 547 } 548 549 exports.prettifyCSS = prettifyCSS; 550 551 /** 552 * Given a node, check to see if it is a ::marker, ::before, or ::after element. 553 * If so, return the node that is accessible from within the document 554 * (the parent of the anonymous node), along with which pseudo element 555 * it was. Otherwise, return the node itself. 556 * 557 * @returns {object} 558 * - {DOMNode} node: The non-anonymous node 559 * - {string|null} pseudo: The label representing the anonymous node 560 * (e.g. '::marker', '::before', '::after', '::view-transition', 561 * '::view-transition-group(root)', …). 562 * null if node isn't an anonymous node or isn't handled 563 * yet. 564 */ 565 function getBindingElementAndPseudo(node) { 566 let bindingElement = node; 567 let pseudo = null; 568 const { implementedPseudoElement } = node; 569 if (implementedPseudoElement) { 570 // we only want to explicitly handle the elements we're displaying in the markup view 571 if ( 572 implementedPseudoElement === "::marker" || 573 implementedPseudoElement === "::before" || 574 implementedPseudoElement === "::after" || 575 implementedPseudoElement === "::backdrop" 576 ) { 577 pseudo = getNodeDisplayName(node); 578 bindingElement = node.parentNode; 579 } else if (implementedPseudoElement.startsWith("::view-transition")) { 580 pseudo = getNodeDisplayName(node); 581 // The binding for all view transition pseudo element is the <html> element, i.e. we 582 // can't use `node.parentNode` as for`::view-transition-old` element, we'd get the 583 // `::view-transition-group`, which is not the binding element. 584 bindingElement = node.getRootNode().documentElement; 585 } 586 } 587 588 return { 589 bindingElement, 590 pseudo, 591 }; 592 } 593 exports.getBindingElementAndPseudo = getBindingElementAndPseudo; 594 595 /** 596 * Returns css rules for a given a node. 597 * This function can handle ::before or ::after pseudo element as well as 598 * normal element. 599 */ 600 function getMatchingCSSRules(node) { 601 const { bindingElement, pseudo } = getBindingElementAndPseudo(node); 602 const rules = InspectorUtils.getMatchingCSSRules(bindingElement, pseudo); 603 return rules; 604 } 605 exports.getMatchingCSSRules = getMatchingCSSRules; 606 607 /** 608 * Returns true if the given node has visited state. 609 */ 610 function hasVisitedState(node) { 611 if (!Element.isInstance(node)) { 612 return false; 613 } 614 615 // ElementState::VISITED 616 const ELEMENT_STATE_VISITED = 1 << 18; 617 618 return ( 619 !!(InspectorUtils.getContentState(node) & ELEMENT_STATE_VISITED) || 620 InspectorUtils.hasPseudoClassLock(node, ":visited") 621 ); 622 } 623 exports.hasVisitedState = hasVisitedState; 624 625 /** 626 * Find the position of [element] in [nodeList]. 627 * 628 * @returns an index of the match, or -1 if there is no match 629 */ 630 function positionInNodeList(element, nodeList) { 631 for (let i = 0; i < nodeList.length; i++) { 632 if (element === nodeList[i]) { 633 return i; 634 } 635 } 636 return -1; 637 } 638 639 /** 640 * For a provided node, find the appropriate container/node couple so that 641 * container.contains(node) and a CSS selector can be created from the 642 * container to the node. 643 */ 644 function findNodeAndContainer(node) { 645 const shadowRoot = node.containingShadowRoot; 646 while (node?.isNativeAnonymous) { 647 node = node.parentNode; 648 } 649 650 if (shadowRoot) { 651 // If the node is under a shadow root, the shadowRoot contains the node and 652 // we can find the node via shadowRoot.querySelector(path). 653 return { 654 containingDocOrShadow: shadowRoot, 655 node, 656 }; 657 } 658 659 // Otherwise, get the root binding parent to get a non anonymous element that 660 // will be accessible from the ownerDocument. 661 return { 662 containingDocOrShadow: node.ownerDocument, 663 node, 664 }; 665 } 666 667 /** 668 * Find a unique CSS selector for a given element 669 * 670 * @returns a string such that: 671 * - ele.containingDocOrShadow.querySelector(reply) === ele 672 * - ele.containingDocOrShadow.querySelectorAll(reply).length === 1 673 */ 674 const findCssSelector = function (ele) { 675 const { node, containingDocOrShadow } = findNodeAndContainer(ele); 676 ele = node; 677 678 if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) { 679 // findCssSelector received element not inside container. 680 return ""; 681 } 682 683 const cssEscape = ele.ownerGlobal.CSS.escape; 684 685 // document.querySelectorAll("#id") returns multiple if elements share an ID 686 if ( 687 ele.id && 688 containingDocOrShadow.querySelectorAll("#" + cssEscape(ele.id)).length === 1 689 ) { 690 return "#" + cssEscape(ele.id); 691 } 692 693 // Inherently unique by tag name 694 const tagName = ele.localName; 695 if (tagName === "html") { 696 return "html"; 697 } 698 if (tagName === "head") { 699 return "head"; 700 } 701 if (tagName === "body") { 702 return "body"; 703 } 704 705 // We might be able to find a unique class name 706 let selector, index, matches; 707 for (let i = 0; i < ele.classList.length; i++) { 708 // Is this className unique by itself? 709 selector = "." + cssEscape(ele.classList.item(i)); 710 matches = containingDocOrShadow.querySelectorAll(selector); 711 if (matches.length === 1) { 712 return selector; 713 } 714 // Maybe it's unique with a tag name? 715 selector = cssEscape(tagName) + selector; 716 matches = containingDocOrShadow.querySelectorAll(selector); 717 if (matches.length === 1) { 718 return selector; 719 } 720 // Maybe it's unique using a tag name and nth-child 721 index = positionInNodeList(ele, ele.parentNode.children) + 1; 722 selector = selector + ":nth-child(" + index + ")"; 723 matches = containingDocOrShadow.querySelectorAll(selector); 724 if (matches.length === 1) { 725 return selector; 726 } 727 } 728 729 // Not unique enough yet. 730 index = positionInNodeList(ele, ele.parentNode.children) + 1; 731 selector = cssEscape(tagName) + ":nth-child(" + index + ")"; 732 if (ele.parentNode !== containingDocOrShadow) { 733 selector = findCssSelector(ele.parentNode) + " > " + selector; 734 } 735 return selector; 736 }; 737 exports.findCssSelector = findCssSelector; 738 739 /** 740 * Get the full CSS path for a given element. 741 * 742 * @returns a string that can be used as a CSS selector for the element. It might not 743 * match the element uniquely. It does however, represent the full path from the root 744 * node to the element. 745 */ 746 function getCssPath(ele) { 747 const { node, containingDocOrShadow } = findNodeAndContainer(ele); 748 ele = node; 749 if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) { 750 // getCssPath received element not inside container. 751 return ""; 752 } 753 754 const nodeGlobal = ele.ownerGlobal.Node; 755 756 const getElementSelector = element => { 757 if (!element.localName) { 758 return ""; 759 } 760 761 let label = 762 element.nodeName == element.nodeName.toUpperCase() 763 ? element.localName.toLowerCase() 764 : element.localName; 765 766 if (element.id) { 767 label += "#" + element.id; 768 } 769 770 if (element.classList) { 771 for (const cl of element.classList) { 772 label += "." + cl; 773 } 774 } 775 776 return label; 777 }; 778 779 const paths = []; 780 781 while (ele) { 782 if (!ele || ele.nodeType !== nodeGlobal.ELEMENT_NODE) { 783 break; 784 } 785 786 paths.splice(0, 0, getElementSelector(ele)); 787 ele = ele.parentNode; 788 } 789 790 return paths.length ? paths.join(" ") : ""; 791 } 792 exports.getCssPath = getCssPath; 793 794 /** 795 * Get the xpath for a given element. 796 * 797 * @param {DomNode} ele 798 * @returns a string that can be used as an XPath to find the element uniquely. 799 */ 800 function getXPath(ele) { 801 const { node, containingDocOrShadow } = findNodeAndContainer(ele); 802 ele = node; 803 if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) { 804 // getXPath received element not inside container. 805 return ""; 806 } 807 808 // Create a short XPath for elements with IDs. 809 if (ele.id) { 810 return `//*[@id="${ele.id}"]`; 811 } 812 813 // Otherwise walk the DOM up and create a part for each ancestor. 814 const parts = []; 815 816 const nodeGlobal = ele.ownerGlobal.Node; 817 // Use nodeName (instead of localName) so namespace prefix is included (if any). 818 while (ele && ele.nodeType === nodeGlobal.ELEMENT_NODE) { 819 let nbOfPreviousSiblings = 0; 820 let hasNextSiblings = false; 821 822 // Count how many previous same-name siblings the element has. 823 let sibling = ele.previousSibling; 824 while (sibling) { 825 // Ignore document type declaration. 826 if ( 827 sibling.nodeType !== nodeGlobal.DOCUMENT_TYPE_NODE && 828 sibling.nodeName == ele.nodeName 829 ) { 830 nbOfPreviousSiblings++; 831 } 832 833 sibling = sibling.previousSibling; 834 } 835 836 // Check if the element has at least 1 next same-name sibling. 837 sibling = ele.nextSibling; 838 while (sibling) { 839 if (sibling.nodeName == ele.nodeName) { 840 hasNextSiblings = true; 841 break; 842 } 843 sibling = sibling.nextSibling; 844 } 845 846 const prefix = ele.prefix ? ele.prefix + ":" : ""; 847 const nth = 848 nbOfPreviousSiblings || hasNextSiblings 849 ? `[${nbOfPreviousSiblings + 1}]` 850 : ""; 851 852 parts.push(prefix + ele.localName + nth); 853 854 ele = ele.parentNode; 855 } 856 857 return parts.length ? "/" + parts.reverse().join("/") : ""; 858 } 859 exports.getXPath = getXPath; 860 861 /** 862 * Build up a regular expression that matches a CSS variable token. This is an 863 * ident token that starts with two dashes "--". 864 * 865 * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram 866 */ 867 var NON_ASCII = "[^\\x00-\\x7F]"; 868 var ESCAPE = "\\\\[^\n\r]"; 869 var VALID_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|"); 870 var IS_VARIABLE_TOKEN = new RegExp(`^--(${VALID_CHAR})*$`, "i"); 871 872 /** 873 * Check that this is a CSS variable. 874 * 875 * @param {string} input 876 * @return {boolean} 877 */ 878 function isCssVariable(input) { 879 return !!input.match(IS_VARIABLE_TOKEN); 880 } 881 exports.isCssVariable = isCssVariable; 882 883 /** 884 * This is a list of all the element backed pseudo elements. 885 * 886 * From https://drafts.csswg.org/css-pseudo-4/#element-backed : 887 * > The element-backed pseudo-elements, interact with most CSS and other platform features 888 * > as if they were real elements (and, in fact, often are real elements that are 889 * > not otherwise selectable). 890 * 891 * Those pseudo elements are not displayed in the markup view, but declarations in rules 892 * targetting them can then be inherited by their "children", and so we need to retrieve 893 * those rules to surface them in the Inspector (e.g. in "Inherited" sections in the Rules 894 * view, in the matched selectors section in the Computed panel, …). 895 * 896 * Any new element-backed pseudo elements should be added into this Set. 897 */ 898 exports.ELEMENT_BACKED_PSEUDO_ELEMENTS = new Set([ 899 "::details-content", 900 "::file-selector-button", 901 ]);