chrome-devtools-frontend
Version:
Chrome DevTools UI
107 lines (93 loc) • 3.67 kB
text/typescript
// 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 a 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');
}