browser-debugger-cli 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.claude/skills/bdg/SKILL.md +268 -0
  2. package/README.md +148 -74
  3. package/dist/commands/css.d.ts +13 -0
  4. package/dist/commands/css.js +53 -0
  5. package/dist/commands/dom/audit.d.ts +14 -0
  6. package/dist/commands/dom/audit.js +87 -0
  7. package/dist/commands/dom/formInteraction.js +36 -6
  8. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  9. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  10. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  11. package/dist/commands/dom/helpers/screenshot.js +158 -38
  12. package/dist/commands/dom/index.js +4 -1
  13. package/dist/commands/dom/screenshot.js +10 -6
  14. package/dist/commands/dom/wait.js +5 -3
  15. package/dist/commands/helpJson.js +1 -1
  16. package/dist/commands/installSkill.d.ts +20 -0
  17. package/dist/commands/installSkill.js +87 -0
  18. package/dist/commands/optionBehaviors.js +21 -6
  19. package/dist/commands/page.js +7 -4
  20. package/dist/commands/peek.d.ts +7 -0
  21. package/dist/commands/peek.js +65 -23
  22. package/dist/commands/shared/optionTypes.d.ts +5 -1
  23. package/dist/commands/start.d.ts +13 -0
  24. package/dist/commands/start.js +19 -2
  25. package/dist/commands/tail.d.ts +7 -1
  26. package/dist/commands/tail.js +13 -62
  27. package/dist/commands.js +5 -0
  28. package/dist/daemon/session/commandRegistry.js +7 -1
  29. package/dist/daemon/session/plugins.js +4 -52
  30. package/dist/daemon.js +7986 -6848
  31. package/dist/errors/messages.d.ts +50 -4
  32. package/dist/errors/messages.js +94 -5
  33. package/dist/index.js +709 -190
  34. package/dist/ipc/client.d.ts +4 -0
  35. package/dist/ipc/client.js +8 -0
  36. package/dist/ipc/protocol/auditTypes.d.ts +129 -0
  37. package/dist/ipc/protocol/auditTypes.js +6 -0
  38. package/dist/ipc/protocol/commands.d.ts +23 -0
  39. package/dist/ipc/protocol/commands.js +2 -0
  40. package/dist/ipc/protocol/domTypes.d.ts +4 -0
  41. package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
  42. package/dist/runtime/css/search.d.ts +39 -0
  43. package/dist/runtime/css/search.js +122 -0
  44. package/dist/runtime/dom/actionEffects.d.ts +4 -1
  45. package/dist/runtime/dom/actionEffects.js +8 -4
  46. package/dist/runtime/dom/audit.d.ts +19 -0
  47. package/dist/runtime/dom/audit.js +36 -0
  48. package/dist/runtime/dom/auditModel.d.ts +45 -0
  49. package/dist/runtime/dom/auditModel.js +215 -0
  50. package/dist/runtime/dom/auditScripts.d.ts +107 -0
  51. package/dist/runtime/dom/auditScripts.js +112 -0
  52. package/dist/runtime/dom/elementGeometry.d.ts +8 -2
  53. package/dist/runtime/dom/elementGeometry.js +24 -8
  54. package/dist/runtime/dom/elementInfo.d.ts +3 -2
  55. package/dist/runtime/dom/elementInfo.js +8 -2
  56. package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
  57. package/dist/runtime/dom/inspect.d.ts +7 -0
  58. package/dist/runtime/dom/inspect.js +88 -23
  59. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  60. package/dist/runtime/dom/inspectAllStyles.js +89 -7
  61. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  62. package/dist/runtime/dom/inspectCascade.js +214 -44
  63. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  64. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  65. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  66. package/dist/runtime/dom/inspectHints.js +125 -9
  67. package/dist/runtime/dom/inspectModel.d.ts +3 -0
  68. package/dist/runtime/dom/inspectModel.js +30 -7
  69. package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
  70. package/dist/runtime/dom/inspectPaintModel.js +180 -68
  71. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  72. package/dist/runtime/dom/inspectRules.js +21 -5
  73. package/dist/runtime/dom/inspectScripts.d.ts +85 -12
  74. package/dist/runtime/dom/inspectScripts.js +314 -28
  75. package/dist/runtime/dom/inspectTree.js +10 -2
  76. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  77. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  78. package/dist/runtime/dom/layout.js +31 -9
  79. package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
  80. package/dist/runtime/dom/reactEventHelpers.js +27 -9
  81. package/dist/runtime/page/emulation.d.ts +13 -4
  82. package/dist/runtime/page/emulation.js +69 -4
  83. package/dist/runtime/page/userAgent.d.ts +17 -0
  84. package/dist/runtime/page/userAgent.js +57 -0
  85. package/dist/types.d.ts +12 -0
  86. package/dist/ui/formatters/audit.d.ts +19 -0
  87. package/dist/ui/formatters/audit.js +106 -0
  88. package/dist/ui/formatters/dom.d.ts +1 -1
  89. package/dist/ui/formatters/dom.js +6 -3
  90. package/dist/ui/formatters/inspect.js +42 -15
  91. package/dist/ui/formatters/installSkill.d.ts +11 -0
  92. package/dist/ui/formatters/installSkill.js +31 -0
  93. package/dist/ui/formatters/status.js +1 -1
  94. package/dist/ui/messages/commands.d.ts +44 -7
  95. package/dist/ui/messages/commands.js +83 -11
  96. package/dist/ui/messages/preview.d.ts +6 -0
  97. package/dist/ui/messages/preview.js +9 -1
  98. package/dist/utils/cssValues.js +36 -4
  99. package/dist/utils/decisionTrees.js +0 -5
  100. package/dist/utils/suggestions.d.ts +4 -2
  101. package/dist/utils/suggestions.js +7 -5
  102. package/dist/utils/taskMappings.js +1 -1
  103. package/package.json +4 -2
@@ -13,6 +13,7 @@
13
13
  * writing). With no declaration of its own, an inherited property comes from
14
14
  * the nearest ancestor that declares it.
15
15
  */
16
+ import { truncateByLength } from '../../utils/strings.js';
16
17
  /**
17
18
  * The four side longhands of a pattern.
18
19
  *
@@ -64,9 +65,37 @@ const SHORTHANDS = {
64
65
  `border-${side}`,
65
66
  ['width', 'style', 'color'].map((part) => `border-${side}-${part}`),
66
67
  ])),
67
- background: ['background-color', 'background-image'],
68
+ background: [
69
+ 'background-color',
70
+ 'background-image',
71
+ 'background-position-x',
72
+ 'background-position-y',
73
+ 'background-size',
74
+ 'background-repeat',
75
+ 'background-origin',
76
+ 'background-clip',
77
+ 'background-attachment',
78
+ ],
68
79
  font: ['font-style', 'font-weight', 'font-size', 'line-height', 'font-family'],
69
80
  flex: ['flex-grow', 'flex-shrink', 'flex-basis'],
81
+ 'flex-flow': ['flex-direction', 'flex-wrap'],
82
+ outline: ['outline-width', 'outline-style', 'outline-color'],
83
+ 'text-decoration': [
84
+ 'text-decoration-line',
85
+ 'text-decoration-style',
86
+ 'text-decoration-color',
87
+ 'text-decoration-thickness',
88
+ ],
89
+ transition: [
90
+ 'transition-property',
91
+ 'transition-duration',
92
+ 'transition-timing-function',
93
+ 'transition-delay',
94
+ 'transition-behavior',
95
+ ],
96
+ 'background-position': ['background-position-x', 'background-position-y'],
97
+ 'list-style': ['list-style-position', 'list-style-image', 'list-style-type'],
98
+ columns: ['column-width', 'column-count'],
70
99
  'place-items': ['align-items', 'justify-items'],
71
100
  'place-content': ['align-content', 'justify-content'],
72
101
  'place-self': ['align-self', 'justify-self'],
@@ -91,35 +120,31 @@ const LOGICAL_SIDES = {
91
120
  'inline-start': 'left',
92
121
  'inline-end': 'right',
93
122
  };
94
- /** Properties that inherit (those `dom inspect` reports) */
123
+ /** Longhands that inherit (CSS, SVG and the prefixed ones Chrome computes) */
95
124
  const INHERITED = new Set([
96
- 'color',
97
- 'font-family',
98
- 'font-size',
99
- 'font-style',
100
- 'font-weight',
101
- 'font-variant',
102
- 'font-feature-settings',
103
- 'font-variation-settings',
104
- 'line-height',
105
- 'letter-spacing',
106
- 'word-spacing',
107
- 'text-align',
108
- 'text-indent',
109
- 'text-transform',
110
- 'text-shadow',
111
- 'white-space-collapse',
112
- 'text-wrap-mode',
113
- 'visibility',
114
- 'cursor',
115
- 'direction',
116
- 'list-style-type',
117
- 'list-style-position',
118
- 'tab-size',
119
- 'pointer-events',
120
- 'user-select',
121
- '-webkit-text-security',
122
- ]);
125
+ 'color caret-color accent-color color-scheme forced-color-adjust print-color-adjust',
126
+ 'font-family font-size font-style font-weight font-stretch font-size-adjust font-kerning',
127
+ 'font-optical-sizing font-palette font-language-override font-feature-settings',
128
+ 'font-variation-settings font-variant font-variant-caps font-variant-ligatures',
129
+ 'font-variant-numeric font-variant-east-asian font-variant-alternates',
130
+ 'font-variant-position font-variant-emoji font-synthesis-weight font-synthesis-style',
131
+ 'font-synthesis-small-caps line-height letter-spacing word-spacing text-align',
132
+ 'text-align-last text-indent text-justify text-transform text-shadow text-rendering',
133
+ 'text-size-adjust text-underline-position text-underline-offset text-decoration-skip-ink',
134
+ 'text-emphasis-style text-emphasis-color text-emphasis-position text-orientation',
135
+ 'text-wrap-mode text-wrap-style text-spacing-trim text-autospace white-space-collapse',
136
+ 'word-break overflow-wrap line-break hyphens hyphenate-character hyphenate-limit-chars',
137
+ 'tab-size direction writing-mode ruby-position ruby-align visibility cursor',
138
+ 'pointer-events user-select interpolate-size list-style-type list-style-position',
139
+ 'list-style-image quotes border-collapse border-spacing caption-side empty-cells orphans',
140
+ 'widows image-rendering fill fill-opacity fill-rule stroke stroke-width stroke-opacity',
141
+ 'stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit',
142
+ 'paint-order marker-start marker-mid marker-end clip-rule color-interpolation',
143
+ 'color-interpolation-filters color-rendering shape-rendering text-anchor dominant-baseline',
144
+ 'math-depth math-style math-shift scrollbar-color text-combine-upright',
145
+ '-webkit-text-security -webkit-text-fill-color',
146
+ '-webkit-text-stroke-color -webkit-text-stroke-width',
147
+ ].flatMap((names) => names.split(' ')));
123
148
  /**
124
149
  * The longhands of a shorthand.
125
150
  *
@@ -133,10 +158,30 @@ export function shorthandLonghands(name) {
133
158
  * Whether a property inherits.
134
159
  *
135
160
  * @param property - Longhand
136
- * @returns True for inherited properties (custom properties inherit)
161
+ * @param notInherited - Custom properties registered with `inherits: false`
162
+ * @returns True for inherited properties (custom properties inherit unless
163
+ * registered otherwise)
164
+ */
165
+ export function isInherited(property, notInherited) {
166
+ if (property.startsWith('--'))
167
+ return !notInherited?.has(property);
168
+ return INHERITED.has(property);
169
+ }
170
+ /**
171
+ * Custom properties registered not to inherit, with `@property` or
172
+ * `CSS.registerProperty`.
173
+ *
174
+ * @param matched - `CSS.getMatchedStylesForNode` response
175
+ * @returns Their names
137
176
  */
138
- export function isInherited(property) {
139
- return INHERITED.has(property) || property.startsWith('--');
177
+ function nonInheritingProperties(matched) {
178
+ const fromRules = (matched.cssPropertyRules ?? [])
179
+ .filter((rule) => rule.style.cssProperties.some((property) => property.name === 'inherits' && property.value.trim() === 'false'))
180
+ .map((rule) => rule.propertyName.text);
181
+ const fromScript = (matched.cssPropertyRegistrations ?? [])
182
+ .filter((registration) => !registration.inherits)
183
+ .map((registration) => registration.propertyName);
184
+ return new Set([...fromRules, ...fromScript]);
140
185
  }
141
186
  /**
142
187
  * The physical longhand a logical property sets.
@@ -211,23 +256,43 @@ function declarationsOf(style, source) {
211
256
  });
212
257
  }
213
258
  /**
214
- * Source of a matched rule: its matching selector, origin, stylesheet,
215
- * layer (nested ones as `outer.inner`), the media or container condition
216
- * it is under (`not all and (…)`, as Chrome writes `not (…)`, shortened) and
217
- * the selector's specificity.
259
+ * The selector of a rule's list that matched with the highest specificity
260
+ * (the one the cascade uses: `.title` of `h1, .title`).
261
+ *
262
+ * @param match - CDP rule match
263
+ * @returns Selector
264
+ */
265
+ function strongestMatchingSelector(match) {
266
+ const weight = (value) => {
267
+ const s = value?.specificity;
268
+ return s ? s.a * 1e6 + s.b * 1e3 + s.c : -1;
269
+ };
270
+ const selectors = match.rule.selectorList.selectors;
271
+ const matching = match.matchingSelectors.map((index) => selectors[index]);
272
+ return (matching.reduce((best, value) => (weight(value) > weight(best) ? value : best), matching[0]) ??
273
+ selectors[0]);
274
+ }
275
+ /**
276
+ * Source of a matched rule: its matching selector (the strongest of a list),
277
+ * origin, stylesheet, layer (nested ones as `outer.inner`), the media,
278
+ * container, `@supports` and `@scope` conditions it is under (`not all and
279
+ * (…)`, as Chrome writes `not (…)`, shortened) and the selector's specificity.
218
280
  *
219
281
  * @param match - CDP rule match
220
282
  * @returns Declaration source
221
283
  */
222
284
  function ruleSource(match) {
223
285
  const { rule } = match;
224
- const matching = rule.selectorList.selectors[match.matchingSelectors[0] ?? 0];
286
+ const matching = strongestMatchingSelector(match);
225
287
  const selector = matching?.text ?? rule.selectorList.text;
226
288
  const layer = rule.layers?.map((l) => l.text || '(anonymous)').join('.');
227
- const condition = [...(rule.media ?? []), ...(rule.containerQueries ?? [])]
228
- .map((c) => c.text.replace(/^not all and /, 'not '))
229
- .join(' and ');
289
+ const condition = [
290
+ ...[...(rule.media ?? []), ...(rule.containerQueries ?? [])].map((c) => c.text.replace(/^not all and /, 'not ')),
291
+ ...(rule.supports ?? []).map((c) => `supports ${c.text}`),
292
+ ...(rule.scopes ?? []).map((c) => `scope ${c.text}`),
293
+ ].join(' and ');
230
294
  const specificity = matching?.specificity;
295
+ const declarations = rule.style.cssText;
231
296
  return {
232
297
  kind: 'rule',
233
298
  selector,
@@ -236,6 +301,9 @@ function ruleSource(match) {
236
301
  ...(rule.styleSheetId && { styleSheetId: rule.styleSheetId }),
237
302
  ...(layer && { layer }),
238
303
  ...(condition && { condition }),
304
+ ...(declarations && {
305
+ rule: { selector: collapse(rule.selectorList.text), declarations: collapse(declarations) },
306
+ }),
239
307
  };
240
308
  }
241
309
  /**
@@ -248,9 +316,23 @@ function orderedDeclarations(entry) {
248
316
  return [
249
317
  ...declarationsOf(entry.attributesStyle, { kind: 'attribute' }),
250
318
  ...(entry.matchedCSSRules ?? []).flatMap((match) => declarationsOf(match.rule.style, ruleSource(match))),
251
- ...declarationsOf(entry.inlineStyle, { kind: 'inline' }),
319
+ ...declarationsOf(entry.inlineStyle, {
320
+ kind: 'inline',
321
+ ...(entry.inlineStyle?.cssText && {
322
+ rule: { declarations: collapse(entry.inlineStyle.cssText) },
323
+ }),
324
+ }),
252
325
  ];
253
326
  }
327
+ /**
328
+ * Text with runs of whitespace collapsed to one space.
329
+ *
330
+ * @param text - CSS text
331
+ * @returns Collapsed, trimmed
332
+ */
333
+ function collapse(text) {
334
+ return text.replace(/\s+/g, ' ').trim();
335
+ }
254
336
  /**
255
337
  * Whether a declaration comes from the page's authors (not the browser's
256
338
  * own stylesheet or an extension's).
@@ -337,6 +419,7 @@ export function ownProperties(matched) {
337
419
  export function resolveCascade(matched, properties) {
338
420
  const own = orderedDeclarations(matched);
339
421
  const ancestors = (matched.inherited ?? []).map(orderedDeclarations);
422
+ const notInherited = nonInheritingProperties(matched);
340
423
  const resolved = new Map();
341
424
  for (const property of properties) {
342
425
  const ranked = byPrecedence(own.filter((d) => d.property === property));
@@ -345,7 +428,7 @@ export function resolveCascade(matched, properties) {
345
428
  resolved.set(property, { winner, overridden });
346
429
  continue;
347
430
  }
348
- resolved.set(property, inheritedWinner(property, ancestors));
431
+ resolved.set(property, inheritedWinner(property, ancestors, notInherited));
349
432
  }
350
433
  return resolved;
351
434
  }
@@ -354,11 +437,12 @@ export function resolveCascade(matched, properties) {
354
437
  *
355
438
  * @param property - Longhand
356
439
  * @param ancestors - Declarations of each ancestor, nearest first
440
+ * @param notInherited - Custom properties registered not to inherit
357
441
  * @returns The winner and the declarations it beat on that ancestor (none for
358
442
  * non-inherited properties or when no ancestor sets it)
359
443
  */
360
- function inheritedWinner(property, ancestors) {
361
- if (!isInherited(property))
444
+ function inheritedWinner(property, ancestors, notInherited) {
445
+ if (!isInherited(property, notInherited))
362
446
  return { overridden: [] };
363
447
  for (const [depth, declarations] of ancestors.entries()) {
364
448
  const ranked = byPrecedence(declarations.filter((d) => d.property === property)).map((declaration) => ({ ...declaration, ancestor: depth + 1 }));
@@ -368,4 +452,90 @@ function inheritedWinner(property, ancestors) {
368
452
  }
369
453
  return { overridden: [] };
370
454
  }
455
+ /** Longest rule given whole; a longer one (minified CSS) is cut to the declaration */
456
+ const RULE_TEXT_LENGTH = 300;
457
+ /**
458
+ * The rule a declaration is in, as written: whole when short, else its
459
+ * selector and that declaration (`.btn { … background-color:var(--bs-btn-bg); … }`).
460
+ *
461
+ * @param declaration - Declaration
462
+ * @returns `{ rule }`, or nothing for a browser, extension or attribute style
463
+ */
464
+ export function ruleField(declaration) {
465
+ const { rule } = declaration.source;
466
+ if (!rule || !isAuthor(declaration))
467
+ return {};
468
+ const whole = ruleText(rule.selector, rule.declarations);
469
+ if (whole.length <= RULE_TEXT_LENGTH)
470
+ return { rule: whole };
471
+ const own = lastDeclarationOf(rule.declarations, declaration.via ?? declaration.property);
472
+ if (!own)
473
+ return { rule: truncateByLength(whole, RULE_TEXT_LENGTH) };
474
+ const selector = rule.selector === undefined ? undefined : truncateByLength(rule.selector, 80);
475
+ return { rule: ruleText(selector, `… ${truncateByLength(own, RULE_TEXT_LENGTH - 100)}; …`) };
476
+ }
477
+ /**
478
+ * A rule's text: `selector { declarations }`, or `style="declarations"`.
479
+ *
480
+ * @param selector - Selector (absent: the style attribute)
481
+ * @param declarations - Declarations
482
+ * @returns Text
483
+ */
484
+ function ruleText(selector, declarations) {
485
+ return selector === undefined
486
+ ? `style="${declarations.replaceAll('"', '\\"')}"`
487
+ : `${selector} { ${declarations} }`;
488
+ }
489
+ /**
490
+ * The last declaration of a property in a declaration block (the one that
491
+ * counts when it is repeated), split at semicolons outside strings and
492
+ * parentheses (`url(data:image/png;base64,…)` stays whole).
493
+ *
494
+ * @param declarations - Declaration block
495
+ * @param property - Property as written (custom properties are case-sensitive)
496
+ * @returns The declaration, without its semicolon
497
+ */
498
+ function lastDeclarationOf(declarations, property) {
499
+ const name = (part) => {
500
+ const raw = part.slice(0, part.indexOf(':')).trim();
501
+ return raw.startsWith('--') ? raw : raw.toLowerCase();
502
+ };
503
+ const wanted = property.startsWith('--') ? property : property.toLowerCase();
504
+ return splitDeclarations(declarations)
505
+ .filter((part) => part.includes(':') && name(part) === wanted)
506
+ .at(-1);
507
+ }
508
+ /**
509
+ * Split a declaration block at its top-level semicolons.
510
+ *
511
+ * @param block - Declarations
512
+ * @returns Declarations, trimmed, empty ones dropped
513
+ */
514
+ function splitDeclarations(block) {
515
+ const parts = [];
516
+ let depth = 0;
517
+ let quote;
518
+ let start = 0;
519
+ for (let i = 0; i < block.length; i++) {
520
+ const char = block[i];
521
+ if (quote) {
522
+ if (char === '\\')
523
+ i++;
524
+ else if (char === quote)
525
+ quote = undefined;
526
+ }
527
+ else if (char === '"' || char === "'")
528
+ quote = char;
529
+ else if (char === '(')
530
+ depth++;
531
+ else if (char === ')')
532
+ depth = Math.max(0, depth - 1);
533
+ else if (char === ';' && depth === 0) {
534
+ parts.push(block.slice(start, i));
535
+ start = i + 1;
536
+ }
537
+ }
538
+ parts.push(block.slice(start));
539
+ return parts.map((part) => part.trim()).filter(Boolean);
540
+ }
371
541
  //# sourceMappingURL=inspectCascade.js.map
@@ -8,6 +8,7 @@ import type { Protocol } from '../../connection/typed-cdp.js';
8
8
  import type { InspectResult } from '../../ipc/protocol/inspectTypes.js';
9
9
  import { type Declaration } from './inspectCascade.js';
10
10
  import type { StyleMap } from './inspectLayoutModel.js';
11
+ import type { InactiveRule } from './inspectScripts.js';
11
12
  /** Longhands reported together, so a shorthand can stand for its sides */
12
13
  export interface PropertyGroup {
13
14
  shorthand?: string;
@@ -25,8 +26,15 @@ export interface CascadeInput {
25
26
  why?: string;
26
27
  /** `--props` names: `--rules` then covers only these */
27
28
  props?: string[];
29
+ /** Longhands of the `--why` shorthand as the browser expands it */
30
+ whyLonghands?: string[];
31
+ /** Computed value of the `--why` shorthand, as the page writes it */
32
+ whyComputed?: string;
33
+ /** Rules that set the `--why` property under a condition that does not apply now */
34
+ whyInactive?: InactiveRule[];
28
35
  /** Check for declarations that have no effect (default true) */
29
36
  hints?: boolean;
37
+ /** A form control that draws text (its font is checked against the parent's) */
30
38
  formControl?: boolean;
31
39
  }
32
40
  /**
@@ -4,9 +4,10 @@
4
4
  * property, sides set by one declaration merged into the shorthand) and
5
5
  * `--why` (every declaration of one property).
6
6
  */
7
- import { normalizeProperty } from './inspectAllStyles.js';
8
- import { physicalName, ownProperties, resolveCascade, shorthandLonghands, } from './inspectCascade.js';
7
+ import { collapsedValue, normalizeProperty } from './inspectAllStyles.js';
8
+ import { physicalName, ownProperties, ruleField, resolveCascade, shorthandLonghands, } from './inspectCascade.js';
9
9
  import { formControlFontHints, inactiveHints, undefinedVariableHints, } from './inspectHints.js';
10
+ import { substituteVariables } from './inspectVariables.js';
10
11
  import { buildWhy } from './inspectWhyModel.js';
11
12
  const SIDES = ['top', 'right', 'bottom', 'left'];
12
13
  const sides = (pattern) => SIDES.map((side) => pattern.replace('{side}', side));
@@ -33,6 +34,11 @@ const RULE_GROUPS = [
33
34
  },
34
35
  { longhands: ['background-color'] },
35
36
  { longhands: ['background-image'] },
37
+ { longhands: ['background-size'] },
38
+ {
39
+ shorthand: 'background-position',
40
+ longhands: ['background-position-x', 'background-position-y'],
41
+ },
36
42
  { longhands: ['color'] },
37
43
  { longhands: ['font-family'] },
38
44
  { longhands: ['font-size'] },
@@ -63,8 +69,10 @@ const RULE_GROUPS = [
63
69
  * @returns `hints` (empty when checked and nothing found), `rules` and `why`
64
70
  */
65
71
  export function buildCascadeFields(input) {
66
- const ruleGroups = input.rules ? (input.props?.map(propertyGroup) ?? RULE_GROUPS) : [];
67
- const whyGroup = input.why ? propertyGroup(input.why) : undefined;
72
+ const ruleGroups = input.rules
73
+ ? (input.props?.map((name) => propertyGroup(name)) ?? RULE_GROUPS)
74
+ : [];
75
+ const whyGroup = input.why ? propertyGroup(input.why, input.whyLonghands) : undefined;
68
76
  const wanted = [
69
77
  ...new Set([
70
78
  ...ownProperties(input.matched),
@@ -73,40 +81,59 @@ export function buildCascadeFields(input) {
73
81
  ]),
74
82
  ];
75
83
  const cascade = resolveCascade(input.matched, wanted);
76
- const hints = input.hints === false ? undefined : buildHints(cascade, input);
84
+ const found = collectHints(cascade, input);
85
+ const hints = input.hints === false ? undefined : found.map((hint) => toInspectHint(hint, input));
77
86
  const rules = buildRules(ruleGroups, cascade, input);
78
87
  return {
79
88
  ...(hints && { hints }),
80
89
  ...(rules.length > 0 && { rules }),
81
- ...(whyGroup && { why: buildWhy(whyGroup, cascade, input) }),
90
+ ...(whyGroup && { why: withInactive(buildWhy(whyGroup, cascade, input, found), input) }),
82
91
  };
83
92
  }
84
93
  /**
85
94
  * The longhands a property name stands for.
86
95
  *
87
96
  * @param name - Shorthand, longhand, logical or custom property
97
+ * @param expanded - Its longhands as the browser expands it, for a
98
+ * shorthand bdg does not list (`transition`, `grid-template`)
88
99
  * @returns Group (a shorthand with its longhands, or one physical longhand)
89
100
  */
90
- function propertyGroup(name) {
91
- const longhands = shorthandLonghands(name);
101
+ function propertyGroup(name, expanded) {
102
+ const longhands = shorthandLonghands(name) ?? (expanded && [...new Set(expanded.map(physicalName))]);
92
103
  return longhands
93
104
  ? { shorthand: name, longhands: [...longhands] }
94
105
  : { longhands: [physicalName(name)] };
95
106
  }
107
+ /**
108
+ * The `--why` answer with the rules that would set the property under a
109
+ * condition that does not apply now (on its first entry).
110
+ *
111
+ * @param whys - `--why` entries
112
+ * @param input - Page-side findings
113
+ * @returns Entries
114
+ */
115
+ function withInactive(whys, input) {
116
+ const [first, ...rest] = whys;
117
+ if (!first || !input.whyInactive?.length)
118
+ return whys;
119
+ return [{ ...first, inactive: input.whyInactive }, ...rest];
120
+ }
96
121
  /**
97
122
  * Hints: declarations with no effect, and var() of unset custom properties;
98
123
  * none without the computed style to check them against.
99
124
  *
100
125
  * @param cascade - Resolved properties
101
- * @param input - Styles and the label function
102
- * @returns Hints
126
+ * @param input - Styles
127
+ * @returns Hints with their declarations
103
128
  */
104
- function buildHints(cascade, input) {
129
+ function collectHints(cascade, input) {
105
130
  if (input.style['display'] === undefined)
106
131
  return [];
132
+ const declaredDisplay = cascade.get('display')?.winner?.value;
107
133
  const ctx = {
108
134
  style: input.style,
109
135
  parentStyle: input.parentStyle,
136
+ declaredDisplay: declaredDisplay === input.style['display'] ? undefined : declaredDisplay,
110
137
  replaced: input.replaced,
111
138
  formControl: input.formControl === true,
112
139
  };
@@ -114,14 +141,26 @@ function buildHints(cascade, input) {
114
141
  ...inactiveHints(cascade, ctx),
115
142
  ...undefinedVariableHints(cascade, input.style),
116
143
  ...formControlFontHints(cascade, ctx),
117
- ].map((hint) => ({
144
+ ];
145
+ }
146
+ /**
147
+ * A hint as the result shows it, with where its declaration is.
148
+ *
149
+ * @param hint - Hint
150
+ * @param input - The label function
151
+ * @returns Result hint
152
+ */
153
+ function toInspectHint(hint, input) {
154
+ return {
118
155
  kind: hint.kind,
119
156
  property: hint.property,
120
157
  value: hint.value,
121
158
  reason: hint.reason,
122
159
  fix: hint.fix,
160
+ ...(hint.only && { only: hint.only }),
161
+ ...(hint.variables && { variables: hint.variables }),
123
162
  source: input.label(hint.declaration),
124
- }));
163
+ };
125
164
  }
126
165
  /**
127
166
  * `--rules`: one row per property set by an author declaration; one row for
@@ -139,16 +178,22 @@ function buildRules(groups, cascade, input) {
139
178
  const rows = groups.flatMap((group) => {
140
179
  const resolutions = group.longhands.map((longhand) => cascade.get(longhand));
141
180
  const winners = resolutions.map((resolution) => resolution?.winner);
142
- const grouped = group.shorthand !== undefined ? groupedRow(group.shorthand, winners) : undefined;
143
- if (grouped)
144
- return [toRule(grouped.property, grouped, resolutions[0], input)];
181
+ const grouped = group.shorthand !== undefined ? groupedRow(winners) : undefined;
182
+ if (grouped) {
183
+ const longhands = shorthandLonghands(grouped.property) ?? group.longhands;
184
+ const partly = longhands.some((longhand) => {
185
+ const owner = cascade.get(longhand)?.winner;
186
+ return (owner !== undefined && !(sameSource(owner, grouped) && owner.via === grouped.property));
187
+ });
188
+ return [toRule(grouped.property, grouped, resolutions[0], input, longhands, partly)];
189
+ }
145
190
  return group.longhands.flatMap((longhand, i) => {
146
191
  const winner = winners[i];
147
- if (!winner || winner.source.origin === 'user-agent')
192
+ if (!winner || (winner.source.origin === 'user-agent' && !input.props))
148
193
  return [];
149
194
  const unexpanded = winner.via !== undefined && winner.value === winner.written;
150
195
  return unexpanded && winner.via
151
- ? [toRule(winner.via, winner, resolutions[i], input)]
196
+ ? [toRule(winner.via, winner, resolutions[i], input, [longhand])]
152
197
  : [toRule(longhand, winner, resolutions[i], input)];
153
198
  });
154
199
  });
@@ -163,28 +208,20 @@ function buildRules(groups, cascade, input) {
163
208
  }
164
209
  /**
165
210
  * One row for a group of longhands that a single author declaration sets:
166
- * the shorthand as written, or (set by a wider shorthand, like `border`
167
- * for `border-width`) the values it gives the sides; a wider shorthand
168
- * Chrome could not expand (`var()`) as written.
211
+ * that declaration as written (`padding: 4px 8px`; a wider shorthand like
212
+ * `border: none` once, not once per `border-width`/`-style`/`-color` group).
169
213
  *
170
- * @param shorthand - The group's shorthand
171
214
  * @param winners - Winning declaration of each longhand
172
215
  * @returns The row's declaration, or undefined when the sides differ in source
173
216
  */
174
- function groupedRow(shorthand, winners) {
217
+ function groupedRow(winners) {
175
218
  const [first] = winners;
176
219
  if (!first?.via || first.source.origin === 'user-agent')
177
220
  return undefined;
178
221
  const same = winners.every((w) => w && w.via === first.via && sameSource(w, first));
179
222
  if (!same)
180
223
  return undefined;
181
- const written = first.written ?? first.value;
182
- if (first.via === shorthand || first.value === written) {
183
- return { ...first, property: first.via, value: written };
184
- }
185
- const values = winners.map((w) => w?.value ?? '');
186
- const value = values.every((v) => v === first.value) ? first.value : values.join(' ');
187
- return { ...first, property: shorthand, value };
224
+ return { ...first, property: first.via, value: first.written ?? first.value };
188
225
  }
189
226
  /**
190
227
  * Whether two declarations come from the same place.
@@ -200,6 +237,26 @@ function sameSource(a, b) {
200
237
  a.source.line === b.source.line &&
201
238
  a.source.column === b.source.column);
202
239
  }
240
+ /**
241
+ * The computed value of a row shown as a shorthand or logical property,
242
+ * from its longhands: as `--all` writes it (`padding 4 8`), else the
243
+ * longhands' values with runs of equal ones written once (`border` →
244
+ * `2px solid red`).
245
+ *
246
+ * @param property - Shorthand or logical property
247
+ * @param longhands - Its longhands
248
+ * @param style - Computed longhands
249
+ * @returns Value, or undefined when no longhand has one
250
+ */
251
+ function shorthandValue(property, longhands, style) {
252
+ const collapsed = collapsedValue(property, style);
253
+ if (collapsed !== undefined)
254
+ return collapsed;
255
+ const values = longhands.map((longhand) => style[longhand]);
256
+ if (values.some((value) => value === undefined))
257
+ return undefined;
258
+ return values.filter((value, i) => i === 0 || value !== values[i - 1]).join(' ');
259
+ }
203
260
  /**
204
261
  * A `--rules` row.
205
262
  *
@@ -207,21 +264,38 @@ function sameSource(a, b) {
207
264
  * @param winner - Winning declaration
208
265
  * @param resolution - Its resolution (for what it overrides)
209
266
  * @param input - Computed style and the label function
267
+ * @param longhands - The longhands the row stands for (a shorthand's value comes from them)
268
+ * @param partly - Other declarations win some of its longhands: the value shown is
269
+ * the declaration's own, `var()` substituted, not the computed one
210
270
  * @returns Rule
211
271
  */
212
- function toRule(property, winner, resolution, input) {
213
- const computed = input.style[property];
272
+ function toRule(property, winner, resolution, input, longhands = [property], partly = false) {
273
+ const substituted = substituteVariables(winner.value, input.style);
274
+ const computed = partly
275
+ ? substituted.includes('var(')
276
+ ? undefined
277
+ : substituted
278
+ : (input.style[property] ?? shorthandValue(property, longhands, input.style));
214
279
  const overrides = [
215
280
  ...new Set((resolution?.overridden ?? [])
216
281
  .filter((d) => d.source.origin !== 'user-agent')
217
- .map((d) => d.source.selector ?? d.source.kind)),
282
+ .map((d) => d.source.selector !== undefined && d.source.selector === winner.source.selector
283
+ ? input.label(d)
284
+ : (d.source.selector ?? d.source.kind))),
218
285
  ];
219
286
  return {
220
287
  property,
221
288
  value: winner.value,
222
289
  ...(winner.value.includes('var(') &&
223
- computed !== undefined && { computed: normalizeProperty(property, computed) }),
290
+ computed !== undefined && {
291
+ computed: `${normalizeProperty(property, computed)}${partly ? ' (partly overridden)' : ''}`,
292
+ }),
293
+ ...(property === 'display' &&
294
+ computed !== undefined &&
295
+ computed !== winner.value &&
296
+ /^(inline|table-|ruby)/.test(winner.value) && { computed: `${computed} (blockified)` }),
224
297
  source: input.label(winner),
298
+ ...ruleField(winner),
225
299
  ...(overrides.length > 0 && { overrides }),
226
300
  ...(winner.ancestor !== undefined && { inherited: winner.ancestor }),
227
301
  ...(winner.important && { important: true }),