/*

Functions


*/

// The following vars need to be set // here, before the rest of the system // variables are set

$root-font-size: if($theme-respect-user-font-size, 100%, $theme-root-font-size);

$root-font-size-equiv: if(

$theme-respect-user-font-size,
16px,
$theme-root-font-size

);

/*

General-purpose functions


*/

/*


uswds-error()


Allow the system to pass an error as text to test error states in unit testing


*/

$_error-output-override: false !default; @function uswds-error($message, $override: $_error-output-override) {

@if $override {
  @return "Error: #{$message}";
}

@error "#{$message}";

}

/*


error-not-token()


Returns a common not-a-token error.


*/

@function error-not-token($token, $type, $valid-token-map: false) {

$valid-token-message: if(
  $valid-token-map,
  " Valid tokens: #{map-keys($valid-token-map)}",
  ""
);
@return uswds-error(
  "'#{$token}' is not a valid USWDS #{$type} token.#{$valid-token-message}"
);

}

/*


map-deep-get()


@author Hugo Giraudel @access public @param {Map} $map - Map @param {Arglist} $keys - Key chain @return {*} - Desired value


*/

@function map-deep-get($map, $keys…) {

@each $key in $keys {
  $map: map-get($map, $key);
}

@return $map;

}

/*


strip-unit()


Remove the unit of a length @author Hugo Giraudel @param {Number} $number - Number to remove unit from @return {Number} - Unitless number


*/

@function strip-unit($number) {

@if type-of($number) == "number" and not unitless($number) {
  @return $number / ($number * 0 + 1);
}

@return $number;

}

/*


multi-cat()


Concatenate two lists


*/

@function multi-cat($list1, $list2) {

$this-list: ();

@each $e in $list1 {
  @each $ee in $list2 {
    $this-block: $e + $ee;
    $this-list: join($this-list, $this-block);
  }
}

@return $this-list;

}

/*


map-collect()


Collect multiple maps into a single large map source: gist.github.com/bigglesrocks/d75091700f8f2be5abfe


*/

@function map-collect($maps…) {

$collection: ();

@each $map in $maps {
  $collection: map-merge($collection, $map);
}

@return $collection;

}

/*


smart-quote()


Quotes strings Inspects `px`, `xs`, and `xl` numbers Leaves bools as is


*/

@function smart-quote($value) {

@if type-of($value) == "string" {
  @return quote($value);
}

@if type-of($value) == "number" and index(("px", "xl", "xs"), unit($value)) {
  @return inspect($value);
}

@if type-of($value) == "color" {
  @error 'Only use quoted color tokens in USWDS functions and mixins. '
    + 'See designsystem.digital.gov/design-tokens/color '
    + 'for more information.';
}

@return $value;

}

/*


remove()


Remove a value from a list


*/

@function remove($list, $value, $recursive: false) {

$result: ();

@for $i from 1 through length($list) {
  @if type-of(nth($list, $i)) == list and $recursive {
    $result: append($result, remove(nth($list, $i), $value, $recursive));
  } @else if nth($list, $i) != $value {
    $result: append($result, nth($list, $i));
  }
}

@return $result;

}

/*


strunquote()


Unquote a string


*/

@function strunquote($value) {

@if type-of($value) == "string" {
  $value: unquote($value);
}

@return $value;

}

/*


to-map()


Convert a single value to a USWDS value map.

Candidate for deprecation if we remove isReadable


*/

@function to-map($key, $values) {

$l: length($values);

@if $key == "noModifier" or $key == "noValue" {
  $key: "";
}

@return (slug: $key, content: $values);

}

/*


base-to-map()


Convert a single base to a USWDS value map.

Candidate for deprecation if we remove isReadable


*/

@function base-to-map($values) {

$l: length($values);

@if $l == 1 or nth($values, $l) != isReadable {
  @return (slug: $values, isReadable: true);
} @else {
  $values: remove($values, isReadable);

  @return (slug: unquote(nth($values, 1)), isReadable: true);
}

}

/*


ns()


Add a namesspace of $type if that namespace is set to output


*/

@function ns($type) {

$type: smart-quote($type);

@if not map-deep-get($theme-namespace, $type, output) {
  @return "";
}

@return map-deep-get($theme-namespace, $type, namespace);

}

/*


de-list()


Transform a one-element list or arglist into that single element.


(1) => 1 ((1)) => (1)


*/

@function de-list($value) {

$types: ("list", "arglist");

@if not index($types, type-of($value)) {
  @return $value;
}

$output: if(length($value) == 1, nth($value, 1), $value);

@return $output;

}

/*


unpack()


Create lists of single items from lists of lists.


(1, (2.1, 2.2), 3) –> (1, 2.1, 2.2, 3)


*/

@function unpack($value) {

$output: ();

@if length($value) == 0 {
  @return $value;
}

@each $i in $value {
  @if type-of($i) == "list" {
    @each $ii in $i {
      $output: append($output, $ii, comma);
    }
  } @else {
    $output: append($output, $i, comma);
  }
}

@return de-list($output);

}

/*


get-last()


Return the last item of a list, Return null if the value is null


*/

@function get-last($props) {

$length: length($props);
$last: if($length == 0, null, nth($props, -1));

@return $last;

}

/*


has-important()


Check to see if `!important` is being passed in a mixin's props


*/

@function has-important($props) {

$props: de-list($props);

@if get-last($props) == "!important" {
  @return true;
}

@return false;

}

/*


append-important()


Append `!important` to a list


*/

@function append-important($source, $destination) {

@if get-last($source) == "!important" {
  @return append($destination, !important, comma);
}

@return $destination;

}

/*


spacing-multiple()


Converts a spacing unit multiple into the desired final units (currently rem)


*/

@function spacing-multiple($unit) {

$grid-to-rem: ($system-spacing-grid-base * $unit) / $root-font-size-equiv *
  1rem;

@return $grid-to-rem;

}

/*


rem-to-px()


Converts a value in rem to a value in px


*/

@function rem-to-px($value-in-rem) {

@if unit($value-in-rem) == "rem" {
  $rem-to-px: ($value-in-rem / 1rem) * $root-font-size-equiv;
  @return $rem-to-px;
}
@if unit($value-in-rem) != "px" {
  @error 'This value must be in either px or rem';
}
@return $value-in-rem;

}

/*


rem-to-user-em()


Converts a value in rem to a value in

user-settings

em for use in media

queries


*/

@function rem-to-user-em($grid-in-rem) {

$rem-to-user-em: ($grid-in-rem / 1rem) * 1em;

@return $rem-to-user-em;

}

/*


validate-typeface-token()


Check to see if a typeface-token exists. Throw an error if a passed token does not exist in the typeface-token map.


*/

@function validate-typeface-token($typeface-token) {

@if not map-has-key($all-typeface-tokens, $typeface-token) {
  @return error-not-token($typeface-token, "typeface", $all-typeface-tokens);
}

@return $typeface-token;

}

/*


cap-height()


Get the cap height of a valid typeface


*/

@function cap-height($typeface-token) {

@if not $typeface-token {
  @return false;
}

$typeface-token: validate-typeface-token($typeface-token);
$token-data: map-get($all-typeface-tokens, $typeface-token);
@return map-get($token-data, "cap-height");

}

/*


px-to-rem()


Converts a value in px to a value in rem


*/

@function px-to-rem($pixels) {

@if not $pixels {
  @return false;
}
$px-to-rem: ($pixels / $root-font-size-equiv) * 1rem;
$px-to-rem: round($px-to-rem * 100) / 100;

@return $px-to-rem;

}

/*


normalize-type-scale()


Normalizes a specific face's optical size to a set target


*/

@function normalize-type-scale($cap-height, $scale) {

@if not $cap-height {
  @return false;
}

$this-scale: $system-base-cap-height * strip-unit($scale) / $cap-height * 1px;

@return px-to-rem($this-scale);

}

/*


utility-font()


Get a normalized font-size in rem from a family and a type size in either system scale or project scale


Not the public-facing function. Used for building the utilities and withholds certain errors.


*/

@function utility-font($family, $scale) {

@if not map-has-key($project-cap-heights, $family) {
  @return error-not-token($family, "font family", $project-cap-heights);
}

$quote-scale: smart-quote($scale);

@if not map-get($all-type-scale, $quote-scale) {
  @return error-not-token($scale, "font scale", $all-type-scale);
}

$this-cap: map-get($project-cap-heights, $family);
$this-scale: map-get($all-type-scale, $quote-scale);

@if not $this-scale and $this-cap {
  @return false;
}

@return normalize-type-scale($this-cap, $this-scale);

}

/*


line-height() lh()


Get a normalized line-height from a family and a line-height scale unit


*/

@function lh($props…) {

$props: unpack($props);

@if not(length($props) == 2) {
  @error 'lh() needs both a valid face and line height token '
    + 'in the format `lh(FACE, HEIGHT)`.';
}

$family: smart-quote(nth($props, 1));
$scale: smart-quote(nth($props, 2));

@if not map-has-key($project-cap-heights, $family) {
  @return error-not-token($family, "font family", $project-cap-heights);
}

@if not map-get($system-line-height, $scale) {
  @return error-not-token($scale, "line-height", $system-line-height);
}

@if not map-get($project-cap-heights, $family) {
  @return false;
}

$this-cap: map-get($project-cap-heights, $family);
$this-line-height: map-get($system-line-height, $scale);
$normalized-line-height: $this-line-height /
  ($system-base-cap-height / $this-cap);
$normalized-line-height: round($normalized-line-height * 10) / 10;

@return $normalized-line-height;

}

@function line-height($props…) {

@return lh($props...);

}

/*


convert-to-font-type()


Converts a font-role token into a font-type token. Leaves font-type tokens unchanged.


*/

@function convert-to-font-type($token) {

@if map-has-key($project-font-role-tokens, $token) {
  @return map-get($project-font-role-tokens, $token);
}

@return $token;

}

/*


get-font-stack()


Get a font stack from a style- or role-based font token.


*/

@function get-font-stack($token) {

// Start by converting to a type token (sans, serif, etc)
$type-token: convert-to-font-type($token);
$output-display-name: true;
$this-stack: null;
// Get the font type metadata
$this-font-map: map-get($project-font-type-tokens, $type-token);
// Only output if the font type has an assigned typeface token
@if map-get($this-font-map, "typeface-token") {
  $this-font-token: map-get($this-font-map, "typeface-token");
  // Get the typeface metadata
  $this-typeface-data: map-get($all-typeface-tokens, $this-font-token);
  $this-name: map-get($this-typeface-data, "display-name");
  // If it's a system typeface, don't output the display name
  @if map-has-key($this-typeface-data, "system-font") {
    $output-display-name: false;
  }
  // If there's a custom stack, use it and output the display name
  @if map-get($this-font-map, "custom-stack") {
    $this-stack: map-get($this-font-map, "custom-stack");
    $output-display-name: true;
  }
  // Otherwise, just get the token's default stack
  @else {
    $this-stack: map-deep-get(
      $all-typeface-tokens,
      $this-font-token,
      "stack"
    );
  }
  // If the typeface has no display name (system fonts), don't output the display name
  @if map-get($this-typeface-data, "display-name") == null {
    $output-display-name: false;
  }
  @if not $output-display-name {
    @return #{$this-stack};
  }
  @return unquote("#{$this-name}, #{$this-stack}");
}
@return false;

}

/*


get-typeface-token()


Get a typeface token from a font-type or font-role token.


*/

@function get-typeface-token($font-token) {

$this-token: $font-token;
@if map-has-key($project-font-role-tokens, $font-token) {
  $this-token: map-get($project-font-role-tokens, $font-token);
}
@return map-deep-get(
  $project-font-type-tokens,
  $this-token,
  "typeface-token"
);

}

/*


get-system-color()


Derive a system color from its family, value, and vivid or a passed variable that is, itself, a list


*/

@function get-system-color(

$color-family: false,
$color-grade: false,
$color-variant: false

) {

// If the arg being passed to the fn
// is a variable defined as a list,
// $color-family will contain this
// entire list, and needs to be
// unpacked.
// ex:
//    in settings:
//      $theme-color-primary.'dark': 'blue', 70
//    in the theme colors map:
//      $color-primary-dark: get-system-color($theme-color-primary.'dark'),

@if type-of($color-family) == "list" {
  @if length($color-family) > 2 {
    $color-variant: nth($color-family, 3);
  }
  $color-grade: nth($color-family, 2);
  $color-family: nth($color-family, 1);
}

$color-family: smart-quote($color-family);
$color-variant: smart-quote($color-variant);

// If the arg being passed to the fn
// is false, it should output as `false`
// to preserve a false value in the
// target map
// ex:
//    in settings:
//      $theme-color-primary.'darkest': false;
//    in the theme colors map:
//      'darkest': get-system-color($theme-color-primary.'darkest'),
//      'darkest': false, // is the desired outcome
// TODO: should a false-pass color function be a separate fn?

@if not $color-family {
  @return false;
}

@if $color-variant {
  $output: map-deep-get(
    $system-colors,
    $color-family,
    $color-variant,
    $color-grade
  );

  @return $output;
}

$output: map-deep-get($system-colors, $color-family, $color-grade);

@return $output;

}

/*


system-type-scale()


Get a value from the system type scale


*/

@function system-type-scale($scale) {

$scale: smart-quote($scale);

@if not $scale {
  @return false;
}

@if not map-has-key($system-type-scale, $scale) {
  @return error-not-token($scale, "type scale", $system-type-scale);
}

@return map-get($system-type-scale, $scale);

}

/*


calc-gap-offset()


Calculate a valid uswds unit that is half the width of a given unit, for calculating gap offset in the layout grid.


*/

@function calc-gap-offset($gap-size) {

$gap-size: smart-quote($gap-size);

@if not map-has-key($spacing-to-value, $gap-size) {
  @return error-not-token($gap-size, "gap size");
}

$numeric-eq: map-get($spacing-to-value, $gap-size);
$numeric-eq-half: inspect($numeric-eq / 2);

@if not map-has-key($spacing-to-token, $numeric-eq-half) {
  @error '`#{$gap-size}` is not a valid USWDS gap size token. '
    + 'Column gaps need to have a standard size half their width.';
}

@return map-get($spacing-to-token, $numeric-eq-half);

}

/*


get-standard-values()


Gets a map of USWDS standard values for a property


*/

@function get-standard-values($property) {

@return map-deep-get($system-properties, $property, standard);

}

/*


number-to-token()


Converts an integer or numeric value into a system value

Ex: 0.5 –> '05'

-1px  --> 'neg-1px'

*/

@function number-to-token($number) {

$number: inspect($number);

@if not map-has-key($number-to-value, $number) {
  @return false;
}

@return map-get($number-to-value, $number);

}

/*


columns()


outputs a grid-col number based on the number of desired columns in the 12-column grid

Ex: columns(2) –> 6

grid-col(columns(2))

*/

@function columns($number) {

$options: "auto", "fill";
$number: smart-quote($number);

@if index($options, $number) {
  @return $number;
}
@if 12 % $number != 0 {
  @error '`#{$number}` must be a divisor of 12.';
}
$columns: 12 / $number;
@return $columns;

}

/*


get-uswds-value()


Finds and outputs a value from the USWDS standard values.

Used to build other standard utility functions and mixins.


*/

@function get-uswds-value($property, $value…) {

@if type-of($value) == "arglist" and nth($value, 1) == override {
  @return nth($value, 2);
}

$value: nth($value, 1);
$converted: number-to-token($value);
$quoted-value: if(
  $converted,
  smart-quote($converted),
  smart-quote(nth($value, 1))
);
$our-standard-values: map-deep-get($system-properties, $property, standard);
$our-extended-values: map-deep-get($system-properties, $property, extended);

@if map-has-key($our-standard-values, $quoted-value) {
  $output: map-get($our-standard-values, $quoted-value);

  @if not $output {
    @if $theme-show-compile-warnings {
      @error '`#{$value}` is set as a `false` value '
        + 'for the #{$property} property in your project settings '
        + 'and will not output properly. '
        + 'Set the value of `#{$value}` in project settings.';
    }
  }

  @return $output;
}

@if map-has-key($our-extended-values, $quoted-value) {
  @if $theme-show-compile-warnings {
    @warn '`#{$value}` is an extended USWDS `#{$property}` token. '
      + 'This is OK, but only components built with standard tokens can be accepted back into the system. '
      + 'Standard `#{$property}` values: #{map-keys($our-standard-values)}';
  }

  @return map-get($our-extended-values, $quoted-value);
}

// TODO: what are these last two cases? Evaluate.
@if not(type-of($value) == "number" and not unitless($value)) {
  @return error-not-token($value, $property, $our-standard-values);
}

@if $theme-show-compile-warnings {
  @warn '`#{$value}` is not a USWDS `#{$property}` token. '
    + 'This is OK, but only components built with standard '
    + 'tokens can be accepted back into the system. '
    + 'Standard `#{$property}` values: #{map-keys($our-standard-values)}';
}

@return $value;

}

/*


pow()


Raises a unitless number to the power of another unitless number

Includes helper functions


*/

@function pow($number, $exponent) {

@if (round($exponent) != $exponent) {
  @return exp($exponent * ln($number));
}

$value: 1;

@if $exponent > 0 {
  @for $i from 1 through $exponent {
    $value: $value * $number;
  }
} @else if $exponent < 0 {
  @for $i from 1 through -$exponent {
    $value: $value / $number;
  }
}

@return $value;

}

@function factorial($value) {

$result: 1;

@if $value == 0 {
  @return $result;
}

@for $index from 1 through $value {
  $result: $result * $index;
}

@return $result;

}

@function summation($iteratee, $input, $initial: 0, $limit: 100) {

$sum: 0;

@for $index from $initial to $limit {
  $sum: $sum + call($iteratee, $input, $index);
}

@return $sum;

}

@function exp-maclaurin($x, $n) {

@return (pow($x, $n) / factorial($n));

}

@function exp($value) {

@return summation(get-function("exp-maclaurin"), $value, 0, 100);

}

@function ln-maclaurin($x, $n) {

@return (pow(-1, $n + 1) / $n) * (pow($x - 1, $n));

}

@function ln($value) {

$ten-exp: 1;
$ln-ten: 2.30258509;

@while ($value > pow(10, $ten-exp)) {
  $ten-exp: $ten-exp + 1;
}

@return summation(
    get-function("ln-maclaurin"),
    $value / pow(10, $ten-exp),
    1,
    100
  ) + $ten-exp * $ln-ten;

}

/// Returns the luminance of `$color` as a float (between 0 and 1) /// 1 is pure white, 0 is pure black /// @param {Color} $color - Color /// @return {Number} /// @link www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef Reference @function luminance($color) {

$colors: (
  "red": red($color),
  "green": green($color),
  "blue": blue($color),
);

@each $name, $value in $colors {
  $adjusted: 0;
  $value: $value / 256;

  @if $value < 0.03928 {
    $value: $value / 12.92;
  } @else {
    $value: ($value + 0.055) / 1.055;
    $value: pow($value, 2.4);
  }

  $colors: map-merge(
    $colors,
    (
      $name: $value,
    )
  );
}

$lum: (map-get($colors, "red") * 0.2126) +
  (map-get($colors, "green") * 0.7152) + (map-get($colors, "blue") * 0.0722);
$lum: round($lum * 1000) / 1000;

@return $lum;

}

/// Casts a string into a number /// /// @param {String | Number} $value - Value to be parsed /// /// @return {Number} /// @function to-number($value) {

@if type-of($value) == "number" {
  @return $value;
} @else if type-of($value) != "string" {
  $_: log("Value for `to-number` should be a number or a string.");
}

$result: 0;
$digits: 0;
$minus: str-slice($value, 1, 1) == "-";
$numbers: (
  "0": 0,
  "1": 1,
  "2": 2,
  "3": 3,
  "4": 4,
  "5": 5,
  "6": 6,
  "7": 7,
  "8": 8,
  "9": 9,
);

@for $i from if($minus, 2, 1) through str-length($value) {
  $character: str-slice($value, $i, $i);

  @if not(index(map-keys($numbers), $character) or $character == ".") {
    @return to-length(if($minus, -$result, $result), str-slice($value, $i));
  }

  @if $character == "." {
    $digits: 1;
  } @else if $digits == 0 {
    $result: $result * 10 + map-get($numbers, $character);
  } @else {
    $digits: $digits * 10;
    $result: $result + map-get($numbers, $character) / $digits;
  }
}

@return if($minus, -$result, $result);

}

/*


decompose-color-token()


Convert a color token into into a list of form [family], [grade], [variant]

Vivid variants return “vivid” as the variant.

If neither grade nor variant exists, returns 'false'


*/

@function decompose-color-token($token) {

$separator: "-";
$family: false;
$grade: false;
$variant: false;
$exceptions: (
  "black": 100,
  "white": 0,
);
$token: if($token == "ink", "base-darkest", $token);
// If there's no separator, set family and grade
@if not str-index($token, $separator) {
  $family: $token;
  $grade: if(
    map-has-key($exceptions, $family),
    map-get($exceptions, $family),
    "root"
  );
} @else {
  $split: str-split($token, $separator);
  $last: nth($split, length($split));
  // If the last string is over 3 char, it's a theme token
  @if str-length($last) > 3 {
    @if $last == "vivid" {
      $variant: "vivid";
      $grade: "root";
    } @else if $last == "warm" or $last == "cool" {
      $grade: "root";
    } @else {
      $grade: $last;
    }
    // Otherwise treat as system token
  } @else {
    // Determine if it's a vivid variant
    @if str-index($last, "v") {
      $variant: "vivid";
      $grade: str-slice($last, 1, (str-index($last, "v") - 1));
    } @else {
      $grade: $last;
    }
    // Make sure the grade is a number
    $grade: if(type-of($grade) == "string", to-number($grade), $grade);
  }
  // Collect compound-word families
  $is-compound-family: false;
  @if length($split) == 3 or index($split, "warm") or index($split, "cool") {
    $is-compound-family: true;
  }
  @if $is-compound-family {
    $family: nth($split, 1) + $separator + nth($split, 2);
  } @else {
    $family: nth($split, 1);
  }
}
@return $family, $grade, $variant;

}

/*


test-colors()


Check to see if all system colors fall between the proper relative luminance range for their grade.

Has a couple quirks, as the luminance() function returns slightly different results than expected.


*/

@function test-colors($map) {

$exceptions: "black", "white", "transparent", "black-transparent",
  "white-transparent";

@each $token, $value in $map {
  $family: nth(decompose-color-token($token), 1);
  $grade: nth(decompose-color-token($token), 2);
  @if not $value {
    // empty block
  } @else if not index($exceptions, $family) {
    $computed: calculate-grade($value);
    @debug "Checked #{$family}-#{$grade}";
    @if $grade <= 5 {
      // empty block
    } @else if $computed != $grade {
      @warn "#{$token} (#{$value}) lum: #{luminance($value)} is not in the range #{map-get($system-color-grades, $grade)}";
    }
  }
}

@return 1;

}

/*


str-split()


Split a string at a given separator and convert into a lisrt of substrings


*/

@function str-split($string, $separator) {

$split-arr: ();
$index: str-index($string, $separator);
@while $index != null {
  $item: str-slice($string, 1, $index - 1);
  $split-arr: append($split-arr, $item);
  $string: str-slice($string, $index + 1);
  $index: str-index($string, $separator);
}
$split-arr: append($split-arr, $string);

@return $split-arr;

}

/*


str-replace()


Replace any substring with another string


*/

@function str-replace($string, $search, $replace: “”) {

$index: str-index($string, $search);

@if $index {
  @return str-slice($string, 1, $index - 1) + $replace +
    str-replace(
      str-slice($string, $index + str-length($search)),
      $search,
      $replace
    );
}

@return $string;

}

/*


is-system-color-token()


Return whether a token is a system color token


*/

@function is-system-color-token($token) {

@if map-has-key($system-color-shortcodes, $token) {
  @return true;
}
@return false;

}

/*


is-theme-color-token()


Return whether a token is a theme color token


*/

@function is-theme-color-token($token) {

@if map-has-key($project-color-shortcodes, $token) {
  @return true;
}
@return false;

}

/*


color-token-assignment()


Get the system token equivalent of any theme color token


*/

@function color-token-assignment($color-token) {

@if is-system-color-token($color-token) {
  $system-token: $color-token;
  @return $system-token;
}

@if not is-theme-color-token($color-token) {
  @return error-not-token($color-token, "color");
}

$theme-token: $color-token;
$theme-token-assignment: map-get($assignments-theme-color, $theme-token);
@return $theme-token-assignment;

}

/*


is-color-token()


Returns whether a given string is a USWDS color token.


*/

@function is-color-token($token) {

$is-color-token: if(map-has-key($all-color-shortcodes, $token), true, false);
@return $is-color-token;

}

/*


calculate-grade()


Derive the grade equivalent any color, even non-token colors


*/

@function calculate-grade($color-token) {

$transparency-error: "USWDS can't calculate the grade of a transparent color. Avoid using transparency in theme colors and text.";
$grade: null;
$lum: null;
$custom-color: false;
$color-token-assignment: false;

// Determine if the color is a custom color
@if type-of($color-token) == "color" {
  $custom-color: $color-token;
} @else {
  $color-token-assignment: color-token-assignment($color-token);
  @if type-of($color-token-assignment) == "color" {
    $custom-color: color-token-assignment($color-token);
  }
}

// If it's custom, compare its rLum to USWDS grade rLum ranges
@if $custom-color {
  // If the color uses transparency, throw an error
  @if alpha($custom-color) != 1 {
    @return uswds-error($transparency-error);
  }
  $lum: luminance($custom-color);
  // Cycle through grades, knowing current AND next grade
  $our-grades: map-keys($system-color-grades);
  $grade-count: length($our-grades);
  @for $i from 1 through $grade-count {
    $this-grade: nth($our-grades, $i);
    $this-grade-min: map-deep-get($system-color-grades, $this-grade, "min");
    $this-grade-max: map-deep-get($system-color-grades, $this-grade, "max");
    $next-grade: if($i < $grade-count, nth($our-grades, $i + 1), false);
    $next-grade-min: if(
      $next-grade,
      map-deep-get($system-color-grades, $next-grade, "min"),
      false
    );
    // If the lum fits the range, assign a USWDS grade
    // Otherwise, set a grade midway between two USWDS grades
    @if ($lum >= $this-grade-min) and ($lum <= $this-grade-max) {
      @return $this-grade;
    }
    @if ($lum > $this-grade-max) and ($lum < $next-grade-min) {
      $custom-grade-midpoint: ($this-grade + $next-grade) / 2;
      $custom-grade: $custom-grade-midpoint;
      @return $custom-grade;
    }
  }
}

@if not is-color-token($color-token-assignment) {
  @return error-not-token($color-token-assignment, "color");
}

$system-token: $color-token-assignment;
$token-split: decompose-color-token($system-token);
$token-family: color-token-family($token-split);
// If the color uses transparency, throw an error
@if str-index($token-family, "transparent") {
  @return uswds-error($transparency-error);
}
// Otherwise, return token grade
$token-grade: color-token-grade($token-split);
@return $token-grade;

}

/*


color()


Derive a color from a color shortcode


*/

@function color($value, $flags…) {

$value: unpack($value);

// Non-token colors may be passed with specific flags
@if type-of($value) == color {
  // override or set-theme will allow any color
  @if index($flags, override) or index($flags, set-theme) {
    // override + no-warn will skip warnings
    @if index($flags, no-warn) {
      @return $value;
    }

    @if $theme-show-compile-warnings {
      @warn 'Override: `#{$value}` is not a USWDS color token.';
    }

    @return $value;
  }
}

// False values may be passed through when setting theme colors
@if $value == false {
  @if index($flags, set-theme) {
    @return $value;
  }
}

// Now, any value should be evaluated as a token

$value: smart-quote($value);

@if map-has-key($system-color-shortcodes, $value) {
  $our-color: map-get($system-color-shortcodes, $value);
  @if $our-color == false {
    @error '`#{$value}` is a color that does not exist '
      + 'or is set to false.';
  }
  @return $our-color;
}

// If we're using the theme flag, $project-color-shortcodes has not yet been set
@if not index($flags, set-theme) {
  @if map-has-key($project-color-shortcodes, $value) {
    $our-color: (map-get($project-color-shortcodes, $value));
    @if $our-color == false {
      @error '`#{$value}` is a color that does not exist '
        + 'or is set to false.';
    }
    @return $our-color;
  }
}

@return error-not-token($value, "color");

}

/*


advanced-color()


Derive a color from a color triplet:

family], [grade], [variant

*/

// color() can have a 1, 2, or 3 arguments passed to it: // // [family] // ex: color('primary') // - the root in a theme palette family // // [family], [grade] // ex: color('red', 50) // - a standard system color // ex: color('accent-warm', 'light') // - a standard theme color // ex: color('primary', 'vivid') // - in theme colors, 'vivid' is considered a grade // // [family], [grade], [vivid] // ex: color('red', 50, 'vivid') // - a vivid system color // - only system colors required three arguments

@function advanced-color(

$color-family: false,
$color-grade: false,
$color-variant: false

) {

// Convert any arglists into lists
$color-family: if(
  type-of($color-family) == "arglist",
  unpack($color-family),
  $color-family
);

// If $color-family is a list, color() had a variable
// passed to it, and args need to be re-set with the
// values from the $color-family list:
@if type-of($color-family) == "list" {
  @if length($color-family) > 2 {
    $color-variant: nth($color-family, 3);
  }
  $color-grade: nth($color-family, 2);
  $color-family: nth($color-family, 1);
}

// Set initial state of vars
$color-family: smart-quote($color-family);
$color-grade: smart-quote($color-grade);
$color-variant: smart-quote($color-variant);

// @debug '#{$color-family}: #{type-of($color-family)}, #{$color-grade}: #{type-of($color-grade)}, #{$color-variant}: #{type-of($color-variant)}' ;

// If there are no args, throw an error
@if not $color-family {
  @error 'Include a color in the form [family], [grade], [vivid]';
}

// If the grade is a number, it's a system color
// ex: ('red', 50)
@if type-of($color-grade) == "number" {
  @return get-system-color($color-family, $color-grade, $color-variant);
}

// non-number grades are associated with non-root theme colors
// ex: ('base', 'darker')
// root theme colors have no grade
// ex: ('base')
@if map-has-key($all-project-colors, $color-family) {
  @if not
    map-has-key(map-get($all-project-colors, $color-family), $color-grade)
  {
    @error '`#{$color-grade}` is not a valid grade of `#{$color-family}`. '
      + 'Valid grades: '
      + '#{map-keys(map-get($all-project-colors, $color-family))}';
  }
} @else {
  @return error-not-token($color-family, "theme family", $all-project-colors);
}
@return map-deep-get($all-project-colors, $color-family, $color-grade);

}

/*


units()


Converts a spacing unit into the desired final units (currently rem)


*/

@function units($value) {

$converted: if(
  type-of($value) == "string",
  quote($value),
  number-to-token($value)
);

@if not map-has-key($project-spacing-standard, $converted) {
  @return error-not-token($value, "spacing unit", $project-spacing-standard);
}

@return map-get($project-spacing-standard, $converted);

}

/*


get-palettes()


Build a single map of plugin values from a list of plugin keys.


*/

@function get-palettes($list) {

$our-palettes: ();

@if type-of($list) == "map" {
  @error 'Use a list of strings as plugin values.';
}

@each $palette in $list {
  @if not map-has-key($palette-registry, $palette) {
    @error '#{$palette} isn\'t in the registry.';
  }

  $our-palettes: map-merge(
    $our-palettes,
    map-get($palette-registry, $palette)
  );
}

@return $our-palettes;

}

/*


border-radius()


Get a border-radius from the system border-radii


*/

@function border-radius($value) {

@if map-has-key($all-border-radius, $value) {
  @return map-get($all-border-radius, $value);
} @else {
  @return error-not-token($value, "border radius", $all-border-radius);
}

}

/*


font-weight() fw()


Get a font-weight value from the system font-weight


*/

@function font-weight($value) {

@return get-uswds-value(font-weight, $value);

}

@function fw($value) {

@return font-weight($value);

}

/*


feature()


Gets a valid USWDS font feature setting


*/

@function feature($value) {

@return get-uswds-value(feature, $value);

}

/*


flex()


Gets a valid USWDS flex value


*/

@function flex($value) {

@return get-uswds-value(flex, $value);

}

/*


font-family() family()


Get a font-family stack from a role-based or type-based font family


*/

@function font-family($value) {

@return get-uswds-value(font-family, $value);

}

@function ff($value) {

@return font-family($value);

}

@function family($value) {

@return font-family($value);

}

/*


letter-spacing() ls()


Get a letter-spacing value from the system letter-spacing


*/

@function letter-spacing($value) {

$lh-map: map-get($system-properties, letter-spacing);
$fn-map: map-get($lh-map, function);
@if map-has-key($fn-map, $value) {
  @return map-get($fn-map, $value);
}
@if type-of($value) == "number" {
  @error '`#{$value}` is a not a valid letter-spacing token. '
    + 'Valid letter-spacing tokens: #{map-keys($fn-map)}';
}
@return get-uswds-value(letter-spacing, $value);

}

@function ls($value) {

@return letter-spacing($value);

}

/*


measure()


Gets a valid USWDS reading line length


*/

@function measure($value) {

@return get-uswds-value(measure, $value);

}

/*


opacity()


Get an opacity from the system opacities


*/

@function opacity($value) {

@return get-uswds-value(opacity, $value);

}

/*


order()


Get an order value from the system orders


*/

@function order($value) {

@return get-uswds-value(order, $value);

}

/*


radius()


Get a border-radius value from the system letter-spacing


*/

@function radius($value) {

@return get-uswds-value(border-radius, $value);

}

/*


font-size()


Get type scale value from a [family] and

scale

*/

@function font-size($family, $scale, $force: false) {

$our-family: smart-quote($family);
$our-scale: smart-quote($scale);

@if not map-has-key($project-cap-heights, $our-family) {
  @return error-not-token($our-family, "font family", $project-cap-heights);
}
@if not map-get($all-type-scale, $our-scale) {
  @return error-not-token($our-scale, "font scale", $all-type-scale);
}

$this-cap: map-get($project-cap-heights, $our-family);
$this-scale: map-get($all-type-scale, $our-scale);

@if not $force {
  @if not($this-scale and $this-cap) {
    @error 'The scale `#{$our-scale}` is disabled '
      + 'in your project\'s theme settings. '
      + 'Set its value to `true` to use this family.';
  }
}

@return normalize-type-scale($this-cap, $this-scale);

}

@function fs($family, $scale) {

@return font-size($family, $scale);

}

@function size($family, $scale) {

@return font-size($family, $scale);

}

/*


z-index() z()


Get a z-index value from the system z-index


*/

@function z-index($value) {

@return get-uswds-value(z-index, $value);

}

@function z($value) {

@return z-index($value);

}

/*


magic-number()


Returns the magic number of two color grades. Takes numbers or color tokens.

magic-number(50, 10) return: 40

magic-number(“red-50”, “red-10”) return: 40


*/

@function magic-number($grade-1, $grade-2) {

$grade-1: if(
  type-of($grade-1) == "number",
  $grade-1,
  calculate-grade($grade-1)
);
$grade-2: if(
  type-of($grade-2) == "number",
  $grade-2,
  calculate-grade($grade-2)
);
$magic-number: abs($grade-1 - $grade-2);
@return $magic-number;

}

/*


get-default()


Returns the default value from a map of project defaults

get-default(“bg-color”) > $theme-body-background-color


*/

@function get-default($var) {

$value: map-get($project-defaults, $var);
@return $value;

}

/*


get-color-token-from-bg()


Returns an accessible foreground color token, given a background, preferred color, fallback color, and WCAG target

returns: color-token

get-color-token-from-bg(

"black",
"red-60",
"red-10",
"AA")

> “red-10”


*/

@function get-color-token-from-bg(

$bg-color: "default",
$preferred-text-token: "default",
$fallback-text-token: "default",
$wcag-target: "AA",
$context: false,
$for: false

) {

$for-text: if($for, "#{$for} ", "");
$context-text: if($context, "[#{$context}] ", "");
// Set defaults
@if $bg-color == "default" {
  $bg-color: get-default("bg-color");
}
@if $preferred-text-token == "default" {
  $preferred-text-token: get-default("preferred-text-token");
}
@if $fallback-text-token == "default" {
  $fallback-text-token: get-default("fallback-text-token");
}
$target-magic-number: map-get($system-wcag-magic-numbers, $wcag-target);
$bg-grade: calculate-grade($bg-color);
$our-color-tokens: ($preferred-text-token, $fallback-text-token);
$accessible-text-token: false;
$accessible-text-grade: false;
// Get the text color token
// Check both text tokens.
// Accept a token if it has specified accessible contrast.
$best-token: false;
$best-magic-number: 0;
@each $token in $our-color-tokens {
  @if not $accessible-text-token {
    $token-grade: calculate-grade($token);
    $this-magic-number: magic-number($token-grade, $bg-grade);
    @if $this-magic-number > $best-magic-number {
      $best-magic-number: $this-magic-number;
      $best-token: $token;
    }
    @if is-accessible-magic-number($token-grade, $bg-grade, $wcag-target) {
      $accessible-text-token: $token;
      $accessible-text-grade: $token-grade;
    }
  }
}
// If neither is accessible,
// warn the user and use the Preferred token
@if not $accessible-text-token {
  $accessible-text-token: $best-token;
  @if $theme-show-compile-warnings {
    @warn "#{$context-text}Neither the specified preferred #{$for-text}color token (`#{$preferred-text-token}`) nor the fallback #{$for-text}color token (`#{$fallback-text-token}`) have #{$wcag-target} contrast on a `#{$bg-color}` background. Using `#{$best-token}`. Please check your source code and project settings.";
  }
}

@return $accessible-text-token;

}

/*


get-link-tokens-from-bg()


Get accessible link colors for a given background color

returns: link-token, hover-token

get-link-tokens-from-bg(

"black",
"red-60",
"red-10",
"AA")

> “red-10”, “red-5”

get-link-tokens-from-bg(

"black",
"red-60v",
"red-10v",
"AA-large")

> “red-60v”, “red-50v”

get-link-tokens-from-bg(

"black",
"red-5v",
"red-60v",
"AA")

> “red-5v”, “white”

get-link-tokens-from-bg(

"black",
"white",
"red-60v",
"AA")

> “white”, “white”


*/

@function get-link-tokens-from-bg(

$bg-color: "default",
$preferred-link-token: "default",
$fallback-link-token: "default",
$wcag-target: "AA",
$context: false

) {

$context-text: if($context, "[#{$context}] ", "");
$is-default: false;
$is-default-preferred: false;
$is-default-fallback: false;
$default-reverse: false;
$default-standard: false;
@if $bg-color == "default" {
  $bg-color: get-default("bg-color");
}
@if $preferred-link-token == "default" {
  $preferred-link-token: get-default("preferred-link-token");
  $default-reverse: true;
}
@if $fallback-link-token == "default" {
  $fallback-link-token: get-default("fallback-link-token");
  $standard-reverse: true;
}
$bg-grade: calculate-grade($bg-color);
$preferred-hover-token: false;
$default-hover-token: false;
$accessible-hover-token: false;
$accessible-link-token: get-color-token-from-bg(
  $bg-color,
  $preferred-link-token,
  $fallback-link-token,
  $wcag-target,
  $context,
  $for: "link"
);
$accessible-link-grade: calculate-grade($accessible-link-token);
// Get the hover color token
// If link is lighter than bg set $is-reverse to true
$is-reverse: if($accessible-link-grade < $bg-grade, true, false);
// If using defaults, set the default hover
// $link-kind is used for error messaging
$link-kind: false;
@if $is-reverse {
  @if $default-reverse {
    $default-hover-token: $theme-link-reverse-hover-color;
    $link-kind: "default reverse";
  }
} @else if $default-standard {
  $default-hover-token: $theme-link-hover-color;
  $link-kind: "default";
}
@if $default-hover-token {
  $default-hover-grade: calculate-grade($default-hover-token);
  @if is-accessible-magic-number(
    $default-hover-grade,
    $bg-grade,
    $wcag-target
  )
  {
    $accessible-hover-token: $default-hover-token;
  }
  @if not $accessible-hover-token and $theme-show-compile-warnings {
    @warn "#{$context-text}The #{$link-kind} link hover (`#{$default-hover-token}`) does not have #{$wcag-target} contrast on a #{$bg-color} background. Please update your project settings.";
  }
}
@if not $accessible-hover-token {
  $direction: if($is-reverse, "lighter", "darker");
  $hover-token: next-token($accessible-link-token, $direction);
  // Use the next token, if it is valid
  @if $hover-token {
    $accessible-hover-token: $hover-token;
    // Otherwise use the token itself as hover, and warn.
  } @else {
    $accessible-hover-token: $accessible-link-token;
    @if $theme-show-compile-warnings {
      @warn "#{$context-text}A `#{$accessible-hover-token}` link does not have #{$direction} hover available. Hover set to link color.";
    }
  }
}
@return $accessible-link-token, $accessible-hover-token;

}

/*


color-token-type()


Returns the type of a color token.

Returns: “system” | “theme”


*/

@function color-token-type($token) {

$type: if(is-system-color-token($token), "system", false);
@if not $type {
  $type: if(is-theme-color-token($token), "theme", false);
}
@if not $type {
  @return error-not-token($token, "color");
}
@return $type;

}

/*


color-token-family()


Returns the family of a color token.

Returns: color-family

color-token-family(“accent-warm-vivid”) > “accent-warm”

color-token-family(“red-50v”) > “red”

color-token-variant((“red”, 50, “vivid”)) > “red”


*/

@function color-token-family($color-token) {

$split: if(
  type-of($color-token) == "list",
  $color-token,
  decompose-color-token($color-token)
);
$family: nth($split, 1);
@return $family;

}

/*


color-token-grade()


Returns the grade of a USWDS color token.

Returns: color-grade

color-token-grade(“accent-warm”) > “root”

color-token-grade(“accent-warm-vivid”) > “root”

color-token-grade(“accent-warm-darker”) > “darker”

color-token-grade(“red-50v”) > 50

color-token-variant((“red”, 50, “vivid”)) > 50


*/

@function color-token-grade($color-token) {

$split: if(
  type-of($color-token) == "list",
  $color-token,
  decompose-color-token($color-token)
);
$grade: nth($split, 2);
@return $grade;

}

/*


color-token-variant()


Returns the variant of color token.

Returns: “vivid” | false

color-token-variant(“accent-warm”) > false

color-token-variant(“accent-warm-vivid”) > “vivid”

color-token-variant(“red-50v”) > “vivid”

color-token-variant((“red”, 50, “vivid”)) > “vivid”


*/

@function color-token-variant($color-token) {

$split: if(
  type-of($color-token) == "list",
  $color-token,
  decompose-color-token($color-token)
);
$variant: nth($split, 3);
@return $variant;

}

/*


next-token()


Returns next “darker” or “lighter” color token of the same token type and variant.

Returns: color-token | false

next-token(“accent-warm”, “lighter”) > “accent-warm-light”

next-token(“gray-10”, “lighter”) > “gray-5”

next-token(“gray-5”, “lighter”) > “white”

next-token(“white”, “lighter”) > false

next-token(“red-50v”, “darker”) > “red-60v”

next-token(“red-50”, “darker”) > “red-60”

next-token(“red-80v”, “darker”) > “red-90”

next-token(“red-90”, “darker”) > “black”

next-token(“white”, “darker”) > “gray-5”

next-token(“black”, “lighter”) > “gray-90”


*/

@function next-token($token, $direction) {

$next-token: false;
$type: color-token-type($token);
$token-split: decompose-color-token($token);
// 1. System case
@if $type == "system" {
  // transparent tokens return don't have a next token
  @if $token == "transparent" {
    @return false;
  }
  // black and white tokens use the gray family for next
  $current-family: if(
    $token == "white" or $token == "black",
    "gray",
    color-token-family($token-split)
  );
  // black- and white-transparent tokens don't have a next
  @if str-index($current-family, "-transparent") {
    @return false;
  }
  $current-grade: color-token-grade($token-split);
  // Nothing can be darker than black or lighter than white
  @if $direction == "darker" and $current-grade == 100 {
    @return false;
  }
  @if $direction == "lighter" and $current-grade == 0 {
    @return false;
  }
  // Grades under 5 should be treated as 5
  @if $current-grade > 0 and $current-grade < 5 {
    $current-grade: 5;
  }
  $system-grade-list: map-keys($system-color-grades);
  $current-grade-index: index($system-grade-list, $current-grade);
  // Note: System grades go from darkest (100) to lightest (0)
  $next-grade: if(
    $direction == "darker",
    nth($system-grade-list, ($current-grade-index - 1)),
    nth($system-grade-list, ($current-grade-index + 1))
  );
  $output-grade: $next-grade;
  // Keep the same vivid variant as the parent
  // Note: Grade 90 has no vivid variant
  @if color-token-variant($token-split) == "vivid" and ($next-grade < 90) {
    $output-grade: $next-grade + "v";
  }
  // Use black and white tokens for grades 100 and 0...
  @if $next-grade == 100 {
    $next-token: "black";
  } @else if $next-grade == 0 {
    $next-token: "white";
    // ...Otherwise output token in expected form
  } @else {
    $next-token: $current-family + "-" + $output-grade;
  }
  // 2. Theme case
} @else {
  $current-grade: color-token-grade($token-split);
  // Vivid theme token should be considered root for ordering
  $current-grade: if($current-grade == "vivid", "root", $current-grade);
  $current-family: color-token-family($token-split);
  // Ink should be considered base-darkest
  // TODO: Should it?
  @if $token == "ink" {
    $current-family: "base";
    $current-grade: "darkest";
  }
  // Black is darker than darkest
  @if $direction == "darker" and $current-grade == "darkest" {
    @return "black";
  }
  // White is lighter than lightest
  @if $direction == "lighter" and $current-grade == "lightest" {
    @return "white";
  }
  $theme-grade-list: map-keys($theme-color-grades);
  $current-grade-index: index($theme-grade-list, $current-grade);
  // Note: Theme grades go from `lightest` to `darkest`
  $next-grade: if(
    $direction == "darker",
    nth($theme-grade-list, ($current-grade-index + 1)),
    nth($theme-grade-list, ($current-grade-index - 1))
  );
  // Exclude `root` from token output
  @if $next-grade == "root" {
    @return $current-family;
  } @else {
    $next-token: $current-family + "-" + $next-grade;
  }
  // If the next color is set to false, use black/white instead
  @if not color-token-assignment($next-token) {
    @if $direction == "darker" {
      @return "black";
    }
    @if $direction == "lighter" {
      @return "white";
    }
  }
}
@return $next-token;

}

/*


wcag-magic-number()


Returns the magic number of a specific wcag grade:

“AA” “AA-Large” “AAA”

wcag-magic-number(“AA”) > 50


*/

@function wcag-magic-number($wcag-target) {

$wcag-magic-number: map-get($system-wcag-magic-numbers, $wcag-target);
@return $wcag-magic-number;

}

/*


is-accessible-magic-number()


Returns whether two grades achieve specified target color contrast

Returns: true | false

is-accessible-magic-number(10, 50, “AA”) > false

is-accessible-magic-number(10, 60, “AA”) > true


*/

@function is-accessible-magic-number($grade-1, $grade-2, $wcag-target) {

$target-magic-number: wcag-magic-number($wcag-target);
$magic-number: magic-number($grade-1, $grade-2);
@if $magic-number >= $target-magic-number {
  @return true;
}
@return false;

}