| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import * as i18n from '../../core/i18n/i18n.js'; | |
| import type * as Protocol from '../../generated/protocol.js'; | |
| const UIStrings = { | |
| /** | |
| * @description Summary line in a tooltip explaining CSS selector specificity. | |
| * @example {1} PH1 | |
| * @example {2} PH2 | |
| * @example {3} PH3 | |
| */ | |
| specificity: 'Specificity: ({PH1},{PH2},{PH3})', | |
| /** | |
| * @description Tooltip line listing the selector parts contributing to the ID-like specificity bucket. | |
| * @example {#main} PH1 | |
| */ | |
| idLikeSpecificity: '(a) ID-like: {PH1}', | |
| /** | |
| * @description Tooltip line listing the selector parts contributing to the class-like specificity bucket. | |
| * @example {.active, :hover} PH1 | |
| */ | |
| classLikeSpecificity: '(b) Class-like: {PH1}', | |
| /** | |
| * @description Tooltip line listing the selector parts contributing to the type-like specificity bucket. | |
| * @example {div} PH1 | |
| */ | |
| typeLikeSpecificity: '(c) Type-like: {PH1}', | |
| } as const; | |
| const str_ = i18n.i18n.registerUIStrings('panels/elements/CSSSpecificityBreakdown.ts', UIStrings); | |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); | |
| export interface SpecificityBreakdown { | |
| ids: string[]; | |
| classes: string[]; | |
| types: string[]; | |
| } | |
| function formatContribution(text: string, contribution: number): string { | |
| return contribution > 1 ? `${text} x${contribution}` : text; | |
| } | |
| function formatComponentList(components: string[]): string { | |
| const formatter = new Intl.ListFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, { | |
| style: 'long', | |
| type: 'conjunction', | |
| }); | |
| return formatter.format(components); | |
| } | |
| export function getSpecificityBreakdown(specificity: Protocol.CSS.Specificity): SpecificityBreakdown { | |
| const ids: string[] = []; | |
| const classes: string[] = []; | |
| const types: string[] = []; | |
| for (const component of specificity.components ?? []) { | |
| if (component.a > 0) { | |
| ids.push(formatContribution(component.text, component.a)); | |
| } | |
| if (component.b > 0) { | |
| classes.push(formatContribution(component.text, component.b)); | |
| } | |
| if (component.c > 0) { | |
| types.push(formatContribution(component.text, component.c)); | |
| } | |
| } | |
| return {ids, classes, types}; | |
| } | |
| /** | |
| * Formats the specificity breakdown into a human-readable multi-line string | |
| * suitable for displaying in a tooltip. | |
| * | |
| * Example output for selector "div#main .active:hover": | |
| * Specificity: (1,2,1) | |
| * (a) ID-like: #main | |
| * (b) Class-like: .active, :hover | |
| * (c) Type-like: div | |
| */ | |
| export function formatSpecificitySummary(specificity: Protocol.CSS.Specificity): string { | |
| return i18nString(UIStrings.specificity, {PH1: specificity.a, PH2: specificity.b, PH3: specificity.c}); | |
| } | |
| export function getSpecificityBreakdownLines(specificity: Protocol.CSS.Specificity): string[] { | |
| const breakdown = getSpecificityBreakdown(specificity); | |
| const lines: string[] = []; | |
| if (breakdown.ids.length > 0) { | |
| lines.push(i18nString(UIStrings.idLikeSpecificity, {PH1: formatComponentList(breakdown.ids)})); | |
| } | |
| if (breakdown.classes.length > 0) { | |
| lines.push(i18nString(UIStrings.classLikeSpecificity, {PH1: formatComponentList(breakdown.classes)})); | |
| } | |
| if (breakdown.types.length > 0) { | |
| lines.push(i18nString(UIStrings.typeLikeSpecificity, {PH1: formatComponentList(breakdown.types)})); | |
| } | |
| return lines; | |
| } | |
| export function formatSpecificityTooltip(specificity: Protocol.CSS.Specificity): string { | |
| return [formatSpecificitySummary(specificity), ...getSpecificityBreakdownLines(specificity)].join('\n'); | |
| } |