page-style.js (51846B)
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 { Actor } = require("resource://devtools/shared/protocol.js"); 8 const { 9 pageStyleSpec, 10 } = require("resource://devtools/shared/specs/page-style.js"); 11 12 const { 13 LongStringActor, 14 } = require("resource://devtools/server/actors/string.js"); 15 16 const { 17 style: { ELEMENT_STYLE }, 18 } = require("resource://devtools/shared/constants.js"); 19 20 loader.lazyRequireGetter( 21 this, 22 "StyleRuleActor", 23 "resource://devtools/server/actors/style-rule.js", 24 true 25 ); 26 loader.lazyRequireGetter( 27 this, 28 "getFontPreviewData", 29 "resource://devtools/server/actors/utils/style-utils.js", 30 true 31 ); 32 loader.lazyRequireGetter( 33 this, 34 "CssLogic", 35 "resource://devtools/server/actors/inspector/css-logic.js", 36 true 37 ); 38 loader.lazyRequireGetter( 39 this, 40 "SharedCssLogic", 41 "resource://devtools/shared/inspector/css-logic.js" 42 ); 43 loader.lazyRequireGetter( 44 this, 45 "getDefinedGeometryProperties", 46 "resource://devtools/server/actors/highlighters/geometry-editor.js", 47 true 48 ); 49 loader.lazyRequireGetter( 50 this, 51 "UPDATE_GENERAL", 52 "resource://devtools/server/actors/utils/stylesheets-manager.js", 53 true 54 ); 55 56 loader.lazyGetter(this, "PSEUDO_ELEMENTS", () => { 57 return InspectorUtils.getCSSPseudoElementNames(); 58 }); 59 loader.lazyGetter(this, "FONT_VARIATIONS_ENABLED", () => { 60 return Services.prefs.getBoolPref("layout.css.font-variations.enabled"); 61 }); 62 63 const NORMAL_FONT_WEIGHT = 400; 64 const BOLD_FONT_WEIGHT = 700; 65 66 /** 67 * The PageStyle actor lets the client look at the styles on a page, as 68 * they are applied to a given node. 69 */ 70 class PageStyleActor extends Actor { 71 /** 72 * Create a PageStyleActor. 73 * 74 * @param inspector 75 * The InspectorActor that owns this PageStyleActor. 76 * 77 * @class 78 */ 79 constructor(inspector) { 80 super(inspector.conn, pageStyleSpec); 81 this.inspector = inspector; 82 if (!this.inspector.walker) { 83 throw Error( 84 "The inspector's WalkerActor must be created before " + 85 "creating a PageStyleActor." 86 ); 87 } 88 this.walker = inspector.walker; 89 this.cssLogic = new CssLogic(); 90 91 // Stores the association of DOM objects -> actors 92 this.refMap = new Map(); 93 94 // Latest node queried for its applied styles. 95 this.selectedElement = null; 96 97 // Maps root node (document|ShadowRoot) to stylesheets, which are used to add new rules. 98 this.styleSheetsByRootNode = new WeakMap(); 99 100 this.onFrameUnload = this.onFrameUnload.bind(this); 101 102 this.inspector.targetActor.on("will-navigate", this.onFrameUnload); 103 104 this.styleSheetsManager = 105 this.inspector.targetActor.getStyleSheetsManager(); 106 107 this.styleSheetsManager.on("stylesheet-updated", this.#onStylesheetUpdated); 108 } 109 110 #observedRules = new Set(); 111 112 destroy() { 113 if (!this.walker) { 114 return; 115 } 116 super.destroy(); 117 this.inspector.targetActor.off("will-navigate", this.onFrameUnload); 118 this.inspector = null; 119 this.walker = null; 120 this.refMap = null; 121 this.selectedElement = null; 122 this.cssLogic = null; 123 this.styleSheetsByRootNode = null; 124 125 this.#observedRules = null; 126 } 127 128 get ownerWindow() { 129 return this.inspector.targetActor.window; 130 } 131 132 form() { 133 // We need to use CSS from the inspected window in order to use CSS.supports() and 134 // detect the right platform features from there. 135 const CSS = this.inspector.targetActor.window.CSS; 136 137 return { 138 actor: this.actorID, 139 traits: { 140 // Whether the page supports values of font-stretch from CSS Fonts Level 4. 141 fontStretchLevel4: CSS.supports("font-stretch: 100%"), 142 // Whether the page supports values of font-style from CSS Fonts Level 4. 143 fontStyleLevel4: CSS.supports("font-style: oblique 20deg"), 144 // Whether getAllUsedFontFaces/getUsedFontFaces accepts the includeVariations 145 // argument. 146 fontVariations: FONT_VARIATIONS_ENABLED, 147 // Whether the page supports values of font-weight from CSS Fonts Level 4. 148 // font-weight at CSS Fonts Level 4 accepts values in increments of 1 rather 149 // than 100. However, CSS.supports() returns false positives, so we guard with the 150 // expected support of font-stretch at CSS Fonts Level 4. 151 fontWeightLevel4: 152 CSS.supports("font-weight: 1") && CSS.supports("font-stretch: 100%"), 153 }, 154 }; 155 } 156 157 /** 158 * Called when a style sheet is updated. 159 */ 160 #styleApplied = kind => { 161 // No matter what kind of update is done, we need to invalidate 162 // the keyframe cache. 163 this.cssLogic.reset(); 164 if (kind === UPDATE_GENERAL) { 165 this.emit("stylesheet-updated"); 166 } 167 }; 168 169 /** 170 * Return or create a StyleRuleActor for the given item. 171 * 172 * @param {CSSStyleRule|Element} item 173 * @param {string} pseudoElement An optional pseudo-element type in cases when the CSS 174 * rule applies to a pseudo-element. 175 * @param {boolean} userAdded: Optional boolean to distinguish rules added by the user. 176 * @return {StyleRuleActor} The newly created, or cached, StyleRuleActor for this item. 177 */ 178 styleRef(item, pseudoElement, userAdded = false) { 179 if (this.refMap.has(item)) { 180 const styleRuleActor = this.refMap.get(item); 181 if (pseudoElement) { 182 styleRuleActor.addPseudo(pseudoElement); 183 } 184 return styleRuleActor; 185 } 186 const actor = new StyleRuleActor({ 187 pageStyle: this, 188 item, 189 userAdded, 190 pseudoElement, 191 }); 192 this.manage(actor); 193 this.refMap.set(item, actor); 194 195 return actor; 196 } 197 198 /** 199 * Update the association between a StyleRuleActor and its 200 * corresponding item. This is used when a StyleRuleActor updates 201 * as style sheet and starts using a new rule. 202 * 203 * @param oldItem The old association; either a CSSStyleRule or a 204 * DOM element. 205 * @param item Either a CSSStyleRule or a DOM element. 206 * @param actor a StyleRuleActor 207 */ 208 updateStyleRef(oldItem, item, actor) { 209 this.refMap.delete(oldItem); 210 this.refMap.set(item, actor); 211 } 212 213 /** 214 * Get the StyleRuleActor matching the given rule id or null if no match is found. 215 * 216 * @param {string} ruleId 217 * Actor ID of the StyleRuleActor 218 * @return {StyleRuleActor|null} 219 */ 220 getRule(ruleId) { 221 let match = null; 222 223 for (const actor of this.refMap.values()) { 224 if (actor.actorID === ruleId) { 225 match = actor; 226 continue; 227 } 228 } 229 230 return match; 231 } 232 233 /** 234 * Get the computed style for a node. 235 * 236 * @param {NodeActor} node 237 * @param {object} options 238 * @param {string} options.filter: A string filter that affects the "matched" handling. 239 * @param {Array<string>} options.filterProperties: An array of properties names that 240 * you would like returned. 241 * @param {boolean} options.markMatched: true if you want the 'matched' property to be 242 * added when a computed property has been modified by a style included by `filter`. 243 * @param {boolean} options.onlyMatched: true if unmatched properties shouldn't be included. 244 * @param {boolean} options.clearCache: true if the cssLogic cache should be cleared. 245 * 246 * @returns a JSON blob with the following form: 247 * { 248 * "property-name": { 249 * value: "property-value", 250 * priority: "!important" <optional> 251 * matched: <true if there are matched selectors for this value> 252 * }, 253 * ... 254 * } 255 */ 256 getComputed(node, options) { 257 const ret = Object.create(null); 258 259 if (options.clearCache) { 260 this.cssLogic.reset(); 261 } 262 const filterProperties = Array.isArray(options.filterProperties) 263 ? options.filterProperties 264 : null; 265 this.cssLogic.sourceFilter = options.filter || SharedCssLogic.FILTER.UA; 266 this.cssLogic.highlight(node.rawNode); 267 const computed = this.cssLogic.computedStyle || []; 268 const targetDocument = this.inspector.targetActor.window.document; 269 270 for (const name of computed) { 271 if (filterProperties && !filterProperties.includes(name)) { 272 continue; 273 } 274 ret[name] = { 275 value: computed.getPropertyValue(name), 276 priority: computed.getPropertyPriority(name) || undefined, 277 }; 278 279 if (name.startsWith("--")) { 280 const registeredProperty = InspectorUtils.getCSSRegisteredProperty( 281 targetDocument, 282 name 283 ); 284 if (registeredProperty) { 285 ret[name].registeredPropertyInitialValue = 286 registeredProperty.initialValue; 287 if ( 288 !InspectorUtils.valueMatchesSyntax( 289 targetDocument, 290 ret[name].value, 291 registeredProperty.syntax 292 ) 293 ) { 294 ret[name].invalidAtComputedValueTime = true; 295 ret[name].registeredPropertySyntax = registeredProperty.syntax; 296 } 297 } 298 } 299 } 300 301 if (options.markMatched || options.onlyMatched) { 302 const matched = this.cssLogic.hasMatchedSelectors(Object.keys(ret)); 303 for (const key in ret) { 304 if (matched.has(key)) { 305 ret[key].matched = options.markMatched ? true : undefined; 306 } else if (options.onlyMatched) { 307 delete ret[key]; 308 } 309 } 310 } 311 312 return ret; 313 } 314 315 /** 316 * Get all the fonts from a page. 317 * 318 * @param object options 319 * `includePreviews`: Whether to also return image previews of the fonts. 320 * `previewText`: The text to display in the previews. 321 * `previewFontSize`: The font size of the text in the previews. 322 * 323 * @returns object 324 * object with 'fontFaces', a list of fonts that apply to this node. 325 */ 326 getAllUsedFontFaces(options) { 327 const windows = this.inspector.targetActor.windows; 328 let fontsList = []; 329 for (const win of windows) { 330 // Fall back to the documentElement for XUL documents. 331 const node = win.document.body 332 ? win.document.body 333 : win.document.documentElement; 334 fontsList = [...fontsList, ...this.getUsedFontFaces(node, options)]; 335 } 336 337 return fontsList; 338 } 339 340 /** 341 * Get the font faces used in an element. 342 * 343 * @param NodeActor node / actual DOM node 344 * The node to get fonts from. 345 * @param object options 346 * `includePreviews`: Whether to also return image previews of the fonts. 347 * `previewText`: The text to display in the previews. 348 * `previewFontSize`: The font size of the text in the previews. 349 * 350 * @returns object 351 * object with 'fontFaces', a list of fonts that apply to this node. 352 */ 353 getUsedFontFaces(node, options) { 354 // node.rawNode is defined for NodeActor objects 355 const actualNode = node.rawNode || node; 356 const contentDocument = actualNode.ownerDocument; 357 // We don't get fonts for a node, but for a range 358 const rng = contentDocument.createRange(); 359 const isPseudoElement = Boolean( 360 CssLogic.getBindingElementAndPseudo(actualNode).pseudo 361 ); 362 if (isPseudoElement) { 363 rng.selectNodeContents(actualNode); 364 } else { 365 rng.selectNode(actualNode); 366 } 367 const fonts = InspectorUtils.getUsedFontFaces(rng); 368 const fontsArray = []; 369 370 for (let i = 0; i < fonts.length; i++) { 371 const font = fonts[i]; 372 const fontFace = { 373 name: font.name, 374 CSSFamilyName: font.CSSFamilyName, 375 CSSGeneric: font.CSSGeneric || null, 376 srcIndex: font.srcIndex, 377 URI: font.URI, 378 format: font.format, 379 localName: font.localName, 380 metadata: font.metadata, 381 version: font.getNameString(InspectorFontFace.NAME_ID_VERSION), 382 description: font.getNameString(InspectorFontFace.NAME_ID_DESCRIPTION), 383 manufacturer: font.getNameString( 384 InspectorFontFace.NAME_ID_MANUFACTURER 385 ), 386 vendorUrl: font.getNameString(InspectorFontFace.NAME_ID_VENDOR_URL), 387 designer: font.getNameString(InspectorFontFace.NAME_ID_DESIGNER), 388 designerUrl: font.getNameString(InspectorFontFace.NAME_ID_DESIGNER_URL), 389 license: font.getNameString(InspectorFontFace.NAME_ID_LICENSE), 390 licenseUrl: font.getNameString(InspectorFontFace.NAME_ID_LICENSE_URL), 391 sampleText: font.getNameString(InspectorFontFace.NAME_ID_SAMPLE_TEXT), 392 }; 393 394 // If this font comes from a @font-face rule 395 if (font.rule) { 396 const styleActor = new StyleRuleActor({ 397 pageStyle: this, 398 item: font.rule, 399 }); 400 this.manage(styleActor); 401 fontFace.rule = styleActor; 402 fontFace.ruleText = font.rule.cssText; 403 } 404 405 // Get the weight and style of this font for the preview and sort order 406 let weight = NORMAL_FONT_WEIGHT, 407 style = ""; 408 if (font.rule) { 409 weight = 410 font.rule.style.getPropertyValue("font-weight") || NORMAL_FONT_WEIGHT; 411 if (weight == "bold") { 412 weight = BOLD_FONT_WEIGHT; 413 } else if (weight == "normal") { 414 weight = NORMAL_FONT_WEIGHT; 415 } 416 style = font.rule.style.getPropertyValue("font-style") || ""; 417 } 418 fontFace.weight = weight; 419 fontFace.style = style; 420 421 if (options.includePreviews) { 422 const opts = { 423 previewText: options.previewText, 424 previewFontSize: options.previewFontSize, 425 fontStyle: style, 426 fontWeight: weight, 427 fillStyle: options.previewFillStyle, 428 }; 429 const { dataURL, size } = getFontPreviewData( 430 font.CSSFamilyName, 431 contentDocument, 432 opts 433 ); 434 fontFace.preview = { 435 data: new LongStringActor(this.conn, dataURL), 436 size, 437 }; 438 } 439 440 if (options.includeVariations && FONT_VARIATIONS_ENABLED) { 441 fontFace.variationAxes = font.getVariationAxes(); 442 fontFace.variationInstances = font.getVariationInstances(); 443 } 444 445 fontsArray.push(fontFace); 446 } 447 448 // @font-face fonts at the top, then alphabetically, then by weight 449 fontsArray.sort(function (a, b) { 450 return a.weight > b.weight ? 1 : -1; 451 }); 452 fontsArray.sort(function (a, b) { 453 if (a.CSSFamilyName == b.CSSFamilyName) { 454 return 0; 455 } 456 return a.CSSFamilyName > b.CSSFamilyName ? 1 : -1; 457 }); 458 fontsArray.sort(function (a, b) { 459 if ((a.rule && b.rule) || (!a.rule && !b.rule)) { 460 return 0; 461 } 462 return !a.rule && b.rule ? 1 : -1; 463 }); 464 465 return fontsArray; 466 } 467 468 /** 469 * Get a list of selectors that match a given property for a node. 470 * 471 * @param NodeActor node 472 * @param string property 473 * @param object options 474 * `filter`: A string filter that affects the "matched" handling. 475 * 'user': Include properties from user style sheets. 476 * 'ua': Include properties from user and user-agent sheets. 477 * Default value is 'ua' 478 * 479 * @returns a JSON object with the following form: 480 * { 481 * // An ordered list of rules that apply 482 * matched: [{ 483 * rule: <rule actorid>, 484 * sourceText: <string>, // The source of the selector, relative 485 * // to the node in question. 486 * selector: <string>, // the selector ID that matched 487 * value: <string>, // the value of the property 488 * status: <int>, 489 * // The status of the match - high numbers are better placed 490 * // to provide styling information: 491 * // 3: Best match, was used. 492 * // 2: Matched, but was overridden. 493 * // 1: Rule from a parent matched. 494 * // 0: Unmatched (never returned in this API) 495 * }, ...], 496 * 497 * // The full form of any domrule referenced. 498 * rules: [ <domrule>, ... ], // The full form of any domrule referenced 499 * 500 * // The full form of any sheets referenced. 501 * sheets: [ <domsheet>, ... ] 502 * } 503 */ 504 getMatchedSelectors(node, property, options) { 505 this.cssLogic.sourceFilter = options.filter || SharedCssLogic.FILTER.UA; 506 this.cssLogic.highlight(node.rawNode); 507 508 const rules = new Set(); 509 const matched = []; 510 511 const targetDocument = this.inspector.targetActor.window.document; 512 let registeredProperty; 513 if (property.startsWith("--")) { 514 registeredProperty = InspectorUtils.getCSSRegisteredProperty( 515 targetDocument, 516 property 517 ); 518 } 519 520 const propInfo = this.cssLogic.getPropertyInfo(property); 521 for (const selectorInfo of propInfo.matchedSelectors) { 522 const cssRule = selectorInfo.selector.cssRule; 523 const domRule = cssRule.sourceElement || cssRule.domRule; 524 525 const rule = this.styleRef(domRule); 526 rules.add(rule); 527 528 const match = { 529 rule, 530 sourceText: this.getSelectorSource(selectorInfo, node.rawNode), 531 selector: selectorInfo.selector.text, 532 name: selectorInfo.property, 533 value: selectorInfo.value, 534 status: selectorInfo.status, 535 }; 536 if ( 537 registeredProperty && 538 !InspectorUtils.valueMatchesSyntax( 539 targetDocument, 540 match.value, 541 registeredProperty.syntax 542 ) 543 ) { 544 match.invalidAtComputedValueTime = true; 545 match.registeredPropertySyntax = registeredProperty.syntax; 546 } 547 matched.push(match); 548 } 549 550 return { 551 matched, 552 rules: [...rules], 553 }; 554 } 555 556 // Get a selector source for a CssSelectorInfo relative to a given 557 // node. 558 getSelectorSource(selectorInfo, relativeTo) { 559 let result = selectorInfo.selector.text; 560 const ruleDeclarationOrigin = 561 selectorInfo.selector.cssRule.domRule.declarationOrigin; 562 if ( 563 ruleDeclarationOrigin === "style-attribute" || 564 ruleDeclarationOrigin === "pres-hints" 565 ) { 566 const source = selectorInfo.sourceElement; 567 if (source === relativeTo) { 568 result = "element"; 569 } else { 570 result = CssLogic.getShortName(source); 571 } 572 573 if (ruleDeclarationOrigin === "pres-hints") { 574 result += " attributes style"; 575 } 576 } 577 578 return result; 579 } 580 581 /** 582 * @typedef {"user" | "ua" } GetAppliedFilterOption 583 */ 584 585 /** 586 * @typedef {object} GetAppliedOptions 587 * 588 * @property {GetAppliedFilterOption} filter - A string filter that affects the "matched" handling. 589 * Possible values are: 590 * - 'user': Include properties from user style sheets. 591 * - 'ua': Include properties from user and user-agent sheets. 592 * Default value is 'ua' 593 * @property {boolean} inherited - Include styles inherited from parent nodes. 594 * @property {boolean} matchedSelectors - Include an array of specific selectors that 595 * caused this rule to match its node. 596 * @property {boolean} skipPseudo - Exclude styles applied to pseudo elements of the 597 * provided node. 598 */ 599 600 /** 601 * Get the set of styles that apply to a given node. 602 * 603 * @param {NodeActor} node 604 * @param {GetAppliedOptions} options 605 */ 606 async getApplied(node, options) { 607 // Clear any previous references to StyleRuleActor instances for CSS rules. 608 // Assume the consumer has switched context to a new node and no longer 609 // interested in state changes of previous rules. 610 this.#observedRules.clear(); 611 this.selectedElement = node?.rawNode || null; 612 613 if (!node) { 614 return { entries: [] }; 615 } 616 617 this.cssLogic.highlight(node.rawNode); 618 619 const entries = this.getAppliedProps( 620 node, 621 this.#getAllElementRules(node, { 622 skipPseudo: options.skipPseudo, 623 filter: options.filter, 624 }), 625 options 626 ); 627 628 const promises = []; 629 for (const entry of entries) { 630 // Reference to instances of StyleRuleActor for CSS rules matching the node. 631 // Assume these are used by a consumer which wants to be notified when their 632 // state or declarations change either directly or indirectly. 633 this.#observedRules.add(entry.rule); 634 // We need to be sure that authoredText has been set before StyleRule#form is called. 635 // This has to be treated specially, for now, because we cannot synchronously compute 636 // the authored text and |form| can't return a promise. 637 // See bug 1205868. 638 promises.push(entry.rule.getAuthoredCssText()); 639 } 640 641 await Promise.all(promises); 642 643 return { entries }; 644 } 645 646 #hasInheritedProps(style) { 647 const doc = this.inspector.targetActor.window.document; 648 return Array.prototype.some.call(style, prop => 649 InspectorUtils.isInheritedProperty(doc, prop) 650 ); 651 } 652 653 async isPositionEditable(node) { 654 if (!node || node.rawNode.nodeType !== node.rawNode.ELEMENT_NODE) { 655 return false; 656 } 657 658 const props = getDefinedGeometryProperties(node.rawNode); 659 660 // Elements with only `width` and `height` are currently not considered 661 // editable. 662 return ( 663 props.has("top") || 664 props.has("right") || 665 props.has("left") || 666 props.has("bottom") 667 ); 668 } 669 670 /** 671 * Helper function for getApplied, gets all the rules from a given 672 * element. See getApplied for documentation on parameters. 673 * 674 * @param {NodeActor} node 675 * @param {object} options 676 * @param {boolean} options.isInherited - Set to true if we want to retrieve inherited rules, 677 * i.e. the passed node actor is an ancestor of the node we want to retrieved the 678 * applied rules for originally. 679 * @param {boolean} options.skipPseudo - Exclude styles applied to pseudo elements of the 680 * provided node 681 * @param {GetAppliedFilterOption} options.filter - will be passed to #getElementRules 682 * 683 * @return Array The rules for a given element. Each item in the 684 * array has the following signature: 685 * - rule RuleActor 686 * - inherited NodeActor 687 * - isSystem Boolean 688 * - pseudoElement String 689 * - darkColorScheme Boolean 690 */ 691 #getAllElementRules(node, { isInherited, skipPseudo, filter }) { 692 const { bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo( 693 node.rawNode 694 ); 695 const rules = []; 696 697 if (!bindingElement) { 698 return rules; 699 } 700 701 if (bindingElement.style) { 702 const elementStyle = this.styleRef( 703 bindingElement, 704 // for inline style, we can't have a related pseudo element 705 null 706 ); 707 const showElementStyles = !isInherited && !pseudo; 708 const showInheritedStyles = 709 isInherited && this.#hasInheritedProps(bindingElement.style); 710 711 const rule = this.#getRuleItem(elementStyle, node.rawNode, { 712 pseudoElement: null, 713 isSystem: false, 714 inherited: null, 715 }); 716 717 // First any inline styles 718 if (showElementStyles) { 719 rules.push(rule); 720 } 721 722 // Now any inherited styles 723 if (showInheritedStyles) { 724 // at this point `isInherited` is true, so we want to put the NodeActor in the 725 // `inherited` property so the client can show this information (for example in 726 // the "Inherited from X" section in the Rules view). 727 rule.inherited = node; 728 rules.push(rule); 729 } 730 } 731 732 // Add normal rules. Typically this is passing in the node passed into the 733 // function, unless if that node was ::before/::after. In which case, 734 // it will pass in the parentNode along with "::before"/"::after". 735 this.#getElementRules( 736 bindingElement, 737 pseudo, 738 isInherited ? node : null, 739 filter 740 ).forEach(oneRule => { 741 // The only case when there would be a pseudo here is 742 // ::before/::after, and in this case we want to tell the 743 // view that it belongs to the element (which is a 744 // _moz_generated_content native anonymous element). 745 oneRule.pseudoElement = null; 746 rules.push(oneRule); 747 }); 748 749 // If we don't want to check pseudo elements rules, we can stop here. 750 if (skipPseudo) { 751 return rules; 752 } 753 754 // Now retrieve any pseudo element rules. 755 // We can have pseudo element that are children of other pseudo elements (e.g. with 756 // ::before::marker , ::marker is a child of ::before). 757 // In such case, we want to call #getElementRules with the actual pseudo element node, 758 // not its binding element. 759 const elementForPseudo = pseudo ? node.rawNode : bindingElement; 760 761 const relevantPseudoElements = []; 762 for (const readPseudo of PSEUDO_ELEMENTS) { 763 if (!this.#pseudoIsRelevant(elementForPseudo, readPseudo, isInherited)) { 764 continue; 765 } 766 767 // FIXME: Bug 1909173. Need to handle view transitions peudo-elements. 768 if (readPseudo === "::highlight") { 769 InspectorUtils.getRegisteredCssHighlights( 770 this.inspector.targetActor.window.document, 771 // only active 772 true 773 ).forEach(name => { 774 relevantPseudoElements.push(`::highlight(${name})`); 775 }); 776 } else { 777 relevantPseudoElements.push(readPseudo); 778 } 779 } 780 781 for (const readPseudo of relevantPseudoElements) { 782 const pseudoRules = this.#getElementRules( 783 elementForPseudo, 784 readPseudo, 785 isInherited ? node : null, 786 filter 787 ); 788 // inherited element backed pseudo element rules (e.g. `::details-content`) should 789 // not be at the same "level" as rules inherited from the binding element (e.g. `<details>`), 790 // so we need to put them before the "regular" rules. 791 if ( 792 SharedCssLogic.ELEMENT_BACKED_PSEUDO_ELEMENTS.has(readPseudo) && 793 isInherited 794 ) { 795 rules.unshift(...pseudoRules); 796 } else { 797 rules.push(...pseudoRules); 798 } 799 } 800 801 return rules; 802 } 803 804 /** 805 * @param {DOMNode} rawNode 806 * @param {StyleRuleActor} styleRuleActor 807 * @param {object} params 808 * @param {NodeActor} params.inherited 809 * @param {boolean} params.isSystem 810 * @param {string | null} params.pseudoElement 811 * @returns Object 812 */ 813 #getRuleItem(rule, rawNode, { inherited, isSystem, pseudoElement }) { 814 return { 815 rule, 816 pseudoElement, 817 isSystem, 818 inherited, 819 // We can't compute the value for the whole document as the color scheme 820 // can be set at the node level (e.g. with `color-scheme`) 821 darkColorScheme: InspectorUtils.isUsedColorSchemeDark(rawNode), 822 }; 823 } 824 825 #nodeIsTextfieldLike(node) { 826 if (node.nodeName == "TEXTAREA") { 827 return true; 828 } 829 return ( 830 node.mozIsTextField && 831 (node.mozIsTextField(false) || node.type == "number") 832 ); 833 } 834 835 #nodeIsListItem(node) { 836 const computed = CssLogic.getComputedStyle(node); 837 if (!computed) { 838 return false; 839 } 840 841 const display = computed.getPropertyValue("display"); 842 // This is written this way to handle `inline list-item` and such. 843 return display.split(" ").includes("list-item"); 844 } 845 846 /** 847 * Returns whether or node the pseudo element is relevant for the passed node 848 * 849 * @param {DOMNode} node 850 * @param {string} pseudo 851 * @param {boolean} isInherited 852 * @returns {boolean} 853 */ 854 // eslint-disable-next-line complexity 855 #pseudoIsRelevant(node, pseudo, isInherited = false) { 856 switch (pseudo) { 857 case "::after": 858 case "::before": 859 case "::first-letter": 860 case "::first-line": 861 case "::selection": 862 case "::highlight": 863 case "::target-text": 864 return !isInherited; 865 case "::marker": 866 return !isInherited && this.#nodeIsListItem(node); 867 case "::backdrop": 868 return !isInherited && node.matches(":modal, :popover-open"); 869 case "::cue": 870 return !isInherited && node.nodeName == "VIDEO"; 871 case "::file-selector-button": 872 return !isInherited && node.nodeName == "INPUT" && node.type == "file"; 873 case "::details-content": { 874 const isDetailsNode = node.nodeName == "DETAILS"; 875 if (!isDetailsNode) { 876 return false; 877 } 878 879 if (!isInherited) { 880 return true; 881 } 882 883 // If we're getting rules on a parent element, we need to check if the selected 884 // element is inside the ::details-content of node 885 // We traverse the flattened parent tree until we find the <slot> that implements 886 // the pseudo element, as it's easier to handle edge cases like nested <details>, 887 // multiple <summary>, etc … 888 let traversedNode = this.selectedElement; 889 while (traversedNode) { 890 if ( 891 // if we found the <slot> implementing the pseudo element 892 traversedNode.implementedPseudoElement === "::details-content" && 893 // and its parent <details> element is the element we're evaluating 894 traversedNode.flattenedTreeParentNode === node 895 ) { 896 // then include the ::details-content rules from that element 897 return true; 898 } 899 // otherwise keep looking up the tree 900 traversedNode = traversedNode.flattenedTreeParentNode; 901 } 902 903 return false; 904 } 905 case "::placeholder": 906 case "::-moz-placeholder": 907 return !isInherited && this.#nodeIsTextfieldLike(node); 908 case "::-moz-meter-bar": 909 return !isInherited && node.nodeName == "METER"; 910 case "::-moz-progress-bar": 911 return !isInherited && node.nodeName == "PROGRESS"; 912 case "::-moz-color-swatch": 913 return !isInherited && node.nodeName == "INPUT" && node.type == "color"; 914 case "::-moz-range-progress": 915 case "::-moz-range-thumb": 916 case "::-moz-range-track": 917 case "::slider-fill": 918 case "::slider-thumb": 919 case "::slider-track": 920 return !isInherited && node.nodeName == "INPUT" && node.type == "range"; 921 case "::view-transition": 922 case "::view-transition-group": 923 case "::view-transition-image-pair": 924 case "::view-transition-old": 925 case "::view-transition-new": 926 // FIXME: Bug 1909173. Need to handle view transitions peudo-elements 927 // for DevTools. For now we skip them. 928 return false; 929 default: 930 console.error("Unhandled pseudo-element " + pseudo); 931 return false; 932 } 933 } 934 935 /** 936 * Helper function for #getAllElementRules, returns the rules from a given 937 * element. See getApplied for documentation on parameters. 938 * 939 * @param {DOMNode} node 940 * @param {string} pseudo 941 * @param {NodeActor} inherited 942 * @param {GetAppliedFilterOption} filter 943 * 944 * @returns Array 945 */ 946 #getElementRules(node, pseudo, inherited, filter) { 947 if (!Element.isInstance(node)) { 948 return []; 949 } 950 951 // we don't need to retrieve inherited starting style rules 952 const includeStartingStyleRules = !inherited; 953 const domRules = InspectorUtils.getMatchingCSSRules( 954 node, 955 pseudo, 956 CssLogic.hasVisitedState(node), 957 includeStartingStyleRules 958 ); 959 960 if (!domRules) { 961 return []; 962 } 963 964 const rules = []; 965 966 const doc = this.inspector.targetActor.window.document; 967 968 // getMatchingCSSRules returns ordered from least-specific to 969 // most-specific. 970 for (let i = domRules.length - 1; i >= 0; i--) { 971 const domRule = domRules[i]; 972 const isSystem = 973 domRule.parentStyleSheet && 974 SharedCssLogic.isAgentStylesheet(domRule.parentStyleSheet); 975 976 // For now, when dealing with InspectorDeclaration, we only care about presentational 977 // hints style (e.g. <img height=100>). 978 if ( 979 domRule.declarationOrigin && 980 domRule.declarationOrigin !== "pres-hints" 981 ) { 982 continue; 983 } 984 985 if (isSystem && filter != SharedCssLogic.FILTER.UA) { 986 continue; 987 } 988 989 if (inherited) { 990 // Don't include inherited rules if none of its properties 991 // are inheritable. 992 let hasInherited = false; 993 // This can be on a hot path, so let's use a simple for rule instead of turning 994 // domRule.style into an Array to use some on it. 995 for (let j = 0, len = domRule.style.length; j < len; j++) { 996 if (InspectorUtils.isInheritedProperty(doc, domRule.style[j])) { 997 hasInherited = true; 998 break; 999 } 1000 } 1001 1002 if (!hasInherited) { 1003 continue; 1004 } 1005 } 1006 1007 const ruleActor = this.styleRef(domRule, pseudo); 1008 1009 rules.push( 1010 this.#getRuleItem(ruleActor, node, { 1011 inherited, 1012 isSystem, 1013 pseudoElement: pseudo, 1014 }) 1015 ); 1016 } 1017 return rules; 1018 } 1019 1020 /** 1021 * Given a node and a CSS rule, walk up the DOM looking for a matching element rule. 1022 * 1023 * @param {NodeActor} nodeActor the node 1024 * @param {CSSStyleRule} matchingRule the rule to find the entry for 1025 * @return {object | null} An entry as returned by #getAllElementRules, or null if no entry 1026 * matching the passed rule was find 1027 */ 1028 findEntryMatchingRule(nodeActor, matchingRule) { 1029 let currentNodeActor = nodeActor; 1030 while ( 1031 currentNodeActor && 1032 currentNodeActor.rawNode.nodeType != Node.DOCUMENT_NODE 1033 ) { 1034 for (const entry of this.#getAllElementRules(currentNodeActor, { 1035 isInherited: nodeActor !== currentNodeActor, 1036 })) { 1037 if (entry.rule.rawRule === matchingRule) { 1038 return entry; 1039 } 1040 } 1041 1042 currentNodeActor = this.walker.parentNode(currentNodeActor); 1043 } 1044 1045 // If we reached the document node without finding the rule, return null 1046 return null; 1047 } 1048 1049 /** 1050 * Helper function for getApplied that fetches a set of style properties that 1051 * apply to the given node and associated rules 1052 * 1053 * @param {NodeActor} node 1054 * @param {Array} entries 1055 * List of appliedstyle objects that lists the rules that apply to the 1056 * node. If adding a new rule to the stylesheet, only the new rule entry 1057 * is provided and only the style properties that apply to the new 1058 * rule is fetched. 1059 * @param {GetAppliedOptions} options 1060 * @returns Array of rule entries that applies to the given node and its associated rules. 1061 */ 1062 getAppliedProps(node, entries, options) { 1063 if (options.inherited) { 1064 let parent = this.walker.parentNode(node); 1065 while (parent && parent.rawNode.nodeType != Node.DOCUMENT_NODE) { 1066 entries = entries.concat( 1067 this.#getAllElementRules(parent, { 1068 isInherited: true, 1069 skipPseudo: options.skipPseudo, 1070 filter: options.filter, 1071 }) 1072 ); 1073 parent = this.walker.parentNode(parent); 1074 } 1075 } 1076 1077 if (options.matchedSelectors) { 1078 for (const entry of entries) { 1079 if (entry.rule.type === ELEMENT_STYLE) { 1080 continue; 1081 } 1082 entry.matchedSelectorIndexes = []; 1083 1084 const domRule = entry.rule.rawRule; 1085 const element = entry.inherited 1086 ? entry.inherited.rawNode 1087 : node.rawNode; 1088 1089 const pseudos = []; 1090 const { bindingElement, pseudo } = 1091 CssLogic.getBindingElementAndPseudo(element); 1092 1093 // if we couldn't find a binding element, we can't call domRule.selectorMatchesElement, 1094 // so bail out 1095 if (!bindingElement) { 1096 continue; 1097 } 1098 1099 if (pseudo) { 1100 pseudos.push(pseudo); 1101 } else if (entry.rule.pseudoElements.size) { 1102 // if `node` is not a pseudo element but the rule applies to some pseudo elements, 1103 // we need to pass those to CSSStyleRule#selectorMatchesElement 1104 pseudos.push(...entry.rule.pseudoElements); 1105 } else { 1106 // If the rule doesn't apply to any pseudo, set a null item so we'll still do 1107 // the proper check below 1108 pseudos.push(null); 1109 } 1110 1111 const relevantLinkVisited = CssLogic.hasVisitedState(bindingElement); 1112 const len = domRule.selectorCount; 1113 for (let i = 0; i < len; i++) { 1114 for (const pseudoElementName of pseudos) { 1115 if ( 1116 domRule.selectorMatchesElement( 1117 i, 1118 bindingElement, 1119 pseudoElementName, 1120 relevantLinkVisited 1121 ) 1122 ) { 1123 entry.matchedSelectorIndexes.push(i); 1124 // if we matched the selector for one pseudo, no need to check the other ones 1125 break; 1126 } 1127 } 1128 } 1129 } 1130 } 1131 1132 const computedStyle = this.cssLogic.computedStyle; 1133 if (computedStyle) { 1134 // Add all the keyframes rule associated with the element 1135 let animationNames = computedStyle.animationName.split(","); 1136 animationNames = animationNames.map(name => name.trim()); 1137 1138 if (animationNames) { 1139 // Traverse through all the available keyframes rule and add 1140 // the keyframes rule that matches the computed animation name 1141 for (const keyframesRule of this.cssLogic.keyframesRules) { 1142 if (!animationNames.includes(keyframesRule.name)) { 1143 continue; 1144 } 1145 1146 for (const rule of keyframesRule.cssRules) { 1147 entries.push({ 1148 rule: this.styleRef(rule), 1149 keyframes: this.styleRef(keyframesRule), 1150 }); 1151 } 1152 } 1153 } 1154 1155 // Add all the @position-try associated with the element 1156 const positionTryIdents = new Set(); 1157 for (const part of computedStyle.positionTryFallbacks.split(",")) { 1158 const name = part.trim(); 1159 if (name.startsWith("--")) { 1160 positionTryIdents.add(name); 1161 } 1162 } 1163 1164 for (const positionTryRule of this.cssLogic.positionTryRules) { 1165 if (!positionTryIdents.has(positionTryRule.name)) { 1166 continue; 1167 } 1168 1169 entries.push({ 1170 rule: this.styleRef(positionTryRule), 1171 }); 1172 } 1173 } 1174 1175 return entries; 1176 } 1177 1178 /** 1179 * Get layout-related information about a node. 1180 * This method returns an object with properties giving information about 1181 * the node's margin, border, padding and content region sizes, as well 1182 * as information about the type of box, its position, z-index, etc... 1183 * 1184 * @param {NodeActor} node 1185 * @param {object} options The only available option is autoMargins. 1186 * If set to true, the element's margins will receive an extra check to see 1187 * whether they are set to "auto" (knowing that the computed-style in this 1188 * case would return "0px"). 1189 * The returned object will contain an extra property (autoMargins) listing 1190 * all margins that are set to auto, e.g. {top: "auto", left: "auto"}. 1191 * @return {object} 1192 */ 1193 getLayout(node, options) { 1194 this.cssLogic.highlight(node.rawNode); 1195 1196 const layout = {}; 1197 1198 // First, we update the first part of the box model view, with 1199 // the size of the element. 1200 1201 const clientRect = node.rawNode.getBoundingClientRect(); 1202 layout.width = parseFloat(clientRect.width.toPrecision(6)); 1203 layout.height = parseFloat(clientRect.height.toPrecision(6)); 1204 1205 // We compute and update the values of margins & co. 1206 const style = CssLogic.getComputedStyle(node.rawNode); 1207 for (const prop of [ 1208 "position", 1209 "top", 1210 "right", 1211 "bottom", 1212 "left", 1213 "margin-top", 1214 "margin-right", 1215 "margin-bottom", 1216 "margin-left", 1217 "padding-top", 1218 "padding-right", 1219 "padding-bottom", 1220 "padding-left", 1221 "border-top-width", 1222 "border-right-width", 1223 "border-bottom-width", 1224 "border-left-width", 1225 "z-index", 1226 "box-sizing", 1227 "display", 1228 "float", 1229 "line-height", 1230 ]) { 1231 layout[prop] = style.getPropertyValue(prop); 1232 } 1233 1234 if (options.autoMargins) { 1235 layout.autoMargins = this.processMargins(this.cssLogic); 1236 } 1237 1238 for (const i in this.map) { 1239 const property = this.map[i].property; 1240 this.map[i].value = parseFloat(style.getPropertyValue(property)); 1241 } 1242 1243 return layout; 1244 } 1245 1246 /** 1247 * Find 'auto' margin properties. 1248 */ 1249 processMargins(cssLogic) { 1250 const margins = {}; 1251 1252 for (const prop of ["top", "bottom", "left", "right"]) { 1253 const info = cssLogic.getPropertyInfo("margin-" + prop); 1254 const selectors = info.matchedSelectors; 1255 if (selectors && !!selectors.length && selectors[0].value == "auto") { 1256 margins[prop] = "auto"; 1257 } 1258 } 1259 1260 return margins; 1261 } 1262 1263 /** 1264 * On page navigation, tidy up remaining objects. 1265 */ 1266 onFrameUnload() { 1267 this.styleSheetsByRootNode = new WeakMap(); 1268 } 1269 1270 #onStylesheetUpdated = ({ resourceId, updateKind, updates = {} }) => { 1271 if (updateKind != "style-applied") { 1272 return; 1273 } 1274 const kind = updates.event.kind; 1275 // Duplicate refMap content before looping as onStyleApplied may mutate it 1276 for (const styleActor of [...this.refMap.values()]) { 1277 // Ignore StyleRuleActor that don't have a parent stylesheet. 1278 // i.e. actor whose type is ELEMENT_STYLE. 1279 if (!styleActor._parentSheet) { 1280 continue; 1281 } 1282 const resId = this.styleSheetsManager.getStyleSheetResourceId( 1283 styleActor._parentSheet 1284 ); 1285 if (resId === resourceId) { 1286 styleActor.onStyleApplied(kind); 1287 } 1288 } 1289 this.#styleApplied(kind); 1290 }; 1291 1292 /** 1293 * Helper function for adding a new rule and getting its applied style 1294 * properties 1295 * 1296 * @param NodeActor node 1297 * @param CSSStyleRule rule 1298 * @returns Array containing its applied style properties 1299 */ 1300 getNewAppliedProps(node, rule) { 1301 const ruleActor = this.styleRef(rule); 1302 return this.getAppliedProps(node, [{ rule: ruleActor }], { 1303 matchedSelectors: true, 1304 }); 1305 } 1306 1307 /** 1308 * Adds a new rule, and returns the new StyleRuleActor. 1309 * 1310 * @param {NodeActor} node 1311 * @param {string} pseudoClasses The list of pseudo classes to append to the 1312 * new selector. 1313 * @returns {StyleRuleActor} the new rule 1314 */ 1315 async addNewRule(node, pseudoClasses) { 1316 let sheet = null; 1317 const doc = node.rawNode.ownerDocument; 1318 const rootNode = node.rawNode.getRootNode(); 1319 1320 if ( 1321 this.styleSheetsByRootNode.has(rootNode) && 1322 this.styleSheetsByRootNode.get(rootNode).ownerNode?.isConnected 1323 ) { 1324 sheet = this.styleSheetsByRootNode.get(rootNode); 1325 } else { 1326 sheet = await this.styleSheetsManager.addStyleSheet( 1327 doc, 1328 node.rawNode.containingShadowRoot || doc.documentElement 1329 ); 1330 this.styleSheetsByRootNode.set(rootNode, sheet); 1331 } 1332 1333 const cssRules = sheet.cssRules; 1334 1335 // Get the binding element in case node is a pseudo element, so we can properly 1336 // build the selector 1337 const { bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo( 1338 node.rawNode 1339 ); 1340 const classes = [...bindingElement.classList]; 1341 1342 let selector; 1343 if (bindingElement.id) { 1344 selector = "#" + CSS.escape(bindingElement.id); 1345 } else if (classes.length) { 1346 selector = "." + classes.map(c => CSS.escape(c)).join("."); 1347 } else { 1348 selector = bindingElement.localName; 1349 } 1350 1351 if (pseudo && pseudoClasses?.length) { 1352 throw new Error( 1353 `Can't set pseudo classes (${JSON.stringify(pseudoClasses)}) onto a pseudo element (${pseudo})` 1354 ); 1355 } 1356 1357 if (pseudo) { 1358 selector += pseudo; 1359 } 1360 if (pseudoClasses && pseudoClasses.length) { 1361 selector += pseudoClasses.join(""); 1362 } 1363 1364 const index = sheet.insertRule(selector + " {}", cssRules.length); 1365 1366 const resourceId = this.styleSheetsManager.getStyleSheetResourceId(sheet); 1367 let authoredText = await this.styleSheetsManager.getText(resourceId); 1368 authoredText += "\n" + selector + " {\n" + "}"; 1369 await this.styleSheetsManager.setStyleSheetText(resourceId, authoredText); 1370 1371 const cssRule = sheet.cssRules.item(index); 1372 const ruleActor = this.styleRef(cssRule, null, true); 1373 1374 this.inspector.targetActor.emit("track-css-change", { 1375 ...ruleActor.metadata, 1376 type: "rule-add", 1377 add: null, 1378 remove: null, 1379 selector, 1380 }); 1381 1382 return { entries: this.getNewAppliedProps(node, cssRule) }; 1383 } 1384 1385 /** 1386 * Cause all StyleRuleActor instances of observed CSS rules to check whether the 1387 * states of their declarations have changed. 1388 * 1389 * Observed rules are the latest rules returned by a call to PageStyleActor.getApplied() 1390 * 1391 * This is necessary because changes in one rule can cause the declarations in another 1392 * to not be applicable (inactive CSS). The observers of those rules should be notified. 1393 * Rules will fire a "rule-updated" event if any of their declarations changed state. 1394 * 1395 * Call this method whenever a CSS rule is mutated: 1396 * - a CSS declaration is added/changed/disabled/removed 1397 * - a selector is added/changed/removed 1398 * 1399 * @param {Array<StyleRuleActor>} rulesToForceRefresh: An array of rules that, 1400 * if observed, should be refreshed even if the state of their declaration 1401 * didn't change. 1402 */ 1403 refreshObservedRules(rulesToForceRefresh) { 1404 for (const rule of this.#observedRules) { 1405 const force = rulesToForceRefresh && rulesToForceRefresh.includes(rule); 1406 rule.maybeRefresh(force); 1407 } 1408 } 1409 1410 /** 1411 * Get an array of existing attribute values in a node document. 1412 * 1413 * @param {string} search: A string to filter attribute value on. 1414 * @param {string} attributeType: The type of attribute we want to retrieve the values. 1415 * @param {Element} node: The element we want to get possible attributes for. This will 1416 * be used to get the document where the search is happening. 1417 * @returns {Array<string>} An array of strings 1418 */ 1419 getAttributesInOwnerDocument(search, attributeType, node) { 1420 if (!search) { 1421 throw new Error("search is mandatory"); 1422 } 1423 1424 // In a non-fission world, a node from an iframe shares the same `rootNode` as a node 1425 // in the top-level document. So here we need to retrieve the document from the node 1426 // in parameter in order to retrieve the right document. 1427 // This may change once we have a dedicated walker for every target in a tab, as we'll 1428 // be able to directly talk to the "right" walker actor. 1429 const targetDocument = node.rawNode.ownerDocument; 1430 1431 // We store the result in a Set which will contain the attribute value 1432 const result = new Set(); 1433 const lcSearch = search.toLowerCase(); 1434 this.#collectAttributesFromDocumentDOM( 1435 result, 1436 lcSearch, 1437 attributeType, 1438 targetDocument, 1439 node.rawNode 1440 ); 1441 this.#collectAttributesFromDocumentStyleSheets( 1442 result, 1443 lcSearch, 1444 attributeType, 1445 targetDocument 1446 ); 1447 1448 return Array.from(result).sort(); 1449 } 1450 1451 /** 1452 * Collect attribute values from the document DOM tree, matching the passed filter and 1453 * type, to the result Set. 1454 * 1455 * @param {Set<string>} result: A Set to which the results will be added. 1456 * @param {string} search: A string to filter attribute value on. 1457 * @param {string} attributeType: The type of attribute we want to retrieve the values. 1458 * @param {Document} targetDocument: The document the search occurs in. 1459 * @param {Node} currentNode: The current element rawNode 1460 */ 1461 #collectAttributesFromDocumentDOM( 1462 result, 1463 search, 1464 attributeType, 1465 targetDocument, 1466 nodeRawNode 1467 ) { 1468 // In order to retrieve attributes from DOM elements in the document, we're going to 1469 // do a query on the root node using attributes selector, to directly get the elements 1470 // matching the attributes we're looking for. 1471 1472 // For classes, we need something a bit different as the className we're looking 1473 // for might not be the first in the attribute value, meaning we can't use the 1474 // "attribute starts with X" selector. 1475 const attributeSelectorPositionChar = attributeType === "class" ? "*" : "^"; 1476 const selector = `[${attributeType}${attributeSelectorPositionChar}=${search} i]`; 1477 1478 const matchingElements = targetDocument.querySelectorAll(selector); 1479 1480 for (const element of matchingElements) { 1481 if (element === nodeRawNode) { 1482 return; 1483 } 1484 // For class attribute, we need to add the elements of the classList that match 1485 // the filter string. 1486 if (attributeType === "class") { 1487 for (const cls of element.classList) { 1488 if (!result.has(cls) && cls.toLowerCase().startsWith(search)) { 1489 result.add(cls); 1490 } 1491 } 1492 } else { 1493 const { value } = element.attributes[attributeType]; 1494 // For other attributes, we can directly use the attribute value. 1495 result.add(value); 1496 } 1497 } 1498 } 1499 1500 /** 1501 * Collect attribute values from the document stylesheets, matching the passed filter 1502 * and type, to the result Set. 1503 * 1504 * @param {Set<string>} result: A Set to which the results will be added. 1505 * @param {string} search: A string to filter attribute value on. 1506 * @param {string} attributeType: The type of attribute we want to retrieve the values. 1507 * It only supports "class" and "id" at the moment. 1508 * @param {Document} targetDocument: The document the search occurs in. 1509 */ 1510 #collectAttributesFromDocumentStyleSheets( 1511 result, 1512 search, 1513 attributeType, 1514 targetDocument 1515 ) { 1516 if (attributeType !== "class" && attributeType !== "id") { 1517 return; 1518 } 1519 1520 // We loop through all the stylesheets and their rules, recursively so we can go through 1521 // nested rules, and then use the lexer to only get the attributes we're looking for. 1522 const traverseRules = ruleList => { 1523 for (const rule of ruleList) { 1524 this.#collectAttributesFromRule(result, rule, search, attributeType); 1525 if (rule.cssRules) { 1526 traverseRules(rule.cssRules); 1527 } 1528 } 1529 }; 1530 for (const styleSheet of targetDocument.styleSheets) { 1531 traverseRules(styleSheet.rules); 1532 } 1533 } 1534 1535 /** 1536 * Collect attribute values from the rule, matching the passed filter and type, to the 1537 * result Set. 1538 * 1539 * @param {Set<string>} result: A Set to which the results will be added. 1540 * @param {Rule} rule: The rule the search occurs in. 1541 * @param {string} search: A string to filter attribute value on. 1542 * @param {string} attributeType: The type of attribute we want to retrieve the values. 1543 * It only supports "class" and "id" at the moment. 1544 */ 1545 #collectAttributesFromRule(result, rule, search, attributeType) { 1546 const shouldRetrieveClasses = attributeType === "class"; 1547 const shouldRetrieveIds = attributeType === "id"; 1548 1549 const { selectorText } = rule; 1550 // If there's no selectorText, or if the selectorText does not include the 1551 // filter, we can bail out. 1552 if (!selectorText || !selectorText.toLowerCase().includes(search)) { 1553 return; 1554 } 1555 1556 // Check if we should parse the selectorText (do we need to check for class/id and 1557 // if so, does the selector contains class/id related chars). 1558 const parseForClasses = 1559 shouldRetrieveClasses && 1560 selectorText.toLowerCase().includes(`.${search}`); 1561 const parseForIds = 1562 shouldRetrieveIds && selectorText.toLowerCase().includes(`#${search}`); 1563 1564 if (!parseForClasses && !parseForIds) { 1565 return; 1566 } 1567 1568 const lexer = new InspectorCSSParser(selectorText); 1569 let token; 1570 while ((token = lexer.nextToken())) { 1571 if ( 1572 token.tokenType === "Delim" && 1573 shouldRetrieveClasses && 1574 token.text === "." 1575 ) { 1576 token = lexer.nextToken(); 1577 if ( 1578 token.tokenType === "Ident" && 1579 token.text.toLowerCase().startsWith(search) 1580 ) { 1581 result.add(token.text); 1582 } 1583 } 1584 if (token.tokenType === "IDHash" && shouldRetrieveIds) { 1585 const idWithoutHash = token.value; 1586 if (idWithoutHash.startsWith(search)) { 1587 result.add(idWithoutHash); 1588 } 1589 } 1590 } 1591 } 1592 } 1593 exports.PageStyleActor = PageStyleActor;