All files / packages/sds-customization-compliance/src/audit categorize.ts

100% Statements 30/30
95.23% Branches 20/21
100% Functions 2/2
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89                            9x                                 9x               9x                                 949x 949x 949x 1852x 1852x   76x 76x 76x 54x   22x 18x                 372x 372x 949x 949x 949x   372x 4464x 372x 372x    
/**
 * Hook category taxonomy.
 *
 * Theme-layer hooks follow the
 * `--<ns>-<scope>-<component>-<element>-<category>-<property>` naming convention
 * from the Theme Layer property-rules RFC. The *category* segment (color,
 * spacing, sizing, …) determines how a hook participates in the theme-data
 * projection: within a selector, reads are grouped into buckets so downstream
 * consumers (compliance checks, the Theme Layer panel, docs) can show them
 * in a stable, category-first order instead of raw source order.
 *
 * This module is pure and has no Node dependencies — it's safe to import
 * from both the CLI and the browser audit path.
 */
export const CATEGORIES = [
  'color',
  'spacing',
  'margin',
  'sizing',
  'radius',
  'font',
  'shadow',
  'image',
  'position',
  'gap',
  'opacity',
  'display',
] as const;
 
export type HookCategory = (typeof CATEGORIES)[number] | 'other';
 
const CATEGORY_SET = new Set<string>(CATEGORIES);
 
/**
 * Sizing-shorthand suffixes that resolve to the `sizing` category even
 * though the hook name doesn't include the literal `sizing` token.
 * Mirrors `packages/design-system-2/scripts/buildCss/buildThemeData.js`
 * so the projection and the on-disk JSON agree.
 */
const SIZING_SHORTHAND_SUFFIXES = new Set(['size', 'height', 'width']);
 
/**
 * Classify a hook name into one of the canonical categories. First scans
 * for an explicit category token (`color`, `spacing`, `sizing`, etc.); if
 * none is found, recognizes a few sizing shorthands (`-size`, `-height`,
 * `-width`) so element-sizing hooks like `--slds-c-button-icon-size` land
 * in `sizing` rather than `other`. The `line-height` compound is a font
 * property, not sizing, so the shorthand match skips a `-height` whose
 * preceding token is `line`.
 *
 * Note: the loop intentionally starts at index 1 — `parts[0]` is the
 * component name (e.g. `alert` in `--slds-c-alert-color-background`) and
 * shouldn't be treated as a category even if a component is ever named
 * `color`.
 */
export function categorizeHook(hook: string): HookCategory {
  const body = hook.startsWith('--slds-c-') ? hook.slice('--slds-c-'.length) : hook;
  const parts = body.split('-');
  for (let i = 1; i < parts.length; i++) {
    const seg = parts[i];
    if (CATEGORY_SET.has(seg)) return seg as HookCategory;
  }
  const tail = parts.at(-1);
  const prev = parts.at(-2);
  if (tail && SIZING_SHORTHAND_SUFFIXES.has(tail) && !(tail === 'height' && prev === 'line')) {
    return 'sizing';
  }
  if (tail === 'height' && prev === 'line') return 'font';
  return 'other';
}
 
/**
 * Group a list of hook names into `{ [category]: hook[] }`, preserving source
 * order within each bucket and emitting categories in {@link CATEGORIES}
 * order (with `other` last when present).
 */
export function groupHooksByCategory(hooks: readonly string[]): Record<string, string[]> {
  const buckets: Record<string, string[]> = {};
  for (const hook of hooks) {
    const cat = categorizeHook(hook);
    if (!buckets[cat]) buckets[cat] = [];
    buckets[cat].push(hook);
  }
  const ordered: Record<string, string[]> = {};
  for (const cat of CATEGORIES) if (buckets[cat]) ordered[cat] = buckets[cat];
  if (buckets.other) ordered.other = buckets.other;
  return ordered;
}