// Utilities to parse params supplied as a map. Values can be defined in terms of // other values, with modifications. For example: // // @include ag-register-params(( // a: ag-derived(b, $times: c, $plus: 2), // b: 4, // c: 10 // )); // @debug ag-param(a); // outputs 42 // Define a derived parameter. Derived values are lazily evaluated. This function is // sugar for defining a data structure to record the derived value's parameters. @function ag-derived( $reference-name, $times: null, $divide: null, $plus: null, $minus: null, $opacity: null, $lighten: null, $darken: null, $mix: null, $self-overlay: null ) { $derived: ( "--ag-is-derived-value": true, "reference-name": $reference-name ); @if $times != null { $derived: map-merge($derived, ("times": $times)); } @if $divide != null { $derived: map-merge($derived, ("divide": $divide)); } @if $plus != null { $derived: map-merge($derived, ("plus": $plus)); } @if $minus != null { $derived: map-merge($derived, ("minus": $minus)); } @if $opacity != null { $derived: map-merge($derived, ("opacity": $opacity)); } @if $lighten != null { $derived: map-merge($derived, ("lighten": $lighten)); } @if $darken != null { $derived: map-merge($derived, ("darken": $darken)); } @if $mix != null { $derived: map-merge($derived, ("mix": $mix)); } @if $self-overlay != null { $derived: map-merge($derived, ("self-overlay": $self-overlay)); } @return $derived; } // Use a parameter in SCSS, e.g. `color: ag-param(foreground-color)` // Note, it is not possible to use this for color params, use the ag-color-property mixin instead @function ag-param($name, $caller: null) { @if $-ag-allow-color-param-access-with-ag-param != true and str-index($name, "-color") and $caller != "permitted internal _ag-theme-params.scss access" { @error "Illegal call to ag-param(#{$name}) - all colour params must be accessed through the ag-color-property mixin."; } $resolved: -ag-param-unchecked($name); @if str-index(inspect($resolved), "ag-derived(") != null { @error "#{$name} param contains a ag-derived() as a CSS function call expression. This means that you have tried to use ag-derived() before the function is defined - you need to include the file that defines it."; } @if type-of($resolved) == map { @error "ag-param(#{$name}) resolved to a map, which is not valid CSS: #{inspect($resolved)}"; } @each $part in $resolved { @if type-of($part) == map { @error "ag-param(#{$name}) resolved to a list containing a map, which is not valid CSS: #{str-slice(inspect($resolved), 0, 1000)}"; } } @return $resolved; } // Return true if a param has a value other than null or false @function ag-param-is-set($name) { $value: -ag-param-unchecked($name); @return $value != null and $value != false; } // Return true if two params have different values @function ag-params-are-different($name-a, $name-b) { @return -ag-param-unchecked($name-a) != -ag-param-unchecked($name-b); } // A mixin to apply a color to an element. This sets the value of a CSS property using a // theme parameter, and also emits CSS that allows the value to be overridden at runtime // using CSS variables. If the mixin is called like this: // // @include ag-color-property(background-color, header-background-color) // // ... and the header-background-color parameter is set to `red` then the emitted CSS will // be something like: // // background-color: red; // background-color: var(--ag-header-background-color, red); // // The optional $important argument can be used to add a CSS !important directive @mixin ag-color-property($property, $param, $important: false) { $value: ag-param($param, $caller: "permitted internal _ag-theme-params.scss access"); $important: if($important, !important, null); @if $value != null { #{$property}: $value $important; } @if not ag-param-is-set(suppress-css-var-overrides) { $value-as-css-var: -ag-param-as-css-var($param); @if $value != $value-as-css-var { #{$property}: $value-as-css-var $important; } } } $-ag-allow-color-param-access-with-ag-param: true; @mixin ag-allow-color-param-access-with-ag-param($allow) { $-ag-allow-color-param-access-with-ag-param: $allow !global; } // Merge params supplied to a theme with the defaults, optionally validate, and register // the resulting map globally for use with ag-param() // // $params: params supplied by the derived theme // $defaults: values for params not in $params @function ag-process-theme-variables($params, $defaults) { $params: -ag-require-type($params, map, "$params argument to ag-process-theme-variables"); // Derived themes can add params, and those new params would trigger validation errors when // passed to the base theme, so don't re-validate params that have already been validated @if not map-has-key($params, "--ag-already-validated") { @each $key in map-keys($params) { @if not map-has-key($defaults, $key) and str-index($key, "--internal-") != 1 { @warn "Unrecognised param \"#{$key}\""; } } } @if map-get($params, "icons-font-codes") and map-get($defaults, "icons-font-codes") { $merged-codes: map-merge(map-get($defaults, "icons-font-codes"), map-get($params, "icons-font-codes")); $params: map-merge($params, ("icons-font-codes": $merged-codes)); } $params: map-merge($defaults, $params); $params: map-merge($params, ("--ag-already-validated": true)); $-ag-params: $params !global; @return $params; } // global map of params used by ag-param() $-ag-params: null !default; // Register a params map globally so that it can be used by ag-param($name) // NOTE: Custom themes should NOT use this, use ag-process-theme-variables() instead @mixin ag-register-params($params) { $params: -ag-require-type($params, "map", "$params argument"); $-ag-params: $params !global; } // // PRIVATE IMPLEMENTATION FUNCTIONS // // Return a parameter value as a CSS variable declaration @function -ag-param-as-css-var($name) { $value: map-get($-ag-params, $name); @if -is-ag-derived($value) { $has-modificatons: length($value) > 2; @if $has-modificatons { $value: ag-param($name, $caller: "permitted internal _ag-theme-params.scss access"); } @else { $reference-name: map-get($value, "reference-name"); $value: -ag-param-as-css-var($reference-name); } } @if $value == null { @return var(--ag-#{$name}); } @return var(--ag-#{$name}, #{$value}); } // Get a parameter, with no checks other than that the parameter exists @function -ag-param-unchecked($name) { @if $-ag-params == null { @error "ag-param() called before ag-register-params"; } @if str-index($name, "--internal-") == 1 { // internal vars are returned without ag-derived resolution or validation that the var exists @return map-get($-ag-params, $name); } @if not map-has-key($-ag-params, $name) { @error "ag-param(#{$name}): no such parameter"; } @return -ag-resolve-param-name($-ag-params, $name); } // Return true if a value is a record returned by ag-derived() @function -is-ag-derived($value) { @return type-of($value) == map and map-get($value, "--ag-is-derived-value") == true; } @function -ag-resolve-param-name($params, $name) { $value: map-get($params, $name); @return -ag-resolve-param-value($params, $value, $name); } @function -ag-resolve-param-value($params, $input-value, $context-name) { @if type-of($input-value) == list { $resolved: $input-value; @for $i from 1 through length($input-value) { $resolved: set-nth($resolved, $i, -ag-resolve-param-value($params, nth($resolved, $i), $context-name)); } @return $resolved; } @if not -is-ag-derived($input-value) { @return $input-value; } $derived: $input-value; $reference-name: map-get($derived, "reference-name"); @if not map-has-key($params, $reference-name) { @error "ag-derived: no such parameter \"#{$reference-name}\""; } $resolved: map-get($params, $reference-name); $resolved: -ag-resolve-param-value($params, $resolved, $reference-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "times", $context-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "divide", $context-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "plus", $context-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "minus", $context-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "opacity", $context-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "mix", $context-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "lighten", $context-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "darken", $context-name); $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "self-overlay", $context-name); @return -ag-resolve-param-value($params, $resolved, $reference-name); } @function -ag-apply-derived-operator($params, $lhs, $derived, $operator, $context-name) { @if $lhs == null { @return $lhs; } $rhs: map-get($derived, $operator); @if $rhs == null { @return $lhs; } @if -ag-is-css-var-token($lhs) { $reference-name: map-get($derived, "reference-name"); @warn "Problem while calculating theme parameter `#{$context-name}: #{-ag-inspect-derived-value($derived)}`. This rule attempts to modify the color of `#{$reference-name}` using $#{$operator}, but (#{$reference-name}: #{$lhs}) is a CSS variable and can't be modified at compile time. Either set `#{$reference-name}` to a CSS color value (e.g. #ffffff) or provide a value for `#{$context-name}` that does not use $#{$operator}"; @return null; } @if type-of($rhs) == string { $rhs: -ag-resolve-param-name($params, $rhs); } $operator-function: "-ag-operator-function-#{$operator}"; @if not function-exists($operator-function) { @error "No such function #{$operator-function}"; } @return call(get-function($operator-function), $params, $lhs, $rhs); } // return a string representation of an ag-derived value for debugging @function -ag-inspect-derived-value($derived) { @return "ag-derived(" + map-get($derived, "reference-name") + if(map-get($derived, "times"), ", $times: #{map-get($derived, "times")}", "") + if(map-get($derived, "divide"), ", $divide: #{map-get($derived, "divide")}", "") + if(map-get($derived, "plus"), ", $plus: #{map-get($derived, "plus")}", "") + if(map-get($derived, "minus"), ", $minus: #{map-get($derived, "minus")}", "") + if(map-get($derived, "opacity"), ", $opacity: #{map-get($derived, "opacity")}", "") + if(map-get($derived, "mix"), ", $mix: #{map-get($derived, "mix")}", "") + if(map-get($derived, "lighten"), ", $lighten: #{map-get($derived, "lighten")}", "") + if(map-get($derived, "darken"), ", $darken: #{map-get($derived, "darken")}", "") + if(map-get($derived, "self-overlay"), ", $self-overlay: #{map-get($derived, "self-overlay")}", "") + ")"; } @function -ag-is-css-var-token($value) { @return type-of($value) == string and str-index($value, "var(") != null } @function -ag-require-type($value, $expected, $context) { @if type-of($value) == $expected or ($expected == "map" and $value == ()) { @return $value; } @error "Expected #{$context} to be a #{$expected} but got a #{type-of($value)} instead (#{inspect($value)})"; } @function -ag-operator-function-times($params, $lhs, $rhs) { $lhs: -ag-require-type($lhs, "number", "value before $times"); $rhs: -ag-require-type($rhs, "number", "argument to $times"); @return $lhs * $rhs; } @function -ag-operator-function-divide($params, $lhs, $rhs) { $lhs: -ag-require-type($lhs, "number", "value before $divide"); $rhs: -ag-require-type($rhs, "number", "argument to $divide"); @return $lhs / $rhs; } @function -ag-operator-function-plus($params, $lhs, $rhs) { $lhs: -ag-require-type($lhs, "number", "value before $plus"); $rhs: -ag-require-type($rhs, "number", "argument to $plus"); @return $lhs + $rhs; } @function -ag-operator-function-minus($params, $lhs, $rhs) { $lhs: -ag-require-type($lhs, "number", "value before $minus"); $rhs: -ag-require-type($rhs, "number", "argument to $minus"); @return $lhs - $rhs; } @function -ag-operator-function-opacity($params, $lhs, $rhs) { $lhs: -ag-require-type($lhs, "color", "value before $opacity"); $rhs: -ag-require-type($rhs, "number", "argument to $opacity"); @if $rhs < 0 or $rhs > 1 { @error "Expected argument to $opacity to be between 0 and 1, got #{inspect($rhs)} instead."; } @return rgba($lhs, $rhs); } @function -ag-operator-function-mix($params, $lhs, $rhs) { $lhs: -ag-require-type($lhs, "color", "value before $mix"); @if length($rhs) != 2 { @error "Expected argument to $mix to be a 2-item array [color, percentage] but got #{inspect($rhs)}"; } $color: nth($rhs, 1); @if type-of($color) == string { $color: -ag-resolve-param-name($params, $color); } $percentage: nth($rhs, 2); @if type-of($color) != color or type-of($percentage) != number { @error "Expected argument to $mix to be a 2-item array [color, number] but got [#{type-of($color)}, #{type-of($percentage)}]: #{inspect($rhs)}"; } @return mix($color, $lhs, $percentage); } @function -ag-operator-function-lighten($params, $lhs, $rhs) { $lhs: -ag-require-type($lhs, "color", "value before $lighten"); $rhs: -ag-require-type($rhs, "number", "argument to $lighten"); @if $rhs < 0 or $rhs > 100 { @error "Expected argument to $lighten to be between 0 and 100, got #{inspect($rhs)} instead."; } @return lighten($lhs, $rhs); } @function -ag-operator-function-darken($params, $lhs, $rhs) { $lhs: -ag-require-type($lhs, "color", "value before $darken"); $rhs: -ag-require-type($rhs, "number", "argument to $darken"); @if $rhs < 0 or $rhs > 100 { @error "Expected argument to $darken to be between 0 and 100, got #{inspect($rhs)} instead."; } @return darken($lhs, $rhs); } @function -ag-operator-function-self-overlay($params, $color, $times) { $color: -ag-require-type($color, "color", "value before $self-overlay"); $times: -ag-require-type($times, "number", "argument to $self-overlay"); @if $times < 0 or $times > 100 { @error "Expected argument to $self-overlay to be between 0 and 100, got #{inspect($times)} instead."; } $solidity: 1 - opacity($color); $output-solidity: 1; @if $times > 0 { @for $i from 1 through $times { $output-solidity: $output-solidity * $solidity; } } @return rgba($color, 1 - $output-solidity); }