template.dart 6.79 KB
Newer Older
1 2 3 4 5 6 7
// 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 {
8
  const TokenTemplate(this.blockName, this.fileName, this.tokens, {
9 10 11
    this.colorSchemePrefix = 'Theme.of(context).colorScheme.',
    this.textThemePrefix = 'Theme.of(context).textTheme.'
  });
12

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
  /// 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 definitians.
  ///
  /// Defaults to 'Theme.of(context).textTheme.'
  final String textThemePrefix;

34 35
  static const String beginGeneratedComment = '''

36
// BEGIN GENERATED TOKEN PROPERTIES''';
37 38 39

  static const String headerComment = '''

40 41 42 43
// 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.
44 45 46 47 48

''';

  static const String endGeneratedComment = '''

49
// END GENERATED TOKEN PROPERTIES''';
50 51 52

  /// Replace or append the contents of the file with the text from [generate].
  ///
53 54 55
  /// 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.
56
  Future<void> updateFile() async {
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
    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 = '';
76
    }
77 78 79

    final StringBuffer buffer = StringBuffer(contentBeforeBlock);
    buffer.write(beginComment);
80
    buffer.write(headerComment);
81
    buffer.write('// Token database version: ${tokens['version']}\n\n');
82
    buffer.write(generate());
83 84
    buffer.write(endComment);
    buffer.write(contentAfterBlock);
85 86 87 88 89 90 91 92 93 94
    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();

95
  /// Generate a [ColorScheme] color name for the given token.
96
  ///
97 98 99 100 101 102 103 104 105
  /// 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)
106 107
      ? '$colorSchemePrefix${tokens[colorToken]}'
      : 'null';
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
  }

  /// 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);
127 128 129
    final String opacityToken = '$componentToken.opacity';
    if (tokens.containsKey(opacityToken)) {
      value += '.withOpacity(${opacity(opacityToken)})';
130 131 132 133
    }
    return value;
  }

134 135 136 137 138 139 140 141 142 143 144 145
  /// 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();
  }

146 147 148
  /// Generate an elevation value for the given component token.
  String elevation(String componentToken) {
    return tokens[tokens['$componentToken.elevation']!]!.toString();
149 150
  }

151 152
  /// Generate a shape constant for the given component token.
  ///
153 154 155
  /// Currently supports family:
  ///   - "SHAPE_FAMILY_ROUNDED_CORNERS" which maps to [RoundedRectangleBorder].
  ///   - "SHAPE_FAMILY_CIRCULAR" which maps to a [StadiumBorder].
156 157
  String shape(String componentToken) {
    final Map<String, dynamic> shape = tokens[tokens['$componentToken.shape']!]! as Map<String, dynamic>;
158 159 160 161 162 163 164 165 166 167 168 169 170
    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 '';
171 172
  }

173 174 175 176 177 178
  /// Generate a [BorderSide] for the given component.
  String border(String componentToken) {
    if (!tokens.containsKey('$componentToken.color')) {
      return 'null';
    }
    final String borderColor = componentColor(componentToken);
179
    final double width = (tokens['$componentToken.width'] ?? 1.0) as double;
180 181 182
    return 'BorderSide(color: $borderColor${width != 1.0 ? ", width: $width" : ""})';
  }

183 184
  /// Generate a [TextTheme] text style name for the given component token.
  String textStyle(String componentToken) {
185
    return '$textThemePrefix${tokens["$componentToken.text-style"]}';
186 187
  }
}