All files / packages/sds-stylelint-config/src/metadata metadata.ts

100% Statements 21/21
73.91% Branches 17/23
100% Functions 6/6
100% Lines 21/21

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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204                        5x                                                                                                                                                                                                                                                               66x 2671x 1043x       65x 65x   65x                 65x             5x     8x 8x 8x 8x                   60x   60x 6x         60x 2x           60x 60x     5x      
/**
 * Canonical metadata for SLDS custom property naming conventions.
 *
 * Owns component hook and shared token vocabularies, the merge logic for consumer
 * options, and the legacy squash-case alias builder.
 *
 * Global per-category allowlists are generated from the design-tokens
 * package and live in src/validators/global/__generated-allowlists.ts.
 */
 
import type { Metadata, RuleOptions } from '../types.js';
 
const metadata: Metadata = {
  scopes: ['g', 'r', 's', 'c'],
 
  global: {
    valid: {
      category: ['color', 'font', 'radius', 'shadow', 'sizing', 'spacing', 'ratio', 'duration'],
    },
  },
 
  reference: {
    valid: {
      category: ['color'],
      role: ['brand', 'error', 'success'],
    },
  },
 
  shared: {
    valid: {},
  },
 
  component: {
    valid: {
      // Categories follow RFC 1157 §Property Rules vocabulary plus
      // provisional natural-growth categories observed in
      // design-system-2 source today (`content`, `outline`, `gap`,
      // `transform`, `position`, `opacity`). New categories are added
      // to this list as components surface them; the list is curated,
      // not closed.
      category: [
        'color',
        'font',
        'radius',
        'shadow',
        'sizing',
        // `size` covers RFC 1157's square / uniform sizing form
        // (`--slds-c-icon-size`). Also appears in `property` for the
        // property-position case (`font-size`); validator's first-match
        // behavior keeps the two senses unambiguous.
        'size',
        'spacing',
        'margin',
        'gap',
        'transform',
        'position',
        'opacity',
        'display',
        // NOTE: `content`, `outline` are also natural CSS-property
        // categories used in the wild but conflict with element-segment
        // uses (e.g. `carousel-content-spacing-block`,
        // `tabs-item-color-outline`). Holding them out of the allowlist
        // until the validator's category resolution is smarter than
        // first-match.
      ],
      property: [
        'background',
        'foreground',
        'border',
        // `size` is the canonical square / uniform sizing form
        // (`--slds-c-icon-size`); per-axis is `sizing-{width|height}`
        // which decomposes into category=`sizing`, property=`width|height`.
        'size',
        'weight',
        'line-height',
        'width',
        'height',
        // Spacing/margin direction + axis properties
        'block',
        'block-start',
        'block-end',
        'inline',
        'inline-start',
        'inline-end',
      ],
      attribute: [
        'inline',
        'inline-start',
        'inline-end',
        'block',
        'block-start',
        'block-end',
        'start',
        'end',
        'gap',
        'max',
        'min',
        // RFC 1157 conditional suffix for sibling-margin contexts
        // (`--slds-c-badge-margin-inline-start-adjacent`).
        'adjacent',
      ],
      // RFC 1157 §State Hooks (line 691): only the four sanctioned
      // state suffixes. Other CSS pseudo-classes (`:visited`,
      // `:focus-visible`, `:checked`) and ARIA-driven states
      // (`[aria-selected]`, `[aria-pressed]`) are expressed at the
      // selector level in the theme file, not as hook suffixes. The
      // legacy `state` / `pseudoState` split predated RFC 1157; both
      // slots now carry the same four tokens for back-compat with
      // `RuleOptions.valid` consumers.
      state: ['hover', 'focus', 'active', 'disabled'],
      pseudoState: ['hover', 'focus', 'active', 'disabled'],
      // RFC 1157: `color` always pairs with a category-property
      // (`-background`, `-foreground`, `-border`). Standing-alone
      // `--slds-c-button-color` is not canonical. Other categories may
      // stand alone (`shadow`, `size`, `display`, ...).
      categoriesRequiringProperty: ['color'],
      // RFC 1157 §State Hooks (line 689): state suffixes are permitted
      // only on these four categories, where per-state customer
      // customization has documented demand. Spacing, sizing, radius,
      // font, etc. reject state suffixes — per-state overrides for
      // those categories are expressed at the selector level in the
      // theme file, not as a dedicated hook.
      categoriesAllowingState: ['color', 'shadow'],
    },
  },
 
  deprecated: {},
  privateSyntax: ['_'],
  legacyPropertyAliases: {},
};
 
// ---------------------------------------------------------------------------
// Legacy squash-case alias support
// ---------------------------------------------------------------------------
 
/**
 * Build a lookup from squash-case to canonical hyphenated form.
 * Remove when squash-case syntax is fully retired.
 */
export function buildLegacyAliases(list: string[]): Record<string, string> {
  return list
    .filter((v) => v.includes('-'))
    .reduce<Record<string, string>>((acc, v) => ({ ...acc, [v.replaceAll('-', '')]: v }), {});
}
 
function computeLegacyAliases(meta: Metadata): Record<string, string> {
  const properties = [...(meta.global.valid?.category || []), ...(meta.component.valid?.property || [])];
  const attributes = [...(meta.component.valid?.attribute || [])];
  // Include property hyphenated forms for global descriptors too
  const globalProps = [
    'line-height',
    'inset-inverse',
    'z-index',
    'inline-start',
    'inline-end',
    'block-start',
    'block-end',
  ];
  return { ...buildLegacyAliases([...properties, ...attributes, ...globalProps]) };
}
 
// ---------------------------------------------------------------------------
// Metadata merging
// ---------------------------------------------------------------------------
 
const GROUP_ALIASES: Record<string, string> = { psuedoState: 'pseudoState' };
 
function mergeValidGroups(target: Record<string, string[]>, overrides: Record<string, string[]>): void {
  for (const [group, values] of Object.entries(overrides)) {
    const key = GROUP_ALIASES[group] ?? group;
    Eif (Object.hasOwn(target, key)) {
      target[key] = [...new Set([...(target[key] || []), ...(values || [])])];
    }
  }
}
 
/**
 * Returns a merged copy of metadata with user options applied.
 * Does not mutate the base metadata.
 */
export function getMergedMetadata(base: Metadata, options: RuleOptions = {}): Metadata {
  const clone: Metadata = structuredClone(base);
 
  if (options.valid && clone.component?.valid) {
    mergeValidGroups(
      clone.component.valid as unknown as Record<string, string[]>,
      options.valid as unknown as Record<string, string[]>,
    );
  }
  if (options.global?.valid && clone.global?.valid) {
    mergeValidGroups(
      clone.global.valid as unknown as Record<string, string[]>,
      options.global.valid as unknown as Record<string, string[]>,
    );
  }
 
  clone.legacyPropertyAliases = computeLegacyAliases(clone);
  return clone;
}
 
metadata.legacyPropertyAliases = computeLegacyAliases(metadata);
 
export default metadata;