// 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'; abstract class TokenTemplate { const TokenTemplate(this.fileName, this.tokens, { this.colorSchemePrefix = 'Theme.of(context).colorScheme.', this.textThemePrefix = 'Theme.of(context).textTheme.' }); static const String beginGeneratedComment = ''' // BEGIN GENERATED TOKEN PROPERTIES '''; static const String headerComment = ''' // Generated code to the end of this file. Do not edit by hand. // These defaults are generated from the Material Design Token // database by the script dev/tools/gen_defaults/bin/gen_defaults.dart. '''; static const String endGeneratedComment = ''' // END GENERATED TOKEN PROPERTIES '''; final String fileName; final Map<String, dynamic> tokens; final String colorSchemePrefix; final String textThemePrefix; /// Replace or append the contents of the file with the text from [generate]. /// /// If the file already contains generated block at the end, 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 { String contents = File(fileName).readAsStringSync(); final int previousGeneratedIndex = contents.indexOf(beginGeneratedComment); if (previousGeneratedIndex != -1) { contents = contents.substring(0, previousGeneratedIndex); } final StringBuffer buffer = StringBuffer(contents); buffer.write(beginGeneratedComment); buffer.write(headerComment); buffer.write(generate()); buffer.write(endGeneratedComment); 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 'null'. /// /// See also: /// * [componentColor], that provides support for an optional opacity. String color(String colorToken) { return tokens.containsKey(colorToken) ? '$colorSchemePrefix${tokens[colorToken]}' : 'null'; } /// 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 (!tokens.containsKey(colorToken)) return 'null'; String value = color(colorToken); final String opacityToken = '$componentToken.opacity'; if (tokens.containsKey(opacityToken)) { value += '.withOpacity(${opacity(opacityToken)})'; } return value; } /// Generate the opacity value for the given token. String? opacity(String token) { final dynamic value = tokens[token]; if (value == null) { return null; } if (value is double) { return value.toString(); } return tokens[value].toString(); } /// Generate an elevation value for the given component token. String elevation(String componentToken) { return tokens[tokens['$componentToken.elevation']!]!.toString(); } /// 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) { final Map<String, dynamic> shape = tokens[tokens['$componentToken.shape']!]! as Map<String, dynamic>; switch (shape['family']) { case 'SHAPE_FAMILY_ROUNDED_CORNERS': return 'const 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 'const StadiumBorder()'; } print('Unsupported shape family type: ${shape['family']} for $componentToken'); return ''; } /// Generate a [BorderSide] for the given component. String border(String componentToken) { if (!tokens.containsKey('$componentToken.color')) { return 'null'; } final String borderColor = componentColor(componentToken); final double width = (tokens['$componentToken.width'] ?? 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${tokens["$componentToken.text-style"]}'; } }