All files / packages/design-system/scripts/gulp/plugins css-variable-value.js

0% Statements 0/41
0% Branches 0/31
0% Functions 0/5
0% Lines 0/32

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                                                                                                                                                                                                               
// Copyright (c) 2015-present, salesforce.com, inc. All rights reserved
// Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license
 
/**
 * Local, AST-based replacement for `@salesforce-ux/postcss-css-variable-value`.
 *
 * The published plugin resolves `var(--name, <fallback>)` down to its fallback
 * using a REGEX, which mangles three real cases in the SLDS legacy CSS:
 *   1. digit-containing names  — `var(--foo-10-opacity-10, rgba(0,0,0,.16))`
 *      became `10-opacity-10, rgba(0,0,0,.16)`
 *   2. nested var() in gradients — dropped the outer structure, producing
 *      unbalanced parens (`rgba(0,0,0,.025)) 25%, transparent`)
 *   3. two var() in one value  — `var(--a,4px) var(--b,8px)` became `4px b, 8px`
 *
 * That malformed output was tolerated by autoprefixer@9 / cssnano@5's lenient
 * value parsers, but crashes the stricter parsers in autoprefixer@10 and the
 * cssnano-family@8 plugins. This implementation walks the postcss-value-parser
 * AST instead, so every case resolves to a spec-valid string.
 *
 * Resolution semantics (matched to the original for unchanged cases):
 *   - `var(--x)` with no fallback is left untouched.
 *   - `var(--x, <fallback>)` is replaced by `<fallback>`.
 *   - Nested `var()` inside a fallback is resolved recursively (drilling to the
 *     innermost fallback), matching the original's behaviour.
 */
 
const valueParser = require('postcss-value-parser');
 
/**
 * Given the argument nodes of a `var(...)` function, return the fallback nodes
 * (everything after the first top-level comma `div`), or null when there is no
 * fallback. postcss-value-parser represents the comma between `--name` and the
 * fallback as a `div` node with `value === ','`.
 */
function getFallbackNodes(fnNodes) {
  const firstCommaIndex = fnNodes.findIndex(
    (n) => n.type === 'div' && n.value === ',',
  );
  if (firstCommaIndex === -1) return null;
 
  // Everything after the first comma is the fallback. Skip the comma node
  // itself and any leading whitespace the parser attached to it.
  const fallback = fnNodes.slice(firstCommaIndex + 1);
  while (fallback.length && fallback[0].type === 'space') fallback.shift();
  return fallback.length ? fallback : null;
}
 
/**
 * Recursively resolve every `var(...)` function within a list of value nodes,
 * mutating them in place so a `var()` becomes its (further-resolved) fallback.
 */
function resolveNodes(nodes) {
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (node.type !== 'function') continue;
 
    // Resolve inner functions first (handles var() nested inside gradients).
    if (node.nodes && node.nodes.length) resolveNodes(node.nodes);
 
    if (node.value !== 'var') continue;
 
    const fallback = getFallbackNodes(node.nodes);
    if (!fallback) continue; // `var(--x)` with no fallback — leave as-is.
 
    // A single-function fallback (e.g. the whole thing is `linear-gradient(...)`
    // or another `var(...)`) replaces the var node directly, preserving its
    // function-ness so stringify emits balanced parens.
    if (fallback.length === 1 && fallback[0].type === 'function') {
      nodes[i] = fallback[0];
      i--; // re-visit in case the replacement is itself a var()
      continue;
    }
 
    // Otherwise splice the fallback nodes in where the var() was.
    nodes.splice(i, 1, ...fallback);
    i--; // re-visit the first spliced node (it may be a var()/function)
  }
}
 
module.exports = (opts = {}) => {
  const options = { preserve: true, ...opts };
 
  return {
    postcssPlugin: 'sds-css-variable-value',
    Declaration(decl) {
      // Cheap guard: only touch declarations that actually contain a var().
      if (!decl.value || decl.value.indexOf('var(') === -1) return;
 
      const parsed = valueParser(decl.value);
      resolveNodes(parsed.nodes);
      const resolved = parsed.toString();
 
      if (resolved === decl.value) return; // nothing resolved (e.g. only var() w/o fallback)
 
      if (options.preserve) {
        decl.cloneBefore({ value: resolved });
      } else {
        decl.value = resolved;
      }
    },
  };
};
module.exports.postcss = true;