Auto-format Framework (#160545)

This auto-formats all *.dart files in the repository outside of the
`engine` subdirectory and enforces that these files stay formatted with
a presubmit check.

**Reviewers:** Please carefully review all the commits except for the
one titled "formatted". The "formatted" commit was auto-generated by
running `dev/tools/format.sh -a -f`. The other commits were hand-crafted
to prepare the repo for the formatting change. I recommend reviewing the
commits one-by-one via the "Commits" tab and avoiding Github's "Files
changed" tab as it will likely slow down your browser because of the
size of this PR.

---------

Co-authored-by: Kate Lovett <katelovett@google.com>
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
This commit is contained in:
Michael Goderbauer
2024-12-19 12:06:21 -08:00
committed by GitHub
parent 8e0993eda8
commit 5491c8c146
4411 changed files with 455108 additions and 415991 deletions

View File

@@ -17,25 +17,23 @@ import 'dart:io';
import 'package:args/args.dart';
import 'package:crypto/crypto.dart';
enum GitRevisionStrategy {
mergeBase,
head,
}
enum GitRevisionStrategy { mergeBase, head }
final RegExp _hashRegex = RegExp(r'^([a-fA-F0-9]+)');
final ArgParser parser = ArgParser()
..addOption(
'strategy',
abbr: 's',
allowed: <String>['head', 'mergeBase'],
defaultsTo: 'head',
allowedHelp: <String, String>{
'head': 'hash from git HEAD',
'mergeBase': 'hash from the merge-base of HEAD and upstream/master',
},
)
..addFlag('help', abbr: 'h', negatable: false);
final ArgParser parser =
ArgParser()
..addOption(
'strategy',
abbr: 's',
allowed: <String>['head', 'mergeBase'],
defaultsTo: 'head',
allowedHelp: <String, String>{
'head': 'hash from git HEAD',
'mergeBase': 'hash from the merge-base of HEAD and upstream/master',
},
)
..addFlag('help', abbr: 'h', negatable: false);
Never printHelp({String? error}) {
final Stdout out = error != null ? stderr : stdout;
@@ -65,14 +63,9 @@ Future<int> main(List<String> args) async {
final String result;
try {
result = await engineHash(
(List<String> command) => Process.run(
command.first,
command.sublist(1),
stdoutEncoding: utf8,
),
revisionStrategy: GitRevisionStrategy.values.byName(
arguments.option('strategy')!,
),
(List<String> command) =>
Process.run(command.first, command.sublist(1), stdoutEncoding: utf8),
revisionStrategy: GitRevisionStrategy.values.byName(arguments.option('strategy')!),
);
} catch (e) {
stderr.writeln('Error calculating engine hash: $e');
@@ -95,14 +88,12 @@ Future<String> engineHash(
case GitRevisionStrategy.head:
base = 'HEAD';
case GitRevisionStrategy.mergeBase:
final ProcessResult processResult = await runProcess(
<String>[
'git',
'merge-base',
'upstream/master',
'HEAD',
],
);
final ProcessResult processResult = await runProcess(<String>[
'git',
'merge-base',
'upstream/master',
'HEAD',
]);
if (processResult.exitCode != 0) {
throw '''
@@ -110,8 +101,7 @@ Unable to find merge-base hash of the repository:
${processResult.stderr}''';
}
final Match? baseHash =
_hashRegex.matchAsPrefix(processResult.stdout as String);
final Match? baseHash = _hashRegex.matchAsPrefix(processResult.stdout as String);
if (baseHash?.groupCount != 1) {
throw '''
Unable to parse merge-base hash of the repository
@@ -124,9 +114,14 @@ ${processResult.stdout}''';
// This is important for future filtering of files, but also do not include
// the developer's changes / in flight PRs.
// The presence `engine` and `DEPS` are signals that you live in a monorepo world.
final ProcessResult processResult = await runProcess(
<String>['git', 'ls-tree', '-r', base, 'engine', 'DEPS'],
);
final ProcessResult processResult = await runProcess(<String>[
'git',
'ls-tree',
'-r',
base,
'engine',
'DEPS',
]);
if (processResult.exitCode != 0) {
throw '''
@@ -140,8 +135,7 @@ ${processResult.stderr}''';
throw 'Not in a monorepo';
}
final Iterable<String> treeLines =
LineSplitter.split(processResult.stdout as String);
final Iterable<String> treeLines = LineSplitter.split(processResult.stdout as String);
// We could call `git hash-object --stdin` which would just take the input, calculate the size,
// and then sha1sum it like: `blob $size\0$string'. However, that can have different line endings.

View File

@@ -20,7 +20,8 @@ void log(String message) {
const String _commitTimestampFormat = '--format=%cI';
DateTime _parseTimestamp(String line) => DateTime.parse(line.trim());
int _countLines(String output) => output.trim().split('/n').where((String line) => line.isNotEmpty).length;
int _countLines(String output) =>
output.trim().split('/n').where((String line) => line.isNotEmpty).length;
String findCommit({
required String primaryRepoDirectory,
@@ -32,20 +33,49 @@ String findCommit({
final DateTime anchor;
if (primaryBranch == primaryTrunk) {
log('on $primaryTrunk, using last commit time');
anchor = _parseTimestamp(git(primaryRepoDirectory, <String>['log', _commitTimestampFormat, '--max-count=1', primaryBranch, '--']));
anchor = _parseTimestamp(
git(primaryRepoDirectory, <String>[
'log',
_commitTimestampFormat,
'--max-count=1',
primaryBranch,
'--',
]),
);
} else {
final String mergeBase = git(primaryRepoDirectory, <String>['merge-base', primaryBranch, primaryTrunk], allowFailure: true).trim();
final String mergeBase =
git(primaryRepoDirectory, <String>[
'merge-base',
primaryBranch,
primaryTrunk,
], allowFailure: true).trim();
if (mergeBase.isEmpty) {
throw StateError('Branch $primaryBranch does not seem to have a common history with trunk $primaryTrunk.');
throw StateError(
'Branch $primaryBranch does not seem to have a common history with trunk $primaryTrunk.',
);
}
anchor = _parseTimestamp(git(primaryRepoDirectory, <String>['log', _commitTimestampFormat, '--max-count=1', mergeBase, '--']));
anchor = _parseTimestamp(
git(primaryRepoDirectory, <String>[
'log',
_commitTimestampFormat,
'--max-count=1',
mergeBase,
'--',
]),
);
if (debugLogging) {
final int missingTrunkCommits = _countLines(git(primaryRepoDirectory, <String>['rev-list', primaryTrunk, '^$primaryBranch', '--']));
final int extraCommits = _countLines(git(primaryRepoDirectory, <String>['rev-list', primaryBranch, '^$primaryTrunk', '--']));
final int missingTrunkCommits = _countLines(
git(primaryRepoDirectory, <String>['rev-list', primaryTrunk, '^$primaryBranch', '--']),
);
final int extraCommits = _countLines(
git(primaryRepoDirectory, <String>['rev-list', primaryBranch, '^$primaryTrunk', '--']),
);
if (missingTrunkCommits == 0 && extraCommits == 0) {
log('$primaryBranch is even with $primaryTrunk at $mergeBase');
} else {
log('$primaryBranch branched from $primaryTrunk $missingTrunkCommits commits ago, trunk has advanced by $extraCommits commits since then.');
log(
'$primaryBranch branched from $primaryTrunk $missingTrunkCommits commits ago, trunk has advanced by $extraCommits commits since then.',
);
}
}
}
@@ -60,7 +90,11 @@ String findCommit({
}
String git(String workingDirectory, List<String> arguments, {bool allowFailure = false}) {
final ProcessResult result = Process.runSync('git', arguments, workingDirectory: workingDirectory);
final ProcessResult result = Process.runSync(
'git',
arguments,
workingDirectory: workingDirectory,
);
if (!allowFailure && result.exitCode != 0 || '${result.stderr}'.isNotEmpty) {
throw ProcessException('git', arguments, '${result.stdout}${result.stderr}', result.exitCode);
}
@@ -68,24 +102,29 @@ String git(String workingDirectory, List<String> arguments, {bool allowFailure =
}
void main(List<String> arguments) {
if (arguments.isEmpty || arguments.length != 4 || arguments.contains('--help') || arguments.contains('-h')) {
if (arguments.isEmpty ||
arguments.length != 4 ||
arguments.contains('--help') ||
arguments.contains('-h')) {
print(
'Usage: dart find_commit.dart <path-to-primary-repo> <primary-trunk> <path-to-secondary-repo> <secondary-branch>\n'
'This script will find the commit in the secondary repo that was contemporary\n'
'when the commit in the primary repo was created. If that commit is on a\n'
"branch, then the date of the branch's last merge is used instead."
"branch, then the date of the branch's last merge is used instead.",
);
} else {
final String primaryRepo = arguments.first;
final String primaryTrunk = arguments[1];
final String secondaryRepo = arguments[2];
final String secondaryBranch = arguments.last;
print(findCommit(
primaryRepoDirectory: primaryRepo,
primaryBranch: git(primaryRepo, <String>['rev-parse', '--abbrev-ref', 'HEAD']).trim(),
primaryTrunk: primaryTrunk,
secondaryRepoDirectory: secondaryRepo,
secondaryBranch: secondaryBranch,
).trim());
print(
findCommit(
primaryRepoDirectory: primaryRepo,
primaryBranch: git(primaryRepo, <String>['rev-parse', '--abbrev-ref', 'HEAD']).trim(),
primaryTrunk: primaryTrunk,
secondaryRepoDirectory: secondaryRepo,
secondaryBranch: secondaryBranch,
).trim(),
);
}
}

View File

@@ -11,13 +11,18 @@ import 'package:process_runner/process_runner.dart';
Future<int> main(List<String> arguments) async {
final ArgParser parser = ArgParser();
parser.addFlag('help', help: 'Print help.', abbr: 'h');
parser.addFlag('fix',
abbr: 'f',
help: 'Instead of just checking for formatting errors, fix them in place.');
parser.addFlag('all-files',
abbr: 'a',
help: 'Instead of just checking for formatting errors in changed files, '
'check for them in all files.');
parser.addFlag(
'fix',
abbr: 'f',
help: 'Instead of just checking for formatting errors, fix them in place.',
);
parser.addFlag(
'all-files',
abbr: 'a',
help:
'Instead of just checking for formatting errors in changed files, '
'check for them in all files.',
);
late final ArgResults options;
try {
@@ -34,10 +39,12 @@ Future<int> main(List<String> arguments) async {
final File script = File.fromUri(Platform.script).absolute;
final Directory flutterRoot = script.parent.parent.parent.parent;
final bool result = (await DartFormatChecker(
flutterRoot: flutterRoot,
allFiles: options['all-files'] as bool,
).check(fix: options['fix'] as bool)) == 0;
final bool result =
(await DartFormatChecker(
flutterRoot: flutterRoot,
allFiles: options['all-files'] as bool,
).check(fix: options['fix'] as bool)) ==
0;
exit(result ? 0 : 1);
}
@@ -49,12 +56,8 @@ void _usage(ArgParser parser, {int exitCode = 1}) {
}
class DartFormatChecker {
DartFormatChecker({
required this.flutterRoot,
required this.allFiles,
}) : processRunner = ProcessRunner(
defaultWorkingDirectory: flutterRoot,
);
DartFormatChecker({required this.flutterRoot, required this.allFiles})
: processRunner = ProcessRunner(defaultWorkingDirectory: flutterRoot);
final Directory flutterRoot;
final bool allFiles;
@@ -67,10 +70,7 @@ class DartFormatChecker {
allFiles: allFiles,
baseGitRef: baseGitRef,
);
return _checkFormat(
filesToCheck: filesToCheck,
fix: fix,
);
return _checkFormat(filesToCheck: filesToCheck, fix: fix);
}
Future<String> _getDiffBaseRevision() async {
@@ -89,10 +89,12 @@ class DartFormatChecker {
// This is the preferred command to use, but developer checkouts often do
// not have a clear fork point, so we fall back to just the regular
// merge-base in that case.
result = await _runGit(
<String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
processRunner,
);
result = await _runGit(<String>[
'merge-base',
'--fork-point',
'FETCH_HEAD',
'HEAD',
], processRunner);
} on ProcessRunnerException {
result = await _runGit(<String>['merge-base', 'FETCH_HEAD', 'HEAD'], processRunner);
}
@@ -100,14 +102,14 @@ class DartFormatChecker {
}
Future<String> _runGit(
List<String> args,
ProcessRunner processRunner, {
bool failOk = false,
}) async {
final ProcessRunnerResult result = await processRunner.runProcess(
<String>['git', ...args],
failOk: failOk,
);
List<String> args,
ProcessRunner processRunner, {
bool failOk = false,
}) async {
final ProcessRunnerResult result = await processRunner.runProcess(<String>[
'git',
...args,
], failOk: failOk);
return result.stdout;
}
@@ -118,11 +120,7 @@ class DartFormatChecker {
}) async {
String output;
if (allFiles) {
output = await _runGit(<String>[
'ls-files',
'--',
...types,
], processRunner);
output = await _runGit(<String>['ls-files', '--', ...types], processRunner);
} else {
output = await _runGit(<String>[
'diff',
@@ -135,13 +133,13 @@ class DartFormatChecker {
...types,
], processRunner);
}
return output.split('\n').where((String line) => line.isNotEmpty).toList();
return output
.split('\n')
.where((String line) => line.isNotEmpty && !line.startsWith('engine'))
.toList();
}
Future<int> _checkFormat({
required List<String> filesToCheck,
required bool fix,
}) async {
Future<int> _checkFormat({required List<String> filesToCheck, required bool fix}) async {
final List<String> cmd = <String>[
path.join(flutterRoot.path, 'bin', 'dart'),
'format',
@@ -170,19 +168,16 @@ class DartFormatChecker {
errorJobs.add(completedJob);
} else if (completedJob.result.exitCode == 1) {
diffJobs.add(
WorkerJob(
<String>[
'git',
'diff',
'--no-index',
'--no-color',
'--ignore-cr-at-eol',
'--',
completedJob.command.last,
'-',
],
stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw),
),
WorkerJob(<String>[
'git',
'diff',
'--no-index',
'--no-color',
'--ignore-cr-at-eol',
'--',
completedJob.command.last,
'-',
], stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw)),
);
}
}
@@ -210,23 +205,31 @@ class DartFormatChecker {
if (incorrect.isNotEmpty) {
final bool plural = incorrect.length > 1;
if (fix) {
stdout.writeln('Fixing ${incorrect.length} dart file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
stdout.writeln(
'Fixing ${incorrect.length} dart file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.',
);
} else {
stderr.writeln('Found ${incorrect.length} Dart file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
final String fileList = incorrect.map(
(WorkerJob job) => job.command[job.command.length - 2]
).join(' ');
stderr.writeln(
'Found ${incorrect.length} Dart file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.',
);
final String fileList = incorrect
.map((WorkerJob job) => job.command[job.command.length - 2])
.join(' ');
stdout.writeln();
stdout.writeln('To fix, run `dart format $fileList` or:');
stdout.writeln();
stdout.writeln('git apply <<DONE');
for (final WorkerJob job in incorrect) {
stdout.write(job.result.stdout
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst(RegExp('\\+Formatted \\d+ files? \\(\\d+ changed\\) in \\d+.\\d+ seconds.\n'), '')
stdout.write(
job.result.stdout
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst(
RegExp('\\+Formatted \\d+ files? \\(\\d+ changed\\) in \\d+.\\d+ seconds.\n'),
'',
),
);
}
stdout.writeln('DONE');
@@ -244,7 +247,9 @@ class DartFormatChecker {
void _printErrorJobs(List<WorkerJob> errorJobs) {
if (errorJobs.isNotEmpty) {
final bool plural = errorJobs.length > 1;
stderr.writeln('The formatter failed to run on ${errorJobs.length} Dart file${plural ? 's' : ''}.');
stderr.writeln(
'The formatter failed to run on ${errorJobs.length} Dart file${plural ? 's' : ''}.',
);
stdout.writeln();
for (final WorkerJob job in errorJobs) {
stdout.writeln('--> ${job.command.last} produced the following error:');
@@ -257,19 +262,20 @@ class DartFormatChecker {
ProcessPoolProgressReporter _namedReport(String name) {
return (int total, int completed, int inProgress, int pending, int failed) {
final String percent =
total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3);
final String percent = total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3);
final String completedStr = completed.toString().padLeft(3);
final String totalStr = total.toString().padRight(3);
final String inProgressStr = inProgress.toString().padLeft(2);
final String pendingStr = pending.toString().padLeft(3);
final String failedStr = failed.toString().padLeft(3);
stdout.write('$name Jobs: $percent% done, '
'$completedStr/$totalStr completed, '
'$inProgressStr in progress, '
'$pendingStr pending, '
'$failedStr failed.${' ' * 20}\r');
stdout.write(
'$name Jobs: $percent% done, '
'$completedStr/$totalStr completed, '
'$inProgressStr in progress, '
'$pendingStr pending, '
'$failedStr failed.${' ' * 20}\r',
);
};
}

View File

@@ -18,23 +18,27 @@ import 'package:file/local.dart';
import 'package:yaml/yaml.dart';
void main(List<String> arguments) {
const String usageMessage = "If you don't wish to re-generate the "
const String usageMessage =
"If you don't wish to re-generate the "
'settings.gradle, build.gradle, and gradle-wrapper.properties files,\n'
'add the flag `--no-gradle-generation`.\n'
'This tool automatically excludes a set of android subdirectories, '
'defined at dev/tools/bin/config/lockfile_exclusion.yaml.\n'
'To disable this behavior, run with `--no-exclusion`.\n';
final ArgParser argParser = ArgParser()
..addFlag(
'gradle-generation',
help: 'Re-generate gradle files in each processed directory.',
defaultsTo: true,
)..addFlag(
'exclusion',
help: 'Run the script using the config file at ./configs/lockfile_exclusion.yaml to skip the specified subdirectories.',
defaultsTo: true,
);
final ArgParser argParser =
ArgParser()
..addFlag(
'gradle-generation',
help: 'Re-generate gradle files in each processed directory.',
defaultsTo: true,
)
..addFlag(
'exclusion',
help:
'Run the script using the config file at ./configs/lockfile_exclusion.yaml to skip the specified subdirectories.',
defaultsTo: true,
);
ArgResults args;
try {
@@ -55,17 +59,16 @@ void main(List<String> arguments) {
const FileSystem fileSystem = LocalFileSystem();
final Directory repoRoot = (() {
final String repoRootPath = exec(
'git',
const <String>['rev-parse', '--show-toplevel'],
).trim();
final Directory repoRoot = fileSystem.directory(repoRootPath);
if (!repoRoot.existsSync()) {
throw StateError("Expected $repoRoot to exist but it didn't!");
}
return repoRoot;
})();
final Directory repoRoot =
(() {
final String repoRootPath =
exec('git', const <String>['rev-parse', '--show-toplevel']).trim();
final Directory repoRoot = fileSystem.directory(repoRootPath);
if (!repoRoot.existsSync()) {
throw StateError("Expected $repoRoot to exist but it didn't!");
}
return repoRoot;
})();
final Iterable<Directory> androidDirectories = discoverAndroidDirectories(repoRoot);
@@ -80,10 +83,9 @@ void main(List<String> arguments) {
final Set<String> exclusionSet;
if (useExclusion) {
exclusionSet = HashSet<String>.from(
(loadYaml(exclusionFile.readAsStringSync()) as YamlList)
.toList()
.cast<String>()
.map((String s) => '${repoRoot.path}/$s')
(loadYaml(exclusionFile.readAsStringSync()) as YamlList).toList().cast<String>().map(
(String s) => '${repoRoot.path}/$s',
),
);
print('Loaded exclusion file from ${exclusionFile.path}.');
} else {
@@ -97,7 +99,9 @@ void main(List<String> arguments) {
}
if (exclusionSet.contains(androidDirectory.path)) {
print('${androidDirectory.path} is included in the exclusion config file at ${exclusionFile.path} - skipping');
print(
'${androidDirectory.path} is included in the exclusion config file at ${exclusionFile.path} - skipping',
);
continue;
}
@@ -117,7 +121,9 @@ void main(List<String> arguments) {
} else if (androidDirectory.childFile('settings.gradle.kts').existsSync()) {
settingsGradle = androidDirectory.childFile('settings.gradle.kts');
} else {
print('${androidDirectory.childFile('settings.gradle').path}(.kts) does not exist - skipping');
print(
'${androidDirectory.childFile('settings.gradle').path}(.kts) does not exist - skipping',
);
continue;
}
@@ -145,15 +151,15 @@ void main(List<String> arguments) {
continue;
}
if (androidDirectory.parent.childFile('pubspec.yaml').readAsStringSync().contains('deferred-components')) {
if (androidDirectory.parent
.childFile('pubspec.yaml')
.readAsStringSync()
.contains('deferred-components')) {
print('${rootBuildGradle.path} uses deferred components - skipping');
continue;
}
if (!androidDirectory.parent
.childDirectory('lib')
.childFile('main.dart')
.existsSync()) {
if (!androidDirectory.parent.childDirectory('lib').childFile('main.dart').existsSync()) {
print('${rootBuildGradle.path} no main.dart under lib - skipping');
continue;
}
@@ -180,43 +186,29 @@ void main(List<String> arguments) {
final String appDirectory = androidDirectory.parent.absolute.path;
// Fetch pub dependencies.
final String flutterPath = repoRoot
.childDirectory('bin')
.childFile('flutter')
.path;
final String flutterPath = repoRoot.childDirectory('bin').childFile('flutter').path;
exec(flutterPath, <String>['pub', 'get'], workingDirectory: appDirectory);
// Verify that the Gradlew wrapper exists.
final File gradleWrapper = androidDirectory.childFile('gradlew');
// Generate Gradle wrapper if it doesn't exist.
if (!gradleWrapper.existsSync()) {
exec(
flutterPath,
<String>['build', 'apk', '--config-only'],
workingDirectory: appDirectory,
);
exec(flutterPath, <String>['build', 'apk', '--config-only'], workingDirectory: appDirectory);
}
// Generate lock files.
exec(
gradleWrapper.absolute.path,
<String>[':generateLockfiles'],
workingDirectory: androidDirectory.absolute.path,
);
exec(gradleWrapper.absolute.path, <String>[
':generateLockfiles',
], workingDirectory: androidDirectory.absolute.path);
print('Processed');
}
}
String exec(
String cmd,
List<String> args, {
String? workingDirectory,
}) {
String exec(String cmd, List<String> args, {String? workingDirectory}) {
final ProcessResult result = Process.runSync(cmd, args, workingDirectory: workingDirectory);
if (result.exitCode != 0) {
throw ProcessException(
cmd, args, '${result.stdout}${result.stderr}', result.exitCode);
throw ProcessException(cmd, args, '${result.stdout}${result.stderr}', result.exitCode);
}
return result.stdout as String;
}
@@ -311,7 +303,8 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
''';
Iterable<Directory> discoverAndroidDirectories(Directory repoRoot) {
return repoRoot.listSync(recursive: true)
return repoRoot
.listSync(recursive: true)
.whereType<Directory>()
.where((FileSystemEntity entity) => entity.basename == 'android');
}

View File

@@ -26,8 +26,8 @@ const String kPlatformIntegrationPackageName = 'platform_integration';
/// Additional package dependencies that we want to have in the docs,
/// but not actually depend on them.
const Map<String, (String path, String version)> kFakeDependencies = <String, (String, String)>{
'flutter_gpu': ('flutter_gpu/gpu.dart', '\n sdk: flutter'),
};
'flutter_gpu': ('flutter_gpu/gpu.dart', '\n sdk: flutter'),
};
class PlatformDocsSection {
const PlatformDocsSection({
@@ -102,7 +102,11 @@ Future<void> main(List<String> arguments) async {
// The place to find customization files and configuration files for docs
// generation.
final Directory docsRoot =
FlutterInformation.instance.getFlutterRoot().childDirectory('dev').childDirectory('docs').absolute;
FlutterInformation.instance
.getFlutterRoot()
.childDirectory('dev')
.childDirectory('docs')
.absolute;
final ArgParser argParser = _createArgsParser(
publishDefault: docsRoot.childDirectory('doc').path,
);
@@ -133,7 +137,10 @@ Future<void> main(List<String> arguments) async {
);
configurator.generateConfiguration();
final PlatformDocGenerator platformGenerator = PlatformDocGenerator(outputDir: publishRoot, filesystem: filesystem);
final PlatformDocGenerator platformGenerator = PlatformDocGenerator(
outputDir: publishRoot,
filesystem: filesystem,
);
await platformGenerator.generatePlatformDocs();
final DartdocGenerator dartdocGenerator = DartdocGenerator(
@@ -154,14 +161,27 @@ Future<void> main(List<String> arguments) async {
ArgParser _createArgsParser({required String publishDefault}) {
final ArgParser parser = ArgParser();
parser.addFlag('help', abbr: 'h', negatable: false, help: 'Show command help.');
parser.addFlag('verbose',
defaultsTo: true,
help: 'Whether to report all error messages (on) or attempt to '
'filter out some known false positives (off). Shut this off '
'locally if you want to address Flutter-specific issues.');
parser.addFlag('json', help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.');
parser.addFlag('validate-links', help: 'Display warnings for broken links generated by dartdoc (slow)');
parser.addOption('output-dir', defaultsTo: publishDefault, help: 'Sets the output directory for the documentation.');
parser.addFlag(
'verbose',
defaultsTo: true,
help:
'Whether to report all error messages (on) or attempt to '
'filter out some known false positives (off). Shut this off '
'locally if you want to address Flutter-specific issues.',
);
parser.addFlag(
'json',
help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.',
);
parser.addFlag(
'validate-links',
help: 'Display warnings for broken links generated by dartdoc (slow)',
);
parser.addOption(
'output-dir',
defaultsTo: publishDefault,
help: 'Sets the output directory for the documentation.',
);
return parser;
}
@@ -216,13 +236,16 @@ class Configurator {
_createPageFooter(packageRoot, version);
_copyCustomizations();
_createSearchMetadata(
docsRoot.childDirectory('lib').childFile('opensearch.xml'), publishRoot.childFile('opensearch.xml'));
docsRoot.childDirectory('lib').childFile('opensearch.xml'),
publishRoot.childFile('opensearch.xml'),
);
}
Future<void> generateOfflineAssetsIfNeeded() async {
// Only create the offline docs if we're running in a non-presubmit build:
// it takes too long otherwise.
if (platform.environment.containsKey('LUCI_CI') && (platform.environment['LUCI_PR'] ?? '').isEmpty) {
if (platform.environment.containsKey('LUCI_CI') &&
(platform.environment['LUCI_PR'] ?? '').isEmpty) {
_createOfflineZipFile();
await _createDocset();
_moveOfflineIntoPlace();
@@ -264,8 +287,7 @@ class Configurator {
'environment:',
" sdk: '>=3.2.0-0 <4.0.0'",
'dependencies:',
for (final String package in findPackageNames(filesystem))
' $package:\n sdk: flutter',
for (final String package in findPackageNames(filesystem)) ' $package:\n sdk: flutter',
' $kPlatformIntegrationPackageName: 0.0.1',
for (final String package in kFakeDependencies.keys)
' $package: ${kFakeDependencies[package]!.$2}',
@@ -329,12 +351,15 @@ class Configurator {
// /foo/../foo/bar/baz won't compare as identical.
if (path.canonicalize(source.absolute.path) != path.canonicalize(destination.absolute.path)) {
source.copySync(destination.path);
print('Copied ${path.canonicalize(source.absolute.path)} to ${path.canonicalize(destination.absolute.path)}');
print(
'Copied ${path.canonicalize(source.absolute.path)} to ${path.canonicalize(destination.absolute.path)}',
);
}
}
final Directory assetsDir = filesystem.directory(publishRoot.childDirectory('assets'));
final Directory assetSource = docsRoot.childDirectory('assets');
if (path.canonicalize(assetSource.absolute.path) == path.canonicalize(assetsDir.absolute.path)) {
if (path.canonicalize(assetSource.absolute.path) ==
path.canonicalize(assetsDir.absolute.path)) {
// Don't try and copy the directory over itself.
return;
}
@@ -346,7 +371,9 @@ class Configurator {
assetSource,
assetsDir,
onFileCopied: (File src, File dest) {
print('Copied ${path.canonicalize(src.absolute.path)} to ${path.canonicalize(dest.absolute.path)}');
print(
'Copied ${path.canonicalize(src.absolute.path)} to ${path.canonicalize(dest.absolute.path)}',
);
},
filesystem: filesystem,
);
@@ -378,17 +405,14 @@ class Configurator {
// never get to see the logs. Thus, we run it in the background and tail the
// logs only if it fails.
final ProcessWrapper result = ProcessWrapper(
await processManager.start(
<String>[
'dashing',
'build',
'--source',
publishRoot.path,
'--config',
docsRoot.childFile('dashing.json').path,
],
workingDirectory: packageRoot.path,
),
await processManager.start(<String>[
'dashing',
'build',
'--source',
publishRoot.path,
'--config',
docsRoot.childFile('dashing.json').path,
], workingDirectory: packageRoot.path),
);
final List<int> buffer = <int>[];
result.stdout.listen(buffer.addAll);
@@ -404,21 +428,28 @@ class Configurator {
buffer.clear();
// Copy the favicon file to the output directory.
final File faviconFile =
publishRoot.childDirectory('flutter').childDirectory('static-assets').childFile('favicon.png');
final File faviconFile = publishRoot
.childDirectory('flutter')
.childDirectory('static-assets')
.childFile('favicon.png');
final File iconFile = packageRoot.childDirectory('flutter.docset').childFile('icon.png');
faviconFile
..createSync(recursive: true)
..copySync(iconFile.path);
// Post-process the dashing output.
final File infoPlist =
packageRoot.childDirectory('flutter.docset').childDirectory('Contents').childFile('Info.plist');
final File infoPlist = packageRoot
.childDirectory('flutter.docset')
.childDirectory('Contents')
.childFile('Info.plist');
String contents = infoPlist.readAsStringSync();
// Since I didn't want to add the XML package as a dependency just for this,
// I just used a regular expression to make this simple change.
final RegExp findRe = RegExp(r'(\s*<key>DocSetPlatformFamily</key>\s*<string>)[^<]+(</string>)', multiLine: true);
final RegExp findRe = RegExp(
r'(\s*<key>DocSetPlatformFamily</key>\s*<string>)[^<]+(</string>)',
multiLine: true,
);
contents = contents.replaceAllMapped(findRe, (Match match) {
return '${match.group(1)}dartlang${match.group(2)}';
});
@@ -427,20 +458,32 @@ class Configurator {
if (!offlineDir.existsSync()) {
offlineDir.createSync(recursive: true);
}
tarDirectory(packageRoot, offlineDir.childFile('flutter.docset.tar.gz'), processManager: processManager);
tarDirectory(
packageRoot,
offlineDir.childFile('flutter.docset.tar.gz'),
processManager: processManager,
);
// Write the Dash/Zeal XML feed file.
final bool isStable = platform.environment['LUCI_BRANCH'] == 'stable';
offlineDir.childFile('flutter.xml').writeAsStringSync('<entry>\n'
' <version>${FlutterInformation.instance.getFlutterVersion()}</version>\n'
' <url>https://${isStable ? '' : 'main-'}api.flutter.dev/offline/flutter.docset.tar.gz</url>\n'
'</entry>\n');
offlineDir
.childFile('flutter.xml')
.writeAsStringSync(
'<entry>\n'
' <version>${FlutterInformation.instance.getFlutterVersion()}</version>\n'
' <url>https://${isStable ? '' : 'main-'}api.flutter.dev/offline/flutter.docset.tar.gz</url>\n'
'</entry>\n',
);
}
// Creates the offline ZIP file containing all of the website HTML files.
void _createOfflineZipFile() {
print('${DateTime.now().toLocal()}: Creating offline docs archive.');
zipDirectory(publishRoot, packageRoot.childFile('flutter.docs.zip'), processManager: processManager);
zipDirectory(
publishRoot,
packageRoot.childFile('flutter.docs.zip'),
processManager: processManager,
);
}
// Moves the generated offline archives into the publish directory so that
@@ -448,7 +491,9 @@ class Configurator {
void _moveOfflineIntoPlace() {
print('${DateTime.now().toUtc()}: Moving offline docs into place.');
final Directory offlineDir = publishRoot.childDirectory('offline')..createSync(recursive: true);
packageRoot.childFile('flutter.docs.zip').renameSync(offlineDir.childFile('flutter.docs.zip').path);
packageRoot
.childFile('flutter.docs.zip')
.renameSync(offlineDir.childFile('flutter.docs.zip').path);
}
// Creates a robots.txt file that disallows indexing unless the branch is the
@@ -522,13 +567,15 @@ class DartdocGenerator {
}
// Run pub.
ProcessWrapper process = ProcessWrapper(await runPubProcess(
arguments: <String>['get'],
workingDirectory: packageRoot,
environment: pubEnvironment,
filesystem: filesystem,
processManager: processManager,
));
ProcessWrapper process = ProcessWrapper(
await runPubProcess(
arguments: <String>['get'],
workingDirectory: packageRoot,
environment: pubEnvironment,
filesystem: filesystem,
processManager: processManager,
),
);
printStream(process.stdout, prefix: 'pub:stdout: ');
printStream(process.stderr, prefix: 'pub:stderr: ');
final int code = await process.done;
@@ -540,20 +587,16 @@ class DartdocGenerator {
// Verify which version of the global activated packages we're using.
final ProcessResult versionResults = processManager.runSync(
<String>[
FlutterInformation.instance.getFlutterBinaryPath().path,
'pub',
'global',
'list',
],
<String>[FlutterInformation.instance.getFlutterBinaryPath().path, 'pub', 'global', 'list'],
workingDirectory: packageRoot.path,
environment: pubEnvironment,
stdoutEncoding: utf8,
);
print('');
final Iterable<RegExpMatch> versionMatches =
RegExp(r'^(?<name>dartdoc) (?<version>[^\s]+)', multiLine: true)
.allMatches(versionResults.stdout as String);
final Iterable<RegExpMatch> versionMatches = RegExp(
r'^(?<name>dartdoc) (?<version>[^\s]+)',
multiLine: true,
).allMatches(versionResults.stdout as String);
for (final RegExpMatch match in versionMatches) {
print('${match.namedGroup('name')} version: ${match.namedGroup('version')}');
}
@@ -658,17 +701,21 @@ class DartdocGenerator {
];
String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
print('Executing: (cd "${packageRoot.path}" ; '
'${FlutterInformation.instance.getFlutterBinaryPath().path} '
'pub '
'${dartdocArgs.map<String>(quote).join(' ')})');
print(
'Executing: (cd "${packageRoot.path}" ; '
'${FlutterInformation.instance.getFlutterBinaryPath().path} '
'pub '
'${dartdocArgs.map<String>(quote).join(' ')})',
);
process = ProcessWrapper(await runPubProcess(
arguments: dartdocArgs,
workingDirectory: packageRoot,
environment: pubEnvironment,
processManager: processManager,
));
process = ProcessWrapper(
await runPubProcess(
arguments: dartdocArgs,
workingDirectory: packageRoot,
environment: pubEnvironment,
processManager: processManager,
),
);
printStream(
process.stdout,
prefix: useJson ? '' : 'dartdoc:stdout: ',
@@ -711,7 +758,8 @@ class DartdocGenerator {
}
} else {
throw Exception(
"Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file.");
"Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file.",
);
}
}
@@ -725,15 +773,19 @@ class DartdocGenerator {
publishRoot.childDirectory('assets').childFile('overrides.css'),
flutterDirectory.childDirectory('dart-io').childFile('File-class.html'),
flutterDirectory.childDirectory('dart-ui').childFile('Canvas-class.html'),
flutterDirectory.childDirectory('dart-ui').childDirectory('Canvas').childFile('drawRect.html'),
flutterDirectory
.childDirectory('dart-ui')
.childDirectory('Canvas')
.childFile('drawRect.html'),
flutterDirectory
.childDirectory('flutter_driver')
.childDirectory('FlutterDriver')
.childFile('FlutterDriver.connectedTo.html'),
flutterDirectory.childDirectory('flutter_gpu').childFile('flutter_gpu-library.html'),
flutterDirectory
.childDirectory('flutter_gpu')
.childFile('flutter_gpu-library.html'),
flutterDirectory.childDirectory('flutter_test').childDirectory('WidgetTester').childFile('pumpWidget.html'),
.childDirectory('flutter_test')
.childDirectory('WidgetTester')
.childFile('pumpWidget.html'),
flutterDirectory.childDirectory('material').childFile('Material-class.html'),
flutterDirectory.childDirectory('material').childFile('Tooltip-class.html'),
widgetsDirectory.childFile('Widget-class.html'),
@@ -745,7 +797,9 @@ class DartdocGenerator {
void _sanityCheckDocs([Platform platform = const LocalPlatform()]) {
for (final File canary in canaries) {
if (!canary.existsSync()) {
throw Exception('Missing "${canary.path}", which probably means the documentation failed to build correctly.');
throw Exception(
'Missing "${canary.path}", which probably means the documentation failed to build correctly.',
);
}
}
// Make sure at least one example of each kind includes source code.
@@ -801,7 +855,10 @@ class DartdocGenerator {
}
void _copyIndexToRootOfDocs() {
publishRoot.childDirectory('flutter').childFile('index.html').copySync(publishRoot.childFile('index.html').path);
publishRoot
.childDirectory('flutter')
.childFile('index.html')
.copySync(publishRoot.childFile('index.html').path);
}
void _changePackageToSdkInTitlebar() {
@@ -844,8 +901,9 @@ class DartdocGenerator {
final Directory snippetsDir = publishRoot.childDirectory('snippets');
if (snippetsDir.existsSync()) {
const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' ');
final Iterable<File> files =
snippetsDir.listSync().whereType<File>().where((File file) => path.extension(file.path) == '.json');
final Iterable<File> files = snippetsDir.listSync().whereType<File>().where(
(File file) => path.extension(file.path) == '.json',
);
// Combine all the metadata into a single JSON array.
final Iterable<String> fileContents = files.map((File file) => file.readAsStringSync());
final List<dynamic> metadataObjects = fileContents.map<dynamic>(json.decode).toList();
@@ -896,13 +954,20 @@ class PlatformDocGenerator {
// On failure print a short snipped from the body in case it's helpful.
final int bodyLength = math.min(1024, response.body.length);
stderr.writeln('Response status code ${response.statusCode}. Body: ${response.body.substring(0, bodyLength)}');
stderr.writeln(
'Response status code ${response.statusCode}. Body: ${response.body.substring(0, bodyLength)}',
);
sleep(const Duration(seconds: 1));
}
return responseBytes == null ? null : ZipDecoder().decodeBytes(responseBytes);
}
Future<void> _extractDocs(String url, String name, PlatformDocsSection platform, Directory outputDir) async {
Future<void> _extractDocs(
String url,
String name,
PlatformDocsSection platform,
Directory outputDir,
) async {
const int maxTries = 5;
final Archive? archive = await _fetchArchive(url, maxTries);
if (archive == null) {
@@ -935,8 +1000,12 @@ class PlatformDocGenerator {
/// specified, for each source/destination file pair.
///
/// Creates `destDir` if needed.
void copyDirectorySync(Directory srcDir, Directory destDir,
{void Function(File srcFile, File destFile)? onFileCopied, required FileSystem filesystem}) {
void copyDirectorySync(
Directory srcDir,
Directory destDir, {
void Function(File srcFile, File destFile)? onFileCopied,
required FileSystem filesystem,
}) {
if (!srcDir.existsSync()) {
throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
}
@@ -959,8 +1028,14 @@ void copyDirectorySync(Directory srcDir, Directory destDir,
}
}
void printStream(Stream<List<int>> stream, {String prefix = '', List<Pattern> filter = const <Pattern>[]}) {
stream.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((String line) {
void printStream(
Stream<List<int>> stream, {
String prefix = '',
List<Pattern> filter = const <Pattern>[],
}) {
stream.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((
String line,
) {
if (!filter.any((Pattern pattern) => line.contains(pattern))) {
print('$prefix$line'.trim());
}
@@ -970,17 +1045,14 @@ void printStream(Stream<List<int>> stream, {String prefix = '', List<Pattern> fi
void zipDirectory(Directory src, File output, {required ProcessManager processManager}) {
// We would use the archive package to do this in one line, but it
// is a lot slower, and doesn't do compression nearly as well.
final ProcessResult zipProcess = processManager.runSync(
<String>[
'zip',
'-r',
'-9',
'-q',
output.path,
'.',
],
workingDirectory: src.path,
);
final ProcessResult zipProcess = processManager.runSync(<String>[
'zip',
'-r',
'-9',
'-q',
output.path,
'.',
], workingDirectory: src.path);
if (zipProcess.exitCode != 0) {
print('Creating offline ZIP archive ${output.path} failed:');
@@ -992,17 +1064,14 @@ void zipDirectory(Directory src, File output, {required ProcessManager processMa
void tarDirectory(Directory src, File output, {required ProcessManager processManager}) {
// We would use the archive package to do this in one line, but it
// is a lot slower, and doesn't do compression nearly as well.
final ProcessResult tarProcess = processManager.runSync(
<String>[
'tar',
'cf',
output.path,
'--use-compress-program',
'gzip --best',
'flutter.docset',
],
workingDirectory: src.path,
);
final ProcessResult tarProcess = processManager.runSync(<String>[
'tar',
'cf',
output.path,
'--use-compress-program',
'gzip --best',
'flutter.docset',
], workingDirectory: src.path);
if (tarProcess.exitCode != 0) {
print('Creating a tarball ${output.path} failed:');
@@ -1026,7 +1095,9 @@ Future<Process> runPubProcess({
}
List<String> findPackageNames(FileSystem filesystem) {
return findPackages(filesystem).map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
return findPackages(
filesystem,
).map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
}
/// Finds all packages in the Flutter SDK
@@ -1141,42 +1212,50 @@ class FlutterInformation {
// that flutter command, otherwise use the first one in the PATH.
String flutterCommand;
if (platform.environment['FLUTTER_ROOT'] != null) {
flutterCommand = filesystem
.directory(platform.environment['FLUTTER_ROOT'])
.childDirectory('bin')
.childFile('flutter')
.absolute
.path;
flutterCommand =
filesystem
.directory(platform.environment['FLUTTER_ROOT'])
.childDirectory('bin')
.childFile('flutter')
.absolute
.path;
} else {
flutterCommand = 'flutter';
}
ProcessResult result;
try {
result = processManager.runSync(
<String>[flutterCommand, '--version', '--machine'],
stdoutEncoding: utf8,
);
result = processManager.runSync(<String>[
flutterCommand,
'--version',
'--machine',
], stdoutEncoding: utf8);
} on ProcessException catch (e) {
throw FlutterInformationException(
'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place the '
'flutter command in your PATH.\n$e');
'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place the '
'flutter command in your PATH.\n$e',
);
}
if (result.exitCode != 0) {
throw FlutterInformationException(
'Unable to determine Flutter information, because of abnormal exit of flutter command.');
'Unable to determine Flutter information, because of abnormal exit of flutter command.',
);
}
// Strip out any non-JSON that might be printed along with the command
// output.
flutterVersionJson = (result.stdout as String)
.replaceAll('Waiting for another flutter command to release the startup lock...', '');
flutterVersionJson = (result.stdout as String).replaceAll(
'Waiting for another flutter command to release the startup lock...',
'',
);
}
final Map<String, dynamic> flutterVersion = json.decode(flutterVersionJson) as Map<String, dynamic>;
final Map<String, dynamic> flutterVersion =
json.decode(flutterVersionJson) as Map<String, dynamic>;
if (flutterVersion['flutterRoot'] == null ||
flutterVersion['frameworkVersion'] == null ||
flutterVersion['dartSdkVersion'] == null) {
throw FlutterInformationException(
'Flutter command output has unexpected format, unable to determine flutter root location.');
'Flutter command output has unexpected format, unable to determine flutter root location.',
);
}
final Map<String, Object> info = <String, Object>{};
@@ -1184,17 +1263,23 @@ class FlutterInformation {
info['flutterRoot'] = flutterRoot;
info['frameworkVersion'] = Version.parse(flutterVersion['frameworkVersion'] as String);
info['engineRevision'] = flutterVersion['engineRevision'] as String;
final File engineRealm = flutterRoot.childDirectory('bin').childDirectory('internal').childFile('engine.realm');
final File engineRealm = flutterRoot
.childDirectory('bin')
.childDirectory('internal')
.childFile('engine.realm');
info['engineRealm'] = engineRealm.existsSync() ? engineRealm.readAsStringSync().trim() : '';
final RegExpMatch? dartVersionRegex = RegExp(r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?')
.firstMatch(flutterVersion['dartSdkVersion'] as String);
final RegExpMatch? dartVersionRegex = RegExp(
r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?',
).firstMatch(flutterVersion['dartSdkVersion'] as String);
if (dartVersionRegex == null) {
throw FlutterInformationException(
'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.');
'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.',
);
}
info['dartSdkVersion'] =
Version.parse(dartVersionRegex.namedGroup('detail') ?? dartVersionRegex.namedGroup('base')!);
info['dartSdkVersion'] = Version.parse(
dartVersionRegex.namedGroup('detail') ?? dartVersionRegex.namedGroup('base')!,
);
info['branchName'] = _getBranchName();
info['flutterGitRevision'] = _getFlutterGitRevision();
@@ -1212,13 +1297,19 @@ class FlutterInformation {
if (luciBranch != null && luciBranch.trim().isNotEmpty) {
return luciBranch.trim();
}
final ProcessResult gitResult = processManager.runSync(<String>['git', 'status', '-b', '--porcelain']);
final ProcessResult gitResult = processManager.runSync(<String>[
'git',
'status',
'-b',
'--porcelain',
]);
if (gitResult.exitCode != 0) {
throw 'git status exit with non-zero exit code: ${gitResult.exitCode}';
}
final RegExp gitBranchRegexp = RegExp(r'^## (.*)');
final RegExpMatch? gitBranchMatch =
gitBranchRegexp.firstMatch((gitResult.stdout as String).trim().split('\n').first);
final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch(
(gitResult.stdout as String).trim().split('\n').first,
);
return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first;
}
@@ -1232,6 +1323,8 @@ class FlutterInformation {
}
final String gitRevision = (gitResult.stdout as String).trim();
return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
return gitRevision.length > kGitRevisionLength
? gitRevision.substring(0, kGitRevisionLength)
: gitRevision;
}
}

View File

@@ -75,7 +75,9 @@ void checkForUnresolvedDirectives(Directory dartDocDir) {
}
if (canaryLibraries.isNotEmpty) {
throw Exception('Did not find docs for the following libraries: ${canaryLibraries.join(', ')}.');
throw Exception(
'Did not find docs for the following libraries: ${canaryLibraries.join(', ')}.',
);
}
if (canaryFiles.isNotEmpty) {
throw Exception('Did not find docs for the following files: ${canaryFiles.join(', ')}.');
@@ -88,8 +90,9 @@ void checkForUnresolvedDirectives(Directory dartDocDir) {
int _scanFile(File file) {
assert(path.extension(file.path) == '.html');
final Iterable<String> matches = _pattern.allMatches(file.readAsStringSync())
.map((RegExpMatch m ) => m.group(0)!);
final Iterable<String> matches = _pattern
.allMatches(file.readAsStringSync())
.map((RegExpMatch m) => m.group(0)!);
if (matches.isNotEmpty) {
stderr.writeln('Found unresolved dartdoc directives in ${file.path}:');

View File

@@ -28,13 +28,7 @@ FutureOr<dynamic> main() async {
return;
}
final Directory flutterDir = _kFilesystem.directory(
path.absolute(
path.dirname(
path.dirname(
path.dirname(_kPlatform.script.toFilePath()),
),
),
),
path.absolute(path.dirname(path.dirname(path.dirname(_kPlatform.script.toFilePath())))),
);
final Directory apiDir = flutterDir.childDirectory('examples').childDirectory('api');
final File integrationTest = await generateTest(apiDir);
@@ -62,13 +56,14 @@ Future<void> runSmokeTests({
required File integrationTest,
required Directory apiDir,
}) async {
final File flutterExe =
flutterDir.childDirectory('bin').childFile(_kPlatform.isWindows ? 'flutter.bat' : 'flutter');
final File flutterExe = flutterDir
.childDirectory('bin')
.childFile(_kPlatform.isWindows ? 'flutter.bat' : 'flutter');
final List<String> cmd = <String>[
// If we're in a container with no X display, then use the virtual framebuffer.
if (_kPlatform.isLinux &&
(_kPlatform.environment['DISPLAY'] == null ||
_kPlatform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run',
(_kPlatform.environment['DISPLAY'] == null || _kPlatform.environment['DISPLAY']!.isEmpty))
'/usr/bin/xvfb-run',
flutterExe.absolute.path,
'test',
'--reporter=expanded',
@@ -82,8 +77,8 @@ Future<void> runSmokeTests({
// from for the tests.
class ExampleInfo {
ExampleInfo(File file, Directory examplesLibDir)
: importPath = _getImportPath(file, examplesLibDir),
importName = '' {
: importPath = _getImportPath(file, examplesLibDir),
importName = '' {
importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_');
}
@@ -91,8 +86,10 @@ class ExampleInfo {
String importName;
static String _getImportPath(File example, Directory examplesLibDir) {
final String relativePath =
path.relative(example.absolute.path, from: examplesLibDir.absolute.path);
final String relativePath = path.relative(
example.absolute.path,
from: examplesLibDir.absolute.path,
);
// So that Windows paths are proper URIs in the import statements.
return path.toUri(relativePath).toFilePath(windows: false);
}
@@ -108,9 +105,7 @@ Future<File> generateTest(Directory apiDir) async {
<String>['git', 'ls-files', '**/*.dart'],
workingDirectory: examplesLibDir,
quiet: true,
)).replaceAll(r'\', '/')
.trim()
.split('\n');
)).replaceAll(r'\', '/').trim().split('\n');
final Iterable<File> examples = gitFiles.map<File>((String examplePath) {
return _kFilesystem.file(path.join(examplesLibDir.absolute.path, examplePath));
});
@@ -126,7 +121,7 @@ Future<File> generateTest(Directory apiDir) async {
"import 'package:flutter_test/flutter_test.dart';",
"import 'package:integration_test/integration_test.dart';",
for (final ExampleInfo info in infoList)
"import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};"
"import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};",
]..sort();
final StringBuffer buffer = StringBuffer();
@@ -172,8 +167,9 @@ void main() {
}
buffer.writeln('}');
final File integrationTest =
apiDir.childDirectory('integration_test').childFile('smoke_integration_test.dart');
final File integrationTest = apiDir
.childDirectory('integration_test')
.childFile('smoke_integration_test.dart');
integrationTest.createSync(recursive: true);
integrationTest.writeAsStringSync(buffer.toString());
return integrationTest;
@@ -206,40 +202,40 @@ Future<String> runCommand(
workingDirectory: workingDirectory.absolute.path,
environment: environment,
);
process.stdout.listen(
(List<int> event) {
stdoutOutput.addAll(event);
combinedOutput.addAll(event);
if (!quiet) {
stdout.add(event);
}
},
onDone: () async => stdoutComplete.complete(),
);
process.stderr.listen(
(List<int> event) {
combinedOutput.addAll(event);
if (!quiet) {
stderr.add(event);
}
},
onDone: () async => stderrComplete.complete(),
);
process.stdout.listen((List<int> event) {
stdoutOutput.addAll(event);
combinedOutput.addAll(event);
if (!quiet) {
stdout.add(event);
}
}, onDone: () async => stdoutComplete.complete());
process.stderr.listen((List<int> event) {
combinedOutput.addAll(event);
if (!quiet) {
stderr.add(event);
}
}, onDone: () async => stderrComplete.complete());
} on ProcessException catch (e) {
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
'failed with:\n$e');
stderr.writeln(
'Running "${cmd.join(' ')}" in ${workingDirectory.path} '
'failed with:\n$e',
);
exitCode = 2;
return utf8.decode(stdoutOutput);
} on ArgumentError catch (e) {
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
'failed with:\n$e');
stderr.writeln(
'Running "${cmd.join(' ')}" in ${workingDirectory.path} '
'failed with:\n$e',
);
exitCode = 3;
return utf8.decode(stdoutOutput);
}
final int processExitCode = await allComplete();
if (processExitCode != 0) {
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode');
stderr.writeln(
'Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode',
);
exitCode = processExitCode;
}

View File

@@ -12,7 +12,9 @@ void _validate(List<String> args) {
}
if (!File('../engine/src/flutter/DEPS').existsSync()) {
errors = true;
print('This program assumes the engine directory is a sibling to the flutter repository directory.');
print(
'This program assumes the engine directory is a sibling to the flutter repository directory.',
);
}
if (args.length != 1) {
errors = true;
@@ -46,57 +48,45 @@ Future<void> main(List<String> args) async {
}
Future<void> _fetchUpstream([String workingDirectory = '.']) async {
print('Fetching remotes for "$workingDirectory" - you may be prompted for SSH credentials by git.');
final ProcessResult fetchResult = await Process.run(
'git',
<String>[
'fetch',
'--all',
],
workingDirectory: workingDirectory,
print(
'Fetching remotes for "$workingDirectory" - you may be prompted for SSH credentials by git.',
);
final ProcessResult fetchResult = await Process.run('git', <String>[
'fetch',
'--all',
], workingDirectory: workingDirectory);
if (fetchResult.exitCode != 0) {
throw Exception('Failed to fetch upstream in repository $workingDirectory');
}
}
Future<String> _tagsForRevision(String flutterRevision) async {
final ProcessResult tagResult = await Process.run(
'git',
<String>[
'tag',
'--contains',
flutterRevision,
],
);
final ProcessResult tagResult = await Process.run('git', <String>[
'tag',
'--contains',
flutterRevision,
]);
return tagResult.stdout as String;
}
Future<bool> containsRevision(String ancestorRevision, String revision) async {
final ProcessResult result = await Process.run(
'git',
<String>[
'merge-base',
'--is-ancestor',
ancestorRevision,
revision,
],
workingDirectory: engineRepo,
);
final ProcessResult result = await Process.run('git', <String>[
'merge-base',
'--is-ancestor',
ancestorRevision,
revision,
], workingDirectory: engineRepo);
return result.exitCode == 0;
}
Stream<FlutterEngineRevision> _logEngineVersions() async* {
final ProcessResult result = await Process.run(
'git',
<String>[
'log',
'--oneline',
'-p',
'--',
'bin/internal/engine.version',
],
);
final ProcessResult result = await Process.run('git', <String>[
'log',
'--oneline',
'-p',
'--',
'bin/internal/engine.version',
]);
if (result.exitCode != 0) {
print(result.stderr);
throw Exception('Failed to log bin/internal/engine.version');

View File

@@ -67,12 +67,7 @@ const String dataDir = 'dev/tools/gen_defaults/data';
Future<void> main(List<String> args) async {
// Parse arguments
final ArgParser parser = ArgParser();
parser.addFlag(
'verbose',
abbr: 'v',
help: 'Enable verbose output',
negatable: false,
);
parser.addFlag('verbose', abbr: 'v', help: 'Enable verbose output', negatable: false);
final ArgResults argResults = parser.parse(args);
final bool verbose = argResults['verbose'] as bool;
@@ -96,6 +91,8 @@ Future<void> main(List<String> args) async {
final Map<String, dynamic> colorLightTokens = _readTokenFile(File('$dataDir/color_light.json'));
final Map<String, dynamic> colorDarkTokens = _readTokenFile(File('$dataDir/color_dark.json'));
// The verifyTokenTemplatesUpdateCorrectFiles check in dev/bots/analyze.dart depends on the exact formatting of the next few lines.
// dart format off
// Generate tokens files.
ChipTemplate('Chip', '$materialLib/chip.dart', tokens).updateFile();
ActionChipTemplate('ActionChip', '$materialLib/action_chip.dart', tokens).updateFile();
@@ -150,6 +147,7 @@ Future<void> main(List<String> args) async {
TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();
TabsTemplate('Tabs', '$materialLib/tabs.dart', tokens).updateFile();
TypographyTemplate('Typography', '$materialLib/typography.dart', tokens).updateFile();
// dart format on
tokenLogger.printVersionUsage(verbose: verbose);
tokenLogger.printTokensUsage(verbose: verbose);

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class ActionChipTemplate extends TokenTemplate {
const ActionChipTemplate(super.blockName, super.fileName, super.tokens, {
const ActionChipTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
static const String tokenGroup = 'md.comp.assist-chip';

View File

@@ -6,10 +6,7 @@ import 'template.dart';
class AppBarTemplate extends TokenTemplate {
const AppBarTemplate(super.blockName, super.fileName, super.tokens)
: super(
colorSchemePrefix: '_colors.',
textThemePrefix: '_textTheme.',
);
: super(colorSchemePrefix: '_colors.', textThemePrefix: '_textTheme.');
@override
String generate() => '''

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class BadgeTemplate extends TokenTemplate {
const BadgeTemplate(super.blockName, super.fileName, super.tokens, {
const BadgeTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class BannerTemplate extends TokenTemplate {
const BannerTemplate(super.blockName, super.fileName, super.tokens, {
const BannerTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
@override

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class BottomAppBarTemplate extends TokenTemplate {
const BottomAppBarTemplate(super.blockName, super.fileName, super.tokens, {
const BottomAppBarTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,7 +5,11 @@
import 'template.dart';
class ButtonTemplate extends TokenTemplate {
const ButtonTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
const ButtonTemplate(
this.tokenGroup,
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,7 +5,11 @@
import 'template.dart';
class CardTemplate extends TokenTemplate {
const CardTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
const CardTemplate(
this.tokenGroup,
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class CheckboxTemplate extends TokenTemplate {
const CheckboxTemplate(super.blockName, super.fileName, super.tokens, {
const CheckboxTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class ChipTemplate extends TokenTemplate {
const ChipTemplate(super.blockName, super.fileName, super.tokens, {
const ChipTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
static const String tokenGroup = 'md.comp.filter-chip';

View File

@@ -6,7 +6,13 @@ import 'template.dart';
import 'token_logger.dart';
class ColorSchemeTemplate extends TokenTemplate {
ColorSchemeTemplate(this._colorTokensLight, this._colorTokensDark, super.blockName, super.fileName, super.tokens);
ColorSchemeTemplate(
this._colorTokensLight,
this._colorTokensDark,
super.blockName,
super.fileName,
super.tokens,
);
// Map of light color scheme token data from tokens.
final Map<String, dynamic> _colorTokensLight;

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class DatePickerTemplate extends TokenTemplate {
const DatePickerTemplate(super.blockName, super.fileName, super.tokens, {
const DatePickerTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
String _layerOpacity(String layerToken) {
@@ -26,9 +29,9 @@ class DatePickerTemplate extends TokenTemplate {
String _stateColor(String componentToken, String? type, String state) {
final String baseColor = color(
type != null
? '$componentToken.$type.$state.state-layer.color'
: '$componentToken.$state.state-layer.color',
''
? '$componentToken.$type.$state.state-layer.color'
: '$componentToken.$state.state-layer.color',
'',
);
if (baseColor.isEmpty) {
return 'null';

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class DialogTemplate extends TokenTemplate {
const DialogTemplate(super.blockName, super.fileName, super.tokens, {
const DialogTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
@override

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class ExpansionTileTemplate extends TokenTemplate {
const ExpansionTileTemplate(super.blockName, super.fileName, super.tokens, {
const ExpansionTileTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class FABTemplate extends TokenTemplate {
const FABTemplate(super.blockName, super.fileName, super.tokens, {
const FABTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.',
});

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class FilterChipTemplate extends TokenTemplate {
const FilterChipTemplate(super.blockName, super.fileName, super.tokens, {
const FilterChipTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
static const String tokenGroup = 'md.comp.filter-chip';

View File

@@ -5,7 +5,11 @@
import 'template.dart';
class IconButtonTemplate extends TokenTemplate {
const IconButtonTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
const IconButtonTemplate(
this.tokenGroup,
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});
@@ -185,7 +189,8 @@ class IconButtonTemplate extends TokenTemplate {
}
String _minimumSize() {
if (tokenAvailable('$tokenGroup.container.height') && tokenAvailable('$tokenGroup.container.width')) {
if (tokenAvailable('$tokenGroup.container.height') &&
tokenAvailable('$tokenGroup.container.width')) {
return '''
const MaterialStatePropertyAll<Size>(Size(${getToken('$tokenGroup.container.width')}, ${getToken('$tokenGroup.container.height')}))''';
@@ -313,5 +318,4 @@ class _${blockName}DefaultsM3 extends ButtonStyle {
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
''';
}

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class InputChipTemplate extends TokenTemplate {
const InputChipTemplate(super.blockName, super.fileName, super.tokens, {
const InputChipTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
static const String tokenGroup = 'md.comp.input-chip';

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class InputDecoratorTemplate extends TokenTemplate {
const InputDecoratorTemplate(super.blockName, super.fileName, super.tokens, {
const InputDecoratorTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
@override
@@ -54,7 +57,7 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme {
}
if (states.contains(MaterialState.error)) {
if (states.contains(MaterialState.focused)) {
return ${mergedBorder('md.comp.filled-text-field.error.focus.active-indicator','md.comp.filled-text-field.focus.active-indicator')};
return ${mergedBorder('md.comp.filled-text-field.error.focus.active-indicator', 'md.comp.filled-text-field.focus.active-indicator')};
}
if (states.contains(MaterialState.hovered)) {
return ${border('md.comp.filled-text-field.error.hover.active-indicator')};
@@ -77,7 +80,7 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme {
}
if (states.contains(MaterialState.error)) {
if (states.contains(MaterialState.focused)) {
return ${mergedBorder('md.comp.outlined-text-field.error.focus.outline','md.comp.outlined-text-field.focus.outline')};
return ${mergedBorder('md.comp.outlined-text-field.error.focus.outline', 'md.comp.outlined-text-field.focus.outline')};
}
if (states.contains(MaterialState.hovered)) {
return ${border('md.comp.outlined-text-field.error.hover.outline')};
@@ -221,15 +224,17 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme {
/// Generate a [BorderSide] for the given components.
String mergedBorder(String componentToken1, String componentToken2) {
final String borderColor = componentColor(componentToken1)!= 'null'
? componentColor(componentToken1)
: componentColor(componentToken2);
final double width = (
getToken('$componentToken1.width', optional: true) ??
getToken('$componentToken1.height', optional: true) ??
getToken('$componentToken2.width', optional: true) ??
getToken('$componentToken2.height', optional: true) ??
1.0) as double;
final String borderColor =
componentColor(componentToken1) != 'null'
? componentColor(componentToken1)
: componentColor(componentToken2);
final double width =
(getToken('$componentToken1.width', optional: true) ??
getToken('$componentToken1.height', optional: true) ??
getToken('$componentToken2.width', optional: true) ??
getToken('$componentToken2.height', optional: true) ??
1.0)
as double;
return 'BorderSide(color: $borderColor${width != 1.0 ? ", width: $width" : ""})';
}
}

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class ListTileTemplate extends TokenTemplate {
const ListTileTemplate(super.blockName, super.fileName, super.tokens, {
const ListTileTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.',
});

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class MenuTemplate extends TokenTemplate {
const MenuTemplate(super.blockName, super.fileName, super.tokens, {
const MenuTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});
@@ -247,10 +250,7 @@ class _MenuDefaultsM3 extends MenuStyle {
@override
MaterialStateProperty<Color?>? get surfaceTintColor {
return ${componentColor('md.comp.menu.container.surface-tint-layer') == 'null'
? 'const MaterialStatePropertyAll<Color?>(Colors.transparent)'
: 'MaterialStatePropertyAll<Color?>(${componentColor('md.comp.menu.container.surface-tint-layer')})'
};
return ${componentColor('md.comp.menu.container.surface-tint-layer') == 'null' ? 'const MaterialStatePropertyAll<Color?>(Colors.transparent)' : 'MaterialStatePropertyAll<Color?>(${componentColor('md.comp.menu.container.surface-tint-layer')})'};
}
@override

View File

@@ -8,33 +8,37 @@ import 'token_logger.dart';
class MotionTemplate extends TokenTemplate {
/// Since we generate the tokens dynamically, we need to store them and log
/// them manually, instead of using [getToken].
MotionTemplate(String blockName, String fileName, this.tokens, this.tokensLogger) : super(blockName, fileName, tokens);
MotionTemplate(String blockName, String fileName, this.tokens, this.tokensLogger)
: super(blockName, fileName, tokens);
Map<String, dynamic> tokens;
TokenLogger tokensLogger;
// List of duration tokens.
late List<MapEntry<String, dynamic>> durationTokens = tokens.entries.where(
(MapEntry<String, dynamic> entry) => entry.key.contains('.duration.')
).toList()
..sort(
(MapEntry<String, dynamic> a, MapEntry<String, dynamic> b) => (a.value as double).compareTo(b.value as double)
);
late List<MapEntry<String, dynamic>> durationTokens =
tokens.entries
.where((MapEntry<String, dynamic> entry) => entry.key.contains('.duration.'))
.toList()
..sort(
(MapEntry<String, dynamic> a, MapEntry<String, dynamic> b) =>
(a.value as double).compareTo(b.value as double),
);
// List of easing curve tokens.
late List<MapEntry<String, dynamic>> easingCurveTokens = tokens.entries.where(
(MapEntry<String, dynamic> entry) => entry.key.contains('.easing.')
).toList()
..sort(
// Sort the legacy curves at the end of the list.
(MapEntry<String, dynamic> a, MapEntry<String, dynamic> b) => a.key.contains('legacy') ? 1 : a.key.compareTo(b.key)
);
late List<MapEntry<String, dynamic>> easingCurveTokens =
tokens.entries
.where((MapEntry<String, dynamic> entry) => entry.key.contains('.easing.'))
.toList()
..sort(
// Sort the legacy curves at the end of the list.
(MapEntry<String, dynamic> a, MapEntry<String, dynamic> b) =>
a.key.contains('legacy') ? 1 : a.key.compareTo(b.key),
);
String durationTokenString(String token, dynamic tokenValue) {
tokensLogger.log(token);
final String tokenName = token.split('.').last.replaceAll('-', '').replaceFirst('Ms', '');
final int milliseconds = (tokenValue as double).toInt();
return
'''
return '''
/// The $tokenName duration (${milliseconds}ms) in the Material specification.
///
/// See also:
@@ -47,11 +51,12 @@ class MotionTemplate extends TokenTemplate {
String easingCurveTokenString(String token, dynamic tokenValue) {
tokensLogger.log(token);
final String tokenName = token
.replaceFirst('md.sys.motion.easing.', '')
.replaceAllMapped(RegExp(r'[-\.](\w)'), (Match match) {
final String tokenName = token.replaceFirst('md.sys.motion.easing.', '').replaceAllMapped(
RegExp(r'[-\.](\w)'),
(Match match) {
return match.group(1)!.toUpperCase();
});
},
);
return '''
/// The $tokenName easing curve in the Material specification.
///

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class NavigationBarTemplate extends TokenTemplate {
const NavigationBarTemplate(super.blockName, super.fileName, super.tokens, {
const NavigationBarTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.',
});

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class NavigationDrawerTemplate extends TokenTemplate {
const NavigationDrawerTemplate(super.blockName, super.fileName, super.tokens, {
const NavigationDrawerTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
@override

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class NavigationRailTemplate extends TokenTemplate {
const NavigationRailTemplate(super.blockName, super.fileName, super.tokens, {
const NavigationRailTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.',
});

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class PopupMenuTemplate extends TokenTemplate {
const PopupMenuTemplate(super.blockName, super.fileName, super.tokens, {
const PopupMenuTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.',
});

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class ProgressIndicatorTemplate extends TokenTemplate {
const ProgressIndicatorTemplate(super.blockName, super.fileName, super.tokens, {
const ProgressIndicatorTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class RadioTemplate extends TokenTemplate {
const RadioTemplate(super.blockName, super.fileName, super.tokens, {
const RadioTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,13 +5,18 @@
import 'template.dart';
class SearchBarTemplate extends TokenTemplate {
const SearchBarTemplate(super.blockName, super.fileName, super.tokens, {
const SearchBarTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
String _surfaceTint() {
final String? color = colorOrTransparent('md.comp.search-bar.container.surface-tint-layer.color');
final String? color = colorOrTransparent(
'md.comp.search-bar.container.surface-tint-layer.color',
);
final String surfaceTintColor = 'MaterialStatePropertyAll<Color>($color);';
if (color == 'Colors.transparent') {
return 'const $surfaceTintColor';

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class SearchViewTemplate extends TokenTemplate {
const SearchViewTemplate(super.blockName, super.fileName, super.tokens, {
const SearchViewTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
@override

View File

@@ -5,7 +5,11 @@
import 'template.dart';
class SegmentedButtonTemplate extends TokenTemplate {
const SegmentedButtonTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
const SegmentedButtonTemplate(
this.tokenGroup,
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -5,7 +5,11 @@
import 'template.dart';
class SliderTemplate extends TokenTemplate {
const SliderTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
const SliderTemplate(
this.tokenGroup,
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});

View File

@@ -6,8 +6,11 @@ import 'template.dart';
class SnackbarTemplate extends TokenTemplate {
const SnackbarTemplate(
this.tokenGroup, super.blockName, super.fileName, super.tokens, {
super.colorSchemePrefix = '_colors.'
this.tokenGroup,
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});
final String tokenGroup;

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class SwitchTemplate extends TokenTemplate {
const SwitchTemplate(super.blockName, super.fileName, super.tokens, {
const SwitchTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
});
@@ -231,5 +234,4 @@ class _SwitchConfigM3 with _SwitchConfig {
Size get switchMinSize => const Size(kMinInteractiveDimension, kMinInteractiveDimension - 8.0);
}
''';
}

View File

@@ -5,7 +5,10 @@
import 'template.dart';
class TabsTemplate extends TokenTemplate {
const TabsTemplate(super.blockName, super.fileName, super.tokens, {
const TabsTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.',
});
@@ -160,5 +163,4 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarThemeData {
static double indicatorWeight = ${getToken('md.comp.secondary-navigation-tab.active-indicator.height')};
}
''';
}

View File

@@ -8,9 +8,12 @@ import 'token_logger.dart';
/// Base class for code generation templates.
abstract class TokenTemplate {
const TokenTemplate(this.blockName, this.fileName, this._tokens, {
const TokenTemplate(
this.blockName,
this.fileName,
this._tokens, {
this.colorSchemePrefix = 'Theme.of(context).colorScheme.',
this.textThemePrefix = 'Theme.of(context).textTheme.'
this.textThemePrefix = 'Theme.of(context).textTheme.',
});
/// Name of the code block that this template will generate.
@@ -58,6 +61,13 @@ abstract class TokenTemplate {
// 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 = '''
@@ -95,6 +105,7 @@ abstract class TokenTemplate {
buffer.write(beginComment);
buffer.write(headerComment);
buffer.write(generate());
buffer.write(footerComment);
buffer.write(endComment);
buffer.write(contentAfterBlock);
File(fileName).writeAsStringSync(buffer.toString());
@@ -194,8 +205,14 @@ abstract class TokenTemplate {
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);
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))})';
@@ -207,8 +224,8 @@ abstract class TokenTemplate {
/// - "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>;
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;
@@ -223,18 +240,18 @@ abstract class TokenTemplate {
}
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)':''}'
'))';
'${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':
'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');
@@ -243,27 +260,24 @@ abstract class TokenTemplate {
/// 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', optional: true) ??
getToken('$componentToken.height', optional: true) ??
1.0
) as double;
final double 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';
}

View File

@@ -5,9 +5,12 @@
import 'template.dart';
class TimePickerTemplate extends TokenTemplate {
const TimePickerTemplate(super.blockName, super.fileName, super.tokens, {
const TimePickerTemplate(
super.blockName,
super.fileName,
super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
super.textThemePrefix = '_textTheme.',
});
static const String tokenGroup = 'md.comp.time-picker';

View File

@@ -13,8 +13,8 @@ class TokenLogger {
void init({
required Map<String, dynamic> allTokens,
required Map<String, List<String>> versionMap
}){
required Map<String, List<String>> versionMap,
}) {
_allTokens = allTokens;
_versionMap = versionMap;
}
@@ -83,7 +83,11 @@ class TokenLogger {
if (_unavailableTokens.isNotEmpty) {
print('');
print('\x1B[31m' 'Some referenced tokens do not exist: ${_unavailableTokens.length}' '\x1B[0m');
print(
'\x1B[31m'
'Some referenced tokens do not exist: ${_unavailableTokens.length}'
'\x1B[0m',
);
for (final String token in _unavailableTokens) {
print(' $token');
}

View File

@@ -20,21 +20,51 @@ abstract final class _M3Typography {
String _textTheme(String name, String baseline) {
final StringBuffer theme = StringBuffer('static const TextTheme $name = TextTheme(\n');
theme.writeln(' displayLarge: ${_textStyleDef('md.sys.typescale.display-large', '$name displayLarge 2021', baseline)},');
theme.writeln(' displayMedium: ${_textStyleDef('md.sys.typescale.display-medium', '$name displayMedium 2021', baseline)},');
theme.writeln(' displaySmall: ${_textStyleDef('md.sys.typescale.display-small', '$name displaySmall 2021', baseline)},');
theme.writeln(' headlineLarge: ${_textStyleDef('md.sys.typescale.headline-large', '$name headlineLarge 2021', baseline)},');
theme.writeln(' headlineMedium: ${_textStyleDef('md.sys.typescale.headline-medium', '$name headlineMedium 2021', baseline)},');
theme.writeln(' headlineSmall: ${_textStyleDef('md.sys.typescale.headline-small', '$name headlineSmall 2021', baseline)},');
theme.writeln(' titleLarge: ${_textStyleDef('md.sys.typescale.title-large', '$name titleLarge 2021', baseline)},');
theme.writeln(' titleMedium: ${_textStyleDef('md.sys.typescale.title-medium', '$name titleMedium 2021', baseline)},');
theme.writeln(' titleSmall: ${_textStyleDef('md.sys.typescale.title-small', '$name titleSmall 2021', baseline)},');
theme.writeln(' labelLarge: ${_textStyleDef('md.sys.typescale.label-large', '$name labelLarge 2021', baseline)},');
theme.writeln(' labelMedium: ${_textStyleDef('md.sys.typescale.label-medium', '$name labelMedium 2021', baseline)},');
theme.writeln(' labelSmall: ${_textStyleDef('md.sys.typescale.label-small', '$name labelSmall 2021', baseline)},');
theme.writeln(' bodyLarge: ${_textStyleDef('md.sys.typescale.body-large', '$name bodyLarge 2021', baseline)},');
theme.writeln(' bodyMedium: ${_textStyleDef('md.sys.typescale.body-medium', '$name bodyMedium 2021', baseline)},');
theme.writeln(' bodySmall: ${_textStyleDef('md.sys.typescale.body-small', '$name bodySmall 2021', baseline)},');
theme.writeln(
' displayLarge: ${_textStyleDef('md.sys.typescale.display-large', '$name displayLarge 2021', baseline)},',
);
theme.writeln(
' displayMedium: ${_textStyleDef('md.sys.typescale.display-medium', '$name displayMedium 2021', baseline)},',
);
theme.writeln(
' displaySmall: ${_textStyleDef('md.sys.typescale.display-small', '$name displaySmall 2021', baseline)},',
);
theme.writeln(
' headlineLarge: ${_textStyleDef('md.sys.typescale.headline-large', '$name headlineLarge 2021', baseline)},',
);
theme.writeln(
' headlineMedium: ${_textStyleDef('md.sys.typescale.headline-medium', '$name headlineMedium 2021', baseline)},',
);
theme.writeln(
' headlineSmall: ${_textStyleDef('md.sys.typescale.headline-small', '$name headlineSmall 2021', baseline)},',
);
theme.writeln(
' titleLarge: ${_textStyleDef('md.sys.typescale.title-large', '$name titleLarge 2021', baseline)},',
);
theme.writeln(
' titleMedium: ${_textStyleDef('md.sys.typescale.title-medium', '$name titleMedium 2021', baseline)},',
);
theme.writeln(
' titleSmall: ${_textStyleDef('md.sys.typescale.title-small', '$name titleSmall 2021', baseline)},',
);
theme.writeln(
' labelLarge: ${_textStyleDef('md.sys.typescale.label-large', '$name labelLarge 2021', baseline)},',
);
theme.writeln(
' labelMedium: ${_textStyleDef('md.sys.typescale.label-medium', '$name labelMedium 2021', baseline)},',
);
theme.writeln(
' labelSmall: ${_textStyleDef('md.sys.typescale.label-small', '$name labelSmall 2021', baseline)},',
);
theme.writeln(
' bodyLarge: ${_textStyleDef('md.sys.typescale.body-large', '$name bodyLarge 2021', baseline)},',
);
theme.writeln(
' bodyMedium: ${_textStyleDef('md.sys.typescale.body-medium', '$name bodyMedium 2021', baseline)},',
);
theme.writeln(
' bodySmall: ${_textStyleDef('md.sys.typescale.body-small', '$name bodySmall 2021', baseline)},',
);
theme.write(' );');
return theme.toString();
}
@@ -57,7 +87,8 @@ abstract final class _M3Typography {
}
String _fontWeight(String textStyleTokenName) {
final String weightValue = getToken(getToken('$textStyleTokenName.weight') as String).toString();
final String weightValue =
getToken(getToken('$textStyleTokenName.weight') as String).toString();
return 'FontWeight.w$weightValue';
}

View File

@@ -43,7 +43,11 @@ void main() {
// Have a test template append new parameterized content to the end of
// the file.
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'Foobar', 'bar': 'Barfoo'};
final Map<String, dynamic> tokens = <String, dynamic>{
'version': '0.0',
'foo': 'Foobar',
'bar': 'Barfoo',
};
TestTemplate('Test', tempFile.path, tokens).updateFile();
expect(tempFile.readAsStringSync(), '''
@@ -58,12 +62,13 @@ void main() {
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
static final String tokenFoo = 'Foobar';
static final String tokenBar = 'Barfoo';
// dart format on
// END GENERATED TOKEN PROPERTIES - Test
''');
} finally {
tempDir.deleteSync(recursive: true);
}
@@ -87,15 +92,21 @@ static final String tokenBar = 'Barfoo';
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
static final String tokenFoo = 'Foobar';
static final String tokenBar = 'Barfoo';
// dart format on
// END GENERATED TOKEN PROPERTIES - Test
''');
// Have a test template append new parameterized content to the end of
// the file.
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'foo', 'bar': 'bar'};
final Map<String, dynamic> tokens = <String, dynamic>{
'version': '0.0',
'foo': 'foo',
'bar': 'bar',
};
TestTemplate('Test', tempFile.path, tokens).updateFile();
expect(tempFile.readAsStringSync(), '''
@@ -110,12 +121,13 @@ static final String tokenBar = 'Barfoo';
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
static final String tokenFoo = 'foo';
static final String tokenBar = 'bar';
// dart format on
// END GENERATED TOKEN PROPERTIES - Test
''');
} finally {
tempDir.deleteSync(recursive: true);
}
@@ -135,7 +147,11 @@ static final String tokenBar = 'bar';
// Update file with a template for 'Block 1'
{
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'foo', 'bar': 'bar'};
final Map<String, dynamic> tokens = <String, dynamic>{
'version': '0.0',
'foo': 'foo',
'bar': 'bar',
};
TestTemplate('Block 1', tempFile.path, tokens).updateFile();
}
expect(tempFile.readAsStringSync(), '''
@@ -150,8 +166,10 @@ static final String tokenBar = 'bar';
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
static final String tokenFoo = 'foo';
static final String tokenBar = 'bar';
// dart format on
// END GENERATED TOKEN PROPERTIES - Block 1
''');
@@ -159,7 +177,11 @@ static final String tokenBar = 'bar';
// Update file with a template for 'Block 2', which should append but not
// disturb the code in 'Block 1'.
{
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'bar', 'bar': 'foo'};
final Map<String, dynamic> tokens = <String, dynamic>{
'version': '0.0',
'foo': 'bar',
'bar': 'foo',
};
TestTemplate('Block 2', tempFile.path, tokens).updateFile();
}
expect(tempFile.readAsStringSync(), '''
@@ -174,8 +196,10 @@ static final String tokenBar = 'bar';
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
static final String tokenFoo = 'foo';
static final String tokenBar = 'bar';
// dart format on
// END GENERATED TOKEN PROPERTIES - Block 1
@@ -186,8 +210,10 @@ static final String tokenBar = 'bar';
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
static final String tokenFoo = 'bar';
static final String tokenBar = 'foo';
// dart format on
// END GENERATED TOKEN PROPERTIES - Block 2
''');
@@ -195,7 +221,11 @@ static final String tokenBar = 'foo';
// Update 'Block 1' again which should just update that block,
// leaving 'Block 2' undisturbed.
{
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'FOO', 'bar': 'BAR'};
final Map<String, dynamic> tokens = <String, dynamic>{
'version': '0.0',
'foo': 'FOO',
'bar': 'BAR',
};
TestTemplate('Block 1', tempFile.path, tokens).updateFile();
}
expect(tempFile.readAsStringSync(), '''
@@ -210,8 +240,10 @@ static final String tokenBar = 'foo';
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
static final String tokenFoo = 'FOO';
static final String tokenBar = 'BAR';
// dart format on
// END GENERATED TOKEN PROPERTIES - Block 1
@@ -222,12 +254,13 @@ static final String tokenBar = 'BAR';
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
static final String tokenFoo = 'bar';
static final String tokenBar = 'foo';
// dart format on
// END GENERATED TOKEN PROPERTIES - Block 2
''');
} finally {
tempDir.deleteSync(recursive: true);
}
@@ -244,12 +277,13 @@ static final String tokenBar = 'foo';
'bottomLeft': 3.0,
'bottomRight': 4.0,
},
'shape.full': <String, dynamic>{
'family': 'SHAPE_FAMILY_CIRCULAR',
},
'shape.full': <String, dynamic>{'family': 'SHAPE_FAMILY_CIRCULAR'},
};
final TestTemplate template = TestTemplate('Test', 'foobar.dart', tokens);
expect(template.shape('foo'), 'const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(1.0), topRight: Radius.circular(2.0), bottomLeft: Radius.circular(3.0), bottomRight: Radius.circular(4.0)))');
expect(
template.shape('foo'),
'const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(1.0), topRight: Radius.circular(2.0), bottomLeft: Radius.circular(3.0), bottomRight: Radius.circular(4.0)))',
);
expect(template.shape('bar'), 'const StadiumBorder()');
});
@@ -263,7 +297,7 @@ static final String tokenBar = 'foo';
final ZoneSpecification spec = ZoneSpecification(
print: (_, __, ___, String msg) {
printLog.add(msg);
}
},
);
return Zone.current.fork(specification: spec).run<void>(testFn);
};
@@ -286,151 +320,190 @@ static final String tokenBar = 'foo';
'v2.0.0': <String>['file_2.json, file_3.json'],
};
test('can print empty usage', overridePrint(() {
logger.printVersionUsage(verbose: true);
expect(printLog, contains('Versions used: '));
test(
'can print empty usage',
overridePrint(() {
logger.printVersionUsage(verbose: true);
expect(printLog, contains('Versions used: '));
logger.printTokensUsage(verbose: true);
expect(printLog, contains('Tokens used: 0/0'));
}));
logger.printTokensUsage(verbose: true);
expect(printLog, contains('Tokens used: 0/0'));
}),
);
test('can print version usage', overridePrint(() {
versionMap.addAll(testVersions);
test(
'can print version usage',
overridePrint(() {
versionMap.addAll(testVersions);
logger.printVersionUsage(verbose: false);
logger.printVersionUsage(verbose: false);
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
}));
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
}),
);
test('can print version usage (verbose)', overridePrint(() {
versionMap.addAll(testVersions);
test(
'can print version usage (verbose)',
overridePrint(() {
versionMap.addAll(testVersions);
logger.printVersionUsage(verbose: true);
logger.printVersionUsage(verbose: true);
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
expect(printLog, contains(' v1.0.0:'));
expect(printLog, contains(' file_1.json'));
expect(printLog, contains(' v2.0.0:'));
expect(printLog, contains(' file_2.json, file_3.json'));
}));
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
expect(printLog, contains(' v1.0.0:'));
expect(printLog, contains(' file_1.json'));
expect(printLog, contains(' v2.0.0:'));
expect(printLog, contains(' file_2.json, file_3.json'));
}),
);
test('can log and print tokens usage', overridePrint(() {
allTokens['foo'] = 'value';
test(
'can log and print tokens usage',
overridePrint(() {
allTokens['foo'] = 'value';
logger.log('foo');
logger.printTokensUsage(verbose: false);
logger.log('foo');
logger.printTokensUsage(verbose: false);
expect(printLog, contains('Tokens used: 1/1'));
}));
expect(printLog, contains('Tokens used: 1/1'));
}),
);
test('can log and print tokens usage (verbose)', overridePrint(() {
allTokens['foo'] = 'value';
test(
'can log and print tokens usage (verbose)',
overridePrint(() {
allTokens['foo'] = 'value';
logger.log('foo');
logger.printTokensUsage(verbose: true);
logger.log('foo');
logger.printTokensUsage(verbose: true);
expect(printLog, contains('✅ foo'));
expect(printLog, contains('Tokens used: 1/1'));
}));
expect(printLog, contains('✅ foo'));
expect(printLog, contains('Tokens used: 1/1'));
}),
);
test('detects invalid logs', overridePrint(() {
allTokens['foo'] = 'value';
test(
'detects invalid logs',
overridePrint(() {
allTokens['foo'] = 'value';
logger.log('baz');
logger.log('foobar');
logger.printTokensUsage(verbose: true);
logger.log('baz');
logger.log('foobar');
logger.printTokensUsage(verbose: true);
expect(printLog, contains('❌ foo'));
expect(printLog, contains('Tokens used: 0/1'));
expect(printLog, contains(errorColoredString('Some referenced tokens do not exist: 2')));
expect(printLog, contains(' baz'));
expect(printLog, contains(' foobar'));
}));
expect(printLog, contains('❌ foo'));
expect(printLog, contains('Tokens used: 0/1'));
expect(printLog, contains(errorColoredString('Some referenced tokens do not exist: 2')));
expect(printLog, contains(' baz'));
expect(printLog, contains(' foobar'));
}),
);
test("color function doesn't log when providing a default", overridePrint(() {
allTokens['color_foo_req'] = 'value';
test(
"color function doesn't log when providing a default",
overridePrint(() {
allTokens['color_foo_req'] = 'value';
// color_foo_opt is not available, but because it has a default value, it won't warn about it
// color_foo_opt is not available, but because it has a default value, it won't warn about it
TestColorTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
TestColorTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
expect(printLog, contains('✅ color_foo_req'));
expect(printLog, contains('Tokens used: 1/1'));
}));
expect(printLog, contains('✅ color_foo_req'));
expect(printLog, contains('Tokens used: 1/1'));
}),
);
test('color function logs when not providing a default', overridePrint(() {
// Nor color_foo_req or color_foo_opt are available, but only color_foo_req will be logged.
// This mimics a token being removed, but expected to exist.
test(
'color function logs when not providing a default',
overridePrint(() {
// Nor color_foo_req or color_foo_opt are available, but only color_foo_req will be logged.
// This mimics a token being removed, but expected to exist.
TestColorTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
TestColorTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
expect(printLog, contains('Tokens used: 0/0'));
expect(printLog, contains(errorColoredString('Some referenced tokens do not exist: 1')));
expect(printLog, contains(' color_foo_req'));
}));
expect(printLog, contains('Tokens used: 0/0'));
expect(printLog, contains(errorColoredString('Some referenced tokens do not exist: 1')));
expect(printLog, contains(' color_foo_req'));
}),
);
test('border function logs width token when available', overridePrint(() {
allTokens['border_foo.color'] = 'red';
allTokens['border_foo.width'] = 3.0;
test(
'border function logs width token when available',
overridePrint(() {
allTokens['border_foo.color'] = 'red';
allTokens['border_foo.width'] = 3.0;
TestBorderTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
TestBorderTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
expect(printLog, contains('✅ border_foo.color'));
expect(printLog, contains('✅ border_foo.width'));
expect(printLog, contains('Tokens used: 2/2'));
}));
expect(printLog, contains('✅ border_foo.color'));
expect(printLog, contains('✅ border_foo.width'));
expect(printLog, contains('Tokens used: 2/2'));
}),
);
test('border function logs height token when width token not available', overridePrint(() {
allTokens['border_foo.color'] = 'red';
allTokens['border_foo.height'] = 3.0;
test(
'border function logs height token when width token not available',
overridePrint(() {
allTokens['border_foo.color'] = 'red';
allTokens['border_foo.height'] = 3.0;
TestBorderTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
TestBorderTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
expect(printLog, contains('✅ border_foo.color'));
expect(printLog, contains('✅ border_foo.height'));
expect(printLog, contains('Tokens used: 2/2'));
}));
expect(printLog, contains('✅ border_foo.color'));
expect(printLog, contains('✅ border_foo.height'));
expect(printLog, contains('Tokens used: 2/2'));
}),
);
test("border function doesn't log when width or height tokens not available", overridePrint(() {
allTokens['border_foo.color'] = 'red';
test(
"border function doesn't log when width or height tokens not available",
overridePrint(() {
allTokens['border_foo.color'] = 'red';
TestBorderTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
TestBorderTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
expect(printLog, contains('✅ border_foo.color'));
expect(printLog, contains('Tokens used: 1/1'));
}));
expect(printLog, contains('✅ border_foo.color'));
expect(printLog, contains('Tokens used: 1/1'));
}),
);
test('can log and dump versions & tokens to a file', overridePrint(() {
versionMap.addAll(testVersions);
allTokens['foo'] = 'value';
allTokens['bar'] = 'value';
test(
'can log and dump versions & tokens to a file',
overridePrint(() {
versionMap.addAll(testVersions);
allTokens['foo'] = 'value';
allTokens['bar'] = 'value';
logger.log('foo');
logger.log('bar');
logger.dumpToFile('test.json');
logger.log('foo');
logger.log('bar');
logger.dumpToFile('test.json');
final String fileContent = File('test.json').readAsStringSync();
expect(fileContent, contains('Versions used, v1.0.0, v2.0.0'));
expect(fileContent, contains('bar,'));
expect(fileContent, contains('foo'));
}));
final String fileContent = File('test.json').readAsStringSync();
expect(fileContent, contains('Versions used, v1.0.0, v2.0.0'));
expect(fileContent, contains('bar,'));
expect(fileContent, contains('foo'));
}),
);
test('integration test', overridePrint(() {
allTokens['foo'] = 'value';
allTokens['bar'] = 'value';
test(
'integration test',
overridePrint(() {
allTokens['foo'] = 'value';
allTokens['bar'] = 'value';
TestTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
TestTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
expect(printLog, contains('✅ foo'));
expect(printLog, contains('✅ bar'));
expect(printLog, contains('Tokens used: 2/2'));
}));
expect(printLog, contains('✅ foo'));
expect(printLog, contains('✅ bar'));
expect(printLog, contains('Tokens used: 2/2'));
}),
);
});
}

View File

@@ -26,25 +26,33 @@ import 'package:path/path.dart' as path;
/// Get contents of the file that contains the physical key mapping in Chromium
/// source.
Future<String> getChromiumCodes() async {
final Uri keyCodesUri = Uri.parse('https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_code_data.inc?format=TEXT');
final Uri keyCodesUri = Uri.parse(
'https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_code_data.inc?format=TEXT',
);
return utf8.decode(base64.decode(await http.read(keyCodesUri)));
}
/// Get contents of the file that contains the logical key mapping in Chromium
/// source.
Future<String> getChromiumKeys() async {
final Uri keyCodesUri = Uri.parse('https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_key_data.inc?format=TEXT');
final Uri keyCodesUri = Uri.parse(
'https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_key_data.inc?format=TEXT',
);
return utf8.decode(base64.decode(await http.read(keyCodesUri)));
}
/// Get contents of the file that contains the key codes in Android source.
Future<String> getAndroidKeyCodes() async {
final Uri keyCodesUri = Uri.parse('https://android.googlesource.com/platform/frameworks/native/+/main/include/android/keycodes.h?format=TEXT');
final Uri keyCodesUri = Uri.parse(
'https://android.googlesource.com/platform/frameworks/native/+/main/include/android/keycodes.h?format=TEXT',
);
return utf8.decode(base64.decode(await http.read(keyCodesUri)));
}
Future<String> getWindowsKeyCodes() async {
final Uri keyCodesUri = Uri.parse('https://raw.githubusercontent.com/tpn/winsdk-10/master/Include/10.0.10240.0/um/WinUser.h');
final Uri keyCodesUri = Uri.parse(
'https://raw.githubusercontent.com/tpn/winsdk-10/master/Include/10.0.10240.0/um/WinUser.h',
);
return http.read(keyCodesUri);
}
@@ -55,17 +63,23 @@ Future<String> getWindowsKeyCodes() async {
/// common keyboards. Other than some special keyboards and game pads, this
/// should be OK.
Future<String> getAndroidScanCodes() async {
final Uri scanCodesUri = Uri.parse('https://android.googlesource.com/platform/frameworks/base/+/main/data/keyboards/Generic.kl?format=TEXT');
final Uri scanCodesUri = Uri.parse(
'https://android.googlesource.com/platform/frameworks/base/+/main/data/keyboards/Generic.kl?format=TEXT',
);
return utf8.decode(base64.decode(await http.read(scanCodesUri)));
}
Future<String> getGlfwKeyCodes() async {
final Uri keyCodesUri = Uri.parse('https://raw.githubusercontent.com/glfw/glfw/master/include/GLFW/glfw3.h');
final Uri keyCodesUri = Uri.parse(
'https://raw.githubusercontent.com/glfw/glfw/master/include/GLFW/glfw3.h',
);
return http.read(keyCodesUri);
}
Future<String> getGtkKeyCodes() async {
final Uri keyCodesUri = Uri.parse('https://gitlab.gnome.org/GNOME/gtk/-/raw/gtk-3-24/gdk/gdkkeysyms.h');
final Uri keyCodesUri = Uri.parse(
'https://gitlab.gnome.org/GNOME/gtk/-/raw/gtk-3-24/gdk/gdkkeysyms.h',
);
return http.read(keyCodesUri);
}
@@ -100,7 +114,8 @@ Future<void> main(List<String> rawArguments) async {
argParser.addOption(
'engine-root',
defaultsTo: path.join(flutterRoot.path, '..', 'engine', 'src', 'flutter'),
help: 'The path to the root of the flutter/engine repository. This is used '
help:
'The path to the root of the flutter/engine repository. This is used '
'to place the generated engine mapping files. If --engine-root is not '
r'specified, it will default to $flutterRoot/../engine/src/flutter, '
'assuming the engine gclient folder is placed at the same folder as '
@@ -109,7 +124,8 @@ Future<void> main(List<String> rawArguments) async {
argParser.addOption(
'physical-data',
defaultsTo: path.join(dataRoot, 'physical_key_data.g.json'),
help: 'The path to where the physical key data file should be written when '
help:
'The path to where the physical key data file should be written when '
'collected, and read from when generating output code. If --physical-data is '
'not specified, the output will be written to/read from the current '
"directory. If the output directory doesn't exist, it, and the path to "
@@ -118,7 +134,8 @@ Future<void> main(List<String> rawArguments) async {
argParser.addOption(
'logical-data',
defaultsTo: path.join(dataRoot, 'logical_key_data.g.json'),
help: 'The path to where the logical key data file should be written when '
help:
'The path to where the logical key data file should be written when '
'collected, and read from when generating output code. If --logical-data is '
'not specified, the output will be written to/read from the current '
"directory. If the output directory doesn't exist, it, and the path to "
@@ -126,33 +143,48 @@ Future<void> main(List<String> rawArguments) async {
);
argParser.addOption(
'code',
defaultsTo: path.join(flutterRoot.path, 'packages', 'flutter', 'lib', 'src', 'services', 'keyboard_key.g.dart'),
help: 'The path to where the output "keyboard_key.g.dart" file should be '
defaultsTo: path.join(
flutterRoot.path,
'packages',
'flutter',
'lib',
'src',
'services',
'keyboard_key.g.dart',
),
help:
'The path to where the output "keyboard_key.g.dart" file should be '
'written. If --code is not specified, the output will be written to the '
'correct directory in the flutter tree. If the output directory does not '
'exist, it, and the path to it, will be created.',
);
argParser.addOption(
'maps',
defaultsTo: path.join(flutterRoot.path, 'packages', 'flutter', 'lib', 'src', 'services', 'keyboard_maps.g.dart'),
help: 'The path to where the output "keyboard_maps.g.dart" file should be '
'written. If --maps is not specified, the output will be written to the '
'correct directory in the flutter tree. If the output directory does not '
'exist, it, and the path to it, will be created.',
defaultsTo: path.join(
flutterRoot.path,
'packages',
'flutter',
'lib',
'src',
'services',
'keyboard_maps.g.dart',
),
help:
'The path to where the output "keyboard_maps.g.dart" file should be '
'written. If --maps is not specified, the output will be written to the '
'correct directory in the flutter tree. If the output directory does not '
'exist, it, and the path to it, will be created.',
);
argParser.addFlag(
'collect',
negatable: false,
help: 'If this flag is set, then collect and parse header files from '
help:
'If this flag is set, then collect and parse header files from '
'Chromium and Android instead of reading pre-parsed data from '
'"physical_key_data.g.json" and "logical_key_data.g.json", and then '
'update these files with the fresh data.',
);
argParser.addFlag(
'help',
negatable: false,
help: 'Print help for this command.',
);
argParser.addFlag('help', negatable: false, help: 'Print help for this command.');
final ArgResults parsedArguments = argParser.parse(rawArguments);
@@ -212,45 +244,59 @@ Future<void> main(List<String> rawArguments) async {
final String logicalJson = encoder.convert(logicalData.toJson());
File(parsedArguments['logical-data'] as String).writeAsStringSync('$logicalJson\n');
} else {
physicalData = PhysicalKeyData.fromJson(json.decode(await File(parsedArguments['physical-data'] as String).readAsString()) as Map<String, dynamic>);
logicalData = LogicalKeyData.fromJson(json.decode(await File(parsedArguments['logical-data'] as String).readAsString()) as Map<String, dynamic>);
physicalData = PhysicalKeyData.fromJson(
json.decode(await File(parsedArguments['physical-data'] as String).readAsString())
as Map<String, dynamic>,
);
logicalData = LogicalKeyData.fromJson(
json.decode(await File(parsedArguments['logical-data'] as String).readAsString())
as Map<String, dynamic>,
);
}
final Map<String, bool> layoutGoals = parseMapOfBool(readDataFile('layout_goals.json'));
await generate('key codes',
parsedArguments['code'] as String,
KeyboardKeysCodeGenerator(physicalData, logicalData));
await generate('key maps',
parsedArguments['maps'] as String,
KeyboardMapsCodeGenerator(physicalData, logicalData));
await generate('engine utils',
path.join(PlatformCodeGenerator.engineRoot,
'shell', 'platform', 'embedder', 'test_utils', 'key_codes.g.h'),
KeyCodesCcGenerator(physicalData, logicalData));
await generate('android utils',
path.join(PlatformCodeGenerator.engineRoot, 'shell', 'platform',
path.join('android', 'test', 'io', 'flutter', 'util', 'KeyCodes.java')),
KeyCodesJavaGenerator(physicalData, logicalData));
await generate(
'key codes',
parsedArguments['code'] as String,
KeyboardKeysCodeGenerator(physicalData, logicalData),
);
await generate(
'key maps',
parsedArguments['maps'] as String,
KeyboardMapsCodeGenerator(physicalData, logicalData),
);
await generate(
'engine utils',
path.join(
PlatformCodeGenerator.engineRoot,
'shell',
'platform',
'embedder',
'test_utils',
'key_codes.g.h',
),
KeyCodesCcGenerator(physicalData, logicalData),
);
await generate(
'android utils',
path.join(
PlatformCodeGenerator.engineRoot,
'shell',
'platform',
path.join('android', 'test', 'io', 'flutter', 'util', 'KeyCodes.java'),
),
KeyCodesJavaGenerator(physicalData, logicalData),
);
final Map<String, PlatformCodeGenerator> platforms = <String, PlatformCodeGenerator>{
'android': AndroidCodeGenerator(
physicalData,
logicalData,
),
'macos': MacOSCodeGenerator(
physicalData,
logicalData,
layoutGoals,
),
'ios': IOSCodeGenerator(
physicalData,
logicalData,
),
'android': AndroidCodeGenerator(physicalData, logicalData),
'macos': MacOSCodeGenerator(physicalData, logicalData, layoutGoals),
'ios': IOSCodeGenerator(physicalData, logicalData),
'windows': WindowsCodeGenerator(
physicalData,
logicalData,
readDataFile('windows_scancode_logical_map.json')
readDataFile('windows_scancode_logical_map.json'),
),
'linux': GtkCodeGenerator(
physicalData,
@@ -265,11 +311,11 @@ Future<void> main(List<String> rawArguments) async {
readDataFile('web_logical_location_mapping.json'),
),
};
await Future.wait(platforms.entries.map((MapEntry<String, PlatformCodeGenerator> entry) {
final String platform = entry.key;
final PlatformCodeGenerator codeGenerator = entry.value;
return generate('$platform map',
codeGenerator.outputPath(platform),
codeGenerator);
}));
await Future.wait(
platforms.entries.map((MapEntry<String, PlatformCodeGenerator> entry) {
final String platform = entry.key;
final PlatformCodeGenerator codeGenerator = entry.value;
return generate('$platform map', codeGenerator.outputPath(platform), codeGenerator);
}),
);
}

View File

@@ -10,7 +10,6 @@ import 'logical_key_data.dart';
import 'physical_key_data.dart';
import 'utils.dart';
/// Generates the key mapping for Android, based on the information in the key
/// data structure given to it.
class AndroidCodeGenerator extends PlatformCodeGenerator {
@@ -21,7 +20,9 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
final StringBuffer androidKeyCodeMap = StringBuffer();
for (final LogicalKeyEntry entry in logicalData.entries) {
for (final int code in entry.androidValues) {
androidKeyCodeMap.writeln(' put(${toHex(code, digits: 10)}L, ${toHex(entry.value, digits: 10)}L); // ${entry.constantName}');
androidKeyCodeMap.writeln(
' put(${toHex(code, digits: 10)}L, ${toHex(entry.value, digits: 10)}L); // ${entry.constantName}',
);
}
}
return androidKeyCodeMap.toString().trimRight();
@@ -32,7 +33,9 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
final StringBuffer androidScanCodeMap = StringBuffer();
for (final PhysicalKeyEntry entry in keyData.entries) {
for (final int code in entry.androidScanCodes.cast<int>()) {
androidScanCodeMap.writeln(' put(${toHex(code, digits: 10)}L, ${toHex(entry.usbHidCode, digits: 10)}L); // ${entry.constantName}');
androidScanCodeMap.writeln(
' put(${toHex(code, digits: 10)}L, ${toHex(entry.usbHidCode, digits: 10)}L); // ${entry.constantName}',
);
}
}
return androidScanCodeMap.toString().trimRight();
@@ -47,35 +50,38 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
};
goalsSource.forEach((String flagName, List<String> keys) {
int? lineId;
final List<String> keysString = keys.map((String keyName) {
final PhysicalKeyEntry physicalKey = keyData.entryByName(keyName);
final LogicalKeyEntry logicalKey = logicalData.entryByName(keyName);
lineId ??= physicalKey.usbHidCode;
return ' new KeyPair(${toHex(physicalKey.usbHidCode)}L, '
'${toHex(logicalKey.value, digits: 10)}L), // ${physicalKey.name}';
}).toList();
lines.add(lineId!,
' new PressingGoal(\n'
' KeyEvent.META_${flagName}_ON,\n'
' new KeyPair[] {\n'
'${keysString.join('\n')}\n'
' }),');
final List<String> keysString =
keys.map((String keyName) {
final PhysicalKeyEntry physicalKey = keyData.entryByName(keyName);
final LogicalKeyEntry logicalKey = logicalData.entryByName(keyName);
lineId ??= physicalKey.usbHidCode;
return ' new KeyPair(${toHex(physicalKey.usbHidCode)}L, '
'${toHex(logicalKey.value, digits: 10)}L), // ${physicalKey.name}';
}).toList();
lines.add(
lineId!,
' new PressingGoal(\n'
' KeyEvent.META_${flagName}_ON,\n'
' new KeyPair[] {\n'
'${keysString.join('\n')}\n'
' }),',
);
});
return lines.sortedJoin().trimRight();
}
String get _togglingGoals {
final OutputLines<int> lines = OutputLines<int>('Android toggling goals');
const Map<String, String> goalsSource = <String, String>{
'CAPS_LOCK': 'CapsLock',
};
const Map<String, String> goalsSource = <String, String>{'CAPS_LOCK': 'CapsLock'};
goalsSource.forEach((String flagName, String keyName) {
final PhysicalKeyEntry physicalKey = keyData.entryByName(keyName);
final LogicalKeyEntry logicalKey = logicalData.entryByName(keyName);
lines.add(physicalKey.usbHidCode,
' new TogglingGoal(KeyEvent.META_${flagName}_ON, '
'${toHex(physicalKey.usbHidCode)}L, '
'${toHex(logicalKey.value, digits: 10)}L),');
lines.add(
physicalKey.usbHidCode,
' new TogglingGoal(KeyEvent.META_${flagName}_ON, '
'${toHex(physicalKey.usbHidCode)}L, '
'${toHex(logicalKey.value, digits: 10)}L),',
);
});
return lines.sortedJoin().trimRight();
}
@@ -89,7 +95,9 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
kAndroidPlane,
];
for (final MaskConstant constant in maskConstants) {
buffer.writeln(' public static final long k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)}L;');
buffer.writeln(
' public static final long k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)}L;',
);
}
return buffer.toString().trimRight();
}
@@ -98,8 +106,12 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
String get templatePath => path.join(dataRoot, 'android_keyboard_map_java.tmpl');
@override
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot, 'shell', 'platform',
path.join('android', 'io', 'flutter', 'embedding', 'android', 'KeyboardMap.java'));
String outputPath(String platform) => path.join(
PlatformCodeGenerator.engineRoot,
'shell',
'platform',
path.join('android', 'io', 'flutter', 'embedding', 'android', 'KeyboardMap.java'),
);
@override
Map<String, String> mappings() {

View File

@@ -11,7 +11,7 @@ class MaskConstant {
: this(
name: '$platform Plane',
value: value,
description: 'The plane value for the private keys defined by the $platform embedding.'
description: 'The plane value for the private keys defined by the $platform embedding.',
);
final String name;
@@ -20,9 +20,9 @@ class MaskConstant {
String get upperCamelName {
return name
.split(' ')
.map<String>((String word) => lowerCamelToUpperCamel(word.toLowerCase()))
.join();
.split(' ')
.map<String>((String word) => lowerCamelToUpperCamel(word.toLowerCase()))
.join();
}
String get lowerCamelName {
@@ -52,7 +52,8 @@ const MaskConstant kUnicodePlane = MaskConstant(
const MaskConstant kUnprintablePlane = MaskConstant(
name: 'Unprintable Plane',
value: 0x0100000000,
description: 'The plane value for keys defined by Chromium and does not have a Unicode representation.',
description:
'The plane value for keys defined by Chromium and does not have a Unicode representation.',
);
const MaskConstant kFlutterPlane = MaskConstant(
@@ -64,45 +65,22 @@ const MaskConstant kFlutterPlane = MaskConstant(
const MaskConstant kStartOfPlatformPlanes = MaskConstant(
name: 'Start Of Platform Planes',
value: 0x1100000000,
description: 'The platform plane with the lowest mask value, beyond which the keys are considered autogenerated.',
description:
'The platform plane with the lowest mask value, beyond which the keys are considered autogenerated.',
);
const MaskConstant kAndroidPlane = MaskConstant.platform(
platform: 'Android',
value: 0x1100000000,
);
const MaskConstant kAndroidPlane = MaskConstant.platform(platform: 'Android', value: 0x1100000000);
const MaskConstant kFuchsiaPlane = MaskConstant.platform(
platform: 'Fuchsia',
value: 0x1200000000,
);
const MaskConstant kFuchsiaPlane = MaskConstant.platform(platform: 'Fuchsia', value: 0x1200000000);
const MaskConstant kIosPlane = MaskConstant.platform(
platform: 'iOS',
value: 0x1300000000,
);
const MaskConstant kIosPlane = MaskConstant.platform(platform: 'iOS', value: 0x1300000000);
const MaskConstant kMacosPlane = MaskConstant.platform(
platform: 'macOS',
value: 0x1400000000,
);
const MaskConstant kMacosPlane = MaskConstant.platform(platform: 'macOS', value: 0x1400000000);
const MaskConstant kGtkPlane = MaskConstant.platform(
platform: 'Gtk',
value: 0x1500000000,
);
const MaskConstant kGtkPlane = MaskConstant.platform(platform: 'Gtk', value: 0x1500000000);
const MaskConstant kWindowsPlane = MaskConstant.platform(
platform: 'Windows',
value: 0x1600000000,
);
const MaskConstant kWindowsPlane = MaskConstant.platform(platform: 'Windows', value: 0x1600000000);
const MaskConstant kWebPlane = MaskConstant.platform(
platform: 'Web',
value: 0x1700000000,
);
const MaskConstant kWebPlane = MaskConstant.platform(platform: 'Web', value: 0x1700000000);
const MaskConstant kGlfwPlane = MaskConstant.platform(
platform: 'GLFW',
value: 0x1800000000,
);
const MaskConstant kGlfwPlane = MaskConstant.platform(platform: 'GLFW', value: 0x1800000000);

View File

@@ -10,7 +10,6 @@ import 'logical_key_data.dart';
import 'physical_key_data.dart';
import 'utils.dart';
/// Generates the key mapping for GTK, based on the information in the key
/// data structure given to it.
class GtkCodeGenerator extends PlatformCodeGenerator {
@@ -28,8 +27,10 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('GTK scancode map');
for (final PhysicalKeyEntry entry in keyData.entries) {
if (entry.xKbScanCode != null) {
lines.add(entry.xKbScanCode!,
' {${toHex(entry.xKbScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}');
lines.add(
entry.xKbScanCode!,
' {${toHex(entry.xKbScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}',
);
}
}
return lines.sortedJoin().trimRight();
@@ -40,18 +41,17 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('GTK keyval map');
for (final LogicalKeyEntry entry in logicalData.entries) {
zipStrict(entry.gtkValues, entry.gtkNames, (int value, String name) {
lines.add(value,
' {${toHex(value)}, ${toHex(entry.value, digits: 11)}}, // $name');
lines.add(value, ' {${toHex(value)}, ${toHex(entry.value, digits: 11)}}, // $name');
});
}
return lines.sortedJoin().trimRight();
}
static String constructMapFromModToKeys(
Map<String, List<String>> source,
PhysicalKeyData physicalData,
LogicalKeyData logicalData,
String debugFunctionName,
Map<String, List<String>> source,
PhysicalKeyData physicalData,
LogicalKeyData logicalData,
String debugFunctionName,
) {
final StringBuffer result = StringBuffer();
source.forEach((String modifierBitName, List<String> keyNames) {
@@ -61,35 +61,49 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
final String? secondaryLogicalName = keyNames.length == 3 ? keyNames[2] : null;
final PhysicalKeyEntry primaryPhysical = physicalData.entryByName(primaryPhysicalName);
final LogicalKeyEntry primaryLogical = logicalData.entryByName(primaryLogicalName);
final LogicalKeyEntry? secondaryLogical = secondaryLogicalName == null ? null : logicalData.entryByName(secondaryLogicalName);
final LogicalKeyEntry? secondaryLogical =
secondaryLogicalName == null ? null : logicalData.entryByName(secondaryLogicalName);
if (secondaryLogical == null && secondaryLogicalName != null) {
print('Unrecognized secondary logical key $secondaryLogicalName specified for $debugFunctionName.');
print(
'Unrecognized secondary logical key $secondaryLogicalName specified for $debugFunctionName.',
);
return;
}
final String pad = secondaryLogical == null ? '' : ' ';
result.writeln('''
result.writeln(
'''
data = g_new(FlKeyEmbedderCheckedKey, 1);
g_hash_table_insert(table, GUINT_TO_POINTER(GDK_${modifierBitName}_MASK), data);
data->is_caps_lock = ${primaryPhysicalName == 'CapsLock' ? 'true' : 'false'};
data->primary_physical_key = ${toHex(primaryPhysical.usbHidCode, digits: 9)};$pad // ${primaryPhysical.constantName}
data->primary_logical_key = ${toHex(primaryLogical.value, digits: 11)};$pad // ${primaryLogical.constantName}''');
data->primary_logical_key = ${toHex(primaryLogical.value, digits: 11)};$pad // ${primaryLogical.constantName}''',
);
if (secondaryLogical != null) {
result.writeln('''
data->secondary_logical_key = ${toHex(secondaryLogical.value, digits: 11)}; // ${secondaryLogical.constantName}''');
result.writeln(
'''
data->secondary_logical_key = ${toHex(secondaryLogical.value, digits: 11)}; // ${secondaryLogical.constantName}''',
);
}
});
return result.toString().trimRight();
}
String get _gtkModifierBitMap {
return constructMapFromModToKeys(_modifierBitMapping, keyData, logicalData, 'gtkModifierBitMap');
return constructMapFromModToKeys(
_modifierBitMapping,
keyData,
logicalData,
'gtkModifierBitMap',
);
}
final Map<String, List<String>> _modifierBitMapping;
String get _gtkModeBitMap {
return constructMapFromModToKeys(_lockBitMapping, keyData, logicalData, 'gtkModeBitMap');
}
final Map<String, List<String>> _lockBitMapping;
final Map<String, bool> _layoutGoals;
@@ -98,14 +112,17 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
_layoutGoals.forEach((String name, bool mandatory) {
final PhysicalKeyEntry physicalEntry = keyData.entryByName(name);
final LogicalKeyEntry logicalEntry = logicalData.entryByName(name);
final String line = 'LayoutGoal{'
final String line =
'LayoutGoal{'
'${toHex(physicalEntry.xKbScanCode, digits: 2)}, '
'${toHex(logicalEntry.value, digits: 2)}, '
'${mandatory ? 'true' : 'false'}'
'},';
lines.add(logicalEntry.value,
' ${line.padRight(39)}'
'// ${logicalEntry.name}');
lines.add(
logicalEntry.value,
' ${line.padRight(39)}'
'// ${logicalEntry.name}',
);
});
return lines.sortedJoin().trimRight();
}
@@ -113,13 +130,11 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
/// This generates the mask values for the part of a key code that defines its plane.
String get _maskConstants {
final StringBuffer buffer = StringBuffer();
const List<MaskConstant> maskConstants = <MaskConstant>[
kValueMask,
kUnicodePlane,
kGtkPlane,
];
const List<MaskConstant> maskConstants = <MaskConstant>[kValueMask, kUnicodePlane, kGtkPlane];
for (final MaskConstant constant in maskConstants) {
buffer.writeln('const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};');
buffer.writeln(
'const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};',
);
}
return buffer.toString().trimRight();
}
@@ -128,8 +143,8 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
String get templatePath => path.join(dataRoot, 'gtk_key_mapping_cc.tmpl');
@override
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
'shell', 'platform', 'linux', 'key_mapping.g.cc');
String outputPath(String platform) =>
path.join(PlatformCodeGenerator.engineRoot, 'shell', 'platform', 'linux', 'key_mapping.g.cc');
@override
Map<String, String> mappings() {

View File

@@ -36,8 +36,10 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('iOS scancode map');
for (final PhysicalKeyEntry entry in keyData.entries) {
if (entry.iOSScanCode != null) {
lines.add(entry.iOSScanCode!,
' {${toHex(entry.iOSScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}');
lines.add(
entry.iOSScanCode!,
' {${toHex(entry.iOSScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}',
);
}
}
return lines.sortedJoin().trimRight();
@@ -62,7 +64,10 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('iOS keycode map');
for (final LogicalKeyEntry entry in logicalData.entries) {
zipStrict(entry.iOSKeyCodeValues, entry.iOSKeyCodeNames, (int iOSValue, String iOSName) {
lines.add(iOSValue, ' {${toHex(iOSValue)}, ${toHex(entry.value, digits: 11)}}, // $iOSName');
lines.add(
iOSValue,
' {${toHex(iOSValue)}, ${toHex(entry.value, digits: 11)}}, // $iOSName',
);
});
}
return lines.sortedJoin().trimRight();
@@ -71,16 +76,14 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
/// This generates the mask values for the part of a key code that defines its plane.
String get _maskConstants {
final StringBuffer buffer = StringBuffer();
const List<MaskConstant> maskConstants = <MaskConstant>[
kValueMask,
kUnicodePlane,
kIosPlane,
];
const List<MaskConstant> maskConstants = <MaskConstant>[kValueMask, kUnicodePlane, kIosPlane];
for (final MaskConstant constant in maskConstants) {
buffer.writeln('/**');
buffer.write(wrapString(constant.description, prefix: ' * '));
buffer.writeln(' */');
buffer.writeln('const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};');
buffer.writeln(
'const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};',
);
buffer.writeln();
}
return buffer.toString().trimRight();
@@ -90,7 +93,8 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
String get _keyToModifierFlagMap {
final StringBuffer modifierKeyMap = StringBuffer();
for (final String name in kModifiersOfInterest) {
final String line = '{${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}, kModifierFlag${lowerCamelToUpperCamel(name)}},';
final String line =
'{${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}, kModifierFlag${lowerCamelToUpperCamel(name)}},';
modifierKeyMap.writeln(' ${line.padRight(42)}// $name');
}
return modifierKeyMap.toString().trimRight();
@@ -100,7 +104,8 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
String get _modifierFlagToKeyMap {
final StringBuffer modifierKeyMap = StringBuffer();
for (final String name in kModifiersOfInterest) {
final String line = '{kModifierFlag${lowerCamelToUpperCamel(name)}, ${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}},';
final String line =
'{kModifierFlag${lowerCamelToUpperCamel(name)}, ${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}},';
modifierKeyMap.writeln(' ${line.padRight(42)}// $name');
}
return modifierKeyMap.toString().trimRight();
@@ -119,10 +124,14 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
String get _specialKeyConstants {
final StringBuffer specialKeyConstants = StringBuffer();
for (final String keyName in kSpecialPhysicalKeys) {
specialKeyConstants.writeln('const uint64_t k${keyName}PhysicalKey = ${toHex(keyData.entryByName(keyName).usbHidCode)};');
specialKeyConstants.writeln(
'const uint64_t k${keyName}PhysicalKey = ${toHex(keyData.entryByName(keyName).usbHidCode)};',
);
}
for (final String keyName in kSpecialLogicalKeys) {
specialKeyConstants.writeln('const uint64_t k${lowerCamelToUpperCamel(keyName)}LogicalKey = ${toHex(logicalData.entryByName(keyName).value)};');
specialKeyConstants.writeln(
'const uint64_t k${lowerCamelToUpperCamel(keyName)}LogicalKey = ${toHex(logicalData.entryByName(keyName).value)};',
);
}
return specialKeyConstants.toString().trimRight();
}
@@ -131,8 +140,16 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
String get templatePath => path.join(dataRoot, 'ios_key_code_map_mm.tmpl');
@override
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
'shell', 'platform', 'darwin', 'ios', 'framework', 'Source', 'KeyCodeMap.g.mm');
String outputPath(String platform) => path.join(
PlatformCodeGenerator.engineRoot,
'shell',
'platform',
'darwin',
'ios',
'framework',
'Source',
'KeyCodeMap.g.mm',
);
@override
Map<String, String> mappings() {

View File

@@ -55,10 +55,14 @@ class KeyboardKeysCodeGenerator extends BaseCodeGenerator {
String get _physicalDefinitions {
final OutputLines<int> lines = OutputLines<int>('Physical Key Definition');
for (final PhysicalKeyEntry entry in keyData.entries) {
final String firstComment = _wrapString('Represents the location of the '
'"${entry.commentName}" key on a generalized keyboard.');
final String otherComments = _wrapString('See the function '
'[RawKeyEvent.physicalKey] for more information.');
final String firstComment = _wrapString(
'Represents the location of the '
'"${entry.commentName}" key on a generalized keyboard.',
);
final String otherComments = _wrapString(
'See the function '
'[RawKeyEvent.physicalKey] for more information.',
);
lines.add(entry.usbHidCode, '''
$firstComment ///
$otherComments static const PhysicalKeyboardKey ${entry.constantName} = PhysicalKeyboardKey(${toHex(entry.usbHidCode)});
@@ -78,10 +82,17 @@ $otherComments static const PhysicalKeyboardKey ${entry.constantName} = Physica
/// Gets the generated definitions of LogicalKeyboardKeys.
String get _logicalDefinitions {
final OutputLines<int> lines = OutputLines<int>('Logical debug names', behavior: DeduplicateBehavior.kSkip);
final OutputLines<int> lines = OutputLines<int>(
'Logical debug names',
behavior: DeduplicateBehavior.kSkip,
);
void printKey(int flutterId, String constantName, String commentName, {String? otherComments}) {
final String firstComment = _wrapString('Represents the logical "$commentName" key on the keyboard.');
otherComments ??= _wrapString('See the function [RawKeyEvent.logicalKey] for more information.');
final String firstComment = _wrapString(
'Represents the logical "$commentName" key on the keyboard.',
);
otherComments ??= _wrapString(
'See the function [RawKeyEvent.logicalKey] for more information.',
);
lines.add(flutterId, '''
$firstComment ///
$otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardKey(${toHex(flutterId, digits: 11)});
@@ -101,11 +112,13 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
String? _otherComments(String name) {
if (synonyms.containsKey(name)) {
final Set<String> unionNames = synonyms[name]!.keys.map(
(LogicalKeyEntry entry) => entry.constantName).toSet();
return _wrapString('This key represents the union of the keys '
'$unionNames when comparing keys. This key will never be generated '
'directly, its main use is in defining key maps.');
final Set<String> unionNames =
synonyms[name]!.keys.map((LogicalKeyEntry entry) => entry.constantName).toSet();
return _wrapString(
'This key represents the union of the keys '
'$unionNames when comparing keys. This key will never be generated '
'directly, its main use is in defining key maps.',
);
}
return null;
}
@@ -125,14 +138,18 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
final StringBuffer result = StringBuffer();
for (final SynonymKeyInfo synonymInfo in synonyms.values) {
final LogicalKeyEntry synonym = logicalData.entryByName(synonymInfo.name);
final List<String> entries = synonymInfo.keys.map<String>((LogicalKeyEntry entry) => entry.constantName).toList();
final List<String> entries =
synonymInfo.keys.map<String>((LogicalKeyEntry entry) => entry.constantName).toList();
result.writeln(' ${synonym.constantName}: <LogicalKeyboardKey>{${entries.join(', ')}},');
}
return result.toString();
}
String get _logicalKeyLabels {
final OutputLines<int> lines = OutputLines<int>('Logical key labels', behavior: DeduplicateBehavior.kSkip);
final OutputLines<int> lines = OutputLines<int>(
'Logical key labels',
behavior: DeduplicateBehavior.kSkip,
);
for (final LogicalKeyEntry entry in logicalData.entries) {
lines.add(entry.value, '''
${toHex(entry.value, digits: 11)}: '${entry.commentName}',''');
@@ -151,7 +168,10 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
/// This generates the map of Flutter key codes to logical keys.
String get _predefinedKeyCodeMap {
final OutputLines<int> lines = OutputLines<int>('Logical key map', behavior: DeduplicateBehavior.kSkip);
final OutputLines<int> lines = OutputLines<int>(
'Logical key map',
behavior: DeduplicateBehavior.kSkip,
);
for (final LogicalKeyEntry entry in logicalData.entries) {
lines.add(entry.value, ' ${toHex(entry.value, digits: 11)}: ${entry.constantName},');
}
@@ -159,7 +179,10 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
}
String get _maskConstantVariables {
final OutputLines<int> lines = OutputLines<int>('Mask constants', behavior: DeduplicateBehavior.kKeep);
final OutputLines<int> lines = OutputLines<int>(
'Mask constants',
behavior: DeduplicateBehavior.kKeep,
);
for (final MaskConstant constant in _maskConstants) {
lines.add(constant.value, '''
${_wrapString(constant.description)} ///
@@ -190,14 +213,11 @@ ${_wrapString(constant.description)} ///
late final Map<String, SynonymKeyInfo> synonyms = Map<String, SynonymKeyInfo>.fromEntries(
LogicalKeyData.synonyms.entries.map((MapEntry<String, List<String>> synonymDefinition) {
final List<LogicalKeyEntry> entries = synonymDefinition.value.map(
(String name) => logicalData.entryByName(name)).toList();
final List<LogicalKeyEntry> entries =
synonymDefinition.value.map((String name) => logicalData.entryByName(name)).toList();
return MapEntry<String, SynonymKeyInfo>(
synonymDefinition.key,
SynonymKeyInfo(
entries,
synonymDefinition.key,
),
SynonymKeyInfo(entries, synonymDefinition.key),
);
}),
);

View File

@@ -22,8 +22,8 @@ bool _isAsciiLetter(String? char) {
const int charLowerZ = 0x7A;
assert(char.length == 1);
final int charCode = char.codeUnitAt(0);
return (charCode >= charUpperA && charCode <= charUpperZ)
|| (charCode >= charLowerA && charCode <= charLowerZ);
return (charCode >= charUpperA && charCode <= charUpperZ) ||
(charCode >= charLowerA && charCode <= charLowerZ);
}
bool _isDigit(String? char) {
@@ -44,7 +44,8 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
List<PhysicalKeyEntry> get _numpadKeyData {
return keyData.entries.where((PhysicalKeyEntry entry) {
return entry.constantName.startsWith('numpad') && LogicalKeyData.printable.containsKey(entry.name);
return entry.constantName.startsWith('numpad') &&
LogicalKeyData.printable.containsKey(entry.name);
}).toList();
}
@@ -57,7 +58,8 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
List<LogicalKeyEntry> get _numpadLogicalKeyData {
return logicalData.entries.where((LogicalKeyEntry entry) {
return entry.constantName.startsWith('numpad') && LogicalKeyData.printable.containsKey(entry.name);
return entry.constantName.startsWith('numpad') &&
LogicalKeyData.printable.containsKey(entry.name);
}).toList();
}
@@ -111,8 +113,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('GTK scancode map');
for (final PhysicalKeyEntry entry in keyData.entries) {
if (entry.xKbScanCode != null) {
lines.add(entry.xKbScanCode!,
' ${toHex(entry.xKbScanCode)}: PhysicalKeyboardKey.${entry.constantName},');
lines.add(
entry.xKbScanCode!,
' ${toHex(entry.xKbScanCode)}: PhysicalKeyboardKey.${entry.constantName},',
);
}
}
return lines.sortedJoin().trimRight();
@@ -156,7 +160,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('Windows scancode map');
for (final PhysicalKeyEntry entry in keyData.entries) {
if (entry.windowsScanCode != null) {
lines.add(entry.windowsScanCode!, ' ${entry.windowsScanCode}: PhysicalKeyboardKey.${entry.constantName},');
lines.add(
entry.windowsScanCode!,
' ${entry.windowsScanCode}: PhysicalKeyboardKey.${entry.constantName},',
);
}
}
return lines.sortedJoin().trimRight();
@@ -179,11 +186,14 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
for (final LogicalKeyEntry entry in logicalData.entries) {
// Letter keys on Windows are not recorded in logical_key_data.g.json,
// because they are not used by the embedding. Add them manually.
final List<int>? keyCodes = entry.windowsValues.isNotEmpty
? entry.windowsValues
: (_isAsciiLetter(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] :
_isDigit(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] :
null);
final List<int>? keyCodes =
entry.windowsValues.isNotEmpty
? entry.windowsValues
: (_isAsciiLetter(entry.keyLabel)
? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)]
: _isDigit(entry.keyLabel)
? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)]
: null);
if (keyCodes != null) {
for (final int code in keyCodes) {
lines.add(code, ' $code: LogicalKeyboardKey.${entry.constantName},');
@@ -198,7 +208,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('macOS scancode map');
for (final PhysicalKeyEntry entry in keyData.entries) {
if (entry.macOSScanCode != null) {
lines.add(entry.macOSScanCode!, ' ${toHex(entry.macOSScanCode)}: PhysicalKeyboardKey.${entry.constantName},');
lines.add(
entry.macOSScanCode!,
' ${toHex(entry.macOSScanCode)}: PhysicalKeyboardKey.${entry.constantName},',
);
}
}
return lines.sortedJoin().trimRight();
@@ -209,7 +222,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('macOS numpad map');
for (final PhysicalKeyEntry entry in _numpadKeyData) {
if (entry.macOSScanCode != null) {
lines.add(entry.macOSScanCode!, ' ${toHex(entry.macOSScanCode)}: LogicalKeyboardKey.${entry.constantName},');
lines.add(
entry.macOSScanCode!,
' ${toHex(entry.macOSScanCode)}: LogicalKeyboardKey.${entry.constantName},',
);
}
}
return lines.sortedJoin().trimRight();
@@ -219,7 +235,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('macOS function key map');
for (final PhysicalKeyEntry entry in _functionKeyData) {
if (entry.macOSScanCode != null) {
lines.add(entry.macOSScanCode!, ' ${toHex(entry.macOSScanCode)}: LogicalKeyboardKey.${entry.constantName},');
lines.add(
entry.macOSScanCode!,
' ${toHex(entry.macOSScanCode)}: LogicalKeyboardKey.${entry.constantName},',
);
}
}
return lines.sortedJoin().trimRight();
@@ -241,7 +260,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('iOS scancode map');
for (final PhysicalKeyEntry entry in keyData.entries) {
if (entry.iOSScanCode != null) {
lines.add(entry.iOSScanCode!, ' ${toHex(entry.iOSScanCode)}: PhysicalKeyboardKey.${entry.constantName},');
lines.add(
entry.iOSScanCode!,
' ${toHex(entry.iOSScanCode)}: PhysicalKeyboardKey.${entry.constantName},',
);
}
}
return lines.sortedJoin().trimRight();
@@ -262,7 +284,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('iOS numpad map');
for (final PhysicalKeyEntry entry in _numpadKeyData) {
if (entry.iOSScanCode != null) {
lines.add(entry.iOSScanCode!,' ${toHex(entry.iOSScanCode)}: LogicalKeyboardKey.${entry.constantName},');
lines.add(
entry.iOSScanCode!,
' ${toHex(entry.iOSScanCode)}: LogicalKeyboardKey.${entry.constantName},',
);
}
}
return lines.sortedJoin().trimRight();
@@ -294,7 +319,9 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
String get _fuchsiaHidCodeMap {
final StringBuffer fuchsiaScanCodeMap = StringBuffer();
for (final PhysicalKeyEntry entry in keyData.entries) {
fuchsiaScanCodeMap.writeln(' ${toHex(entry.usbHidCode)}: PhysicalKeyboardKey.${entry.constantName},');
fuchsiaScanCodeMap.writeln(
' ${toHex(entry.usbHidCode)}: PhysicalKeyboardKey.${entry.constantName},',
);
}
return fuchsiaScanCodeMap.toString().trimRight();
}
@@ -312,7 +339,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
/// This generates the map of Web KeyboardEvent codes to physical keys.
String get _webPhysicalKeyMap {
final OutputLines<String> lines = OutputLines<String>('Web physical key map', behavior: DeduplicateBehavior.kKeep);
final OutputLines<String> lines = OutputLines<String>(
'Web physical key map',
behavior: DeduplicateBehavior.kKeep,
);
for (final PhysicalKeyEntry entry in keyData.entries) {
for (final String webCodes in entry.webCodes()) {
lines.add(entry.name, " '$webCodes': PhysicalKeyboardKey.${entry.constantName},");
@@ -331,14 +361,18 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
/// This generates the map of Web number pad codes to logical keys.
String get _webLocationMap {
final String jsonRaw = File(path.join(dataRoot, 'web_logical_location_mapping.json')).readAsStringSync();
final String jsonRaw =
File(path.join(dataRoot, 'web_logical_location_mapping.json')).readAsStringSync();
final Map<String, List<String?>> locationMap = parseMapOfListOfNullableString(jsonRaw);
final OutputLines<String> lines = OutputLines<String>('Web location map');
locationMap.forEach((String key, List<String?> keyNames) {
final String keyStrings = keyNames.map((String? keyName) {
final String? constantName = keyName == null ? null : logicalData.entryByName(keyName).constantName;
return constantName != null ? 'LogicalKeyboardKey.$constantName' : 'null';
}).join(', ');
final String keyStrings = keyNames
.map((String? keyName) {
final String? constantName =
keyName == null ? null : logicalData.entryByName(keyName).constantName;
return constantName != null ? 'LogicalKeyboardKey.$constantName' : 'null';
})
.join(', ');
lines.add(key, " '$key': <LogicalKeyboardKey?>[$keyStrings],");
});
return lines.sortedJoin().trimRight();

View File

@@ -62,10 +62,11 @@ class LogicalKeyData {
_readFuchsiaKeyCodes(data, physicalKeyData);
_readGlfwKeyCodes(data, glfwHeaderFile, parseMapOfListOfString(glfwNameMap));
// Sort entries by value
final List<MapEntry<String, LogicalKeyEntry>> sortedEntries = data.entries.toList()..sort(
(MapEntry<String, LogicalKeyEntry> a, MapEntry<String, LogicalKeyEntry> b) =>
LogicalKeyEntry.compareByValue(a.value, b.value),
);
final List<MapEntry<String, LogicalKeyEntry>> sortedEntries =
data.entries.toList()..sort(
(MapEntry<String, LogicalKeyEntry> a, MapEntry<String, LogicalKeyEntry> b) =>
LogicalKeyEntry.compareByValue(a.value, b.value),
);
data
..clear()
..addEntries(sortedEntries);
@@ -75,10 +76,14 @@ class LogicalKeyData {
/// Parses the given JSON data and populates the data structure from it.
factory LogicalKeyData.fromJson(Map<String, dynamic> contentMap) {
final Map<String, LogicalKeyEntry> data = <String, LogicalKeyEntry>{};
data.addEntries(contentMap.values.map((dynamic value) {
final LogicalKeyEntry entry = LogicalKeyEntry.fromJsonMapEntry(value as Map<String, dynamic>);
return MapEntry<String, LogicalKeyEntry>(entry.name, entry);
}));
data.addEntries(
contentMap.values.map((dynamic value) {
final LogicalKeyEntry entry = LogicalKeyEntry.fromJsonMapEntry(
value as Map<String, dynamic>,
);
return MapEntry<String, LogicalKeyEntry>(entry.name, entry);
}),
);
return LogicalKeyData._(data);
}
@@ -102,8 +107,7 @@ class LogicalKeyData {
///
/// Asserts if the name is not found.
LogicalKeyEntry entryByName(String name) {
assert(_data.containsKey(name),
'Unable to find logical entry by name $name.');
assert(_data.containsKey(name), 'Unable to find logical entry by name $name.');
return _data[name]!;
}
@@ -150,12 +154,17 @@ class LogicalKeyData {
if (webName.startsWith('.')) {
continue;
}
final String name = LogicalKeyEntry.computeName(webName.replaceAll(RegExp('[^A-Za-z0-9]'), ''));
final int value = match.namedGroup('unicode') != null ?
getHex(match.namedGroup('unicode')!) :
match.namedGroup('char')!.codeUnitAt(0);
final String? keyLabel = (match.namedGroup('kind')! == 'UNI' && !_isControlCharacter(value)) ?
String.fromCharCode(value) : null;
final String name = LogicalKeyEntry.computeName(
webName.replaceAll(RegExp('[^A-Za-z0-9]'), ''),
);
final int value =
match.namedGroup('unicode') != null
? getHex(match.namedGroup('unicode')!)
: match.namedGroup('char')!.codeUnitAt(0);
final String? keyLabel =
(match.namedGroup('kind')! == 'UNI' && !_isControlCharacter(value))
? String.fromCharCode(value)
: null;
// Skip modifier keys from DOM. They will be added with supplemental data.
if (_chromeModifiers.containsKey(name) && source == 'DOM') {
continue;
@@ -163,20 +172,17 @@ class LogicalKeyData {
final bool isPrintable = keyLabel != null;
final int entryValue = toPlane(value, _sourceToPlane(source, isPrintable));
final LogicalKeyEntry entry = dataByValue.putIfAbsent(entryValue, () =>
LogicalKeyEntry.fromName(
value: entryValue,
name: name,
keyLabel: keyLabel,
),
final LogicalKeyEntry entry = dataByValue.putIfAbsent(
entryValue,
() => LogicalKeyEntry.fromName(value: entryValue, name: name, keyLabel: keyLabel),
);
if (source == 'DOM' && !isPrintable) {
entry.webNames.add(webName);
}
}
return Map<String, LogicalKeyEntry>.fromEntries(
dataByValue.values.map((LogicalKeyEntry entry) =>
MapEntry<String, LogicalKeyEntry>(entry.name, entry),
dataByValue.values.map(
(LogicalKeyEntry entry) => MapEntry<String, LogicalKeyEntry>(entry.name, entry),
),
);
}
@@ -186,16 +192,21 @@ class LogicalKeyData {
PhysicalKeyData physicalKeyData,
Map<String, List<String>> logicalToPhysical,
) {
final Map<String, String> physicalToLogical = reverseMapOfListOfString(logicalToPhysical,
(String logicalKeyName, String physicalKeyName) { print('Duplicate logical key name $logicalKeyName for macOS'); });
final Map<String, String> physicalToLogical = reverseMapOfListOfString(logicalToPhysical, (
String logicalKeyName,
String physicalKeyName,
) {
print('Duplicate logical key name $logicalKeyName for macOS');
});
physicalToLogical.forEach((String physicalKeyName, String logicalKeyName) {
final PhysicalKeyEntry physicalEntry = physicalKeyData.entryByName(physicalKeyName);
assert(physicalEntry.macOSScanCode != null,
'Physical entry $physicalKeyName does not have a macOSScanCode.');
assert(
physicalEntry.macOSScanCode != null,
'Physical entry $physicalKeyName does not have a macOSScanCode.',
);
final LogicalKeyEntry? logicalEntry = data[logicalKeyName];
assert(logicalEntry != null,
'Unable to find logical entry by name $logicalKeyName.');
assert(logicalEntry != null, 'Unable to find logical entry by name $logicalKeyName.');
logicalEntry!.macOSKeyCodeNames.add(physicalEntry.name);
logicalEntry.macOSKeyCodeValues.add(physicalEntry.macOSScanCode!);
});
@@ -206,16 +217,21 @@ class LogicalKeyData {
PhysicalKeyData physicalKeyData,
Map<String, List<String>> logicalToPhysical,
) {
final Map<String, String> physicalToLogical = reverseMapOfListOfString(logicalToPhysical,
(String logicalKeyName, String physicalKeyName) { print('Duplicate logical key name $logicalKeyName for iOS'); });
final Map<String, String> physicalToLogical = reverseMapOfListOfString(logicalToPhysical, (
String logicalKeyName,
String physicalKeyName,
) {
print('Duplicate logical key name $logicalKeyName for iOS');
});
physicalToLogical.forEach((String physicalKeyName, String logicalKeyName) {
final PhysicalKeyEntry physicalEntry = physicalKeyData.entryByName(physicalKeyName);
assert(physicalEntry.iOSScanCode != null,
'Physical entry $physicalKeyName does not have an iosScanCode.');
assert(
physicalEntry.iOSScanCode != null,
'Physical entry $physicalKeyName does not have an iosScanCode.',
);
final LogicalKeyEntry? logicalEntry = data[logicalKeyName];
assert(logicalEntry != null,
'Unable to find logical entry by name $logicalKeyName.');
assert(logicalEntry != null, 'Unable to find logical entry by name $logicalKeyName.');
logicalEntry!.iOSKeyCodeNames.add(physicalEntry.name);
logicalEntry.iOSKeyCodeValues.add(physicalEntry.iOSScanCode!);
});
@@ -226,14 +242,22 @@ class LogicalKeyData {
/// Lines in this file look like this (without the ///):
/// /** Space key. */
/// #define GDK_KEY_space 0x020
static void _readGtkKeyCodes(Map<String, LogicalKeyEntry> data, String headerFile, Map<String, List<String>> nameToGtkName) {
static void _readGtkKeyCodes(
Map<String, LogicalKeyEntry> data,
String headerFile,
Map<String, List<String>> nameToGtkName,
) {
final RegExp definedCodes = RegExp(
r'#define '
r'GDK_KEY_(?<name>[a-zA-Z0-9_]+)\s*'
r'0x(?<value>[0-9a-f]+),?',
);
final Map<String, String> gtkNameToFlutterName = reverseMapOfListOfString(nameToGtkName,
(String flutterName, String gtkName) { print('Duplicate GTK logical name $gtkName'); });
final Map<String, String> gtkNameToFlutterName = reverseMapOfListOfString(nameToGtkName, (
String flutterName,
String gtkName,
) {
print('Duplicate GTK logical name $gtkName');
});
for (final RegExpMatch match in definedCodes.allMatches(headerFile)) {
final String gtkName = match.namedGroup('name')!;
@@ -255,11 +279,19 @@ class LogicalKeyData {
}
}
static void _readWindowsKeyCodes(Map<String, LogicalKeyEntry> data, String headerFile, Map<String, List<String>> nameMap) {
static void _readWindowsKeyCodes(
Map<String, LogicalKeyEntry> data,
String headerFile,
Map<String, List<String>> nameMap,
) {
// The mapping from the Flutter name (e.g. "enter") to the Windows name (e.g.
// "RETURN").
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap,
(String flutterName, String windowsName) { print('Duplicate Windows logical name $windowsName'); });
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap, (
String flutterName,
String windowsName,
) {
print('Duplicate Windows logical name $windowsName');
});
final RegExp definedCodes = RegExp(
r'define '
@@ -279,12 +311,7 @@ class LogicalKeyData {
print('Invalid logical entry by name $name (from Windows $windowsName)');
continue;
}
addNameValue(
entry.windowsNames,
entry.windowsValues,
windowsName,
value,
);
addNameValue(entry.windowsNames, entry.windowsValues, windowsName, value);
}
}
@@ -293,9 +320,17 @@ class LogicalKeyData {
/// Lines in this file look like this (without the ///):
/// /** Left Control modifier key. */
/// AKEYCODE_CTRL_LEFT = 113,
static void _readAndroidKeyCodes(Map<String, LogicalKeyEntry> data, String headerFile, Map<String, List<String>> nameMap) {
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap,
(String flutterName, String androidName) { print('Duplicate Android logical name $androidName'); });
static void _readAndroidKeyCodes(
Map<String, LogicalKeyEntry> data,
String headerFile,
Map<String, List<String>> nameMap,
) {
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap, (
String flutterName,
String androidName,
) {
print('Duplicate Android logical name $androidName');
});
final RegExp enumBlock = RegExp(r'enum\s*\{(.*)\};', multiLine: true);
// Eliminate everything outside of the enum block.
@@ -324,22 +359,26 @@ class LogicalKeyData {
}
}
static void _readFuchsiaKeyCodes(Map<String, LogicalKeyEntry> data, PhysicalKeyData physicalData) {
static void _readFuchsiaKeyCodes(
Map<String, LogicalKeyEntry> data,
PhysicalKeyData physicalData,
) {
for (final LogicalKeyEntry entry in data.values) {
final int? value = (() {
if (entry.value == 0) {
return 0;
}
final String? keyLabel = printable[entry.constantName];
if (keyLabel != null && !entry.constantName.startsWith('numpad')) {
return toPlane(keyLabel.codeUnitAt(0), kUnicodePlane.value);
} else {
final PhysicalKeyEntry? physicalEntry = physicalData.tryEntryByName(entry.name);
if (physicalEntry != null) {
return toPlane(physicalEntry.usbHidCode, kFuchsiaPlane.value);
}
}
})();
final int? value =
(() {
if (entry.value == 0) {
return 0;
}
final String? keyLabel = printable[entry.constantName];
if (keyLabel != null && !entry.constantName.startsWith('numpad')) {
return toPlane(keyLabel.codeUnitAt(0), kUnicodePlane.value);
} else {
final PhysicalKeyEntry? physicalEntry = physicalData.tryEntryByName(entry.name);
if (physicalEntry != null) {
return toPlane(physicalEntry.usbHidCode, kFuchsiaPlane.value);
}
}
})();
if (value != null) {
entry.fuchsiaValues.add(value);
}
@@ -352,9 +391,17 @@ class LogicalKeyData {
/// /** Space key. */
/// #define GLFW_KEY_SPACE 32,
/// #define GLFW_KEY_LAST GLFW_KEY_MENU
static void _readGlfwKeyCodes(Map<String, LogicalKeyEntry> data, String headerFile, Map<String, List<String>> nameMap) {
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap,
(String flutterName, String glfwName) { print('Duplicate GLFW logical name $glfwName'); });
static void _readGlfwKeyCodes(
Map<String, LogicalKeyEntry> data,
String headerFile,
Map<String, List<String>> nameMap,
) {
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap, (
String flutterName,
String glfwName,
) {
print('Duplicate GLFW logical name $glfwName');
});
// Only get the KEY definitions, ignore the rest (mouse, joystick, etc).
final RegExp definedCodes = RegExp(
@@ -388,47 +435,47 @@ class LogicalKeyData {
print('Invalid logical entry by name $name (from GLFW $glfwName)');
return;
}
addNameValue(
entry.glfwNames,
entry.glfwValues,
glfwName,
value,
);
addNameValue(entry.glfwNames, entry.glfwValues, glfwName, value);
});
}
// Map Web key to the pair of key names
static final Map<String, _ModifierPair> _chromeModifiers = () {
final String rawJson = File(path.join(dataRoot, 'chromium_modifiers.json',)).readAsStringSync();
final String rawJson = File(path.join(dataRoot, 'chromium_modifiers.json')).readAsStringSync();
return (json.decode(rawJson) as Map<String, dynamic>).map((String key, dynamic value) {
final List<dynamic> pair = value as List<dynamic>;
return MapEntry<String, _ModifierPair>(key, _ModifierPair(pair[0] as String, pair[1] as String));
return MapEntry<String, _ModifierPair>(
key,
_ModifierPair(pair[0] as String, pair[1] as String),
);
});
}();
/// Returns the static map of printable representations.
static final Map<String, String> printable = (() {
final String printableKeys = File(path.join(dataRoot, 'printable.json',)).readAsStringSync();
return (json.decode(printableKeys) as Map<String, dynamic>)
.cast<String, String>();
})();
static final Map<String, String> printable =
(() {
final String printableKeys = File(path.join(dataRoot, 'printable.json')).readAsStringSync();
return (json.decode(printableKeys) as Map<String, dynamic>).cast<String, String>();
})();
/// Returns the static map of synonym representations.
///
/// These include synonyms for keys which don't have printable
/// representations, and appear in more than one place on the keyboard (e.g.
/// SHIFT, ALT, etc.).
static final Map<String, List<String>> synonyms = (() {
final String synonymKeys = File(path.join(dataRoot, 'synonyms.json',)).readAsStringSync();
final Map<String, dynamic> dynamicSynonym = json.decode(synonymKeys) as Map<String, dynamic>;
return dynamicSynonym.map((String name, dynamic values) {
// The keygen and algorithm of macOS relies on synonyms being pairs.
// See siblingKeyMap in macos_code_gen.dart.
final List<String> names = (values as List<dynamic>).whereType<String>().toList();
assert(names.length == 2);
return MapEntry<String, List<String>>(name, names);
});
})();
static final Map<String, List<String>> synonyms =
(() {
final String synonymKeys = File(path.join(dataRoot, 'synonyms.json')).readAsStringSync();
final Map<String, dynamic> dynamicSynonym =
json.decode(synonymKeys) as Map<String, dynamic>;
return dynamicSynonym.map((String name, dynamic values) {
// The keygen and algorithm of macOS relies on synonyms being pairs.
// See siblingKeyMap in macos_code_gen.dart.
final List<String> names = (values as List<dynamic>).whereType<String>().toList();
assert(names.length == 2);
return MapEntry<String, List<String>>(name, names);
});
})();
static int _sourceToPlane(String source, bool isPrintable) {
if (isPrintable) {
@@ -446,41 +493,30 @@ class LogicalKeyData {
}
}
/// A single entry in the key data structure.
///
/// Can be read from JSON with the [LogicalKeyEntry.fromJsonMapEntry] constructor, or
/// written with the [toJson] method.
class LogicalKeyEntry {
/// Creates a single key entry from available data.
LogicalKeyEntry({
required this.value,
required this.name,
this.keyLabel,
}) : webNames = <String>[],
macOSKeyCodeNames = <String>[],
macOSKeyCodeValues = <int>[],
iOSKeyCodeNames = <String>[],
iOSKeyCodeValues = <int>[],
gtkNames = <String>[],
gtkValues = <int>[],
windowsNames = <String>[],
windowsValues = <int>[],
androidNames = <String>[],
androidValues = <int>[],
fuchsiaValues = <int>[],
glfwNames = <String>[],
glfwValues = <int>[];
LogicalKeyEntry({required this.value, required this.name, this.keyLabel})
: webNames = <String>[],
macOSKeyCodeNames = <String>[],
macOSKeyCodeValues = <int>[],
iOSKeyCodeNames = <String>[],
iOSKeyCodeValues = <int>[],
gtkNames = <String>[],
gtkValues = <int>[],
windowsNames = <String>[],
windowsValues = <int>[],
androidNames = <String>[],
androidValues = <int>[],
fuchsiaValues = <int>[],
glfwNames = <String>[],
glfwValues = <int>[];
LogicalKeyEntry.fromName({
required int value,
required String name,
String? keyLabel,
}) : this(
value: value,
name: name,
keyLabel: keyLabel,
);
LogicalKeyEntry.fromName({required int value, required String name, String? keyLabel})
: this(value: value, name: name, keyLabel: keyLabel);
/// Populates the key from a JSON map.
LogicalKeyEntry.fromJsonMapEntry(Map<String, dynamic> map)
@@ -628,23 +664,37 @@ class LogicalKeyEntry {
/// separate words (e.g. "wakeUp" converts to "Wake Up").
static String computeCommentName(String name) {
final String replaced = name.replaceAllMapped(
RegExp(r'(Digit|Numpad|Lang|Button|Left|Right)([0-9]+)'), (Match match) => '${match.group(1)} ${match.group(2)}',
RegExp(r'(Digit|Numpad|Lang|Button|Left|Right)([0-9]+)'),
(Match match) => '${match.group(1)} ${match.group(2)}',
);
return replaced
// 'fooBar' => 'foo Bar', 'fooBAR' => 'foo BAR'
.replaceAllMapped(RegExp(r'([^A-Z])([A-Z])'), (Match match) => '${match.group(1)} ${match.group(2)}')
// 'ABCDoo' => 'ABC Doo'
.replaceAllMapped(RegExp(r'([A-Z])([A-Z])([a-z])'), (Match match) => '${match.group(1)} ${match.group(2)}${match.group(3)}')
// 'AB1' => 'AB 1', 'F1' => 'F1'
.replaceAllMapped(RegExp(r'([A-Z]{2,})([0-9])'), (Match match) => '${match.group(1)} ${match.group(2)}')
// 'Foo1' => 'Foo 1'
.replaceAllMapped(RegExp(r'([a-z])([0-9])'), (Match match) => '${match.group(1)} ${match.group(2)}')
.trim();
// 'fooBar' => 'foo Bar', 'fooBAR' => 'foo BAR'
.replaceAllMapped(
RegExp(r'([^A-Z])([A-Z])'),
(Match match) => '${match.group(1)} ${match.group(2)}',
)
// 'ABCDoo' => 'ABC Doo'
.replaceAllMapped(
RegExp(r'([A-Z])([A-Z])([a-z])'),
(Match match) => '${match.group(1)} ${match.group(2)}${match.group(3)}',
)
// 'AB1' => 'AB 1', 'F1' => 'F1'
.replaceAllMapped(
RegExp(r'([A-Z]{2,})([0-9])'),
(Match match) => '${match.group(1)} ${match.group(2)}',
)
// 'Foo1' => 'Foo 1'
.replaceAllMapped(
RegExp(r'([a-z])([0-9])'),
(Match match) => '${match.group(1)} ${match.group(2)}',
)
.trim();
}
static String computeConstantName(String commentName) {
// Convert the first word in the comment name.
final String lowerCamelSpace = commentName.replaceFirstMapped(RegExp(r'^[^ ]+'),
final String lowerCamelSpace = commentName.replaceFirstMapped(
RegExp(r'^[^ ]+'),
(Match match) => match[0]!.toLowerCase(),
);
final String result = lowerCamelSpace.replaceAll(' ', '');
@@ -654,6 +704,5 @@ class LogicalKeyEntry {
return result;
}
static int compareByValue(LogicalKeyEntry a, LogicalKeyEntry b) =>
a.value.compareTo(b.value);
static int compareByValue(LogicalKeyEntry a, LogicalKeyEntry b) => a.value.compareTo(b.value);
}

View File

@@ -35,7 +35,10 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
final OutputLines<int> lines = OutputLines<int>('macOS scancode map');
for (final PhysicalKeyEntry entry in keyData.entries) {
if (entry.macOSScanCode != null) {
lines.add(entry.macOSScanCode!, ' @${toHex(entry.macOSScanCode)} : @${toHex(entry.usbHidCode)}, // ${entry.constantName}');
lines.add(
entry.macOSScanCode!,
' @${toHex(entry.macOSScanCode)} : @${toHex(entry.usbHidCode)}, // ${entry.constantName}',
);
}
}
return lines.sortedJoin().trimRight();
@@ -44,9 +47,14 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
String get _keyCodeToLogicalMap {
final OutputLines<int> lines = OutputLines<int>('macOS keycode map');
for (final LogicalKeyEntry entry in logicalData.entries) {
zipStrict(entry.macOSKeyCodeValues, entry.macOSKeyCodeNames, (int macOSValue, String macOSName) {
lines.add(macOSValue,
' @${toHex(macOSValue)} : @${toHex(entry.value, digits: 11)}, // $macOSName -> ${entry.constantName}');
zipStrict(entry.macOSKeyCodeValues, entry.macOSKeyCodeNames, (
int macOSValue,
String macOSName,
) {
lines.add(
macOSValue,
' @${toHex(macOSValue)} : @${toHex(entry.value, digits: 11)}, // $macOSName -> ${entry.constantName}',
);
});
}
return lines.sortedJoin().trimRight();
@@ -55,13 +63,11 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
/// This generates the mask values for the part of a key code that defines its plane.
String get _maskConstants {
final StringBuffer buffer = StringBuffer();
const List<MaskConstant> maskConstants = <MaskConstant>[
kValueMask,
kUnicodePlane,
kMacosPlane,
];
const List<MaskConstant> maskConstants = <MaskConstant>[kValueMask, kUnicodePlane, kMacosPlane];
for (final MaskConstant constant in maskConstants) {
buffer.writeln('const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};');
buffer.writeln(
'const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};',
);
}
return buffer.toString().trimRight();
}
@@ -70,7 +76,9 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
String get _keyToModifierFlagMap {
final StringBuffer modifierKeyMap = StringBuffer();
for (final String name in kModifiersOfInterest) {
modifierKeyMap.writeln(' @${toHex(logicalData.entryByName(name).macOSKeyCodeValues[0])} : @(kModifierFlag${lowerCamelToUpperCamel(name)}),');
modifierKeyMap.writeln(
' @${toHex(logicalData.entryByName(name).macOSKeyCodeValues[0])} : @(kModifierFlag${lowerCamelToUpperCamel(name)}),',
);
}
return modifierKeyMap.toString().trimRight();
}
@@ -79,7 +87,9 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
String get _modifierFlagToKeyMap {
final StringBuffer modifierKeyMap = StringBuffer();
for (final String name in kModifiersOfInterest) {
modifierKeyMap.writeln(' @(kModifierFlag${lowerCamelToUpperCamel(name)}) : @${toHex(logicalData.entryByName(name).macOSKeyCodeValues[0])},');
modifierKeyMap.writeln(
' @(kModifierFlag${lowerCamelToUpperCamel(name)}) : @${toHex(logicalData.entryByName(name).macOSKeyCodeValues[0])},',
);
}
return modifierKeyMap.toString().trimRight();
}
@@ -88,10 +98,14 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
String get _specialKeyConstants {
final StringBuffer specialKeyConstants = StringBuffer();
for (final String keyName in kSpecialPhysicalKeys) {
specialKeyConstants.writeln('const uint64_t k${keyName}PhysicalKey = ${toHex(keyData.entryByName(keyName).usbHidCode)};');
specialKeyConstants.writeln(
'const uint64_t k${keyName}PhysicalKey = ${toHex(keyData.entryByName(keyName).usbHidCode)};',
);
}
for (final String keyName in kSpecialLogicalKeys) {
specialKeyConstants.writeln('const uint64_t k${lowerCamelToUpperCamel(keyName)}LogicalKey = ${toHex(logicalData.entryByName(keyName).value)};');
specialKeyConstants.writeln(
'const uint64_t k${lowerCamelToUpperCamel(keyName)}LogicalKey = ${toHex(logicalData.entryByName(keyName).value)};',
);
}
return specialKeyConstants.toString().trimRight();
}
@@ -102,14 +116,17 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
_layoutGoals.forEach((String name, bool mandatory) {
final PhysicalKeyEntry physicalEntry = keyData.entryByName(name);
final LogicalKeyEntry logicalEntry = logicalData.entryByName(name);
final String line = 'LayoutGoal{'
final String line =
'LayoutGoal{'
'${toHex(physicalEntry.macOSScanCode, digits: 2)}, '
'${toHex(logicalEntry.value, digits: 2)}, '
'${mandatory ? 'true' : 'false'}'
'},';
lines.add(logicalEntry.value,
' ${line.padRight(32)}'
'// ${logicalEntry.name}');
lines.add(
logicalEntry.value,
' ${line.padRight(32)}'
'// ${logicalEntry.name}',
);
});
return lines.sortedJoin().trimRight();
}
@@ -118,8 +135,16 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
String get templatePath => path.join(dataRoot, 'macos_key_code_map_cc.tmpl');
@override
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
'shell', 'platform', 'darwin', 'macos', 'framework', 'Source', 'KeyCodeMap.g.mm');
String outputPath(String platform) => path.join(
PlatformCodeGenerator.engineRoot,
'shell',
'platform',
'darwin',
'macos',
'framework',
'Source',
'KeyCodeMap.g.mm',
);
@override
Map<String, String> mappings() {

View File

@@ -18,15 +18,19 @@ class PhysicalKeyData {
String androidKeyboardLayout,
String androidNameMap,
) {
final Map<String, List<int>> nameToAndroidScanCodes = _readAndroidScanCodes(androidKeyboardLayout, androidNameMap);
final Map<String, List<int>> nameToAndroidScanCodes = _readAndroidScanCodes(
androidKeyboardLayout,
androidNameMap,
);
final Map<String, PhysicalKeyEntry> data = _readHidEntries(
chromiumHidCodes,
nameToAndroidScanCodes,
);
final List<MapEntry<String, PhysicalKeyEntry>> sortedEntries = data.entries.toList()..sort(
(MapEntry<String, PhysicalKeyEntry> a, MapEntry<String, PhysicalKeyEntry> b) =>
PhysicalKeyEntry.compareByUsbHidCode(a.value, b.value),
);
final List<MapEntry<String, PhysicalKeyEntry>> sortedEntries =
data.entries.toList()..sort(
(MapEntry<String, PhysicalKeyEntry> a, MapEntry<String, PhysicalKeyEntry> b) =>
PhysicalKeyEntry.compareByUsbHidCode(a.value, b.value),
);
data
..clear()
..addEntries(sortedEntries);
@@ -37,7 +41,9 @@ class PhysicalKeyData {
factory PhysicalKeyData.fromJson(Map<String, dynamic> contentMap) {
final Map<String, PhysicalKeyEntry> data = <String, PhysicalKeyEntry>{};
for (final MapEntry<String, dynamic> jsonEntry in contentMap.entries) {
final PhysicalKeyEntry entry = PhysicalKeyEntry.fromJsonMapEntry(jsonEntry.value as Map<String, dynamic>);
final PhysicalKeyEntry entry = PhysicalKeyEntry.fromJsonMapEntry(
jsonEntry.value as Map<String, dynamic>,
);
data[entry.name] = entry;
}
return PhysicalKeyData._(data);
@@ -55,8 +61,7 @@ class PhysicalKeyData {
/// Asserts if the name is not found.
PhysicalKeyEntry entryByName(String name) {
final PhysicalKeyEntry? entry = tryEntryByName(name);
assert(entry != null,
'Unable to find logical entry by name $name.');
assert(entry != null, 'Unable to find logical entry by name $name.');
return entry!;
}
@@ -98,7 +103,7 @@ class PhysicalKeyData {
r'key\s+' // Literal "key"
r'(?<id>[0-9]+)\s*' // ID section
r'"?(?:KEY_)?(?<name>[0-9A-Z_]+|\(undefined\))"?\s*' // Name section
r'(?<function>FUNCTION)?' // Optional literal "FUNCTION"
r'(?<function>FUNCTION)?', // Optional literal "FUNCTION"
);
final Map<String, List<int>> androidNameToScanCodes = <String, List<int>>{};
for (final RegExpMatch match in keyEntry.allMatches(keyboardLayout)) {
@@ -111,18 +116,23 @@ class PhysicalKeyData {
// Skip undefined scan codes.
continue;
}
androidNameToScanCodes.putIfAbsent(name, () => <int>[])
.add(int.parse(match.namedGroup('id')!));
androidNameToScanCodes
.putIfAbsent(name, () => <int>[])
.add(int.parse(match.namedGroup('id')!));
}
// Cast Android dom map
final Map<String, List<String>> nameToAndroidNames = (json.decode(nameMap) as Map<String, dynamic>)
.cast<String, List<dynamic>>()
.map<String, List<String>>((String key, List<dynamic> value) {
return MapEntry<String, List<String>>(key, value.cast<String>());
});
final Map<String, List<String>> nameToAndroidNames = (json.decode(nameMap)
as Map<String, dynamic>)
.cast<String, List<dynamic>>()
.map<String, List<String>>((String key, List<dynamic> value) {
return MapEntry<String, List<String>>(key, value.cast<String>());
});
final Map<String, List<int>> result = nameToAndroidNames.map((String name, List<String> androidNames) {
final Map<String, List<int>> result = nameToAndroidNames.map((
String name,
List<String> androidNames,
) {
final Set<int> scanCodes = <int>{};
for (final String androidName in androidNames) {
scanCodes.addAll(androidNameToScanCodes[androidName] ?? <int>[]);
@@ -179,14 +189,16 @@ class PhysicalKeyData {
if (existing != null && existing.name != 'Fn') {
// If it's an existing entry, the only thing we currently support is
// to insert an extra DOMKey. The other entries must be empty.
assert(evdevCode == 0
&& xKbScanCode == 0
&& windowsScanCode == 0
&& macScanCode == 0xffff
&& chromiumCode != null
&& chromiumCode.isNotEmpty,
'Duplicate usbHidCode ${existing.usbHidCode} of key ${existing.name} '
'conflicts with existing ${entries[existing.usbHidCode]!.name}.');
assert(
evdevCode == 0 &&
xKbScanCode == 0 &&
windowsScanCode == 0 &&
macScanCode == 0xffff &&
chromiumCode != null &&
chromiumCode.isNotEmpty,
'Duplicate usbHidCode ${existing.usbHidCode} of key ${existing.name} '
'conflicts with existing ${entries[existing.usbHidCode]!.name}.',
);
existing.otherWebCodes.add(chromiumCode!);
continue;
}
@@ -203,8 +215,9 @@ class PhysicalKeyData {
);
entries[newEntry.usbHidCode] = newEntry;
}
return entries.map((int code, PhysicalKeyEntry entry) =>
MapEntry<String, PhysicalKeyEntry>(entry.name, entry));
return entries.map(
(int code, PhysicalKeyEntry entry) => MapEntry<String, PhysicalKeyEntry>(entry.name, entry),
);
}
}
@@ -250,24 +263,32 @@ class PhysicalKeyEntry {
/// The Evdev scan code of the key, from Chromium's header file.
final int? evdevCode;
/// The XKb scan code of the key from Chromium's header file.
final int? xKbScanCode;
/// The Windows scan code of the key from Chromium's header file.
final int? windowsScanCode;
/// The macOS scan code of the key from Chromium's header file.
final int? macOSScanCode;
/// The iOS scan code of the key from UIKey's documentation (USB Hid table)
final int? iOSScanCode;
/// The list of Android scan codes matching this key, created by looking up
/// the Android name in the Chromium data, and substituting the Android scan
/// code value.
final List<int> androidScanCodes;
/// The name of the key, mostly derived from the DomKey name in Chromium,
/// but where there was no DomKey representation, derived from the Chromium
/// symbol name.
final String name;
/// The Chromium event code for the key.
final String? chromiumCode;
/// Other codes used by Web besides chromiumCode.
final List<String> otherWebCodes;
@@ -281,10 +302,7 @@ class PhysicalKeyEntry {
/// Creates a JSON map from the key data.
Map<String, dynamic> toJson() {
return removeEmptyValues(<String, dynamic>{
'names': <String, dynamic>{
'name': name,
'chromium': chromiumCode,
},
'names': <String, dynamic>{'name': name, 'chromium': chromiumCode},
'otherWebCodes': otherWebCodes,
'scanCodes': <String, dynamic>{
'android': androidScanCodes,
@@ -304,7 +322,9 @@ class PhysicalKeyEntry {
RegExp(r'(Digit|Numpad|Lang|Button|Left|Right)([0-9]+)'),
(Match match) => '${match.group(1)} ${match.group(2)}',
);
return upperCamel.replaceAllMapped(RegExp(r'([A-Z])'), (Match match) => ' ${match.group(1)}').trim();
return upperCamel
.replaceAllMapped(RegExp(r'([A-Z])'), (Match match) => ' ${match.group(1)}')
.trim();
}
/// Gets the name of the key suitable for placing in comments.
@@ -320,26 +340,26 @@ class PhysicalKeyEntry {
/// the name from the various different names available, making sure that the
/// name isn't a Dart reserved word (if it is, then it adds the word "Key" to
/// the end of the name).
late final String constantName = (() {
String? result;
if (name.isEmpty) {
// If it doesn't have a DomKey name then use the Chromium symbol name.
result = chromiumCode;
} else {
result = upperCamelToLowerCamel(name);
}
result ??= 'Key${toHex(usbHidCode)}';
if (kDartReservedWords.contains(result)) {
return '${result}Key';
}
return result;
})();
late final String constantName =
(() {
String? result;
if (name.isEmpty) {
// If it doesn't have a DomKey name then use the Chromium symbol name.
result = chromiumCode;
} else {
result = upperCamelToLowerCamel(name);
}
result ??= 'Key${toHex(usbHidCode)}';
if (kDartReservedWords.contains(result)) {
return '${result}Key';
}
return result;
})();
@override
String toString() {
final String otherWebStr = otherWebCodes.isEmpty
? ''
: ', otherWebCodes: [${otherWebCodes.join(', ')}]';
final String otherWebStr =
otherWebCodes.isEmpty ? '' : ', otherWebCodes: [${otherWebCodes.join(', ')}]';
return """'$constantName': (name: "$name", usbHidCode: ${toHex(usbHidCode)}, """
'linuxScanCode: ${toHex(evdevCode)}, xKbScanCode: ${toHex(xKbScanCode)}, '
'windowsKeyCode: ${toHex(windowsScanCode)}, macOSScanCode: ${toHex(macOSScanCode)}, '

View File

@@ -30,10 +30,16 @@ constexpr uint64_t kPhysical${_toUpperCamel(entry.constantName)} = ${toHex(entry
/// Gets the generated definitions of PhysicalKeyboardKeys.
String get _logicalDefinitions {
final OutputLines<int> lines = OutputLines<int>('Logical Key list', behavior: DeduplicateBehavior.kSkip);
final OutputLines<int> lines = OutputLines<int>(
'Logical Key list',
behavior: DeduplicateBehavior.kSkip,
);
for (final LogicalKeyEntry entry in logicalData.entries) {
lines.add(entry.value, '''
constexpr uint64_t kLogical${_toUpperCamel(entry.constantName)} = ${toHex(entry.value, digits: 11)};''');
lines.add(
entry.value,
'''
constexpr uint64_t kLogical${_toUpperCamel(entry.constantName)} = ${toHex(entry.value, digits: 11)};''',
);
}
return lines.sortedJoin().trimRight();
}

View File

@@ -11,18 +11,16 @@ import 'utils.dart';
String _toUpperSnake(String lowerCamel) {
// Converts 'myTVFoo' to 'myTvFoo'.
final String trueUpperCamel = lowerCamel.replaceAllMapped(
RegExp(r'([A-Z]{3,})'),
(Match match) {
final String matched = match.group(1)!;
return matched.substring(0, 1)
+ matched.substring(1, matched.length - 2).toLowerCase()
+ matched.substring(matched.length - 2, matched.length - 1);
});
final String trueUpperCamel = lowerCamel.replaceAllMapped(RegExp(r'([A-Z]{3,})'), (Match match) {
final String matched = match.group(1)!;
return matched.substring(0, 1) +
matched.substring(1, matched.length - 2).toLowerCase() +
matched.substring(matched.length - 2, matched.length - 1);
});
// Converts 'myTvFoo' to 'MY_TV_FOO'.
return trueUpperCamel.replaceAllMapped(
RegExp(r'([A-Z])'),
(Match match) => '_${match.group(1)!}').toUpperCase();
return trueUpperCamel
.replaceAllMapped(RegExp(r'([A-Z])'), (Match match) => '_${match.group(1)!}')
.toUpperCase();
}
/// Generates the common/testing/key_codes.h based on the information in the key
@@ -34,18 +32,27 @@ class KeyCodesJavaGenerator extends BaseCodeGenerator {
String get _physicalDefinitions {
final OutputLines<int> lines = OutputLines<int>('Physical Key list');
for (final PhysicalKeyEntry entry in keyData.entries) {
lines.add(entry.usbHidCode, '''
public static final long PHYSICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.usbHidCode)}L;''');
lines.add(
entry.usbHidCode,
'''
public static final long PHYSICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.usbHidCode)}L;''',
);
}
return lines.sortedJoin().trimRight();
}
/// Gets the generated definitions of PhysicalKeyboardKeys.
String get _logicalDefinitions {
final OutputLines<int> lines = OutputLines<int>('Logical Key list', behavior: DeduplicateBehavior.kSkip);
final OutputLines<int> lines = OutputLines<int>(
'Logical Key list',
behavior: DeduplicateBehavior.kSkip,
);
for (final LogicalKeyEntry entry in logicalData.entries) {
lines.add(entry.value, '''
public static final long LOGICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.value, digits: 11)}L;''');
lines.add(
entry.value,
'''
public static final long LOGICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.value, digits: 11)}L;''',
);
}
return lines.sortedJoin().trimRight();
}

View File

@@ -12,7 +12,8 @@ import 'constants.dart';
/// The location of the Flutter root directory, based on the known location of
/// this script.
final Directory flutterRoot = Directory(path.dirname(Platform.script.toFilePath())).parent.parent.parent.parent;
final Directory flutterRoot =
Directory(path.dirname(Platform.script.toFilePath())).parent.parent.parent.parent;
String get dataRoot => testDataRoot ?? _dataRoot;
String _dataRoot = path.join(flutterRoot.path, 'dev', 'tools', 'gen_keycodes', 'data');
@@ -169,14 +170,16 @@ Map<String, String> parseMapOfString(String jsonString) {
/// Read a Map<String, List<String>> out of its string representation in JSON.
Map<String, List<String>> parseMapOfListOfString(String jsonString) {
final Map<String, List<dynamic>> dynamicMap = (json.decode(jsonString) as Map<String, dynamic>).cast<String, List<dynamic>>();
final Map<String, List<dynamic>> dynamicMap =
(json.decode(jsonString) as Map<String, dynamic>).cast<String, List<dynamic>>();
return dynamicMap.map<String, List<String>>((String key, List<dynamic> value) {
return MapEntry<String, List<String>>(key, value.cast<String>());
});
}
Map<String, List<String?>> parseMapOfListOfNullableString(String jsonString) {
final Map<String, List<dynamic>> dynamicMap = (json.decode(jsonString) as Map<String, dynamic>).cast<String, List<dynamic>>();
final Map<String, List<dynamic>> dynamicMap =
(json.decode(jsonString) as Map<String, dynamic>).cast<String, List<dynamic>>();
return dynamicMap.map<String, List<String?>>((String key, List<dynamic> value) {
return MapEntry<String, List<String?>>(key, value.cast<String?>());
});
@@ -187,7 +190,10 @@ Map<String, bool> parseMapOfBool(String jsonString) {
}
/// Reverse the map of { fromValue -> list of toValue } to { toValue -> fromValue } and return.
Map<String, String> reverseMapOfListOfString(Map<String, List<String>> inMap, void Function(String fromValue, String newToValue) onDuplicate) {
Map<String, String> reverseMapOfListOfString(
Map<String, List<String>> inMap,
void Function(String fromValue, String newToValue) onDuplicate,
) {
final Map<String, String> result = <String, String>{};
inMap.forEach((String fromValue, List<String> toValues) {
for (final String toValue in toValues) {
@@ -205,12 +211,14 @@ Map<String, String> reverseMapOfListOfString(Map<String, List<String>> inMap, vo
///
/// Will modify the input map.
Map<String, dynamic> removeEmptyValues(Map<String, dynamic> map) {
return map..removeWhere((String key, dynamic value) => switch (value) {
null => true,
Map<String, dynamic>() => removeEmptyValues(value).isEmpty,
Iterable<dynamic>() => value.isEmpty,
_ => false,
});
return map..removeWhere(
(String key, dynamic value) => switch (value) {
null => true,
Map<String, dynamic>() => removeEmptyValues(value).isEmpty,
Iterable<dynamic>() => value.isEmpty,
_ => false,
},
);
}
void addNameValue(List<String> names, List<int> values, String name, int value) {
@@ -238,8 +246,7 @@ enum DeduplicateBehavior {
/// The information for a line used by [OutputLines].
class OutputLine<T extends Comparable<Object>> {
OutputLine(this.key, String value)
: values = <String>[value];
OutputLine(this.key, String value) : values = <String>[value];
final T key;
final List<String> values;
@@ -268,7 +275,9 @@ class OutputLines<T extends Comparable<Object>> {
if (existing != null) {
switch (behavior) {
case DeduplicateBehavior.kWarn:
print('Warn: Request to add $key to map "$mapName" as:\n $line\n but it already exists as:\n ${existing.values[0]}');
print(
'Warn: Request to add $key to map "$mapName" as:\n $line\n but it already exists as:\n ${existing.values[0]}',
);
return;
case DeduplicateBehavior.kSkip:
return;
@@ -286,9 +295,9 @@ class OutputLines<T extends Comparable<Object>> {
String sortedJoin() {
return (lines.values.toList()
..sort((OutputLine<T> a, OutputLine<T> b) => a.key.compareTo(b.key)))
.map((OutputLine<T> line) => line.values.join('\n'))
.join('\n');
..sort((OutputLine<T> a, OutputLine<T> b) => a.key.compareTo(b.key)))
.map((OutputLine<T> line) => line.values.join('\n'))
.join('\n');
}
}

View File

@@ -13,11 +13,8 @@ import 'utils.dart';
/// Generates the key mapping for Web, based on the information in the key
/// data structure given to it.
class WebCodeGenerator extends PlatformCodeGenerator {
WebCodeGenerator(
super.keyData,
super.logicalData,
String logicalLocationMap,
) : _logicalLocationMap = parseMapOfListOfNullableString(logicalLocationMap);
WebCodeGenerator(super.keyData, super.logicalData, String logicalLocationMap)
: _logicalLocationMap = parseMapOfListOfNullableString(logicalLocationMap);
/// This generates the map of Web KeyboardEvent codes to logical key ids.
String get _webLogicalKeyCodeMap {
@@ -38,8 +35,7 @@ class WebCodeGenerator extends PlatformCodeGenerator {
final OutputLines<String> lines = OutputLines<String>('Web physical map');
for (final PhysicalKeyEntry entry in keyData.entries) {
for (final String webCode in entry.webCodes()) {
lines.add(webCode,
" '$webCode': ${toHex(entry.usbHidCode)}, // ${entry.constantName}");
lines.add(webCode, " '$webCode': ${toHex(entry.usbHidCode)}, // ${entry.constantName}");
}
}
return lines.sortedJoin().trimRight();
@@ -49,24 +45,36 @@ class WebCodeGenerator extends PlatformCodeGenerator {
String get _webLogicalLocationMap {
final OutputLines<String> lines = OutputLines<String>('Web logical location map');
_logicalLocationMap.forEach((String webKey, List<String?> locations) {
final String valuesString = locations.map((String? value) {
return value == null ? 'null' : toHex(logicalData.entryByName(value).value, digits: 11);
}).join(', ');
final String namesString = locations.map((String? value) {
return value == null ? 'null' : logicalData.entryByName(value).constantName;
}).join(', ');
final String valuesString = locations
.map((String? value) {
return value == null ? 'null' : toHex(logicalData.entryByName(value).value, digits: 11);
})
.join(', ');
final String namesString = locations
.map((String? value) {
return value == null ? 'null' : logicalData.entryByName(value).constantName;
})
.join(', ');
lines.add(webKey, " '$webKey': <int?>[$valuesString], // $namesString");
});
return lines.sortedJoin().trimRight();
}
final Map<String, List<String?>> _logicalLocationMap;
@override
String get templatePath => path.join(dataRoot, 'web_key_map_dart.tmpl');
@override
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
'lib', 'web_ui', 'lib', 'src', 'engine', 'key_map.g.dart');
String outputPath(String platform) => path.join(
PlatformCodeGenerator.engineRoot,
'lib',
'web_ui',
'lib',
'src',
'engine',
'key_map.g.dart',
);
@override
Map<String, String> mappings() {

View File

@@ -13,19 +13,18 @@ import 'utils.dart';
/// Generates the key mapping for Windows, based on the information in the key
/// data structure given to it.
class WindowsCodeGenerator extends PlatformCodeGenerator {
WindowsCodeGenerator(
super.keyData,
super.logicalData,
String scancodeToLogical,
) : _scancodeToLogical = parseMapOfString(scancodeToLogical);
WindowsCodeGenerator(super.keyData, super.logicalData, String scancodeToLogical)
: _scancodeToLogical = parseMapOfString(scancodeToLogical);
/// This generates the map of Windows scan codes to physical keys.
String get _windowsScanCodeMap {
final OutputLines<int> lines = OutputLines<int>('Windows scancode map');
for (final PhysicalKeyEntry entry in keyData.entries) {
if (entry.windowsScanCode != null) {
lines.add(entry.windowsScanCode!,
' {${toHex(entry.windowsScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}');
lines.add(
entry.windowsScanCode!,
' {${toHex(entry.windowsScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}',
);
}
}
return lines.sortedJoin().trimRight();
@@ -35,13 +34,13 @@ class WindowsCodeGenerator extends PlatformCodeGenerator {
String get _windowsLogicalKeyCodeMap {
final OutputLines<int> lines = OutputLines<int>('Windows logical map');
for (final LogicalKeyEntry entry in logicalData.entries) {
zipStrict(entry.windowsValues, entry.windowsNames,
(int windowsValue, String windowsName) {
lines.add(windowsValue,
' {${toHex(windowsValue)}, ${toHex(entry.value, digits: 11)}}, '
'// $windowsName -> ${entry.constantName}');
},
);
zipStrict(entry.windowsValues, entry.windowsNames, (int windowsValue, String windowsName) {
lines.add(
windowsValue,
' {${toHex(windowsValue)}, ${toHex(entry.value, digits: 11)}}, '
'// $windowsName -> ${entry.constantName}',
);
});
}
return lines.sortedJoin().trimRight();
}
@@ -56,12 +55,15 @@ class WindowsCodeGenerator extends PlatformCodeGenerator {
_scancodeToLogical.forEach((String scanCodeName, String logicalName) {
final PhysicalKeyEntry physicalEntry = keyData.entryByName(scanCodeName);
final LogicalKeyEntry logicalEntry = logicalData.entryByName(logicalName);
lines.add(physicalEntry.windowsScanCode!,
' {${toHex(physicalEntry.windowsScanCode)}, ${toHex(logicalEntry.value, digits: 11)}}, '
'// ${physicalEntry.constantName} -> ${logicalEntry.constantName}');
lines.add(
physicalEntry.windowsScanCode!,
' {${toHex(physicalEntry.windowsScanCode)}, ${toHex(logicalEntry.value, digits: 11)}}, '
'// ${physicalEntry.constantName} -> ${logicalEntry.constantName}',
);
});
return lines.sortedJoin().trimRight();
}
final Map<String, String> _scancodeToLogical;
/// This generates the mask values for the part of a key code that defines its plane.
@@ -73,7 +75,9 @@ class WindowsCodeGenerator extends PlatformCodeGenerator {
kWindowsPlane,
];
for (final MaskConstant constant in maskConstants) {
buffer.writeln('const uint64_t KeyboardKeyEmbedderHandler::${constant.lowerCamelName} = ${toHex(constant.value, digits: 11)};');
buffer.writeln(
'const uint64_t KeyboardKeyEmbedderHandler::${constant.lowerCamelName} = ${toHex(constant.value, digits: 11)};',
);
}
return buffer.toString().trimRight();
}
@@ -82,8 +86,13 @@ class WindowsCodeGenerator extends PlatformCodeGenerator {
String get templatePath => path.join(dataRoot, 'windows_flutter_key_map_cc.tmpl');
@override
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
'shell', 'platform', 'windows', 'flutter_key_map.g.cc');
String outputPath(String platform) => path.join(
PlatformCodeGenerator.engineRoot,
'shell',
'platform',
'windows',
'flutter_key_map.g.cc',
);
@override
Map<String, String> mappings() {

View File

@@ -23,18 +23,19 @@ String readDataFile(String fileName) {
}
final PhysicalKeyData physicalData = PhysicalKeyData.fromJson(
json.decode(readDataFile('physical_key_data.g.json')) as Map<String, dynamic>);
json.decode(readDataFile('physical_key_data.g.json')) as Map<String, dynamic>,
);
final LogicalKeyData logicalData = LogicalKeyData.fromJson(
json.decode(readDataFile('logical_key_data.g.json')) as Map<String, dynamic>);
final Map<String, bool> keyGoals = parseMapOfBool(
readDataFile('layout_goals.json'));
json.decode(readDataFile('logical_key_data.g.json')) as Map<String, dynamic>,
);
final Map<String, bool> keyGoals = parseMapOfBool(readDataFile('layout_goals.json'));
void main() {
setUp(() {
testDataRoot = path.canonicalize(path.join(Directory.current.absolute.path, 'data'));
});
tearDown((){
tearDown(() {
testDataRoot = null;
});
@@ -50,10 +51,7 @@ void main() {
test('Generate Keycodes for Android', () {
const String platform = 'android';
final PlatformCodeGenerator codeGenerator = AndroidCodeGenerator(
physicalData,
logicalData,
);
final PlatformCodeGenerator codeGenerator = AndroidCodeGenerator(physicalData, logicalData);
final String output = codeGenerator.generate();
expect(codeGenerator.outputPath(platform), endsWith('KeyboardMap.java'));
@@ -84,10 +82,7 @@ void main() {
});
test('Generate Keycodes for iOS', () {
const String platform = 'ios';
final PlatformCodeGenerator codeGenerator = IOSCodeGenerator(
physicalData,
logicalData,
);
final PlatformCodeGenerator codeGenerator = IOSCodeGenerator(physicalData, logicalData);
final String output = codeGenerator.generate();
expect(codeGenerator.outputPath(platform), endsWith('KeyCodeMap.g.mm'));
@@ -152,20 +147,12 @@ void main() {
// Regression tests for https://github.com/flutter/flutter/pull/87098
expect(
entries.indexWhere((LogicalKeyEntry entry) => entry.name == 'ShiftLeft'),
isNot(-1));
expect(
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('ShiftLeft')),
-1);
expect(entries.indexWhere((LogicalKeyEntry entry) => entry.name == 'ShiftLeft'), isNot(-1));
expect(entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('ShiftLeft')), -1);
// 'Shift' maps to both 'ShiftLeft' and 'ShiftRight', and should be resolved
// by other ways.
expect(
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Shift')),
-1);
expect(entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Shift')), -1);
// Printable keys must not be added with Web key of their names.
expect(
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Slash')),
-1);
expect(entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Slash')), -1);
});
}

View File

@@ -37,10 +37,11 @@ void _encodeBundleTranslations(Map<String, dynamic> bundle) {
final String translation = bundle[key] as String;
// Rewrite the string as a series of unicode characters in JSON format.
// Like "\u0012\u0123\u1234".
bundle[key] = translation.runes.map((int code) {
final String codeString = '00${code.toRadixString(16)}';
return '\\u${codeString.substring(codeString.length - 4)}';
}).join();
bundle[key] =
translation.runes.map((int code) {
final String codeString = '00${code.toRadixString(16)}';
return '\\u${codeString.substring(codeString.length - 4)}';
}).join();
}
}
@@ -49,7 +50,9 @@ void _checkEncodedTranslations(Map<String, dynamic> encodedBundle, Map<String, d
const JsonDecoder decoder = JsonDecoder();
for (final String key in bundle.keys) {
if (decoder.convert('"${encodedBundle[key]}"') != bundle[key]) {
stderr.writeln(' encodedTranslation for $key does not match original value "${bundle[key]}"');
stderr.writeln(
' encodedTranslation for $key does not match original value "${bundle[key]}"',
);
errorFound = true;
}
}

View File

@@ -46,19 +46,21 @@ Future<void> main(List<String> rawArgs) async {
final bool writeToFile = parseArgs(rawArgs).writeToFile;
final File packageConfigFile = File(path.join('packages', 'flutter_localizations', '.dart_tool', 'package_config.json'));
final File packageConfigFile = File(
path.join('packages', 'flutter_localizations', '.dart_tool', 'package_config.json'),
);
final bool packageConfigExists = packageConfigFile.existsSync();
if (!packageConfigExists) {
exitWithError(
'File not found: ${packageConfigFile.path}. $_kCommandName must be run '
'after a successful "flutter update-packages".'
'after a successful "flutter update-packages".',
);
}
final List<Object?> packages = (
json.decode(packageConfigFile.readAsStringSync()) as Map<String, Object?>
)['packages']! as List<Object?>;
final List<Object?> packages =
(json.decode(packageConfigFile.readAsStringSync()) as Map<String, Object?>)['packages']!
as List<Object?>;
String? pathToIntl;
for (final Object? package in packages) {
@@ -72,19 +74,22 @@ Future<void> main(List<String> rawArgs) async {
if (pathToIntl == null) {
exitWithError(
'Could not find "intl" package. $_kCommandName must be run '
'after a successful "flutter update-packages".'
'after a successful "flutter update-packages".',
);
}
final Directory dateSymbolsDirectory = Directory(path.join(pathToIntl!, 'lib', 'src', 'data', 'dates', 'symbols'));
final Directory dateSymbolsDirectory = Directory(
path.join(pathToIntl!, 'lib', 'src', 'data', 'dates', 'symbols'),
);
final Map<String, File> symbolFiles = _listIntlData(dateSymbolsDirectory);
final Directory datePatternsDirectory = Directory(path.join(pathToIntl, 'lib', 'src', 'data', 'dates', 'patterns'));
final Directory datePatternsDirectory = Directory(
path.join(pathToIntl, 'lib', 'src', 'data', 'dates', 'patterns'),
);
final Map<String, File> patternFiles = _listIntlData(datePatternsDirectory);
final StringBuffer buffer = StringBuffer();
final Set<String> supportedLocales = _supportedLocales();
buffer.writeln(
'''
buffer.writeln('''
// 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.
@@ -95,8 +100,7 @@ Future<void> main(List<String> rawArgs) async {
import 'package:intl/date_symbols.dart' as intl;
'''
);
''');
buffer.writeln('''
/// The subset of date symbols supported by the intl package which are also
/// supported by flutter_localizations.''');
@@ -104,7 +108,8 @@ import 'package:intl/date_symbols.dart' as intl;
symbolFiles.forEach((String locale, File data) {
currentLocale = locale;
if (supportedLocales.contains(locale)) {
final Map<String, Object?> objData = json.decode(data.readAsStringSync()) as Map<String, Object?>;
final Map<String, Object?> objData =
json.decode(data.readAsStringSync()) as Map<String, Object?>;
buffer.writeln("'$locale': intl.DateSymbols(");
objData.forEach((String key, Object? value) {
if (value == null) {
@@ -123,10 +128,13 @@ import 'package:intl/date_symbols.dart' as intl;
buffer.writeln('''
/// The subset of date patterns supported by the intl package which are also
/// supported by flutter_localizations.''');
buffer.writeln('const Map<String, Map<String, String>> datePatterns = <String, Map<String, String>> {');
buffer.writeln(
'const Map<String, Map<String, String>> datePatterns = <String, Map<String, String>> {',
);
patternFiles.forEach((String locale, File data) {
if (supportedLocales.contains(locale)) {
final Map<String, dynamic> patterns = json.decode(data.readAsStringSync()) as Map<String, dynamic>;
final Map<String, dynamic> patterns =
json.decode(data.readAsStringSync()) as Map<String, dynamic>;
buffer.writeln("'$locale': <String, String>{");
patterns.forEach((String key, dynamic value) {
assert(value is String);
@@ -138,13 +146,22 @@ import 'package:intl/date_symbols.dart' as intl;
buffer.writeln('};');
if (writeToFile) {
final File dateLocalizationsFile = File(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'generated_date_localizations.dart'));
final File dateLocalizationsFile = File(
path.join(
'packages',
'flutter_localizations',
'lib',
'src',
'l10n',
'generated_date_localizations.dart',
),
);
dateLocalizationsFile.writeAsStringSync(buffer.toString());
final String extension = Platform.isWindows ? '.exe' : '';
final ProcessResult result = Process.runSync(path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart$extension'), <String>[
'format',
dateLocalizationsFile.path,
]);
final ProcessResult result = Process.runSync(
path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart$extension'),
<String>['format', dateLocalizationsFile.path],
);
if (result.exitCode != 0) {
print(result.exitCode);
print(result.stdout);
@@ -220,11 +237,11 @@ Set<String> _supportedLocales() {
// date patterns and symbols may cause problems.
//
// For more context, see https://github.com/flutter/flutter/issues/67644.
final Set<String> supportedLocales = <String>{
'en_US',
};
final Set<String> supportedLocales = <String>{'en_US'};
final RegExp filenameRE = RegExp(r'(?:material|cupertino)_(\w+)\.arb$');
final Directory supportedLocalesDirectory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
final Directory supportedLocalesDirectory = Directory(
path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'),
);
for (final FileSystemEntity entity in supportedLocalesDirectory.listSync()) {
final String filePath = entity.path;
if (FileSystemEntity.isFileSync(filePath) && filenameRE.hasMatch(filePath)) {
@@ -237,10 +254,9 @@ Set<String> _supportedLocales() {
Map<String, File> _listIntlData(Directory directory) {
final Map<String, File> localeFiles = <String, File>{};
final Iterable<File> files = directory
.listSync()
.whereType<File>()
.where((File file) => file.path.endsWith('.json'));
final Iterable<File> files = directory.listSync().whereType<File>().where(
(File file) => file.path.endsWith('.json'),
);
for (final File file in files) {
final String locale = path.basenameWithoutExtension(file.path);
localeFiles[locale] = file;

View File

@@ -83,7 +83,9 @@ String generateArbBasedLocalizationSubclasses({
assert(supportedLanguagesDocMacro.isNotEmpty);
generateConstructorForCountrySubClass ??= generateConstructor;
final StringBuffer output = StringBuffer();
output.writeln(generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'));
output.writeln(
generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'),
);
final StringBuffer supportedLocales = StringBuffer();
@@ -141,7 +143,8 @@ String generateArbBasedLocalizationSubclasses({
final Map<String, String> languageResources = localeToResources[languageLocale]!;
for (final String key in allKeys) {
final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
final Map<String, dynamic>? attributes =
localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
output.writeln(generateGetter(key, languageResources[key], attributes, languageLocale));
}
output.writeln('}');
@@ -153,18 +156,21 @@ String generateArbBasedLocalizationSubclasses({
// script default values before language default values.
for (final String scriptCode in languageToScriptCodes[languageName]!) {
final LocaleInfo scriptBaseLocale = LocaleInfo.fromString('${languageName}_$scriptCode');
output.writeln(generateClassDeclaration(
scriptBaseLocale,
generatedClassPrefix,
'$generatedClassPrefix${languageLocale.camelCase()}',
));
output.writeln(
generateClassDeclaration(
scriptBaseLocale,
generatedClassPrefix,
'$generatedClassPrefix${languageLocale.camelCase()}',
),
);
output.writeln(generateConstructorForCountrySubClass(scriptBaseLocale));
final Map<String, String> scriptResources = localeToResources[scriptBaseLocale]!;
for (final String key in scriptResources.keys.toList()..sort()) {
if (languageResources[key] == scriptResources[key]) {
continue;
}
final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
final Map<String, dynamic>? attributes =
localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale));
}
output.writeln('}');
@@ -181,22 +187,27 @@ String generateArbBasedLocalizationSubclasses({
continue;
}
countryCodeCount += 1;
output.writeln(generateClassDeclaration(
locale,
generatedClassPrefix,
'$generatedClassPrefix${scriptBaseLocale.camelCase()}',
));
output.writeln(
generateClassDeclaration(
locale,
generatedClassPrefix,
'$generatedClassPrefix${scriptBaseLocale.camelCase()}',
),
);
output.writeln(generateConstructorForCountrySubClass(locale));
final Map<String, String> localeResources = localeToResources[locale]!;
for (final String key in localeResources.keys) {
// When script fallback contains the key, we compare to it instead of language fallback.
if (scriptResources.containsKey(key) ? scriptResources[key] == localeResources[key] : languageResources[key] == localeResources[key]) {
if (scriptResources.containsKey(key)
? scriptResources[key] == localeResources[key]
: languageResources[key] == localeResources[key]) {
continue;
}
final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
final Map<String, dynamic>? attributes =
localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
}
output.writeln('}');
output.writeln('}');
}
}
} else {
@@ -209,35 +220,46 @@ String generateArbBasedLocalizationSubclasses({
}
countryCodeCount += 1;
final Map<String, String> localeResources = localeToResources[locale]!;
output.writeln(generateClassDeclaration(
locale,
generatedClassPrefix,
'$generatedClassPrefix${languageLocale.camelCase()}',
));
output.writeln(
generateClassDeclaration(
locale,
generatedClassPrefix,
'$generatedClassPrefix${languageLocale.camelCase()}',
),
);
output.writeln(generateConstructorForCountrySubClass(locale));
for (final String key in localeResources.keys) {
if (languageResources[key] == localeResources[key]) {
continue;
}
final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
final Map<String, dynamic>? attributes =
localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
}
output.writeln('}');
output.writeln('}');
}
}
final String scriptCodeMessage = scriptCodeCount == 0 ? '' : ' and $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'}';
final String scriptCodeMessage =
scriptCodeCount == 0
? ''
: ' and $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'}';
if (countryCodeCount == 0) {
if (scriptCodeCount == 0) {
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)}');
} else {
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'})');
supportedLocales.writeln(
'/// * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'})',
);
}
} else if (countryCodeCount == 1) {
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)');
supportedLocales.writeln(
'/// * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)',
);
} else {
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)');
supportedLocales.writeln(
'/// * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)',
);
}
}
@@ -279,10 +301,13 @@ $factoryDeclaration
for (final String language in languageToLocales.keys) {
// Only one instance of the language.
if (languageToLocales[language]!.length == 1) {
output.writeln('''
output.writeln(
'''
case '$language':
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${languageToLocales[language]![0].camelCase()}($factoryArguments);''');
} else if (!languageToScriptCodes.containsKey(language)) { // Does not distinguish between scripts. Switch on countryCode directly.
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${languageToLocales[language]![0].camelCase()}($factoryArguments);''',
);
} else if (!languageToScriptCodes.containsKey(language)) {
// Does not distinguish between scripts. Switch on countryCode directly.
output.writeln('''
case '$language': {
switch (locale.countryCode) {''');
@@ -292,15 +317,18 @@ $factoryDeclaration
}
assert(locale.length > 1);
final String countryCode = locale.countryCode!;
output.writeln('''
output.writeln(
'''
case '$countryCode':
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
);
}
output.writeln('''
}
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
}''');
} else { // Language has scriptCode, add additional switch logic.
} else {
// Language has scriptCode, add additional switch logic.
bool hasCountryCode = false;
output.writeln('''
case '$language': {
@@ -325,9 +353,11 @@ $factoryDeclaration
continue;
}
final String countryCode = locale.countryCode!;
output.writeln('''
output.writeln(
'''
case '$countryCode':
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
);
}
}
// Return a fallback locale that matches scriptCode, but not countryCode.
@@ -339,7 +369,7 @@ $factoryDeclaration
}''');
}
output.writeln('''
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
}''');
} else {
// Not Explicitly defined, fallback to first locale with the same language and
@@ -353,7 +383,7 @@ $factoryDeclaration
}''');
}
output.writeln('''
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
}''');
break;
}
@@ -362,7 +392,7 @@ $factoryDeclaration
output.writeln('''
}''');
if (hasCountryCode) {
output.writeln('''
output.writeln('''
switch (locale.countryCode) {''');
for (final LocaleInfo locale in languageToLocales[language]!) {
if (locale.originalString == language) {
@@ -373,15 +403,17 @@ $factoryDeclaration
continue;
}
final String countryCode = locale.countryCode!;
output.writeln('''
output.writeln(
'''
case '$countryCode':
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
);
}
output.writeln('''
}''');
}
output.writeln('''
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
}''');
}
}
@@ -472,7 +504,7 @@ String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInf
throw Exception(
'"$value" is not one of the ICU short time patterns supported '
'by the material library. Here is the list of supported '
'patterns:\n ${_icuTimeOfDayToEnum.keys.join('\n ')}'
'patterns:\n ${_icuTimeOfDayToEnum.keys.join('\n ')}',
);
}
return _icuTimeOfDayToEnum[value];
@@ -481,23 +513,28 @@ String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInf
throw Exception(
'"$value" is not one of the scriptCategory values supported '
'by the material library. Here is the list of supported '
'values:\n ${_scriptCategoryToEnum.keys.join('\n ')}'
'values:\n ${_scriptCategoryToEnum.keys.join('\n ')}',
);
}
return _scriptCategoryToEnum[value];
}
}
return generateEncodedString(locale.languageCode, value);
return generateEncodedString(locale.languageCode, value);
}
/// Combines [generateType], [generateKey], and [generateValue] to return
/// the source of getters for the GlobalMaterialLocalizations subclass.
/// The locale is the locale for which the getter is being generated.
String generateGetter(String key, String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
String generateGetter(
String key,
String? value,
Map<String, dynamic>? attributes,
LocaleInfo locale,
) {
final String type = generateType(attributes);
key = generateKey(key, attributes);
final String? generatedValue = generateValue(value, attributes, locale);
return '''
return '''
@override
$type get $key => $generatedValue;''';
@@ -511,7 +548,9 @@ void main(List<String> rawArgs) {
// is the 2nd command line argument, lc is a language code and cc is the country
// code. In most cases both codes are just two characters.
final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
final Directory directory = Directory(
path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'),
);
final RegExp widgetsFilenameRE = RegExp(r'widgets_(\w+)\.arb$');
final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$');
final RegExp cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$');
@@ -537,22 +576,28 @@ void main(List<String> rawArgs) {
precacheLanguageAndRegionTags();
// Maps of locales to resource key/value pairs for Widgets ARBs.
final Map<LocaleInfo, Map<String, String>> widgetsLocaleToResources = <LocaleInfo, Map<String, String>>{};
final Map<LocaleInfo, Map<String, String>> widgetsLocaleToResources =
<LocaleInfo, Map<String, String>>{};
// Maps of locales to resource key/attributes pairs for Widgets ARBs.
// https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
final Map<LocaleInfo, Map<String, dynamic>> widgetsLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
final Map<LocaleInfo, Map<String, dynamic>> widgetsLocaleToResourceAttributes =
<LocaleInfo, Map<String, dynamic>>{};
// Maps of locales to resource key/value pairs for Material ARBs.
final Map<LocaleInfo, Map<String, String>> materialLocaleToResources = <LocaleInfo, Map<String, String>>{};
final Map<LocaleInfo, Map<String, String>> materialLocaleToResources =
<LocaleInfo, Map<String, String>>{};
// Maps of locales to resource key/attributes pairs for Material ARBs.
// https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
final Map<LocaleInfo, Map<String, dynamic>> materialLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
final Map<LocaleInfo, Map<String, dynamic>> materialLocaleToResourceAttributes =
<LocaleInfo, Map<String, dynamic>>{};
// Maps of locales to resource key/value pairs for Cupertino ARBs.
final Map<LocaleInfo, Map<String, String>> cupertinoLocaleToResources = <LocaleInfo, Map<String, String>>{};
final Map<LocaleInfo, Map<String, String>> cupertinoLocaleToResources =
<LocaleInfo, Map<String, String>>{};
// Maps of locales to resource key/attributes pairs for Cupertino ARBs.
// https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
final Map<LocaleInfo, Map<String, dynamic>> cupertinoLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
final Map<LocaleInfo, Map<String, dynamic>> cupertinoLocaleToResourceAttributes =
<LocaleInfo, Map<String, dynamic>>{};
loadMatchingArbsIntoBundleMaps(
directory: directory,
@@ -574,9 +619,21 @@ void main(List<String> rawArgs) {
);
try {
validateLocalizations(widgetsLocaleToResources, widgetsLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
validateLocalizations(
widgetsLocaleToResources,
widgetsLocaleToResourceAttributes,
removeUndefined: options.removeUndefined,
);
validateLocalizations(
materialLocaleToResources,
materialLocaleToResourceAttributes,
removeUndefined: options.removeUndefined,
);
validateLocalizations(
cupertinoLocaleToResources,
cupertinoLocaleToResourceAttributes,
removeUndefined: options.removeUndefined,
);
} on ValidationError catch (exception) {
exitWithError('$exception');
}
@@ -586,62 +643,71 @@ void main(List<String> rawArgs) {
removeUndefinedLocalizations(cupertinoLocaleToResources);
}
final String? widgetsLocalizations = options.writeToFile || !options.cupertinoOnly
? generateArbBasedLocalizationSubclasses(
localeToResources: widgetsLocaleToResources,
localeToResourceAttributes: widgetsLocaleToResourceAttributes,
generatedClassPrefix: 'WidgetsLocalization',
baseClass: 'GlobalWidgetsLocalizations',
generateHeader: generateWidgetsHeader,
generateConstructor: generateWidgetsConstructor,
generateConstructorForCountrySubClass: generateWidgetsConstructorForCountrySubclass,
factoryName: widgetsFactoryName,
factoryDeclaration: widgetsFactoryDeclaration,
callsFactoryWithConst: true,
factoryArguments: widgetsFactoryArguments,
supportedLanguagesConstant: widgetsSupportedLanguagesConstant,
supportedLanguagesDocMacro: widgetsSupportedLanguagesDocMacro,
)
: null;
final String? materialLocalizations = options.writeToFile || !options.cupertinoOnly
? generateArbBasedLocalizationSubclasses(
localeToResources: materialLocaleToResources,
localeToResourceAttributes: materialLocaleToResourceAttributes,
generatedClassPrefix: 'MaterialLocalization',
baseClass: 'GlobalMaterialLocalizations',
generateHeader: generateMaterialHeader,
generateConstructor: generateMaterialConstructor,
factoryName: materialFactoryName,
factoryDeclaration: materialFactoryDeclaration,
callsFactoryWithConst: false,
factoryArguments: materialFactoryArguments,
supportedLanguagesConstant: materialSupportedLanguagesConstant,
supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
)
: null;
final String? cupertinoLocalizations = options.writeToFile || !options.materialOnly
? generateArbBasedLocalizationSubclasses(
localeToResources: cupertinoLocaleToResources,
localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
generatedClassPrefix: 'CupertinoLocalization',
baseClass: 'GlobalCupertinoLocalizations',
generateHeader: generateCupertinoHeader,
generateConstructor: generateCupertinoConstructor,
factoryName: cupertinoFactoryName,
factoryDeclaration: cupertinoFactoryDeclaration,
callsFactoryWithConst: false,
factoryArguments: cupertinoFactoryArguments,
supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
)
: null;
final String? widgetsLocalizations =
options.writeToFile || !options.cupertinoOnly
? generateArbBasedLocalizationSubclasses(
localeToResources: widgetsLocaleToResources,
localeToResourceAttributes: widgetsLocaleToResourceAttributes,
generatedClassPrefix: 'WidgetsLocalization',
baseClass: 'GlobalWidgetsLocalizations',
generateHeader: generateWidgetsHeader,
generateConstructor: generateWidgetsConstructor,
generateConstructorForCountrySubClass: generateWidgetsConstructorForCountrySubclass,
factoryName: widgetsFactoryName,
factoryDeclaration: widgetsFactoryDeclaration,
callsFactoryWithConst: true,
factoryArguments: widgetsFactoryArguments,
supportedLanguagesConstant: widgetsSupportedLanguagesConstant,
supportedLanguagesDocMacro: widgetsSupportedLanguagesDocMacro,
)
: null;
final String? materialLocalizations =
options.writeToFile || !options.cupertinoOnly
? generateArbBasedLocalizationSubclasses(
localeToResources: materialLocaleToResources,
localeToResourceAttributes: materialLocaleToResourceAttributes,
generatedClassPrefix: 'MaterialLocalization',
baseClass: 'GlobalMaterialLocalizations',
generateHeader: generateMaterialHeader,
generateConstructor: generateMaterialConstructor,
factoryName: materialFactoryName,
factoryDeclaration: materialFactoryDeclaration,
callsFactoryWithConst: false,
factoryArguments: materialFactoryArguments,
supportedLanguagesConstant: materialSupportedLanguagesConstant,
supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
)
: null;
final String? cupertinoLocalizations =
options.writeToFile || !options.materialOnly
? generateArbBasedLocalizationSubclasses(
localeToResources: cupertinoLocaleToResources,
localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
generatedClassPrefix: 'CupertinoLocalization',
baseClass: 'GlobalCupertinoLocalizations',
generateHeader: generateCupertinoHeader,
generateConstructor: generateCupertinoConstructor,
factoryName: cupertinoFactoryName,
factoryDeclaration: cupertinoFactoryDeclaration,
callsFactoryWithConst: false,
factoryArguments: cupertinoFactoryArguments,
supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
)
: null;
if (options.writeToFile) {
final File widgetsLocalizationsFile = File(path.join(directory.path, 'generated_widgets_localizations.dart'));
final File widgetsLocalizationsFile = File(
path.join(directory.path, 'generated_widgets_localizations.dart'),
);
widgetsLocalizationsFile.writeAsStringSync(widgetsLocalizations!, flush: true);
final File materialLocalizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart'));
final File materialLocalizationsFile = File(
path.join(directory.path, 'generated_material_localizations.dart'),
);
materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true);
final File cupertinoLocalizationsFile = File(path.join(directory.path, 'generated_cupertino_localizations.dart'));
final File cupertinoLocalizationsFile = File(
path.join(directory.path, 'generated_cupertino_localizations.dart'),
);
cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true);
} else {
if (options.cupertinoOnly) {

View File

@@ -33,7 +33,13 @@ Future<void> main(List<String> rawArgs) async {
}
checkCwdIsRepoRoot('gen_missing_localizations');
final String localizationPath = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n');
final String localizationPath = path.join(
'packages',
'flutter_localizations',
'lib',
'src',
'l10n',
);
updateMissingResources(localizationPath, 'material', removeUndefined: removeUndefined);
updateMissingResources(localizationPath, 'cupertino', removeUndefined: removeUndefined);
}
@@ -58,7 +64,7 @@ void writeBundle(File file, Map<String, dynamic> bundle) {
Set<String> resourceKeys(Map<String, dynamic> bundle) {
return Set<String>.from(
// Skip any attribute keys
bundle.keys.where((String key) => !key.startsWith('@'))
bundle.keys.where((String key) => !key.startsWith('@')),
);
}
@@ -79,14 +85,21 @@ bool isPluralVariation(String key, Map<String, dynamic> bundle) {
return bundle.containsKey('${prefix}Other');
}
void updateMissingResources(String localizationPath, String groupPrefix, {bool removeUndefined = false}) {
void updateMissingResources(
String localizationPath,
String groupPrefix, {
bool removeUndefined = false,
}) {
final Directory localizationDir = Directory(localizationPath);
final RegExp filenamePattern = RegExp('${groupPrefix}_(\\w+)\\.arb');
final Map<String, dynamic> englishBundle = loadBundle(File(path.join(localizationPath, '${groupPrefix}_en.arb')));
final Map<String, dynamic> englishBundle = loadBundle(
File(path.join(localizationPath, '${groupPrefix}_en.arb')),
);
final Set<String> requiredKeys = resourceKeys(englishBundle);
for (final FileSystemEntity entity in localizationDir.listSync().toList()..sort(sortFilesByPath)) {
for (final FileSystemEntity entity
in localizationDir.listSync().toList()..sort(sortFilesByPath)) {
final String entityPath = entity.path;
if (FileSystemEntity.isFileSync(entityPath) && filenamePattern.hasMatch(entityPath)) {
final String localeString = filenamePattern.firstMatch(entityPath)![1]!;
@@ -105,17 +118,15 @@ void updateMissingResources(String localizationPath, String groupPrefix, {bool r
// --remove-undefined is passed.
if (removeUndefined) {
bool isIncluded(String key) {
return !isPluralVariation(key, localeBundle)
&& !intentionallyOmitted(key, localeBundle);
return !isPluralVariation(key, localeBundle) &&
!intentionallyOmitted(key, localeBundle);
}
// Find any resources in this locale that don't appear in the
// canonical locale, and skipping any which should not be included
// (plurals and intentionally omitted).
final Set<String> extraResources = localeResources
.difference(requiredKeys)
.where(isIncluded)
.toSet();
final Set<String> extraResources =
localeResources.difference(requiredKeys).where(isIncluded).toSet();
// Remove them.
localeBundle.removeWhere((String key, dynamic value) {
@@ -132,12 +143,21 @@ void updateMissingResources(String localizationPath, String groupPrefix, {bool r
// Add in any resources that are in the canonical locale and not present
// in this locale.
final Set<String> missingResources = requiredKeys.difference(localeResources).where(
(String key) => !isPluralVariation(key, localeBundle) && !intentionallyOmitted(key, localeBundle)
).toSet();
final Set<String> missingResources =
requiredKeys
.difference(localeResources)
.where(
(String key) =>
!isPluralVariation(key, localeBundle) &&
!intentionallyOmitted(key, localeBundle),
)
.toSet();
if (missingResources.isNotEmpty) {
localeBundle.addEntries(missingResources.map((String k) =>
MapEntry<String, String>(k, englishBundle[k].toString())));
localeBundle.addEntries(
missingResources.map(
(String k) => MapEntry<String, String>(k, englishBundle[k].toString()),
),
);
shouldWrite = true;
print('Updating $entityPath with missing entries for $missingResources');
}

View File

@@ -5,7 +5,8 @@
import 'dart:convert';
import 'dart:io';
const String registry = 'https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry';
const String registry =
'https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry';
/// A script to generate a Dart cache of https://www.iana.org. This should be
/// run occasionally. It was created since iana.org was found to be flaky.
@@ -15,9 +16,12 @@ Future<void> main() async {
final HttpClient client = HttpClient();
final HttpClientRequest request = await client.getUrl(Uri.parse(registry));
final HttpClientResponse response = await request.close();
final String body = (await response.cast<List<int>>().transform<String>(utf8.decoder).toList()).join();
final String body =
(await response.cast<List<int>>().transform<String>(utf8.decoder).toList()).join();
final File subtagRegistry = File('../language_subtag_registry.dart');
final File subtagRegistryFlutterTools = File('../../../../packages/flutter_tools/lib/src/localizations/language_subtag_registry.dart');
final File subtagRegistryFlutterTools = File(
'../../../../packages/flutter_tools/lib/src/localizations/language_subtag_registry.dart',
);
final String content = '''
// Copyright 2014 The Flutter Authors. All rights reserved.
@@ -27,7 +31,6 @@ Future<void> main() async {
/// Cache of $registry.
const String languageSubtagRegistry = \'\'\'$body\'\'\';''';
subtagRegistry.writeAsStringSync(content);
subtagRegistryFlutterTools.writeAsStringSync(content);

View File

@@ -26,7 +26,10 @@ import '../cupertino_localizations.dart';
//
// These classes are constructed by the [getCupertinoTranslation] method at the
// bottom of this file, and used by the [_GlobalCupertinoLocalizationsDelegate.load]
// method defined in `flutter_localizations/lib/src/cupertino_localizations.dart`.''';
// method defined in `flutter_localizations/lib/src/cupertino_localizations.dart`.
// TODO(goderbauer): Extend the generator to properly format the output.
// dart format off''';
}
/// Returns the source of the constructor for a GlobalCupertinoLocalizations

View File

@@ -26,7 +26,10 @@ import '../material_localizations.dart';
//
// These classes are constructed by the [getMaterialTranslation] method at the
// bottom of this file, and used by the [_MaterialLocalizationsDelegate.load]
// method defined in `flutter_localizations/lib/src/material_localizations.dart`.''';
// method defined in `flutter_localizations/lib/src/material_localizations.dart`.
// TODO(goderbauer): Extend the generator to properly format the output.
// dart format off''';
}
/// Returns the source of the constructor for a GlobalMaterialLocalizations

View File

@@ -33,7 +33,10 @@ import '../widgets_localizations.dart';
//
// These classes are constructed by the [getWidgetsTranslation] method at the
// bottom of this file, and used by the [_WidgetsLocalizationsDelegate.load]
// method defined in `flutter_localizations/lib/src/widgets_localizations.dart`.''';
// method defined in `flutter_localizations/lib/src/widgets_localizations.dart`.
// TODO(goderbauer): Extend the generator to properly format the output.
// dart format off''';
}
/// Returns the source of the constructor for a GlobalWidgetsLocalizations
@@ -41,7 +44,8 @@ import '../widgets_localizations.dart';
String generateWidgetsConstructor(LocaleInfo locale) {
final String localeName = locale.originalString;
final String language = locale.languageCode.toLowerCase();
final String textDirection = _rtlLanguages.contains(language) ? 'TextDirection.rtl' : 'TextDirection.ltr';
final String textDirection =
_rtlLanguages.contains(language) ? 'TextDirection.rtl' : 'TextDirection.ltr';
return '''
/// Create an instance of the translation bundle for ${describeLocale(localeName)}.
///

View File

@@ -13,7 +13,7 @@ import 'language_subtag_registry.dart';
typedef HeaderGenerator = String Function(String regenerateInstructions);
typedef ConstructorGenerator = String Function(LocaleInfo locale);
int sortFilesByPath (FileSystemEntity a, FileSystemEntity b) {
int sortFilesByPath(FileSystemEntity a, FileSystemEntity b) {
return a.path.compareTo(b.path);
}
@@ -36,7 +36,7 @@ class LocaleInfo implements Comparable<LocaleInfo> {
///
/// When `deriveScriptCode` is true, if [scriptCode] was unspecified, it will
/// be derived from the [languageCode] and [countryCode] if possible.
factory LocaleInfo.fromString(String locale, { bool deriveScriptCode = false }) {
factory LocaleInfo.fromString(String locale, {bool deriveScriptCode = false}) {
final List<String> codes = locale.split('_'); // [language, script, country]
assert(codes.isNotEmpty && codes.length < 4);
final String languageCode = codes[0];
@@ -94,20 +94,21 @@ class LocaleInfo implements Comparable<LocaleInfo> {
final String languageCode;
final String? scriptCode;
final String? countryCode;
final int length; // The number of fields. Ranges from 1-3.
final String originalString; // Original un-parsed locale string.
final int length; // The number of fields. Ranges from 1-3.
final String originalString; // Original un-parsed locale string.
String camelCase() {
return originalString
.split('_')
.map<String>((String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase())
.join();
.split('_')
.map<String>(
(String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase(),
)
.join();
}
@override
bool operator ==(Object other) {
return other is LocaleInfo
&& other.originalString == originalString;
return other is LocaleInfo && other.originalString == originalString;
}
@override
@@ -132,7 +133,6 @@ void loadMatchingArbsIntoBundleMaps({
required Map<LocaleInfo, Map<String, String>> localeToResources,
required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes,
}) {
/// Set that holds the locales that were assumed from the existing locales.
///
/// For example, when the data lacks data for zh_Hant, we will use the data of
@@ -151,7 +151,8 @@ void loadMatchingArbsIntoBundleMaps({
void populateResources(LocaleInfo locale, File file) {
final Map<String, String> resources = localeToResources[locale]!;
final Map<String, dynamic> attributes = localeToResourceAttributes[locale]!;
final Map<String, dynamic> bundle = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
final Map<String, dynamic> bundle =
json.decode(file.readAsStringSync()) as Map<String, dynamic>;
for (final String key in bundle.keys) {
// The ARB file resource "attributes" for foo are called @foo.
if (key.startsWith('@')) {
@@ -161,10 +162,14 @@ void loadMatchingArbsIntoBundleMaps({
}
}
}
// Only pre-assume scriptCode if there is a country or script code to assume off of.
// When we assume scriptCode based on languageCode-only, we want this initial pass
// to use the un-assumed version as a base class.
LocaleInfo locale = LocaleInfo.fromString(localeString, deriveScriptCode: localeString.split('_').length > 1);
LocaleInfo locale = LocaleInfo.fromString(
localeString,
deriveScriptCode: localeString.split('_').length > 1,
);
// Allow overwrite if the existing data is assumed.
if (assumedLocales.contains(locale)) {
localeToResources[locale] = <String, String>{};
@@ -178,7 +183,9 @@ void loadMatchingArbsIntoBundleMaps({
// Add an assumed locale to default to when there is no info on scriptOnly locales.
locale = LocaleInfo.fromString(localeString, deriveScriptCode: true);
if (locale.scriptCode != null) {
final LocaleInfo scriptLocale = LocaleInfo.fromString('${locale.languageCode}_${locale.scriptCode}');
final LocaleInfo scriptLocale = LocaleInfo.fromString(
'${locale.languageCode}_${locale.scriptCode}',
);
if (!localeToResources.containsKey(scriptLocale)) {
assumedLocales.add(scriptLocale);
localeToResources[scriptLocale] ??= <String, String>{};
@@ -201,39 +208,35 @@ void checkCwdIsRepoRoot(String commandName) {
if (!isRepoRoot) {
exitWithError(
'$commandName must be run from the root of the Flutter repository. The '
'current working directory is: ${Directory.current.path}'
'current working directory is: ${Directory.current.path}',
);
}
}
GeneratorOptions parseArgs(List<String> rawArgs) {
final argslib.ArgParser argParser = argslib.ArgParser()
..addFlag(
'help',
abbr: 'h',
help: 'Print the usage message for this command',
)
..addFlag(
'overwrite',
abbr: 'w',
help: 'Overwrite existing localizations',
)
..addFlag(
'remove-undefined',
help: 'Remove any localizations that are not defined in the canonical locale.',
)
..addFlag(
'widgets',
help: 'Whether to print the generated classes for the Widgets package only. Ignored when --overwrite is passed.',
)
..addFlag(
'material',
help: 'Whether to print the generated classes for the Material package only. Ignored when --overwrite is passed.',
)
..addFlag(
'cupertino',
help: 'Whether to print the generated classes for the Cupertino package only. Ignored when --overwrite is passed.',
);
final argslib.ArgParser argParser =
argslib.ArgParser()
..addFlag('help', abbr: 'h', help: 'Print the usage message for this command')
..addFlag('overwrite', abbr: 'w', help: 'Overwrite existing localizations')
..addFlag(
'remove-undefined',
help: 'Remove any localizations that are not defined in the canonical locale.',
)
..addFlag(
'widgets',
help:
'Whether to print the generated classes for the Widgets package only. Ignored when --overwrite is passed.',
)
..addFlag(
'material',
help:
'Whether to print the generated classes for the Material package only. Ignored when --overwrite is passed.',
)
..addFlag(
'cupertino',
help:
'Whether to print the generated classes for the Cupertino package only. Ignored when --overwrite is passed.',
);
final argslib.ArgResults args = argParser.parse(rawArgs);
if (args.wasParsed('help') && args['help'] == true) {
stderr.writeln(argParser.usage);
@@ -305,12 +308,19 @@ const String kParentheticalPrefix = ' (';
/// The data is obtained from the official IANA registry.
void precacheLanguageAndRegionTags() {
final List<Map<String, List<String>>> sections =
languageSubtagRegistry.split('%%').skip(1).map<Map<String, List<String>>>(_parseSection).toList();
languageSubtagRegistry
.split('%%')
.skip(1)
.map<Map<String, List<String>>>(_parseSection)
.toList();
for (final Map<String, List<String>> section in sections) {
assert(section.containsKey('Type'), section.toString());
final String type = section['Type']!.single;
if (type == 'language' || type == 'region' || type == 'script') {
assert(section.containsKey('Subtag') && section.containsKey('Description'), section.toString());
assert(
section.containsKey('Subtag') && section.containsKey('Description'),
section.toString(),
);
final String subtag = section['Subtag']!.single;
String description = section['Description']!.join(' ');
if (description.startsWith('United ')) {
@@ -364,11 +374,7 @@ String describeLocale(String tag) {
}
/// Writes the header of each class which corresponds to a locale.
String generateClassDeclaration(
LocaleInfo locale,
String classNamePrefix,
String superClass,
) {
String generateClassDeclaration(LocaleInfo locale, String classNamePrefix, String superClass) {
final String camelCaseName = locale.camelCase();
return '''
@@ -417,23 +423,23 @@ String generateString(String value) {
!value.contains(backslash),
'Input string cannot contain the sequence: '
'"__BACKSLASH__", as it is used as part of '
'backslash character processing.'
'backslash character processing.',
);
value = value
// Replace backslashes with a placeholder for now to properly parse
// other special characters.
.replaceAll(r'\', backslash)
.replaceAll(r'$', r'\$')
.replaceAll("'", r"\'")
.replaceAll('"', r'\"')
.replaceAll('\n', r'\n')
.replaceAll('\f', r'\f')
.replaceAll('\t', r'\t')
.replaceAll('\r', r'\r')
.replaceAll('\b', r'\b')
// Reintroduce escaped backslashes into generated Dart string.
.replaceAll(backslash, r'\\');
// Replace backslashes with a placeholder for now to properly parse
// other special characters.
.replaceAll(r'\', backslash)
.replaceAll(r'$', r'\$')
.replaceAll("'", r"\'")
.replaceAll('"', r'\"')
.replaceAll('\n', r'\n')
.replaceAll('\f', r'\f')
.replaceAll('\t', r'\t')
.replaceAll('\r', r'\r')
.replaceAll('\b', r'\b')
// Reintroduce escaped backslashes into generated Dart string.
.replaceAll(backslash, r'\\');
return "'$value'";
}
@@ -446,6 +452,7 @@ String generateEncodedString(String? locale, String value) {
return generateString(value);
}
final String unicodeEscapes = value.runes.map((int code) => '\\u{${code.toRadixString(16)}}').join();
final String unicodeEscapes =
value.runes.map((int code) => '\\u{${code.toRadixString(16)}}').join();
return "'$unicodeEscapes'";
}

View File

@@ -13,7 +13,7 @@ const List<String> kPluralSuffixes = <String>['Other', 'Zero', 'One', 'Two', 'Fe
final RegExp kPluralRegexp = RegExp(r'(\w*)(' + kPluralSuffixes.skip(1).join(r'|') + r')$');
class ValidationError implements Exception {
ValidationError(this. message);
ValidationError(this.message);
final String message;
@override
String toString() => message;
@@ -51,6 +51,7 @@ void validateEnglishLocalizations(File file) {
final int suffixIndex = resourceId.indexOf(suffix);
return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
}
if (kPluralSuffixes.any(checkPluralResource)) {
continue;
}
@@ -107,10 +108,9 @@ void validateEnglishLocalizations(File file) {
/// keys in the resources for this locale, but which _aren't_ keys in the
/// canonical list.
/// 5. Removes the invalid mappings from this resource's locale.
void removeUndefinedLocalizations(
Map<LocaleInfo, Map<String, String>> localeToResources,
) {
final Map<String, String> canonicalLocalizations = localeToResources[LocaleInfo.fromString('en')]!;
void removeUndefinedLocalizations(Map<LocaleInfo, Map<String, String>> localeToResources) {
final Map<String, String> canonicalLocalizations =
localeToResources[LocaleInfo.fromString('en')]!;
final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
localeToResources.forEach((LocaleInfo locale, Map<String, String> resources) {
@@ -124,7 +124,7 @@ void removeUndefinedLocalizations(
}
final Set<String> keys = Set<String>.from(
resources.keys.where((String key) => !isPluralVariation(key))
resources.keys.where((String key) => !isPluralVariation(key)),
);
final Set<String> invalidKeys = keys.difference(canonicalKeys);
@@ -146,7 +146,8 @@ void validateLocalizations(
Map<LocaleInfo, Map<String, dynamic>> localeToAttributes, {
bool removeUndefined = false,
}) {
final Map<String, String> canonicalLocalizations = localeToResources[LocaleInfo.fromString('en')]!;
final Map<String, String> canonicalLocalizations =
localeToResources[LocaleInfo.fromString('en')]!;
final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
final StringBuffer errorMessages = StringBuffer();
bool explainMissingKeys = false;
@@ -168,14 +169,16 @@ void validateLocalizations(
}
final Set<String> keys = Set<String>.from(
resources.keys.where((String key) => !isPluralVariation(key))
resources.keys.where((String key) => !isPluralVariation(key)),
);
// Make sure keys are valid (i.e. they also exist in the canonical
// localizations)
final Set<String> invalidKeys = keys.difference(canonicalKeys);
if (invalidKeys.isNotEmpty && !removeUndefined) {
errorMessages.writeln('Locale "$locale" contains invalid resource keys: ${invalidKeys.join(', ')}');
errorMessages.writeln(
'Locale "$locale" contains invalid resource keys: ${invalidKeys.join(', ')}',
);
}
// For language-level locales only, check that they have a complete list of
@@ -183,7 +186,7 @@ void validateLocalizations(
if (locale.length == 1) {
final Map<String, dynamic>? attributes = localeToAttributes[locale];
final List<String?> missingKeys = <String?>[];
for (final String missingKey in canonicalKeys.difference(keys)) {
for (final String missingKey in canonicalKeys.difference(keys)) {
final dynamic attribute = attributes?[missingKey];
final bool intentionallyOmitted = attribute is Map && attribute.containsKey('notUsed');
if (!intentionallyOmitted && !isPluralVariation(missingKey)) {
@@ -192,23 +195,25 @@ void validateLocalizations(
}
if (missingKeys.isNotEmpty) {
explainMissingKeys = true;
errorMessages.writeln('Locale "$locale" is missing the following resource keys: ${missingKeys.join(', ')}');
errorMessages.writeln(
'Locale "$locale" is missing the following resource keys: ${missingKeys.join(', ')}',
);
}
}
}
if (errorMessages.isNotEmpty) {
if (explainMissingKeys) {
errorMessages
..writeln()
..writeln(
'If a resource key is intentionally omitted, add an attribute corresponding '
'to the key name with a "notUsed" property explaining why. Example:'
)
..writeln()
..writeln('"@anteMeridiemAbbreviation": {')
..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
..writeln('}');
errorMessages
..writeln()
..writeln(
'If a resource key is intentionally omitted, add an attribute corresponding '
'to the key name with a "notUsed" property explaining why. Example:',
)
..writeln()
..writeln('"@anteMeridiemAbbreviation": {')
..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
..writeln('}');
}
throw ValidationError(errorMessages.toString());
}

View File

@@ -62,7 +62,9 @@ void main(List<String> args) {
print('');
print('Stats:');
print(' packages/flutter : ${getStatsFor(Directory("packages/flutter"))}');
print(' dev/integration_tests/flutter_gallery : ${getStatsFor(Directory("dev/integration_tests/flutter_gallery"))}');
print(
' dev/integration_tests/flutter_gallery : ${getStatsFor(Directory("dev/integration_tests/flutter_gallery"))}',
);
final Directory lib = _dir(out, 'lib');
if (lib.existsSync()) {
@@ -86,15 +88,13 @@ void main(List<String> args) {
_file(out, 'pubspec.yaml').writeAsStringSync(pubspec);
// Replace the (flutter_gallery specific) analysis_options.yaml file with a default one.
_file(out, 'analysis_options.yaml').writeAsStringSync(
'''
_file(out, 'analysis_options.yaml').writeAsStringSync('''
analyzer:
errors:
# See analysis_options.yaml in the flutter root for context.
deprecated_member_use: ignore
deprecated_member_use_from_same_package: ignore
'''
);
''');
_file(out, '.dartignore').writeAsStringSync('');

View File

@@ -45,19 +45,12 @@ final class NativeDriverCommandExtension implements CommandExtension {
if (await _builtInCall(command.method) case final Result result) {
return result;
}
final Object? result = await _channel.invokeMethod<Object>(
command.method,
command.arguments,
);
final Object? result = await _channel.invokeMethod<Object>(command.method, command.arguments);
if (result == null) {
return const _MethodChannelResult(<String, Object?>{});
}
if (result is! Map<String, Object?>) {
throw ArgumentError.value(
result,
'result',
'Expected a Map<String, Object?>',
);
throw ArgumentError.value(result, 'result', 'Expected a Map<String, Object?>');
}
return _MethodChannelResult(result);
}
@@ -68,14 +61,12 @@ final class NativeDriverCommandExtension implements CommandExtension {
Future<Result?> _builtInCall(String method) async {
switch (method) {
case 'rotate_landscape':
await flt.SystemChrome.setPreferredOrientations(
const <flt.DeviceOrientation>[flt.DeviceOrientation.landscapeLeft],
);
await flt.SystemChrome.setPreferredOrientations(const <flt.DeviceOrientation>[
flt.DeviceOrientation.landscapeLeft,
]);
return Result.empty;
case 'rotate_default':
await flt.SystemChrome.setPreferredOrientations(
const <flt.DeviceOrientation>[],
);
await flt.SystemChrome.setPreferredOrientations(const <flt.DeviceOrientation>[]);
return Result.empty;
default:
return null;
@@ -102,11 +93,7 @@ final class NativeDriverCommandExtension implements CommandExtension {
} else {
final Object? intermediate = json.decode(arguments);
if (intermediate is! Map<String, Object?>) {
throw ArgumentError.value(
arguments,
'arguments',
'Expected a Map<String, Object?>',
);
throw ArgumentError.value(arguments, 'arguments', 'Expected a Map<String, Object?>');
}
decoded = intermediate;
}

View File

@@ -20,8 +20,7 @@ import 'package:test_api/test_api.dart';
import 'src/common.dart';
export 'src/backend/android.dart' show AndroidDeviceTarget, AndroidNativeDriver;
export 'src/common.dart'
show ByNativeAccessibilityLabel, ByNativeIntegerId, NativeFinder;
export 'src/common.dart' show ByNativeAccessibilityLabel, ByNativeIntegerId, NativeFinder;
part 'src/driver.dart';
part 'src/goldens.dart';

View File

@@ -46,12 +46,8 @@ Future<void> enableSkiaGoldComparator({String? namePrefix}) async {
'Set it to use Skia Gold.',
);
}
final io.Directory tmpDir = io.Directory.systemTemp.createTempSync(
'android_driver_test',
);
final bool isPresubmit = io.Platform.environment.containsKey(
_kGoldctlPresubmitKey,
);
final io.Directory tmpDir = io.Directory.systemTemp.createTempSync('android_driver_test');
final bool isPresubmit = io.Platform.environment.containsKey(_kGoldctlPresubmitKey);
io.stderr.writeln(
'=== Using Skia Gold ===\n'
'Environment variable $_kGoldctlKey is set, using Skia Gold: \n'
@@ -76,12 +72,8 @@ Future<void> enableSkiaGoldComparator({String? namePrefix}) async {
}
final class _GoldenFileComparator extends GoldenFileComparator {
_GoldenFileComparator(
this.skiaClient, {
required this.isPresubmit,
this.namePrefix,
Uri? baseDir,
}) : baseDir = baseDir ?? Uri.parse(path.dirname(io.Platform.script.path));
_GoldenFileComparator(this.skiaClient, {required this.isPresubmit, this.namePrefix, Uri? baseDir})
: baseDir = baseDir ?? Uri.parse(path.dirname(io.Platform.script.path));
final Uri baseDir;
final SkiaGoldClient skiaClient;
@@ -104,14 +96,10 @@ final class _GoldenFileComparator extends GoldenFileComparator {
_localFs.file(goldenFile.path),
);
if (result != null) {
io.stderr.writeln(
'Skia Gold detected an error when comparing "$golden":\n\n$result',
);
io.stderr.writeln('Skia Gold detected an error when comparing "$golden":\n\n$result');
io.stderr.writeln('Still succeeding, will be triaged in Flutter Gold');
} else {
io.stderr.writeln(
'Skia Gold comparison succeeded comparing "$golden".',
);
io.stderr.writeln('Skia Gold comparison succeeded comparing "$golden".');
}
return true;
} else {
@@ -121,9 +109,7 @@ final class _GoldenFileComparator extends GoldenFileComparator {
@override
Future<io.File> update(Uri golden, Uint8List imageBytes) async {
io.stderr.writeln(
'Updating golden file: $golden (${imageBytes.length} bytes)...',
);
io.stderr.writeln('Updating golden file: $golden (${imageBytes.length} bytes)...');
final io.File goldenFile = _getGoldenFile(golden);
await goldenFile.parent.create(recursive: true);
await goldenFile.writeAsBytes(imageBytes, flush: true);
@@ -140,10 +126,12 @@ final class _GoldenFileComparator extends GoldenFileComparator {
'Golden files in the Flutter framework must end with the file extension '
'.png.',
);
return Uri.parse(<String>[
if (namePrefix != null) namePrefix!,
baseDir.pathSegments[baseDir.pathSegments.length - 2],
golden.toString(),
].join('.'));
return Uri.parse(
<String>[
if (namePrefix != null) namePrefix!,
baseDir.pathSegments[baseDir.pathSegments.length - 2],
golden.toString(),
].join('.'),
);
}
}

View File

@@ -29,10 +29,10 @@ class Adb {
}) async {
target ??= const AndroidDeviceTarget.onlyEmulatorOrDevice();
final String tool = adbPath ?? 'adb';
final Adb adb = Adb._(
<String>[tool, ...target._toAdbArgs()],
processManager ?? const LocalProcessManager(),
);
final Adb adb = Adb._(<String>[
tool,
...target._toAdbArgs(),
], processManager ?? const LocalProcessManager());
final (bool connected, String? error) = await adb.isDeviceConnected();
if (!connected) {
throw StateError('No device connected: $error');
@@ -41,12 +41,7 @@ class Adb {
}
Future<AdbStringResult> _runString(List<String> args) async {
final io.ProcessResult result = await _process.run(
<String>[
..._prefixArgs,
...args,
],
);
final io.ProcessResult result = await _process.run(<String>[..._prefixArgs, ...args]);
return AdbStringResult(
result.stdout as String,
exitCode: result.exitCode,
@@ -55,13 +50,10 @@ class Adb {
}
Future<AdbBinaryResult> _runBinary(List<String> args) async {
final io.ProcessResult result = await _process.run(
<String>[
..._prefixArgs,
...args,
],
stdoutEncoding: null,
);
final io.ProcessResult result = await _process.run(<String>[
..._prefixArgs,
...args,
], stdoutEncoding: null);
return AdbBinaryResult(
result.stdout as Uint8List,
exitCode: result.exitCode,
@@ -78,11 +70,7 @@ class Adb {
/// and an error message if the device is not connected. If the device is
/// connected, the error message is `null`.
Future<(bool connected, String? error)> isDeviceConnected() async {
final AdbStringResult result = await _runString(<String>[
'shell',
'echo',
'connected',
]);
final AdbStringResult result = await _runString(<String>['shell', 'echo', 'connected']);
if (result.exitCode != 0) {
return (false, result.stderr);
} else {
@@ -92,11 +80,7 @@ class Adb {
/// Takes a screenshot of the device.
Future<Uint8List> screencap() async {
final AdbBinaryResult result = await _runBinary(<String>[
'exec-out',
'screencap',
'-p',
]);
final AdbBinaryResult result = await _runBinary(<String>['exec-out', 'screencap', '-p']);
if (result.exitCode != 0) {
throw StateError('Failed to take screenshot: ${result.stderr}');
}
@@ -105,13 +89,7 @@ class Adb {
/// Taps on the screen at the given [x] and [y] coordinates.
Future<void> tap(int x, int y) async {
final AdbStringResult result = await _runString(<String>[
'shell',
'input',
'tap',
'$x',
'$y',
]);
final AdbStringResult result = await _runString(<String>['shell', 'input', 'tap', '$x', '$y']);
if (result.exitCode != 0) {
throw StateError('Failed to tap at $x, $y: ${result.stderr}');
}
@@ -145,10 +123,7 @@ class Adb {
}
/// Resume a backgrounded app (i.e. after [sendToHome]).
Future<void> resumeApp({
required String appName,
String activityName = '.MainActivity',
}) async {
Future<void> resumeApp({required String appName, String activityName = '.MainActivity'}) async {
final AdbStringResult result = await _runString(<String>[
'shell',
'am',
@@ -172,9 +147,7 @@ class Adb {
'confirmed',
]);
if (result.exitCode != 0) {
throw StateError(
'Failed to disable immersive mode confirmations: ${result.stderr}',
);
throw StateError('Failed to disable immersive mode confirmations: ${result.stderr}');
}
}
@@ -250,9 +223,7 @@ sealed class AndroidDeviceTarget {
/// ```dart
/// const AndroidDeviceTarget target = AndroidDeviceTarget.bySerial('emulator-5554');
/// ```
const factory AndroidDeviceTarget.bySerial(
String serialNumber,
) = _SerialDeviceTarget;
const factory AndroidDeviceTarget.bySerial(String serialNumber) = _SerialDeviceTarget;
/// Represents the only running emulator _or_ connected device.
///
@@ -314,5 +285,5 @@ enum AdbUserRotation {
reversePortrait,
/// Reverse landscape orientation, i.e., landscape upside down.
reverseLandscape;
reverseLandscape,
}

View File

@@ -44,10 +44,7 @@ final class AndroidNativeDriver implements NativeDriver {
String? adbPath,
io.Directory? tempDirectory,
}) async {
final Adb adb = await Adb.create(
adbPath: adbPath,
target: target,
);
final Adb adb = await Adb.create(adbPath: adbPath, target: target);
tempDirectory ??= io.Directory.systemTemp.createTempSync('native_driver.');
final AndroidNativeDriver nativeDriver = AndroidNativeDriver.forTesting(
adb: adb,

View File

@@ -19,19 +19,13 @@ final class NativeCommand extends Command {
}
/// Requests that the device be rotated to landscape mode.
static const NativeCommand rotateLandscape = NativeCommand(
'rotate_landscape',
);
static const NativeCommand rotateLandscape = NativeCommand('rotate_landscape');
/// Requests that the device reset its rotation to the default orientation.
static const NativeCommand rotateDefault = NativeCommand(
'rotate_default',
);
static const NativeCommand rotateDefault = NativeCommand('rotate_default');
/// Pings the device to ensure it is responsive.
static const NativeCommand ping = NativeCommand(
'ping',
);
static const NativeCommand ping = NativeCommand('ping');
/// The method to call on the plugin.
final String method;
@@ -88,10 +82,7 @@ final class ByNativeAccessibilityLabel extends NativeFinder {
@override
Map<String, String> toJson() {
return <String, String>{
'kind': 'byNativeAccessibilityLabel',
'label': label,
};
return <String, String>{'kind': 'byNativeAccessibilityLabel', 'label': label};
}
/// Deserializes this finder from the value generated by [serialize].
@@ -118,10 +109,7 @@ final class ByNativeIntegerId extends NativeFinder {
@override
Map<String, String> toJson() {
return <String, String>{
'kind': 'byNativeIntegerId',
'id': id.toString(),
};
return <String, String>{'kind': 'byNativeIntegerId', 'id': id.toString()};
}
/// Deserializes this finder from the value generated by [serialize].

View File

@@ -171,8 +171,7 @@ AsyncMatcher matchesGoldenFile(Object key, {int? version}) {
return switch (key) {
Uri() => _MatchesGoldenFile(key, version),
String() => _MatchesGoldenFile.forStringPath(key, version),
_ => throw ArgumentError(
'Unexpected type for golden file: ${key.runtimeType}'),
_ => throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}'),
};
}
@@ -182,8 +181,7 @@ final class _MatchesGoldenFile extends AsyncMatcher {
const _MatchesGoldenFile(this.key, this.version);
/// Creates an instance of [MatchesGoldenFile] from a [String] path.
_MatchesGoldenFile.forStringPath(String path, this.version)
: key = Uri.parse(path);
_MatchesGoldenFile.forStringPath(String path, this.version) : key = Uri.parse(path);
/// The [key] to the golden image.
final Uri key;
@@ -201,9 +199,7 @@ final class _MatchesGoldenFile extends AsyncMatcher {
} else if (item is FutureOr<NativeScreenshot>) {
buffer = await (await item).readAsBytes();
} else {
throw ArgumentError(
'Unexpected type for golden file: ${item.runtimeType}',
);
throw ArgumentError('Unexpected type for golden file: ${item.runtimeType}');
}
if (autoUpdateGoldenFiles) {
@@ -211,10 +207,7 @@ final class _MatchesGoldenFile extends AsyncMatcher {
return null;
}
try {
final bool success = await goldenFileComparator.compare(
buffer,
testNameUri,
);
final bool success = await goldenFileComparator.compare(buffer, testNameUri);
return success ? null : 'does not match';
} on TestFailure catch (e) {
return e.message;

View File

@@ -18,10 +18,7 @@ void main() {
setUp(() {
caughtArgs = <String>[];
processManager = FakeProcessManager((
String exec,
List<String> args,
) async {
processManager = FakeProcessManager((String exec, List<String> args) async {
assert(caughtArgs.isEmpty, 'Should only catch one call to run');
caughtArgs = args;
return FakeProcessManager.ok();
@@ -74,14 +71,14 @@ void main() {
});
expect(
Adb.create(
processManager: processManager,
Adb.create(processManager: processManager),
throwsA(
isA<StateError>().having(
(StateError e) => e.message,
'message',
'No device connected: error',
),
),
throwsA(isA<StateError>().having(
(StateError e) => e.message,
'message',
'No device connected: error',
)),
);
});
@@ -100,9 +97,7 @@ void main() {
}
});
final Adb adb = await Adb.create(
processManager: processManager,
);
final Adb adb = await Adb.create(processManager: processManager);
final Uint8List result = await adb.screencap();
expect(result, <int>[0, 1, 2, 3]);
@@ -123,9 +118,7 @@ void main() {
}
});
final Adb adb = await Adb.create(
processManager: processManager,
);
final Adb adb = await Adb.create(processManager: processManager);
await adb.tap(1, 2);
});

View File

@@ -13,9 +13,7 @@ import 'src/fake_driver.dart';
void main() async {
test('screenshot calls ADB screencap', () async {
final FakeAdb adb = FakeAdb(
screencap: () async => Uint8List.fromList(<int>[1, 2, 3, 4]),
);
final FakeAdb adb = FakeAdb(screencap: () async => Uint8List.fromList(<int>[1, 2, 3, 4]));
final io.Directory tmpDir = io.Directory.systemTemp.createTempSync();
final AndroidNativeDriver driver = AndroidNativeDriver.forTesting(
adb: adb,

View File

@@ -19,14 +19,14 @@ final class FakeAdb implements Adb {
Future<void> Function()? sendToHome,
Future<void> Function(String appName, [String? activityName])? resumeApp,
Future<void> Function(String appName)? trimMemory,
}) : _isDeviceConnected = isDeviceConnected,
_screencap = screencap,
_tap = tap,
_disableImmersiveModeConfirmations = disableImmersiveModeConfirmations,
_disableAnimations = disableAnimations,
_sendToHome = sendToHome,
_resumeApp = resumeApp,
_trimMemory = trimMemory;
}) : _isDeviceConnected = isDeviceConnected,
_screencap = screencap,
_tap = tap,
_disableImmersiveModeConfirmations = disableImmersiveModeConfirmations,
_disableAnimations = disableAnimations,
_sendToHome = sendToHome,
_resumeApp = resumeApp,
_trimMemory = trimMemory;
@override
Future<(bool, String?)> isDeviceConnected() async {
@@ -75,10 +75,7 @@ final class FakeAdb implements Adb {
return _resumeApp?.call(appName, activityName) ?? Future<void>.value();
}
final Future<void> Function(
String appName, [
String? activityName,
])? _resumeApp;
final Future<void> Function(String appName, [String? activityName])? _resumeApp;
@override
Future<void> trimMemory({required String appName}) {

View File

@@ -13,10 +13,7 @@ import 'package:process/process.dart';
final class FakeProcessManager extends ProcessManager {
/// Creates a new [FakeProcessManager] with the given delegate callback.
FakeProcessManager(this._runDelegate);
final Future<ProcessResult> Function(
String executable,
List<String> arguments,
) _runDelegate;
final Future<ProcessResult> Function(String executable, List<String> arguments) _runDelegate;
@override
Never noSuchMethod(_) => throw UnimplementedError();

View File

@@ -36,85 +36,75 @@ void main() {
});
test('getBranchName does not call git if env LUCI_BRANCH provided', () {
setUpWithEnvironment(
<String, String>{
'LUCI_BRANCH': branchName,
},
setUpWithEnvironment(<String, String>{'LUCI_BRANCH': branchName});
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'rev-parse', 'HEAD'],
));
expect(
apidocs.FlutterInformation.instance.getBranchName(),
branchName,
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
test('getBranchName calls git if env LUCI_BRANCH not provided', () {
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'rev-parse', 'HEAD'],
));
expect(
apidocs.FlutterInformation.instance.getBranchName(),
branchName,
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
test('getBranchName calls git if env LUCI_BRANCH is empty', () {
setUpWithEnvironment(
<String, String>{
'LUCI_BRANCH': '',
},
setUpWithEnvironment(<String, String>{'LUCI_BRANCH': ''});
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'rev-parse', 'HEAD'],
));
expect(
apidocs.FlutterInformation.instance.getBranchName(),
branchName,
);
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
test("runPubProcess doesn't use the pub binary", () {
final Platform platform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': '/flutter',
},
environment: <String, String>{'FLUTTER_ROOT': '/flutter'},
);
final ProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>['/flutter/bin/flutter', 'pub', '--one', '--two'],
),
],
final ProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', '--one', '--two']),
]);
apidocs.FlutterInformation.instance = apidocs.FlutterInformation(
platform: platform,
processManager: processManager,
filesystem: memoryFileSystem,
);
apidocs.FlutterInformation.instance =
apidocs.FlutterInformation(platform: platform, processManager: processManager, filesystem: memoryFileSystem);
apidocs.runPubProcess(
arguments: <String>['--one', '--two'],
@@ -126,48 +116,56 @@ void main() {
});
test('calls out to flutter if FLUTTER_VERSION is not set', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'rev-parse', 'HEAD'],
));
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(info['frameworkVersion'], equals(Version.parse('2.5.0')));
});
test("doesn't call out to flutter if FLUTTER_VERSION is set", () async {
setUpWithEnvironment(<String, String>{
'FLUTTER_VERSION': testVersionInfo,
});
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'rev-parse', 'HEAD'],
));
setUpWithEnvironment(<String, String>{'FLUTTER_VERSION': testVersionInfo});
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(info['frameworkVersion'], equals(Version.parse('2.5.0')));
});
test('getFlutterRoot calls out to flutter if FLUTTER_ROOT is not set', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <Pattern>['git', 'rev-parse', 'HEAD'],
));
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
);
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
);
final Directory root = flutterInformation.getFlutterRoot();
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(root.path, equals('/home/user/flutter'));
@@ -185,9 +183,7 @@ void main() {
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
]);
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
expect(info['frameworkVersion'], isNotNull);
@@ -215,9 +211,7 @@ void main() {
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
]);
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
expect(fakeProcessManager, hasNoRemainingExpectations);
@@ -286,12 +280,8 @@ void main() {
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
),
const FakeCommand(
command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
FakeCommand(
command: <Pattern>[
'/flutter/bin/flutter',
@@ -349,30 +339,51 @@ void main() {
configurator.generateConfiguration();
expect(packageRoot.childFile('pubspec.yaml').existsSync(), isTrue);
expect(packageRoot.childFile('pubspec.yaml').readAsStringSync(), contains('flutter_gpu:'));
expect(packageRoot.childFile('pubspec.yaml').readAsStringSync(), contains('dependency_overrides:'));
expect(packageRoot.childFile('pubspec.yaml').readAsStringSync(), contains('platform_integration:'));
expect(
packageRoot.childFile('pubspec.yaml').readAsStringSync(),
contains('dependency_overrides:'),
);
expect(
packageRoot.childFile('pubspec.yaml').readAsStringSync(),
contains('platform_integration:'),
);
});
test('.generateConfiguration generates fake lib', () async {
configurator.generateConfiguration();
expect(packageRoot.childDirectory('lib').existsSync(), isTrue);
expect(packageRoot.childDirectory('lib').childFile('temp_doc.dart').existsSync(), isTrue);
expect(packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(), contains('library temp_doc;'));
expect(packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(), contains("import 'package:flutter_gpu/gpu.dart';"));
expect(
packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(),
contains('library temp_doc;'),
);
expect(
packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(),
contains("import 'package:flutter_gpu/gpu.dart';"),
);
});
test('.generateConfiguration generates page footer', () async {
configurator.generateConfiguration();
expect(packageRoot.childFile('footer.html').existsSync(), isTrue);
expect(packageRoot.childFile('footer.html').readAsStringSync(), contains('<script src="footer.js">'));
expect(
packageRoot.childFile('footer.html').readAsStringSync(),
contains('<script src="footer.js">'),
);
expect(publishRoot.childDirectory('flutter').childFile('footer.js').existsSync(), isTrue);
expect(publishRoot.childDirectory('flutter').childFile('footer.js').readAsStringSync(), contains(RegExp(r'Flutter 2.5.0 •.*• stable')));
expect(
publishRoot.childDirectory('flutter').childFile('footer.js').readAsStringSync(),
contains(RegExp(r'Flutter 2.5.0 •.*• stable')),
);
});
test('.generateConfiguration generates search metadata', () async {
configurator.generateConfiguration();
expect(publishRoot.childFile('opensearch.xml').existsSync(), isTrue);
expect(publishRoot.childFile('opensearch.xml').readAsStringSync(), contains('https://api.flutter.dev/'));
expect(
publishRoot.childFile('opensearch.xml').readAsStringSync(),
contains('https://api.flutter.dev/'),
);
});
});
@@ -398,9 +409,7 @@ void main() {
apidocs.FlutterInformation.instance = apidocs.FlutterInformation(
filesystem: fs,
processManager: processManager,
platform: FakePlatform(environment: <String, String>{
'FLUTTER_ROOT': repoRoot.path,
}),
platform: FakePlatform(environment: <String, String>{'FLUTTER_ROOT': repoRoot.path}),
);
});
@@ -415,12 +424,8 @@ void main() {
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
),
const FakeCommand(
command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
FakeCommand(
command: <Pattern>[
'/flutter/bin/flutter',
@@ -480,7 +485,11 @@ void main() {
isA<Exception>().having(
(Exception e) => e.toString(),
'message',
contains(RegExp(r'Missing .* which probably means the documentation failed to build correctly.')),
contains(
RegExp(
r'Missing .* which probably means the documentation failed to build correctly.',
),
),
),
),
);
@@ -499,12 +508,8 @@ void main() {
command: <Pattern>['git', 'status', '-b', '--porcelain'],
stdout: '## $branchName',
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
),
const FakeCommand(
command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
FakeCommand(
command: <Pattern>[
'/flutter/bin/flutter',
@@ -562,28 +567,26 @@ void main() {
publishRoot.childDirectory('flutter').childFile(path).createSync(recursive: true);
}
for (final String path in dartdocDirectiveCanaryLibraries) {
publishRoot.childDirectory('flutter').childDirectory(path).createSync(recursive: true);
publishRoot
.childDirectory('flutter')
.childDirectory(path)
.createSync(recursive: true);
}
publishRoot.childDirectory('flutter').childFile('index.html').createSync();
final Directory widgetsDir = publishRoot
.childDirectory('flutter')
.childDirectory('widgets')
..createSync(recursive: true);
.childDirectory('flutter')
.childDirectory('widgets')..createSync(recursive: true);
widgetsDir.childFile('showGeneralDialog.html').writeAsStringSync('''
<pre id="longSnippet1">
<code class="language-dart">
import &#39;package:flutter&#47;material.dart&#39;;
</code>
</pre>
''',
);
''');
expect(publishRoot.childDirectory('flutter').existsSync(), isTrue);
(widgetsDir
.childDirectory('ModalRoute')
..createSync(recursive: true))
.childFile('barrierColor.html')
.writeAsStringSync('''
(widgetsDir.childDirectory('ModalRoute')
..createSync(recursive: true)).childFile('barrierColor.html').writeAsStringSync('''
<pre id="sample-code">
<code class="language-dart">
class FooClass {
@@ -592,12 +595,13 @@ void main() {
</code>
</pre>
''');
const String queryParams = 'split=1&run=true&sample_id=widgets.Listener.123&channel=main';
const String queryParams =
'split=1&run=true&sample_id=widgets.Listener.123&channel=main';
widgetsDir.childFile('Listener-class.html').writeAsStringSync('''
<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?$queryParams">
</iframe>
''');
}
},
),
]);

View File

@@ -11,25 +11,26 @@ import '../bin/engine_hash.dart' show GitRevisionStrategy, engineHash;
void main() {
test('Produces an engine hash for merge-base', () async {
final Future<io.ProcessResult> Function(List<String>) runProcess =
_fakeProcesses(processes: <FakeProcess>[
(
exe: 'git',
command: 'merge-base',
rest: <String>['upstream/master', 'HEAD'],
exitCode: 0,
stdout: 'abcdef1234',
stderr: null
),
(
exe: 'git',
command: 'ls-tree',
rest: <String>['-r', 'abcdef1234', 'engine', 'DEPS'],
exitCode: 0,
stdout: 'one\r\ntwo\r\n',
stderr: null
),
]);
final Future<io.ProcessResult> Function(List<String>) runProcess = _fakeProcesses(
processes: <FakeProcess>[
(
exe: 'git',
command: 'merge-base',
rest: <String>['upstream/master', 'HEAD'],
exitCode: 0,
stdout: 'abcdef1234',
stderr: null,
),
(
exe: 'git',
command: 'ls-tree',
rest: <String>['-r', 'abcdef1234', 'engine', 'DEPS'],
exitCode: 0,
stdout: 'one\r\ntwo\r\n',
stderr: null,
),
],
);
final Future<String> result = engineHash(runProcess);
@@ -37,8 +38,7 @@ void main() {
});
test('Produces an engine hash for HEAD', () async {
final Future<io.ProcessResult> Function(List<String>) runProcess =
_fakeProcesses(
final Future<io.ProcessResult> Function(List<String>) runProcess = _fakeProcesses(
processes: <FakeProcess>[
(
exe: 'git',
@@ -46,57 +46,54 @@ void main() {
rest: <String>['-r', 'HEAD', 'engine', 'DEPS'],
exitCode: 0,
stdout: 'one\ntwo\n',
stderr: null
stderr: null,
),
],
);
final Future<String> result =
engineHash(runProcess, revisionStrategy: GitRevisionStrategy.head);
final Future<String> result = engineHash(
runProcess,
revisionStrategy: GitRevisionStrategy.head,
);
expect(result, completion('c708d7ef841f7e1748436b8ef5670d0b2de1a227'));
});
test('Returns error in non-monorepo', () async {
final Future<io.ProcessResult> Function(List<String>) runProcess =
_fakeProcesses(processes: <FakeProcess>[
(
exe: 'git',
command: 'ls-tree',
rest: <String>['-r', 'HEAD', 'engine', 'DEPS'],
exitCode: 0,
stdout: '',
stderr: null
),
]);
final Future<io.ProcessResult> Function(List<String>) runProcess = _fakeProcesses(
processes: <FakeProcess>[
(
exe: 'git',
command: 'ls-tree',
rest: <String>['-r', 'HEAD', 'engine', 'DEPS'],
exitCode: 0,
stdout: '',
stderr: null,
),
],
);
final Future<String> result =
engineHash(runProcess, revisionStrategy: GitRevisionStrategy.head);
final Future<String> result = engineHash(
runProcess,
revisionStrategy: GitRevisionStrategy.head,
);
expect(result, throwsA('Not in a monorepo'));
});
}
typedef FakeProcess = ({
String exe,
String command,
List<String> rest,
dynamic stdout,
dynamic stderr,
int exitCode
});
typedef FakeProcess =
({String exe, String command, List<String> rest, dynamic stdout, dynamic stderr, int exitCode});
Future<io.ProcessResult> Function(List<String>) _fakeProcesses({
required List<FakeProcess> processes,
}) =>
(List<String> cmd) async {
for (final FakeProcess process in processes) {
if (process.exe.endsWith(cmd[0]) &&
process.command.endsWith(cmd[1]) &&
process.rest.equals(cmd.sublist(2))) {
return io.ProcessResult(
1, process.exitCode, process.stdout, process.stderr);
}
}
return io.ProcessResult(1, -42, '', '404 command not found: $cmd');
};
}) => (List<String> cmd) async {
for (final FakeProcess process in processes) {
if (process.exe.endsWith(cmd[0]) &&
process.command.endsWith(cmd[1]) &&
process.rest.equals(cmd.sublist(2))) {
return io.ProcessResult(1, process.exitCode, process.stdout, process.stderr);
}
}
return io.ProcessResult(1, -42, '', '404 command not found: $cmd');
};

View File

@@ -75,38 +75,47 @@ void main() {
'format.${io.Platform.isWindows ? 'bat' : 'sh'}',
);
test('Can fix Dart formatting errors', () {
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[
dartContentPair,
], flutterRoot);
try {
fixture.gitAdd();
io.Process.runSync(formatterPath, <String>['--fix'], workingDirectory: flutterRoot.path);
test(
'Can fix Dart formatting errors',
() {
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[
dartContentPair,
], flutterRoot);
try {
fixture.gitAdd();
io.Process.runSync(formatterPath, <String>['--fix'], workingDirectory: flutterRoot.path);
final Iterable<FileContentPair> files = fixture.getFileContents();
for (final FileContentPair pair in files) {
expect(pair.original, equals(pair.formatted));
final Iterable<FileContentPair> files = fixture.getFileContents();
for (final FileContentPair pair in files) {
expect(pair.original, equals(pair.formatted));
}
} finally {
fixture.gitRemove();
}
} finally {
fixture.gitRemove();
}
});
},
// TODO(goderbauer): Re-enable after the formatting changes have landed.
skip: true,
);
test('Prints error if dart formatter fails', () {
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[], flutterRoot);
final io.File dartFile = io.File('${flutterRoot.path}/format_test2.dart');
dartFile.writeAsStringSync('P\n');
fixture.files.add(dartFile);
test(
'Prints error if dart formatter fails',
() {
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[], flutterRoot);
final io.File dartFile = io.File('${flutterRoot.path}/format_test2.dart');
dartFile.writeAsStringSync('P\n');
fixture.files.add(dartFile);
try {
fixture.gitAdd();
final io.ProcessResult result = io.Process.runSync(formatterPath, <String>[
'--fix',
], workingDirectory: flutterRoot.path);
expect(result.stdout, contains('format_test2.dart produced the following error'));
expect(result.exitCode, isNot(0));
} finally {
fixture.gitRemove();
}
});
try {
fixture.gitAdd();
final io.ProcessResult result = io.Process.runSync(formatterPath, <String>[
'--fix',
], workingDirectory: flutterRoot.path);
expect(result.stdout, contains('format_test2.dart produced the following error'));
expect(result.exitCode, isNot(0));
} finally {
fixture.gitRemove();
}
}, // TODO(goderbauer): Re-enable after the formatting changes have landed.
skip: true,
);
}

View File

@@ -6,20 +6,10 @@ import 'package:test/test.dart';
import '../update_icons.dart';
Map<String, String> codepointsA = <String, String>{
'airplane': '111',
'boat': '222',
};
Map<String, String> codepointsB = <String, String>{
'airplane': '333',
};
Map<String, String> codepointsC = <String, String>{
'airplane': '111',
'train': '444',
};
Map<String, String> codepointsUnderscore = <String, String>{
'airplane__123': '111',
};
Map<String, String> codepointsA = <String, String>{'airplane': '111', 'boat': '222'};
Map<String, String> codepointsB = <String, String>{'airplane': '333'};
Map<String, String> codepointsC = <String, String>{'airplane': '111', 'train': '444'};
Map<String, String> codepointsUnderscore = <String, String>{'airplane__123': '111'};
void main() {
group('safety checks', () {
@@ -45,17 +35,11 @@ void main() {
});
test('usage string is correct', () {
expect(
Icon(const MapEntry<String, String>('abc', '')).usage,
'Icon(Icons.abc),',
);
expect(Icon(const MapEntry<String, String>('abc', '')).usage, 'Icon(Icons.abc),');
});
test('usage string is correct with replacement', () {
expect(
Icon(const MapEntry<String, String>('123', '')).usage,
'Icon(Icons.onetwothree),',
);
expect(Icon(const MapEntry<String, String>('123', '')).usage, 'Icon(Icons.onetwothree),');
expect(
Icon(const MapEntry<String, String>('123_rounded', '')).usage,
'Icon(Icons.onetwothree_rounded),',
@@ -64,58 +48,22 @@ void main() {
test('certain icons should be mirrored in RTL', () {
// Exact match
expect(
Icon(const MapEntry<String, String>('help', '')).isMirroredInRTL,
true,
);
expect(Icon(const MapEntry<String, String>('help', '')).isMirroredInRTL, true);
// Variant
expect(
Icon(const MapEntry<String, String>('help_rounded', '')).isMirroredInRTL,
true,
);
expect(Icon(const MapEntry<String, String>('help_rounded', '')).isMirroredInRTL, true);
// Common suffixes
expect(
Icon(const MapEntry<String, String>('help_alt', '')).isMirroredInRTL,
true,
);
expect(
Icon(const MapEntry<String, String>('help_new', '')).isMirroredInRTL,
true,
);
expect(
Icon(const MapEntry<String, String>('help_off', '')).isMirroredInRTL,
true,
);
expect(
Icon(const MapEntry<String, String>('help_on', '')).isMirroredInRTL,
true,
);
expect(Icon(const MapEntry<String, String>('help_alt', '')).isMirroredInRTL, true);
expect(Icon(const MapEntry<String, String>('help_new', '')).isMirroredInRTL, true);
expect(Icon(const MapEntry<String, String>('help_off', '')).isMirroredInRTL, true);
expect(Icon(const MapEntry<String, String>('help_on', '')).isMirroredInRTL, true);
// Common suffixes + variant
expect(
Icon(const MapEntry<String, String>('help_alt_rounded', '')).isMirroredInRTL,
true,
);
expect(
Icon(const MapEntry<String, String>('help_new_rounded', '')).isMirroredInRTL,
true,
);
expect(
Icon(const MapEntry<String, String>('help_off_rounded', '')).isMirroredInRTL,
true,
);
expect(
Icon(const MapEntry<String, String>('help_on_rounded', '')).isMirroredInRTL,
true,
);
expect(Icon(const MapEntry<String, String>('help_alt_rounded', '')).isMirroredInRTL, true);
expect(Icon(const MapEntry<String, String>('help_new_rounded', '')).isMirroredInRTL, true);
expect(Icon(const MapEntry<String, String>('help_off_rounded', '')).isMirroredInRTL, true);
expect(Icon(const MapEntry<String, String>('help_on_rounded', '')).isMirroredInRTL, true);
// No match
expect(
Icon(const MapEntry<String, String>('help_center_rounded', '')).isMirroredInRTL,
false,
);
expect(Icon(const MapEntry<String, String>('help_center_rounded', '')).isMirroredInRTL, false);
// No match
expect(
Icon(const MapEntry<String, String>('arrow', '')).isMirroredInRTL,
false,
);
expect(Icon(const MapEntry<String, String>('arrow', '')).isMirroredInRTL, false);
});
}

View File

@@ -29,11 +29,7 @@ const String _defaultIconsPath = 'packages/flutter/lib/src/material/icons.dart';
const String _defaultNewCodepointsPath = 'codepoints';
const String _defaultOldCodepointsPath = 'bin/cache/artifacts/material_fonts/codepoints';
const String _defaultFontFamily = 'MaterialIcons';
const List<String> _defaultPossibleStyleSuffixes = <String>[
'_outlined',
'_rounded',
'_sharp',
];
const List<String> _defaultPossibleStyleSuffixes = <String>['_outlined', '_rounded', '_sharp'];
const String _defaultClassName = 'Icons';
const String _defaultDemoFilePath = '/tmp/new_icons_demo.dart';
@@ -181,12 +177,16 @@ void main(List<String> args) {
final ArgResults argResults = _handleArguments(args);
final File iconsFile = File(path.normalize(path.absolute(argResults[_iconsPathOption] as String)));
final File iconsFile = File(
path.normalize(path.absolute(argResults[_iconsPathOption] as String)),
);
if (!iconsFile.existsSync()) {
stderr.writeln('Error: Icons file not found: ${iconsFile.path}');
exit(1);
}
final File iconsTemplateFile = File(path.normalize(path.absolute(argResults[_iconsTemplatePathOption] as String)));
final File iconsTemplateFile = File(
path.normalize(path.absolute(argResults[_iconsTemplatePathOption] as String)),
);
if (!iconsTemplateFile.existsSync()) {
stderr.writeln('Error: Icons template file not found: ${iconsTemplateFile.path}');
exit(1);
@@ -216,7 +216,9 @@ void main(List<String> args) {
}
final String iconsTemplateContents = iconsTemplateFile.readAsStringSync();
stderr.writeln("Generating icons ${argResults[_dryRunOption] as bool ? '' : 'to ${iconsFile.path}'}");
stderr.writeln(
"Generating icons ${argResults[_dryRunOption] as bool ? '' : 'to ${iconsFile.path}'}",
);
final String newIconsContents = _regenerateIconsFile(
iconsTemplateContents,
newTokenPairMap,
@@ -230,56 +232,83 @@ void main(List<String> args) {
} else {
iconsFile.writeAsStringSync(newIconsContents);
final SplayTreeMap<String, String> sortedNewTokenPairMap = SplayTreeMap<String, String>.of(newTokenPairMap);
final SplayTreeMap<String, String> sortedNewTokenPairMap = SplayTreeMap<String, String>.of(
newTokenPairMap,
);
_regenerateCodepointsFile(oldCodepointsFile, sortedNewTokenPairMap);
sortedNewTokenPairMap.removeWhere((String key, String value) => oldTokenPairMap.containsKey(key));
sortedNewTokenPairMap.removeWhere(
(String key, String value) => oldTokenPairMap.containsKey(key),
);
_generateIconDemo(File(_defaultDemoFilePath), sortedNewTokenPairMap);
}
}
ArgResults _handleArguments(List<String> args) {
final ArgParser argParser = ArgParser()
..addOption(_iconsPathOption,
defaultsTo: _defaultIconsPath,
help: 'Location of the material icons file')
..addOption(_iconsTemplatePathOption,
defaultsTo: _defaultIconsPath,
help:
'Location of the material icons file template. Usually the same as --$_iconsPathOption')
..addOption(_newCodepointsPathOption,
defaultsTo: _defaultNewCodepointsPath,
help: 'Location of the new codepoints directory')
..addOption(_oldCodepointsPathOption,
defaultsTo: _defaultOldCodepointsPath,
help: 'Location of the existing codepoints directory')
..addOption(_fontFamilyOption,
defaultsTo: _defaultFontFamily,
help: 'The font family to use for the IconData constants')
..addMultiOption(_possibleStyleSuffixesOption,
defaultsTo: _defaultPossibleStyleSuffixes,
help: 'A comma-separated list of suffixes (typically an optional '
'family + a style) e.g. _outlined, _monoline_filled')
..addOption(_classNameOption,
defaultsTo: _defaultClassName,
help: 'The containing class for all icons')
..addFlag(_enforceSafetyChecks,
defaultsTo: true,
help: 'Whether to exit if safety checks fail (e.g. codepoints are missing or unstable')
..addFlag(_dryRunOption);
argParser.addFlag('help', abbr: 'h', negatable: false, callback: (bool help) {
if (help) {
print(argParser.usage);
exit(1);
}
});
final ArgParser argParser =
ArgParser()
..addOption(
_iconsPathOption,
defaultsTo: _defaultIconsPath,
help: 'Location of the material icons file',
)
..addOption(
_iconsTemplatePathOption,
defaultsTo: _defaultIconsPath,
help:
'Location of the material icons file template. Usually the same as --$_iconsPathOption',
)
..addOption(
_newCodepointsPathOption,
defaultsTo: _defaultNewCodepointsPath,
help: 'Location of the new codepoints directory',
)
..addOption(
_oldCodepointsPathOption,
defaultsTo: _defaultOldCodepointsPath,
help: 'Location of the existing codepoints directory',
)
..addOption(
_fontFamilyOption,
defaultsTo: _defaultFontFamily,
help: 'The font family to use for the IconData constants',
)
..addMultiOption(
_possibleStyleSuffixesOption,
defaultsTo: _defaultPossibleStyleSuffixes,
help:
'A comma-separated list of suffixes (typically an optional '
'family + a style) e.g. _outlined, _monoline_filled',
)
..addOption(
_classNameOption,
defaultsTo: _defaultClassName,
help: 'The containing class for all icons',
)
..addFlag(
_enforceSafetyChecks,
defaultsTo: true,
help: 'Whether to exit if safety checks fail (e.g. codepoints are missing or unstable',
)
..addFlag(_dryRunOption);
argParser.addFlag(
'help',
abbr: 'h',
negatable: false,
callback: (bool help) {
if (help) {
print(argParser.usage);
exit(1);
}
},
);
return argParser.parse(args);
}
Map<String, String> stringToTokenPairMap(String codepointData) {
final Iterable<String> cleanData = LineSplitter.split(codepointData)
.map((String line) => line.trim())
.where((String line) => line.isNotEmpty);
final Iterable<String> cleanData = LineSplitter.split(
codepointData,
).map((String line) => line.trim()).where((String line) => line.isNotEmpty);
final Map<String, String> pairs = <String, String>{};
@@ -295,16 +324,19 @@ Map<String, String> stringToTokenPairMap(String codepointData) {
}
String _regenerateIconsFile(
String templateFileContents,
Map<String, String> tokenPairMap,
String fontFamily,
String className,
bool enforceSafetyChecks,
) {
final List<Icon> newIcons = tokenPairMap.entries
.map((MapEntry<String, String> entry) =>
Icon(entry, fontFamily: fontFamily, className: className))
.toList();
String templateFileContents,
Map<String, String> tokenPairMap,
String fontFamily,
String className,
bool enforceSafetyChecks,
) {
final List<Icon> newIcons =
tokenPairMap.entries
.map(
(MapEntry<String, String> entry) =>
Icon(entry, fontFamily: fontFamily, className: className),
)
.toList();
newIcons.sort((Icon a, Icon b) => a._compareTo(b));
final StringBuffer buf = StringBuffer();
@@ -324,11 +356,13 @@ String _regenerateIconsFile(
for (final String style in <String>['', '_outlined', '_rounded', '_sharp']) {
try {
final Icon agnosticIcon = newIcons.firstWhere(
(Icon icon) => icon.id == '${ids[0]}$style',
orElse: () => throw ids[0]);
(Icon icon) => icon.id == '${ids[0]}$style',
orElse: () => throw ids[0],
);
final Icon iOSIcon = newIcons.firstWhere(
(Icon icon) => icon.id == '${ids[1]}$style',
orElse: () => throw ids[1]);
(Icon icon) => icon.id == '${ids[1]}$style',
orElse: () => throw ids[1],
);
platformAdaptiveDeclarations.add(
agnosticIcon.platformAdaptiveDeclaration('$flutterId$style', iOSIcon),
);
@@ -355,7 +389,8 @@ String _regenerateIconsFile(
// Generate for Icons
if (line.contains(_beginGeneratedMark)) {
generating = true;
final String iconDeclarationsString = newIcons.map((Icon icon) => icon.fullDeclaration).join();
final String iconDeclarationsString =
newIcons.map((Icon icon) => icon.fullDeclaration).join();
buf.write(iconDeclarationsString);
} else if (line.contains(_endGeneratedMark)) {
generating = false;
@@ -376,11 +411,14 @@ bool testIsSuperset(Map<String, String> newCodepoints, Map<String, String> oldCo
}
if (!newCodepointsSet.containsAll(oldCodepointsSet)) {
stderr.writeln(
'❌ new codepoints file does not contain all ${oldCodepointsSet.length} '
'existing codepoints. Missing: ${oldCodepointsSet.difference(newCodepointsSet)}');
'❌ new codepoints file does not contain all ${oldCodepointsSet.length} '
'existing codepoints. Missing: ${oldCodepointsSet.difference(newCodepointsSet)}',
);
return false;
} else {
stderr.writeln('✅ new codepoints file contains all ${oldCodepointsSet.length} existing codepoints');
stderr.writeln(
'✅ new codepoints file contains all ${oldCodepointsSet.length} existing codepoints',
);
}
return true;
}
@@ -394,7 +432,9 @@ bool testIsStable(Map<String, String> newCodepoints, Map<String, String> oldCode
];
if (unstable.isNotEmpty) {
stderr.writeln('❌ out of $oldCodepointsCount existing codepoints, ${unstable.length} were unstable: $unstable');
stderr.writeln(
'❌ out of $oldCodepointsCount existing codepoints, ${unstable.length} were unstable: $unstable',
);
return false;
} else {
stderr.writeln('✅ all existing $oldCodepointsCount codepoints are stable');
@@ -449,7 +489,8 @@ void _generateIconDemo(File demoFilePath, Map<String, String> tokenPairMap) {
class Icon {
// Parse tokenPair (e.g. {"6_ft_apart_outlined": "e004"}).
Icon(MapEntry<String, String> tokenPair, {
Icon(
MapEntry<String, String> tokenPair, {
this.fontFamily = _defaultFontFamily,
this.possibleStyleSuffixes = _defaultPossibleStyleSuffixes,
this.className = _defaultClassName,
@@ -488,7 +529,6 @@ class Icon {
_generateFlutterId();
}
late String id; // e.g. 5g, 5g_outlined, 5g_rounded, 5g_sharp
late String shortId; // e.g. 5g
late String flutterId; // e.g. five_g, five_g_outlined, five_g_rounded, five_g_sharp
@@ -496,12 +536,14 @@ class Icon {
late String dartdocFamily; // e.g. material
late String dartdocHtmlSuffix = ''; // The suffix for the 'material-icons' HTML class.
String fontFamily; // The IconData font family.
List<String> possibleStyleSuffixes; // A list of possible suffixes e.g. _outlined, _monoline_filled.
List<String>
possibleStyleSuffixes; // A list of possible suffixes e.g. _outlined, _monoline_filled.
String className; // The containing class.
String get name => shortId.replaceAll('_', ' ').trim();
String get style => dartdocHtmlSuffix == '' ? '' : ' (${dartdocHtmlSuffix.replaceFirst('-', '')})';
String get style =>
dartdocHtmlSuffix == '' ? '' : ' (${dartdocHtmlSuffix.replaceFirst('-', '')})';
String get dartDoc =>
'<i class="material-icons$dartdocHtmlSuffix md-36">$shortId</i> &#x2014; $dartdocFamily icon named "$name"$style';
@@ -511,7 +553,9 @@ class Icon {
bool get isMirroredInRTL {
// Remove common suffixes (e.g. "_new" or "_alt") from the shortId.
final String normalizedShortId = shortId.replaceAll(RegExp(r'_(new|alt|off|on)$'), '');
return _iconsMirroredWhenRTL.any((String shortIdMirroredWhenRTL) => normalizedShortId == shortIdMirroredWhenRTL);
return _iconsMirroredWhenRTL.any(
(String shortIdMirroredWhenRTL) => normalizedShortId == shortIdMirroredWhenRTL,
);
}
String get declaration =>
@@ -562,19 +606,13 @@ class Icon {
// Exact identifier rewrites.
for (final MapEntry<String, String> rewritePair in _identifierExactRewrites.entries) {
if (shortId == rewritePair.key) {
flutterId = id.replaceFirst(
rewritePair.key,
_identifierExactRewrites[rewritePair.key]!,
);
flutterId = id.replaceFirst(rewritePair.key, _identifierExactRewrites[rewritePair.key]!);
}
}
// Prefix identifier rewrites.
for (final MapEntry<String, String> rewritePair in _identifierPrefixRewrites.entries) {
if (id.startsWith(rewritePair.key)) {
flutterId = id.replaceFirst(
rewritePair.key,
_identifierPrefixRewrites[rewritePair.key]!,
);
flutterId = id.replaceFirst(rewritePair.key, _identifierPrefixRewrites[rewritePair.key]!);
}
}

View File

@@ -8,58 +8,48 @@ import 'package:args/args.dart';
import 'package:vitool/vitool.dart';
const String kCodegenComment =
'// AUTOGENERATED FILE DO NOT EDIT!\n'
'// This file was generated by vitool.\n';
'// AUTOGENERATED FILE DO NOT EDIT!\n'
'// This file was generated by vitool.\n';
void main(List<String> args) {
final ArgParser parser = ArgParser();
parser.addFlag(
'help',
abbr: 'h',
negatable: false,
help: "Display the tool's usage instructions and quit.",
'help',
abbr: 'h',
negatable: false,
help: "Display the tool's usage instructions and quit.",
);
parser.addOption(
'output',
abbr: 'o',
help: 'Target path to write the generated Dart file to.',
);
parser.addOption('output', abbr: 'o', help: 'Target path to write the generated Dart file to.');
parser.addOption('asset-name', abbr: 'n', help: 'Name to be used for the generated constant.');
parser.addOption('part-of', abbr: 'p', help: "Library name to add a dart 'part of' clause for.");
parser.addOption(
'asset-name',
abbr: 'n',
help: 'Name to be used for the generated constant.',
);
parser.addOption(
'part-of',
abbr: 'p',
help: "Library name to add a dart 'part of' clause for.",
);
parser.addOption(
'header',
abbr: 'd',
help: 'File whose contents are to be prepended to the beginning of '
'the generated Dart file; this can be used for a license comment.',
'header',
abbr: 'd',
help:
'File whose contents are to be prepended to the beginning of '
'the generated Dart file; this can be used for a license comment.',
);
parser.addFlag(
'codegen_comment',
abbr: 'c',
defaultsTo: true,
help: 'Whether to include the following comment after the header:\n'
'$kCodegenComment',
'codegen_comment',
abbr: 'c',
defaultsTo: true,
help:
'Whether to include the following comment after the header:\n'
'$kCodegenComment',
);
final ArgResults argResults = parser.parse(args);
if (argResults['help'] as bool ||
!argResults.wasParsed('output') ||
!argResults.wasParsed('asset-name') ||
argResults.rest.isEmpty) {
!argResults.wasParsed('output') ||
!argResults.wasParsed('asset-name') ||
argResults.rest.isEmpty) {
printUsage(parser);
return;
}
@@ -80,7 +70,9 @@ void main(List<String> args) {
}
if (argResults.wasParsed('part-of')) {
generatedSb.write('part of ${argResults['part-of']}; // ignore: use_string_in_part_of_directives\n');
generatedSb.write(
'part of ${argResults['part-of']}; // ignore: use_string_in_part_of_directives\n',
);
}
final Animation animation = Animation.fromFrameData(frames);
@@ -92,6 +84,8 @@ void main(List<String> args) {
void printUsage(ArgParser parser) {
print('Usage: vitool --asset-name=<asset_name> --output=<output_path> <frames_list>');
print('\nExample: vitool --asset-name=_\$menu_arrow --output=lib/data/menu_arrow.g.dart assets/svg/menu_arrow/*.svg\n');
print(
'\nExample: vitool --asset-name=_\$menu_arrow --output=lib/data/menu_arrow.g.dart assets/svg/menu_arrow/*.svg\n',
);
print(parser.usage);
}

View File

@@ -40,16 +40,16 @@ class Animation {
final FrameData frame = frames[i];
if (size != frame.size) {
throw Exception(
'All animation frames must have the same size,\n'
'first frame size was: (${size.x}, ${size.y})\n'
'frame $i size was: (${frame.size.x}, ${frame.size.y})'
'All animation frames must have the same size,\n'
'first frame size was: (${size.x}, ${size.y})\n'
'frame $i size was: (${frame.size.x}, ${frame.size.y})',
);
}
if (numPaths != frame.paths.length) {
throw Exception(
'All animation frames must have the same number of paths,\n'
'first frame has $numPaths paths\n'
'frame $i has ${frame.paths.length} paths'
'All animation frames must have the same number of paths,\n'
'first frame has $numPaths paths\n'
'frame $i has ${frame.paths.length} paths',
);
}
}
@@ -79,18 +79,25 @@ class PathAnimation {
}
final List<PathCommandAnimation> commands = <PathCommandAnimation>[];
for (int commandIdx = 0; commandIdx < frames[0].paths[pathIdx].commands.length; commandIdx += 1) {
for (
int commandIdx = 0;
commandIdx < frames[0].paths[pathIdx].commands.length;
commandIdx += 1
) {
final int numPointsInCommand = frames[0].paths[pathIdx].commands[commandIdx].points.length;
final List<List<Point<double>>> points = List<List<Point<double>>>.filled(numPointsInCommand, <Point<double>>[]);
final List<List<Point<double>>> points = List<List<Point<double>>>.filled(
numPointsInCommand,
<Point<double>>[],
);
final String commandType = frames[0].paths[pathIdx].commands[commandIdx].type;
for (int i = 0; i < frames.length; i += 1) {
final FrameData frame = frames[i];
final String currentCommandType = frame.paths[pathIdx].commands[commandIdx].type;
if (commandType != currentCommandType) {
throw Exception(
'Paths must be built from the same commands in all frames '
"command $commandIdx at frame 0 was of type '$commandType' "
"command $commandIdx at frame $i was of type '$currentCommandType'"
'Paths must be built from the same commands in all frames '
"command $commandIdx at frame 0 was of type '$commandType' "
"command $commandIdx at frame $i was of type '$currentCommandType'",
);
}
for (int j = 0; j < numPointsInCommand; j += 1) {
@@ -101,13 +108,14 @@ class PathAnimation {
}
final List<double> opacities =
frames.map<double>((FrameData d) => d.paths[pathIdx].opacity).toList();
frames.map<double>((FrameData d) => d.paths[pathIdx].opacity).toList();
return PathAnimation(commands, opacities: opacities);
}
/// List of commands for drawing the path.
final List<PathCommandAnimation> commands;
/// The path opacity for each animation frame.
final List<double> opacities;
@@ -190,8 +198,7 @@ FrameData interpretSvg(String svgFilePath) {
final double width = parsePixels(_extractAttr(svgElement, 'width')).toDouble();
final double height = parsePixels(_extractAttr(svgElement, 'height')).toDouble();
final List<SvgPath> paths =
_interpretSvgGroup(svgElement.children, _Transform());
final List<SvgPath> paths = _interpretSvgGroup(svgElement.children, _Transform());
return FrameData(Point<double>(width, height), paths);
}
@@ -216,7 +223,8 @@ List<SvgPath> _interpretSvgGroup(List<XmlNode> children, _Transform transform) {
Matrix3 transformMatrix = transform.transformMatrix;
if (_hasAttr(element, 'transform')) {
transformMatrix = transformMatrix.multiplied(
_parseSvgTransform(_extractAttr(element, 'transform')));
_parseSvgTransform(_extractAttr(element, 'transform')),
);
}
final _Transform subtreeTransform = _Transform(
@@ -249,10 +257,7 @@ List<Point<double>> parsePoints(String points) {
final List<Point<double>> result = <Point<double>>[];
while (unParsed.isNotEmpty && _pointMatcher.hasMatch(unParsed)) {
final Match m = _pointMatcher.firstMatch(unParsed)!;
result.add(Point<double>(
double.parse(m.group(1)!),
double.parse(m.group(2)!),
));
result.add(Point<double>(double.parse(m.group(1)!), double.parse(m.group(2)!)));
unParsed = m.group(3)!;
}
return result;
@@ -271,9 +276,9 @@ class FrameData {
if (other.runtimeType != runtimeType) {
return false;
}
return other is FrameData
&& other.size == size
&& const ListEquality<SvgPath>().equals(other.paths, paths);
return other is FrameData &&
other.size == size &&
const ListEquality<SvgPath>().equals(other.paths, paths);
}
@override
@@ -317,7 +322,7 @@ class SvgPath {
SvgPath _applyTransform(_Transform transform) {
final List<SvgPathCommand> transformedCommands =
commands.map<SvgPathCommand>((SvgPathCommand c) => c._applyTransform(transform)).toList();
commands.map<SvgPathCommand>((SvgPathCommand c) => c._applyTransform(transform)).toList();
return SvgPath(id, transformedCommands, opacity: opacity * transform.opacity);
}
@@ -326,10 +331,10 @@ class SvgPath {
if (other.runtimeType != runtimeType) {
return false;
}
return other is SvgPath
&& other.id == id
&& other.opacity == opacity
&& const ListEquality<SvgPathCommand>().equals(other.commands, commands);
return other is SvgPath &&
other.id == id &&
other.opacity == opacity &&
const ListEquality<SvgPathCommand>().equals(other.commands, commands);
}
@override
@@ -339,7 +344,6 @@ class SvgPath {
String toString() {
return 'SvgPath(id: $id, opacity: $opacity, commands: $commands)';
}
}
/// Represents a single SVG path command from an SVG d element.
@@ -363,11 +367,8 @@ class SvgPathCommand {
final List<Point<double>> points;
SvgPathCommand _applyTransform(_Transform transform) {
final List<Point<double>> transformedPoints =
_vector3ArrayToPoints(
transform.transformMatrix.applyToVector3Array(
_pointsToVector3Array(points)
)
final List<Point<double>> transformedPoints = _vector3ArrayToPoints(
transform.transformMatrix.applyToVector3Array(_pointsToVector3Array(points)),
);
return SvgPathCommand(type, transformedPoints);
}
@@ -377,9 +378,9 @@ class SvgPathCommand {
if (other.runtimeType != runtimeType) {
return false;
}
return other is SvgPathCommand
&& other.type == type
&& const ListEquality<Point<double>>().equals(other.points, points);
return other is SvgPathCommand &&
other.type == type &&
const ListEquality<Point<double>>().equals(other.points, points);
}
@override
@@ -392,7 +393,7 @@ class SvgPathCommand {
}
class SvgPathCommandBuilder {
static const Map<String, void> kRelativeCommands = <String, void> {
static const Map<String, void> kRelativeCommands = <String, void>{
'c': null,
'l': null,
'm': null,
@@ -440,8 +441,7 @@ List<double> _pointsToVector3Array(List<Point<double>> points) {
List<Point<double>> _vector3ArrayToPoints(List<double> vector) {
final int numPoints = (vector.length / 3).floor();
final List<Point<double>> points = <Point<double>>[
for (int i = 0; i < numPoints; i += 1)
Point<double>(vector[i*3], vector[i*3 + 1]),
for (int i = 0; i < numPoints; i += 1) Point<double>(vector[i * 3], vector[i * 3 + 1]),
];
return points;
}
@@ -451,23 +451,21 @@ List<Point<double>> _vector3ArrayToPoints(List<double> vector) {
/// This includes more transforms than the ones described by the SVG transform
/// attribute, e.g opacity.
class _Transform {
/// Constructs a new _Transform, default arguments create a no-op transform.
_Transform({Matrix3? transformMatrix, this.opacity = 1.0}) :
transformMatrix = transformMatrix ?? Matrix3.identity();
_Transform({Matrix3? transformMatrix, this.opacity = 1.0})
: transformMatrix = transformMatrix ?? Matrix3.identity();
final Matrix3 transformMatrix;
final double opacity;
_Transform applyTransform(_Transform transform) {
return _Transform(
transformMatrix: transform.transformMatrix.multiplied(transformMatrix),
opacity: transform.opacity * opacity,
transformMatrix: transform.transformMatrix.multiplied(transformMatrix),
opacity: transform.opacity * opacity,
);
}
}
const String _transformCommandAtom = r' *([^(]+)\(([^)]*)\)';
final RegExp _transformValidator = RegExp('^($_transformCommandAtom)*\$');
final RegExp _transformCommand = RegExp(_transformCommandAtom);
@@ -476,7 +474,7 @@ Matrix3 _parseSvgTransform(String transform) {
if (!_transformValidator.hasMatch(transform)) {
throw Exception('illegal or unsupported transform: $transform');
}
final Iterable<Match> matches =_transformCommand.allMatches(transform).toList().reversed;
final Iterable<Match> matches = _transformCommand.allMatches(transform).toList().reversed;
Matrix3 result = Matrix3.identity();
for (final Match m in matches) {
final String command = m.group(1)!;
@@ -539,19 +537,19 @@ int parsePixels(String pixels) {
if (!_pixelsExp.hasMatch(pixels)) {
throw ArgumentError(
"illegal pixels expression: '$pixels'"
' (the tool currently only support pixel units).');
' (the tool currently only support pixel units).',
);
}
return int.parse(_pixelsExp.firstMatch(pixels)!.group(1)!);
}
String _extractAttr(XmlElement element, String name) {
try {
return element.attributes.singleWhere((XmlAttribute x) => x.name.local == name)
.value;
return element.attributes.singleWhere((XmlAttribute x) => x.name.local == name).value;
} catch (e) {
throw ArgumentError(
"Can't find a single '$name' attributes in ${element.name}, "
'attributes were: ${element.attributes}'
"Can't find a single '$name' attributes in ${element.name}, "
'attributes were: ${element.attributes}',
);
}
}
@@ -562,9 +560,10 @@ bool _hasAttr(XmlElement element, String name) {
XmlElement _extractSvgElement(XmlDocument document) {
return document.children.singleWhere(
(XmlNode node) => node.nodeType == XmlNodeType.ELEMENT &&
_asElement(node).name.local == 'svg'
) as XmlElement;
(XmlNode node) =>
node.nodeType == XmlNodeType.ELEMENT && _asElement(node).name.local == 'svg',
)
as XmlElement;
}
XmlElement _asElement(XmlNode node) => node as XmlElement;

View File

@@ -11,29 +11,24 @@ import 'package:path/path.dart' as path;
import 'package:vitool/vitool.dart';
void main() {
test('parsePixels', () {
expect(parsePixels('23px'), 23);
expect(parsePixels('9px'), 9);
expect(() { parsePixels('9pt'); }, throwsArgumentError);
expect(() {
parsePixels('9pt');
}, throwsArgumentError);
});
test('parsePoints', () {
expect(parsePoints('1.0, 2.0'),
const <Point<double>>[Point<double>(1.0, 2.0)],
);
expect(parsePoints('12.0, 34.0 5.0, 6.6'),
const <Point<double>>[
Point<double>(12.0, 34.0),
Point<double>(5.0, 6.6),
],
);
expect(parsePoints('12.0 34.0 5.0 6.6'),
const <Point<double>>[
Point<double>(12.0, 34.0),
Point<double>(5.0, 6.6),
],
);
expect(parsePoints('1.0, 2.0'), const <Point<double>>[Point<double>(1.0, 2.0)]);
expect(parsePoints('12.0, 34.0 5.0, 6.6'), const <Point<double>>[
Point<double>(12.0, 34.0),
Point<double>(5.0, 6.6),
]);
expect(parsePoints('12.0 34.0 5.0 6.6'), const <Point<double>>[
Point<double>(12.0, 34.0),
Point<double>(5.0, 6.6),
]);
});
group('parseSvg', () {
@@ -43,21 +38,20 @@ void main() {
});
test('illegal SVGs', () {
expect(
() { interpretSvg(testAsset('illegal_svg_multiple_roots.svg')); },
throwsA(anything),
);
expect(() {
interpretSvg(testAsset('illegal_svg_multiple_roots.svg'));
}, throwsA(anything));
});
test('SVG size', () {
expect(
interpretSvg(testAsset('empty_svg_1_48x48.svg')).size,
const Point<double>(48.0, 48.0),
interpretSvg(testAsset('empty_svg_1_48x48.svg')).size,
const Point<double>(48.0, 48.0),
);
expect(
interpretSvg(testAsset('empty_svg_2_100x50.svg')).size,
const Point<double>(100.0, 50.0),
interpretSvg(testAsset('empty_svg_2_100x50.svg')).size,
const Point<double>(100.0, 50.0),
);
});
@@ -79,13 +73,11 @@ void main() {
});
test('SVG illegal path', () {
expect(
() { interpretSvg(testAsset('illegal_path.svg')); },
throwsA(anything),
);
expect(() {
interpretSvg(testAsset('illegal_path.svg'));
}, throwsA(anything));
});
test('SVG group', () {
final FrameData frameData = interpretSvg(testAsset('bars_group.svg'));
expect(frameData.paths, const <SvgPath>[
@@ -122,8 +114,7 @@ void main() {
test('SVG group scale', () {
final FrameData frameData = interpretSvg(testAsset('bar_group_scale.svg'));
expect(frameData.paths, const <SvgPath>[
SvgPath(
'path_1', <SvgPathCommand>[
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 9.5)]),
SvgPathCommand('L', <Point<double>>[Point<double>(24.0, 9.5)]),
SvgPathCommand('L', <Point<double>>[Point<double>(24.0, 14.5)]),
@@ -137,40 +128,34 @@ void main() {
final FrameData frameData = interpretSvg(testAsset('bar_group_rotate_scale.svg'));
expect(frameData.paths, const <PathMatcher>[
PathMatcher(
SvgPath(
'path_1', <SvgPathCommand>[
SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 0.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 48.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(19.0, 48.0)]),
SvgPathCommand('M', <Point<double>>[Point<double>(19.0, 0.0)]),
SvgPathCommand('Z', <Point<double>>[]),
]),
margin: precisionErrorTolerance,
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 0.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 48.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(19.0, 48.0)]),
SvgPathCommand('M', <Point<double>>[Point<double>(19.0, 0.0)]),
SvgPathCommand('Z', <Point<double>>[]),
]),
margin: precisionErrorTolerance,
),
]);
});
test('SVG illegal transform', () {
expect(
() { interpretSvg(testAsset('illegal_transform.svg')); },
throwsA(anything),
);
expect(() {
interpretSvg(testAsset('illegal_transform.svg'));
}, throwsA(anything));
});
test('SVG group opacity', () {
final FrameData frameData = interpretSvg(testAsset('bar_group_opacity.svg'));
expect(frameData.paths, const <SvgPath>[
SvgPath(
'path_1',
<SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 29.0)]),
SvgPathCommand('Z', <Point<double>>[]),
],
opacity: 0.5,
),
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 29.0)]),
SvgPathCommand('Z', <Point<double>>[]),
], opacity: 0.5),
]);
});
@@ -178,8 +163,7 @@ void main() {
// This asset uses the relative 'l' command instead of 'L'.
final FrameData frameData = interpretSvg(testAsset('horizontal_bar_relative.svg'));
expect(frameData.paths, const <SvgPath>[
SvgPath(
'path_1', <SvgPathCommand>[
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]),
@@ -193,8 +177,7 @@ void main() {
// This asset uses the relative 'l' command instead of 'L'.
final FrameData frameData = interpretSvg(testAsset('close_path_in_middle.svg'));
expect(frameData.paths, const <SvgPath>[
SvgPath(
'path_1', <SvgPathCommand>[
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(50.0, 50.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(60.0, 50.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(60.0, 60.0)]),
@@ -210,115 +193,96 @@ void main() {
group('create PathAnimation', () {
test('single path', () {
const List<FrameData> frameData = <FrameData>[
FrameData(
Point<double>(10.0, 10.0),
<SvgPath>[
SvgPath(
'path_1',
<SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(10.0, 10.0)]),
],
),
],
),
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
SvgPathCommand('L', <Point<double>>[Point<double>(10.0, 10.0)]),
]),
]),
];
expect(PathAnimation.fromFrameData(frameData, 0),
const PathAnimationMatcher(PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0)],
]),
PathCommandAnimation('L', <List<Point<double>>>[
<Point<double>>[Point<double>(10.0, 10.0)],
]),
],
opacities: <double>[1.0],
)),
expect(
PathAnimation.fromFrameData(frameData, 0),
const PathAnimationMatcher(
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0)],
]),
PathCommandAnimation('L', <List<Point<double>>>[
<Point<double>>[Point<double>(10.0, 10.0)],
]),
],
opacities: <double>[1.0],
),
),
);
});
test('multiple paths', () {
const List<FrameData> frameData = <FrameData>[
FrameData(
Point<double>(10.0, 10.0),
<SvgPath>[
SvgPath(
'path_1',
<SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
],
),
SvgPath(
'path_2',
<SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]),
],
),
],
),
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
]),
SvgPath('path_2', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]),
]),
]),
];
expect(PathAnimation.fromFrameData(frameData, 0),
const PathAnimationMatcher(PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0)],
]),
],
opacities: <double>[1.0],
)),
expect(
PathAnimation.fromFrameData(frameData, 0),
const PathAnimationMatcher(
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0)],
]),
],
opacities: <double>[1.0],
),
),
);
expect(PathAnimation.fromFrameData(frameData, 1),
const PathAnimationMatcher(PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(5.0, 6.0)],
]),
],
opacities: <double>[1.0],
)),
expect(
PathAnimation.fromFrameData(frameData, 1),
const PathAnimationMatcher(
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(5.0, 6.0)],
]),
],
opacities: <double>[1.0],
),
),
);
});
test('multiple frames', () {
const List<FrameData> frameData = <FrameData>[
FrameData(
Point<double>(10.0, 10.0),
<SvgPath>[
SvgPath(
'path_1',
<SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
],
opacity: 0.5,
),
],
),
FrameData(
Point<double>(10.0, 10.0),
<SvgPath>[
SvgPath(
'path_1',
<SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(10.0, 10.0)]),
],
),
],
),
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
], opacity: 0.5),
]),
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(10.0, 10.0)]),
]),
]),
];
expect(PathAnimation.fromFrameData(frameData, 0),
const PathAnimationMatcher(PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[
Point<double>(0.0, 0.0),
Point<double>(10.0, 10.0),
],
]),
],
opacities: <double>[0.5, 1.0],
)),
expect(
PathAnimation.fromFrameData(frameData, 0),
const PathAnimationMatcher(
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0), Point<double>(10.0, 10.0)],
]),
],
opacities: <double>[0.5, 1.0],
),
),
);
});
});
@@ -326,45 +290,42 @@ void main() {
group('create Animation', () {
test('multiple paths', () {
const List<FrameData> frameData = <FrameData>[
FrameData(
Point<double>(10.0, 10.0),
<SvgPath>[
SvgPath(
'path_1',
<SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
],
),
SvgPath(
'path_1',
<SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]),
],
),
],
),
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
]),
SvgPath('path_1', <SvgPathCommand>[
SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]),
]),
]),
];
final Animation animation = Animation.fromFrameData(frameData);
expect(animation.paths[0],
const PathAnimationMatcher(PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0)],
]),
],
opacities: <double>[1.0],
)),
expect(
animation.paths[0],
const PathAnimationMatcher(
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0)],
]),
],
opacities: <double>[1.0],
),
),
);
expect(animation.paths[1],
const PathAnimationMatcher(PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(5.0, 6.0)],
]),
],
opacities: <double>[1.0],
)),
expect(
animation.paths[1],
const PathAnimationMatcher(
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(5.0, 6.0)],
]),
],
opacities: <double>[1.0],
),
),
);
expect(animation.size, const Point<double>(10.0, 10.0));
@@ -373,230 +334,184 @@ void main() {
group('toDart', () {
test('_PathMoveTo', () {
const PathCommandAnimation command = PathCommandAnimation(
'M',
<List<Point<double>>>[
<Point<double>>[
Point<double>(1.0, 2.0),
Point<double>(3.0, 4.0),
],
],
);
expect(command.toDart(),
' const _PathMoveTo(\n'
' const <Offset>[\n'
' const Offset(1.0, 2.0),\n'
' const Offset(3.0, 4.0),\n'
' ],\n'
' ),\n',
const PathCommandAnimation command = PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(1.0, 2.0), Point<double>(3.0, 4.0)],
]);
expect(
command.toDart(),
' const _PathMoveTo(\n'
' const <Offset>[\n'
' const Offset(1.0, 2.0),\n'
' const Offset(3.0, 4.0),\n'
' ],\n'
' ),\n',
);
});
test('_PathLineTo', () {
const PathCommandAnimation command = PathCommandAnimation(
'L',
<List<Point<double>>>[
<Point<double>>[
Point<double>(1.0, 2.0),
Point<double>(3.0, 4.0),
],
],
);
expect(command.toDart(),
' const _PathLineTo(\n'
' const <Offset>[\n'
' const Offset(1.0, 2.0),\n'
' const Offset(3.0, 4.0),\n'
' ],\n'
' ),\n',
const PathCommandAnimation command = PathCommandAnimation('L', <List<Point<double>>>[
<Point<double>>[Point<double>(1.0, 2.0), Point<double>(3.0, 4.0)],
]);
expect(
command.toDart(),
' const _PathLineTo(\n'
' const <Offset>[\n'
' const Offset(1.0, 2.0),\n'
' const Offset(3.0, 4.0),\n'
' ],\n'
' ),\n',
);
});
test('_PathCubicTo', () {
const PathCommandAnimation command = PathCommandAnimation(
'C',
<List<Point<double>>>[
<Point<double>>[
Point<double>(16.0, 24.0),
Point<double>(16.0, 10.0),
],
<Point<double>>[
Point<double>(16.0, 25.0),
Point<double>(16.0, 11.0),
],
<Point<double>>[
Point<double>(40.0, 40.0),
Point<double>(40.0, 40.0),
],
],
);
expect(command.toDart(),
' const _PathCubicTo(\n'
' const <Offset>[\n'
' const Offset(16.0, 24.0),\n'
' const Offset(16.0, 10.0),\n'
' ],\n'
' const <Offset>[\n'
' const Offset(16.0, 25.0),\n'
' const Offset(16.0, 11.0),\n'
' ],\n'
' const <Offset>[\n'
' const Offset(40.0, 40.0),\n'
' const Offset(40.0, 40.0),\n'
' ],\n'
' ),\n',
const PathCommandAnimation command = PathCommandAnimation('C', <List<Point<double>>>[
<Point<double>>[Point<double>(16.0, 24.0), Point<double>(16.0, 10.0)],
<Point<double>>[Point<double>(16.0, 25.0), Point<double>(16.0, 11.0)],
<Point<double>>[Point<double>(40.0, 40.0), Point<double>(40.0, 40.0)],
]);
expect(
command.toDart(),
' const _PathCubicTo(\n'
' const <Offset>[\n'
' const Offset(16.0, 24.0),\n'
' const Offset(16.0, 10.0),\n'
' ],\n'
' const <Offset>[\n'
' const Offset(16.0, 25.0),\n'
' const Offset(16.0, 11.0),\n'
' ],\n'
' const <Offset>[\n'
' const Offset(40.0, 40.0),\n'
' const Offset(40.0, 40.0),\n'
' ],\n'
' ),\n',
);
});
test('_PathClose', () {
const PathCommandAnimation command = PathCommandAnimation(
'Z',
<List<Point<double>>>[],
);
expect(command.toDart(),
' const _PathClose(\n'
' ),\n',
const PathCommandAnimation command = PathCommandAnimation('Z', <List<Point<double>>>[]);
expect(
command.toDart(),
' const _PathClose(\n'
' ),\n',
);
});
test('Unsupported path command', () {
const PathCommandAnimation command = PathCommandAnimation(
'h',
<List<Point<double>>>[],
);
const PathCommandAnimation command = PathCommandAnimation('h', <List<Point<double>>>[]);
expect(
() { command.toDart(); },
throwsA(anything),
);
expect(() {
command.toDart();
}, throwsA(anything));
});
test('_PathFrames', () {
const PathAnimation pathAnimation = PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[
Point<double>(0.0, 0.0),
Point<double>(10.0, 10.0),
],
]),
PathCommandAnimation('L', <List<Point<double>>>[
<Point<double>>[
Point<double>(48.0, 10.0),
Point<double>(0.0, 0.0),
],
]),
],
opacities: <double>[0.5, 1.0],
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0), Point<double>(10.0, 10.0)],
]),
PathCommandAnimation('L', <List<Point<double>>>[
<Point<double>>[Point<double>(48.0, 10.0), Point<double>(0.0, 0.0)],
]),
],
opacities: <double>[0.5, 1.0],
);
expect(pathAnimation.toDart(),
' const _PathFrames(\n'
' opacities: const <double>[\n'
' 0.5,\n'
' 1.0,\n'
' ],\n'
' commands: const <_PathCommand>[\n'
' const _PathMoveTo(\n'
' const <Offset>[\n'
' const Offset(0.0, 0.0),\n'
' const Offset(10.0, 10.0),\n'
' ],\n'
' ),\n'
' const _PathLineTo(\n'
' const <Offset>[\n'
' const Offset(48.0, 10.0),\n'
' const Offset(0.0, 0.0),\n'
' ],\n'
' ),\n'
' ],\n'
' ),\n',
expect(
pathAnimation.toDart(),
' const _PathFrames(\n'
' opacities: const <double>[\n'
' 0.5,\n'
' 1.0,\n'
' ],\n'
' commands: const <_PathCommand>[\n'
' const _PathMoveTo(\n'
' const <Offset>[\n'
' const Offset(0.0, 0.0),\n'
' const Offset(10.0, 10.0),\n'
' ],\n'
' ),\n'
' const _PathLineTo(\n'
' const <Offset>[\n'
' const Offset(48.0, 10.0),\n'
' const Offset(0.0, 0.0),\n'
' ],\n'
' ),\n'
' ],\n'
' ),\n',
);
});
test('Animation', () {
const Animation animation = Animation(
Point<double>(48.0, 48.0),
<PathAnimation>[
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[
Point<double>(0.0, 0.0),
Point<double>(10.0, 10.0),
],
]),
PathCommandAnimation('L', <List<Point<double>>>[
<Point<double>>[
Point<double>(48.0, 10.0),
Point<double>(0.0, 0.0),
],
]),
],
opacities: <double>[0.5, 1.0],
),
const Animation animation = Animation(Point<double>(48.0, 48.0), <PathAnimation>[
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0), Point<double>(10.0, 10.0)],
]),
PathCommandAnimation('L', <List<Point<double>>>[
<Point<double>>[Point<double>(48.0, 10.0), Point<double>(0.0, 0.0)],
]),
],
opacities: <double>[0.5, 1.0],
),
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[
Point<double>(0.0, 0.0),
Point<double>(10.0, 10.0),
],
]),
],
opacities: <double>[0.5, 1.0],
),
]);
PathAnimation(
<PathCommandAnimation>[
PathCommandAnimation('M', <List<Point<double>>>[
<Point<double>>[Point<double>(0.0, 0.0), Point<double>(10.0, 10.0)],
]),
],
opacities: <double>[0.5, 1.0],
),
]);
expect(animation.toDart('_AnimatedIconData', r'_$data1'),
'const _AnimatedIconData _\$data1 = const _AnimatedIconData(\n'
' const Size(48.0, 48.0),\n'
' const <_PathFrames>[\n'
' const _PathFrames(\n'
' opacities: const <double>[\n'
' 0.5,\n'
' 1.0,\n'
' ],\n'
' commands: const <_PathCommand>[\n'
' const _PathMoveTo(\n'
' const <Offset>[\n'
' const Offset(0.0, 0.0),\n'
' const Offset(10.0, 10.0),\n'
' ],\n'
' ),\n'
' const _PathLineTo(\n'
' const <Offset>[\n'
' const Offset(48.0, 10.0),\n'
' const Offset(0.0, 0.0),\n'
' ],\n'
' ),\n'
' ],\n'
' ),\n'
' const _PathFrames(\n'
' opacities: const <double>[\n'
' 0.5,\n'
' 1.0,\n'
' ],\n'
' commands: const <_PathCommand>[\n'
' const _PathMoveTo(\n'
' const <Offset>[\n'
' const Offset(0.0, 0.0),\n'
' const Offset(10.0, 10.0),\n'
' ],\n'
' ),\n'
' ],\n'
' ),\n'
' ],\n'
');',
expect(
animation.toDart('_AnimatedIconData', r'_$data1'),
'const _AnimatedIconData _\$data1 = const _AnimatedIconData(\n'
' const Size(48.0, 48.0),\n'
' const <_PathFrames>[\n'
' const _PathFrames(\n'
' opacities: const <double>[\n'
' 0.5,\n'
' 1.0,\n'
' ],\n'
' commands: const <_PathCommand>[\n'
' const _PathMoveTo(\n'
' const <Offset>[\n'
' const Offset(0.0, 0.0),\n'
' const Offset(10.0, 10.0),\n'
' ],\n'
' ),\n'
' const _PathLineTo(\n'
' const <Offset>[\n'
' const Offset(48.0, 10.0),\n'
' const Offset(0.0, 0.0),\n'
' ],\n'
' ),\n'
' ],\n'
' ),\n'
' const _PathFrames(\n'
' opacities: const <double>[\n'
' 0.5,\n'
' 1.0,\n'
' ],\n'
' commands: const <_PathCommand>[\n'
' const _PathMoveTo(\n'
' const <Offset>[\n'
' const Offset(0.0, 0.0),\n'
' const Offset(10.0, 10.0),\n'
' ],\n'
' ),\n'
' ],\n'
' ),\n'
' ],\n'
');',
);
});
});
@@ -700,8 +615,8 @@ class PathAnimationMatcher extends Matcher {
for (int i = 0; i < other.points.length; i += 1) {
if (!const ListEquality<Point<double>>().equals(other.points[i], expected.points[i])) {
return false;
}
}
}
return true;
}