diff --git a/src/material/core/_core-theme.scss b/src/material/core/_core-theme.scss index a9ade883cc70..8e6b3061d1d8 100644 --- a/src/material/core/_core-theme.scss +++ b/src/material/core/_core-theme.scss @@ -71,15 +71,19 @@ } } -@mixin overrides($tokens: ()) { +// This theme is a special case where not all of the imported tokens are supported in `overrides`. +// To aid the docs token extraction, we have to pull the `overrides` token config out into a +// separate function. +// !!!Important!!! renaming or removal of this function requires the `extract-tokens.ts` script to +// be updated as well. +@function _get-supported-overrides-tokens() { $app-tokens: tokens-mat-app.get-token-slots(); $ripple-tokens: tokens-mat-ripple.get-token-slots(); $option-tokens: tokens-mat-option.get-token-slots(); $full-pseudo-checkbox-tokens: tokens-mat-full-pseudo-checkbox.get-token-slots(); $minimal-pseudo-checkbox-tokens: tokens-mat-minimal-pseudo-checkbox.get-token-slots(); - @include token-utils.batch-create-token-values( - $tokens, + @return ( (prefix: tokens-mat-app.$prefix, tokens: $app-tokens), (prefix: tokens-mat-ripple.$prefix, tokens: $ripple-tokens), (prefix: tokens-mat-option.$prefix, tokens: $option-tokens), @@ -88,6 +92,10 @@ ); } +@mixin overrides($tokens: ()) { + @include token-utils.batch-create-token-values($tokens, _get-supported-overrides-tokens()...); +} + // Mixin that renders all of the core styles that depend on the theme. @mixin theme($theme, $options...) { // Wrap the sub-theme includes in the duplicate theme styles mixin. This ensures that diff --git a/tools/extract-tokens/extract-tokens.ts b/tools/extract-tokens/extract-tokens.ts index c8ad44b0f253..62b94e9595d7 100644 --- a/tools/extract-tokens/extract-tokens.ts +++ b/tools/extract-tokens/extract-tokens.ts @@ -7,12 +7,15 @@ import {compileString} from 'sass'; interface Token { /** Name of the token. */ name: string; - /** Prefix under which the token is exposed in the DOM. */ - prefix: string; /** Type of the token (color, typography etc.) */ type: string; - /** System token that it was derived from. */ - derivedFrom?: string; + /** Places from which the token with the specific name can originate. */ + sources: { + /** Prefix of the source. */ + prefix: string; + /** System-level token from which the source dervies its value. */ + derivedFrom?: string; + }[]; } /** Extracted map of tokens from the source Sass files. */ @@ -41,11 +44,17 @@ if (require.main === module) { throw new Error(`Could not find theme files in ${packagePath}`); } - const themes: {name: string; tokens: Token[]}[] = []; + const themes: {name: string; overridesMixin: string; tokens: Token[]}[] = []; themeFiles.forEach(theme => { const tokens = extractTokens(theme.filePath); - themes.push({name: theme.mixinPrefix, tokens}); + themes.push({ + name: theme.mixinPrefix, + // This can be derived from the `name` already, but we want the source + // of truth to be in this repo, instead of whatever page consumes the data. + overridesMixin: `${theme.mixinPrefix}-overrides`, + tokens, + }); }); writeFileSync(outputPath, JSON.stringify(themes)); @@ -125,25 +134,46 @@ function extractTokens(themePath: string): Token[] { * @param groups Extracted data from the Sass file. */ function createTokens(type: string, groups: ExtractedTokenMap): Token[] { - const result: Token[] = []; + const data: Map> = new Map(); + // First step is to go through the whole data and group the tokens by their name. We need to + // group the tokens, because the same name can be used by different prefixes (e.g. both + // `mdc-filled-text-field` and `mdc-outlined-text-field` have a `label-text-color` token). Object.keys(groups).forEach(prefix => { const tokens = groups[prefix]; // The token data for some components may be null. if (tokens) { Object.keys(tokens).forEach(name => { + let tokenData = data.get(name); + + if (!tokenData) { + tokenData = new Map(); + data.set(name, tokenData); + } + const value = tokens[name]; const derivedFrom = typeof value === 'string' ? textBetween(value, 'var(', ')') : null; - const token: Token = {name, prefix, type}; - if (derivedFrom) { - token.derivedFrom = derivedFrom.replace('--sys-', ''); - } - result.push(token); + tokenData.set(prefix, derivedFrom ? derivedFrom.replace('--sys-', '') : null); }); } }); + // After the tokens have been grouped, we can create the `Token` object for each one. + const result: Token[] = []; + + data.forEach((tokenData, name) => { + const token: Token = {name, type, sources: []}; + + tokenData.forEach((derivedFrom, prefix) => { + // Set `derivedFrom` to `undefined` if it hasn't been set so it doesn't show up in the JSON. + token.sources.push({prefix, derivedFrom: derivedFrom || undefined}); + }); + + result.push(token); + }); + + // Sort the tokens by name so they're easier to scan. return result.sort((a, b) => a.name.localeCompare(b.name)); } @@ -187,13 +217,35 @@ function getTokenExtractionCode( // Goes through all the available namespaces looking for a `$prefix` variable. This allows us to // determine the prefixes that are used within the theme. Once we know the prefixes we can use - // them to extract only the tokens we care about from the full token set. + // them to extract only the tokens we care about from the full token set. Note: `core-theme` + // is a special case where not all of the imported tokens are supported in the `overrides` + // mixin. Such cases will expose a `_get-supported-overrides-tokens` private function that + // can be used to determine the exact set of prefixes that are used. // After the tokens are extracted, we log them out using `@debug` and then intercept the log // in order to get the raw data. We have to go through `@debug`, because there's no way to // output a Sass map to CSS. // The tokens are encoded as JSON so we don't have to implement parsing of Sass maps in TS. const append = ` - $__namespaces: (${useStatements.map(stmt => `"${extractNamespace(stmt)}"`).join(', ')}); + $__prefixes: (); + + @if ${meta}.function-exists('_get-supported-overrides-tokens') { + $__configs: _get-supported-overrides-tokens(); + + @each $config in $__configs { + $__prefixes: ${list}.append($__prefixes, ${map}.get($config, prefix)); + } + } @else { + $__namespaces: (${useStatements.map(stmt => `"${extractNamespace(stmt)}"`).join(', ')}); + + @each $name in $__namespaces { + $prefix: ${map}.get(${meta}.module-variables($name), prefix); + + @if ($prefix) { + $__prefixes: ${list}.append($__prefixes, $prefix); + } + } + } + $__all-color: ${m3Tokens}.generate-color-tokens(light, ${palettes}.$azure-palette, ${palettes}.$azure-palette, ${palettes}.$azure-palette, 'sys'); $__all-typography: ${m3Tokens}.generate-typography-tokens(font, 100, 100, 100, 100, 'sys'); @@ -204,15 +256,11 @@ function getTokenExtractionCode( $__density: (); $__base: (); - @each $name in $__namespaces { - $prefix: map-get(${meta}.module-variables($name), 'prefix'); - - @if ($prefix) { - $__color: ${map}.set($__color, $prefix, map-get($__all-color, $prefix)); - $__typography: ${map}.set($__typography, $prefix, map-get($__all-typography, $prefix)); - $__density: ${map}.set($__density, $prefix, map-get($__all-density, $prefix)); - $__base: ${map}.set($__base, $prefix, map-get($__all-base, $prefix)); - } + @each $prefix in $__prefixes { + $__color: ${map}.set($__color, $prefix, ${map}.get($__all-color, $prefix)); + $__typography: ${map}.set($__typography, $prefix, ${map}.get($__all-typography, $prefix)); + $__density: ${map}.set($__density, $prefix, ${map}.get($__all-density, $prefix)); + $__base: ${map}.set($__base, $prefix, ${map}.get($__all-base, $prefix)); } // Define our JSON.stringify implementation so it can be used below.