| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:io'; | |
| import 'token_logger.dart'; | |
| /// Base class for code generation templates. | |
| abstract class TokenTemplate { | |
| const TokenTemplate( | |
| this.blockName, | |
| this.fileName, | |
| this._tokens, { | |
| this.colorSchemePrefix = 'Theme.of(context).colorScheme.', | |
| this.textThemePrefix = 'Theme.of(context).textTheme.', | |
| }); | |
| /// Name of the code block that this template will generate. | |
| /// | |
| /// Used to identify an existing block when updating it. | |
| final String blockName; | |
| /// Name of the file that will be updated with the generated code. | |
| final String fileName; | |
| /// Map of token data extracted from the Material Design token database. | |
| final Map<String, dynamic> _tokens; | |
| /// Optional prefix prepended to color definitions. | |
| /// | |
| /// Defaults to 'Theme.of(context).colorScheme.' | |
| final String colorSchemePrefix; | |
| /// Optional prefix prepended to text style definitions. | |
| /// | |
| /// Defaults to 'Theme.of(context).textTheme.' | |
| final String textThemePrefix; | |
| /// Check if a token is available. | |
| bool tokenAvailable(String tokenName) => _tokens.containsKey(tokenName); | |
| /// Resolve a token while logging its usage. | |
| /// There will be no log if [optional] is true and the token doesn't exist. | |
| dynamic getToken(String tokenName, {bool optional = false}) { | |
| if (optional && !tokenAvailable(tokenName)) { | |
| return null; | |
| } | |
| tokenLogger.log(tokenName); | |
| return _tokens[tokenName]; | |
| } | |
| static const String beginGeneratedComment = ''' | |
| // BEGIN GENERATED TOKEN PROPERTIES'''; | |
| static const String headerComment = ''' | |
| // Do not edit by hand. The code between the "BEGIN GENERATED" and | |
| // "END GENERATED" comments are generated from data in the Material | |
| // Design token database by the script: | |
| // dev/tools/gen_defaults/bin/gen_defaults.dart. | |
| // dart format off | |
| '''; | |
| // TODO(goderbauer): Update the script to output auto-formatted code and remove | |
| // "dart format off/on" from headerComment and footerComment. | |
| static const String footerComment = ''' | |
| // dart format on | |
| '''; | |
| static const String endGeneratedComment = ''' | |
| // END GENERATED TOKEN PROPERTIES'''; | |
| /// Replace or append the contents of the file with the text from [generate]. | |
| /// | |
| /// If the file already contains a generated text block matching the | |
| /// [blockName], it will be replaced by the [generate] output. Otherwise | |
| /// the content will just be appended to the end of the file. | |
| void updateFile() { | |
| final String contents = File(fileName).readAsStringSync(); | |
| final beginComment = '$beginGeneratedComment - $blockName\n'; | |
| final endComment = '$endGeneratedComment - $blockName\n'; | |
| final int beginPreviousBlock = contents.indexOf(beginComment); | |
| final int endPreviousBlock = contents.indexOf(endComment); | |
| late String contentBeforeBlock; | |
| late String contentAfterBlock; | |
| if (beginPreviousBlock != -1) { | |
| if (endPreviousBlock < beginPreviousBlock) { | |
| print('Unable to find block named $blockName in $fileName, skipping code generation.'); | |
| return; | |
| } | |
| // Found a valid block matching the name, so record the content before and after. | |
| contentBeforeBlock = contents.substring(0, beginPreviousBlock); | |
| contentAfterBlock = contents.substring(endPreviousBlock + endComment.length); | |
| } else { | |
| // Just append to the bottom. | |
| contentBeforeBlock = contents; | |
| contentAfterBlock = ''; | |
| } | |
| final buffer = StringBuffer(contentBeforeBlock); | |
| buffer.write(beginComment); | |
| buffer.write(headerComment); | |
| buffer.write(generate()); | |
| buffer.write(footerComment); | |
| buffer.write(endComment); | |
| buffer.write(contentAfterBlock); | |
| File(fileName).writeAsStringSync(buffer.toString()); | |
| } | |
| /// Provide the generated content for the template. | |
| /// | |
| /// This abstract method needs to be implemented by subclasses | |
| /// to provide the content that [updateFile] will append to the | |
| /// bottom of the file. | |
| String generate(); | |
| /// Generate a [ColorScheme] color name for the given token. | |
| /// | |
| /// If there is a value for the given token, this will return | |
| /// the value prepended with [colorSchemePrefix]. | |
| /// | |
| /// Otherwise it will return [defaultValue] if provided or 'null' if not. | |
| /// | |
| /// If a [defaultValue] is not provided and the token doesn't exist, the token | |
| /// lookup is logged and a warning will be shown at the end of the process. | |
| /// | |
| /// See also: | |
| /// * [componentColor], that provides support for an optional opacity. | |
| String color(String colorToken, [String? defaultValue]) { | |
| final String effectiveDefault = defaultValue ?? 'null'; | |
| final dynamic tokenVal = getToken(colorToken, optional: defaultValue != null); | |
| return tokenVal == null ? effectiveDefault : '$colorSchemePrefix$tokenVal'; | |
| } | |
| /// Generate a [ColorScheme] color name for the given token or a transparent | |
| /// color if there is no value for the token. | |
| /// | |
| /// If there is a value for the given token, this will return | |
| /// the value prepended with [colorSchemePrefix]. | |
| /// | |
| /// Otherwise it will return 'Colors.transparent'. | |
| /// | |
| /// See also: | |
| /// * [componentColor], that provides support for an optional opacity. | |
| String? colorOrTransparent(String token) => color(token, 'Colors.transparent'); | |
| /// Generate a [ColorScheme] color name for the given component's color | |
| /// with opacity if available. | |
| /// | |
| /// If there is a value for the given component's color, this will return | |
| /// the value prepended with [colorSchemePrefix]. If there is also | |
| /// an opacity specified for the component, then the returned value | |
| /// will include this opacity calculation. | |
| /// | |
| /// If there is no value for the component's color, 'null' will be returned. | |
| /// | |
| /// See also: | |
| /// * [color], that provides support for looking up a raw color token. | |
| String componentColor(String componentToken) { | |
| final colorToken = '$componentToken.color'; | |
| if (!tokenAvailable(colorToken)) { | |
| return 'null'; | |
| } | |
| String value = color(colorToken); | |
| final opacityToken = '$componentToken.opacity'; | |
| if (tokenAvailable(opacityToken)) { | |
| value += '.withOpacity(${opacity(opacityToken)})'; | |
| } | |
| return value; | |
| } | |
| /// Generate the opacity value for the given token. | |
| String? opacity(String token) { | |
| tokenLogger.log(token); | |
| return _numToString(getToken(token)); | |
| } | |
| String? _numToString(Object? value, [int? digits]) { | |
| return switch (value) { | |
| null => null, | |
| double.infinity => 'double.infinity', | |
| num() when digits == null => value.toString(), | |
| num() => value.toStringAsFixed(digits!), | |
| _ => getToken(value as String).toString(), | |
| }; | |
| } | |
| /// Generate an elevation value for the given component token. | |
| String elevation(String componentToken) { | |
| return getToken(getToken('$componentToken.elevation')! as String)!.toString(); | |
| } | |
| /// Generate a size value for the given component token. | |
| /// | |
| /// Non-square sizes are specified as width and height. | |
| String size(String componentToken) { | |
| final sizeToken = '$componentToken.size'; | |
| if (!tokenAvailable(sizeToken)) { | |
| final widthToken = '$componentToken.width'; | |
| final heightToken = '$componentToken.height'; | |
| if (!tokenAvailable(widthToken) && !tokenAvailable(heightToken)) { | |
| throw Exception('Unable to find width, height, or size tokens for $componentToken'); | |
| } | |
| final String? width = _numToString( | |
| tokenAvailable(widthToken) ? getToken(widthToken)! as num : double.infinity, | |
| 0, | |
| ); | |
| final String? height = _numToString( | |
| tokenAvailable(heightToken) ? getToken(heightToken)! as num : double.infinity, | |
| 0, | |
| ); | |
| return 'const Size($width, $height)'; | |
| } | |
| return 'const Size.square(${_numToString(getToken(sizeToken))})'; | |
| } | |
| /// Generate a shape constant for the given component token. | |
| /// | |
| /// Currently supports family: | |
| /// - "SHAPE_FAMILY_ROUNDED_CORNERS" which maps to [RoundedRectangleBorder]. | |
| /// - "SHAPE_FAMILY_CIRCULAR" which maps to a [StadiumBorder]. | |
| String shape(String componentToken, [String prefix = 'const ']) { | |
| final shape = getToken(getToken('$componentToken.shape') as String) as Map<String, dynamic>; | |
| switch (shape['family']) { | |
| case 'SHAPE_FAMILY_ROUNDED_CORNERS': | |
| final topLeft = shape['topLeft'] as double; | |
| final topRight = shape['topRight'] as double; | |
| final bottomLeft = shape['bottomLeft'] as double; | |
| final bottomRight = shape['bottomRight'] as double; | |
| if (topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight) { | |
| if (topLeft == 0) { | |
| return '${prefix}RoundedRectangleBorder()'; | |
| } | |
| return '${prefix}RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular($topLeft)))'; | |
| } | |
| if (topLeft == topRight && bottomLeft == bottomRight) { | |
| return '${prefix}RoundedRectangleBorder(borderRadius: BorderRadius.vertical(' | |
| '${topLeft > 0 ? 'top: Radius.circular($topLeft)' : ''}' | |
| '${topLeft > 0 && bottomLeft > 0 ? ',' : ''}' | |
| '${bottomLeft > 0 ? 'bottom: Radius.circular($bottomLeft)' : ''}' | |
| '))'; | |
| } | |
| return '${prefix}RoundedRectangleBorder(borderRadius: ' | |
| 'BorderRadius.only(' | |
| 'topLeft: Radius.circular(${shape['topLeft']}), ' | |
| 'topRight: Radius.circular(${shape['topRight']}), ' | |
| 'bottomLeft: Radius.circular(${shape['bottomLeft']}), ' | |
| 'bottomRight: Radius.circular(${shape['bottomRight']})))'; | |
| case 'SHAPE_FAMILY_CIRCULAR': | |
| return '${prefix}StadiumBorder()'; | |
| } | |
| print('Unsupported shape family type: ${shape['family']} for $componentToken'); | |
| return ''; | |
| } | |
| /// Generate a [BorderSide] for the given component. | |
| String border(String componentToken) { | |
| if (!tokenAvailable('$componentToken.color')) { | |
| return 'null'; | |
| } | |
| final String borderColor = componentColor(componentToken); | |
| final width = | |
| (getToken('$componentToken.width', optional: true) ?? | |
| getToken('$componentToken.height', optional: true) ?? | |
| 1.0) | |
| as double; | |
| return 'BorderSide(color: $borderColor${width != 1.0 ? ", width: $width" : ""})'; | |
| } | |
| /// Generate a [TextTheme] text style name for the given component token. | |
| String textStyle(String componentToken) { | |
| return '$textThemePrefix${getToken("$componentToken.text-style")}'; | |
| } | |
| String textStyleWithColor(String componentToken) { | |
| if (!tokenAvailable('$componentToken.text-style')) { | |
| return 'null'; | |
| } | |
| String style = textStyle(componentToken); | |
| if (tokenAvailable('$componentToken.color')) { | |
| style = '$style?.copyWith(color: ${componentColor(componentToken)})'; | |
| } | |
| return style; | |
| } | |
| } |