mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

## Description This improves defaults generation with logging, stats, and token validation. This PR includes these changes: * introduce `TokenLogger`, with a verbose mode * prints versions and tokens usage to the console * outputs `generated/used_tokens.csv`, a list of all used tokens, for use by Google * find token files in `data` automatically * hide tokens `Map` * tokens can be obtained using existing resolvers (e.g. `color`, `shape`), or directly through `getToken`. * tokens can be checked for existence with `tokenAvailable` * remove version from template, since the tokens are aggregated and multiple versions are possible (as is the case currently), it does not make sense to attribute a single version * improve documentation ## Related Issues - Fixes https://github.com/flutter/flutter/issues/122602 ## Tests - Added tests for `TokenLogger` - Regenerated tokens, no-op except version removal ## Future work A future PR should replace or remove the following invalid tokens usages <img width="578" alt="image" src="https://github.com/flutter/flutter/assets/6655696/b6f9e5a7-523f-4f72-94f9-1b0bf4cc9f00">
269 lines
10 KiB
Dart
269 lines
10 KiB
Dart
// 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.
|
|
dynamic getToken(String tokenName) {
|
|
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.
|
|
|
|
''';
|
|
|
|
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.
|
|
Future<void> updateFile() async {
|
|
final String contents = File(fileName).readAsStringSync();
|
|
final String beginComment = '$beginGeneratedComment - $blockName\n';
|
|
final String 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 StringBuffer buffer = StringBuffer(contentBeforeBlock);
|
|
buffer.write(beginComment);
|
|
buffer.write(headerComment);
|
|
buffer.write(generate());
|
|
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].
|
|
///
|
|
/// See also:
|
|
/// * [componentColor], that provides support for an optional opacity.
|
|
String color(String colorToken, [String defaultValue = 'null']) {
|
|
return tokenAvailable(colorToken)
|
|
? '$colorSchemePrefix${getToken(colorToken)}'
|
|
: defaultValue;
|
|
}
|
|
|
|
/// 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 String colorToken = '$componentToken.color';
|
|
if (!tokenAvailable(colorToken)) {
|
|
return 'null';
|
|
}
|
|
String value = color(colorToken);
|
|
final String 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]) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
if (value is num) {
|
|
if (value == double.infinity) {
|
|
return 'double.infinity';
|
|
}
|
|
return digits == null ? value.toString() : value.toStringAsFixed(digits);
|
|
}
|
|
return 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 String sizeToken = '$componentToken.size';
|
|
if (!tokenAvailable(sizeToken)) {
|
|
final String widthToken = '$componentToken.width';
|
|
final String 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 Map<String, dynamic> shape = getToken(getToken('$componentToken.shape') as String) as Map<String, dynamic>;
|
|
switch (shape['family']) {
|
|
case 'SHAPE_FAMILY_ROUNDED_CORNERS':
|
|
final double topLeft = shape['topLeft'] as double;
|
|
final double topRight = shape['topRight'] as double;
|
|
final double bottomLeft = shape['bottomLeft'] as double;
|
|
final double 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 double width = (getToken('$componentToken.width') ?? getToken('$componentToken.height') ?? 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;
|
|
}
|
|
}
|