
Part 2 from https://github.com/flutter/flutter/pull/146085 In preparation to add the lint `missing_code_block_language_in_doc_comment`, added `none` info strings to a bunch of fenced code blocks that have miscellaneous text or output text. Related to issue: https://github.com/dart-lang/linter/issues/4904 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [Data Driven Fixes]: https://github.com/flutter/flutter/wiki/Data-driven-Fixes
591 lines
20 KiB
Dart
591 lines
20 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:meta/meta.dart';
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
import '../base/common.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/logger.dart';
|
|
import '../runner/flutter_command.dart';
|
|
import 'gen_l10n_types.dart';
|
|
import 'language_subtag_registry.dart';
|
|
|
|
typedef HeaderGenerator = String Function(String regenerateInstructions);
|
|
typedef ConstructorGenerator = String Function(LocaleInfo locale);
|
|
|
|
int sortFilesByPath (File a, File b) {
|
|
return a.path.compareTo(b.path);
|
|
}
|
|
|
|
/// Simple data class to hold parsed locale. Does not promise validity of any data.
|
|
@immutable
|
|
class LocaleInfo implements Comparable<LocaleInfo> {
|
|
const LocaleInfo({
|
|
required this.languageCode,
|
|
required this.scriptCode,
|
|
required this.countryCode,
|
|
required this.length,
|
|
required this.originalString,
|
|
});
|
|
|
|
/// Simple parser. Expects the locale string to be in the form of 'language_script_COUNTRY'
|
|
/// where the language is 2 characters, script is 4 characters with the first uppercase,
|
|
/// and country is 2-3 characters and all uppercase.
|
|
///
|
|
/// 'language_COUNTRY' or 'language_script' are also valid. Missing fields will be null.
|
|
///
|
|
/// 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 }) {
|
|
final List<String> codes = locale.split('_'); // [language, script, country]
|
|
assert(codes.isNotEmpty && codes.length < 4);
|
|
final String languageCode = codes[0];
|
|
String? scriptCode;
|
|
String? countryCode;
|
|
int length = codes.length;
|
|
String originalString = locale;
|
|
if (codes.length == 2) {
|
|
scriptCode = codes[1].length >= 4 ? codes[1] : null;
|
|
countryCode = codes[1].length < 4 ? codes[1] : null;
|
|
} else if (codes.length == 3) {
|
|
scriptCode = codes[1].length > codes[2].length ? codes[1] : codes[2];
|
|
countryCode = codes[1].length < codes[2].length ? codes[1] : codes[2];
|
|
}
|
|
assert(codes[0].isNotEmpty);
|
|
assert(countryCode == null || countryCode.isNotEmpty);
|
|
assert(scriptCode == null || scriptCode.isNotEmpty);
|
|
|
|
/// Adds scriptCodes to locales where we are able to assume it to provide
|
|
/// finer granularity when resolving locales.
|
|
///
|
|
/// The basis of the assumptions here are based off of known usage of scripts
|
|
/// across various countries. For example, we know Taiwan uses traditional (Hant)
|
|
/// script, so it is safe to apply (Hant) to Taiwanese languages.
|
|
if (deriveScriptCode && scriptCode == null) {
|
|
scriptCode = switch ((languageCode, countryCode)) {
|
|
('zh', 'CN' || 'SG' || null) => 'Hans',
|
|
('zh', 'TW' || 'HK' || 'MO') => 'Hant',
|
|
('sr', null) => 'Cyrl',
|
|
_ => null,
|
|
};
|
|
// Increment length if we were able to assume a scriptCode.
|
|
if (scriptCode != null) {
|
|
length += 1;
|
|
}
|
|
// Update the base string to reflect assumed scriptCodes.
|
|
originalString = languageCode;
|
|
if (scriptCode != null) {
|
|
originalString += '_$scriptCode';
|
|
}
|
|
if (countryCode != null) {
|
|
originalString += '_$countryCode';
|
|
}
|
|
}
|
|
|
|
return LocaleInfo(
|
|
languageCode: languageCode,
|
|
scriptCode: scriptCode,
|
|
countryCode: countryCode,
|
|
length: length,
|
|
originalString: originalString,
|
|
);
|
|
}
|
|
|
|
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.
|
|
|
|
String camelCase() {
|
|
return originalString
|
|
.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;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => originalString.hashCode;
|
|
|
|
@override
|
|
String toString() {
|
|
return originalString;
|
|
}
|
|
|
|
@override
|
|
int compareTo(LocaleInfo other) {
|
|
return originalString.compareTo(other.originalString);
|
|
}
|
|
}
|
|
|
|
// See also //master/tools/gen_locale.dart in the engine repo.
|
|
Map<String, List<String>> _parseSection(String section) {
|
|
final Map<String, List<String>> result = <String, List<String>>{};
|
|
late List<String> lastHeading;
|
|
for (final String line in section.split('\n')) {
|
|
if (line == '') {
|
|
continue;
|
|
}
|
|
if (line.startsWith(' ')) {
|
|
lastHeading[lastHeading.length - 1] = '${lastHeading.last}${line.substring(1)}';
|
|
continue;
|
|
}
|
|
final int colon = line.indexOf(':');
|
|
if (colon <= 0) {
|
|
throw Exception('not sure how to deal with "$line"');
|
|
}
|
|
final String name = line.substring(0, colon);
|
|
final String value = line.substring(colon + 2);
|
|
lastHeading = result.putIfAbsent(name, () => <String>[]);
|
|
result[name]!.add(value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
final Map<String, String> _languages = <String, String>{};
|
|
final Map<String, String> _regions = <String, String>{};
|
|
final Map<String, String> _scripts = <String, String>{};
|
|
const String kProvincePrefix = ', Province of ';
|
|
const String kParentheticalPrefix = ' (';
|
|
|
|
/// Prepares the data for the [describeLocale] method below.
|
|
///
|
|
/// 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();
|
|
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());
|
|
final String subtag = section['Subtag']!.single;
|
|
String description = section['Description']!.join(' ');
|
|
if (description.startsWith('United ')) {
|
|
description = 'the $description';
|
|
}
|
|
if (description.contains(kParentheticalPrefix)) {
|
|
description = description.substring(0, description.indexOf(kParentheticalPrefix));
|
|
}
|
|
if (description.contains(kProvincePrefix)) {
|
|
description = description.substring(0, description.indexOf(kProvincePrefix));
|
|
}
|
|
if (description.endsWith(' Republic')) {
|
|
description = 'the $description';
|
|
}
|
|
switch (type) {
|
|
case 'language':
|
|
_languages[subtag] = description;
|
|
case 'region':
|
|
_regions[subtag] = description;
|
|
case 'script':
|
|
_scripts[subtag] = description;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
String describeLocale(String tag) {
|
|
final List<String> subtags = tag.split('_');
|
|
assert(subtags.isNotEmpty);
|
|
final String languageCode = subtags[0];
|
|
if (!_languages.containsKey(languageCode)) {
|
|
throw L10nException(
|
|
'"$languageCode" is not a supported language code.\n'
|
|
'See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry '
|
|
'for the supported list.',
|
|
);
|
|
}
|
|
final String language = _languages[languageCode]!;
|
|
String output = language;
|
|
String? region;
|
|
String? script;
|
|
if (subtags.length == 2) {
|
|
region = _regions[subtags[1]];
|
|
script = _scripts[subtags[1]];
|
|
assert(region != null || script != null);
|
|
} else if (subtags.length >= 3) {
|
|
region = _regions[subtags[2]];
|
|
script = _scripts[subtags[1]];
|
|
assert(region != null && script != null);
|
|
}
|
|
if (region != null) {
|
|
output += ', as used in $region';
|
|
}
|
|
if (script != null) {
|
|
output += ', using the $script script';
|
|
}
|
|
return output;
|
|
}
|
|
|
|
/// Return the input string as a Dart-parsable string.
|
|
///
|
|
/// ```none
|
|
/// foo => 'foo'
|
|
/// foo "bar" => 'foo "bar"'
|
|
/// foo 'bar' => "foo 'bar'"
|
|
/// foo 'bar' "baz" => '''foo 'bar' "baz"'''
|
|
/// ```
|
|
///
|
|
/// This function is used by tools that take in a JSON-formatted file to
|
|
/// generate Dart code. For this reason, characters with special meaning
|
|
/// in JSON files are escaped. For example, the backspace character (\b)
|
|
/// has to be properly escaped by this function so that the generated
|
|
/// Dart code correctly represents this character:
|
|
/// ```none
|
|
/// foo\bar => 'foo\\bar'
|
|
/// foo\nbar => 'foo\\nbar'
|
|
/// foo\\nbar => 'foo\\\\nbar'
|
|
/// foo\\bar => 'foo\\\\bar'
|
|
/// foo\ bar => 'foo\\ bar'
|
|
/// foo$bar = 'foo\$bar'
|
|
/// ```
|
|
String generateString(String value) {
|
|
const String backslash = '__BACKSLASH__';
|
|
assert(
|
|
!value.contains(backslash),
|
|
'Input string cannot contain the sequence: '
|
|
'"__BACKSLASH__", as it is used as part of '
|
|
'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'\\');
|
|
|
|
return value;
|
|
}
|
|
|
|
/// Given a list of normal strings or interpolated variables, concatenate them
|
|
/// into a single dart string to be returned. An example of a normal string
|
|
/// would be "'Hello world!'" and an example of a interpolated variable would be
|
|
/// "'$placeholder'".
|
|
///
|
|
/// Each of the strings in [expressions] should be a raw string, which, if it
|
|
/// were to be added to a dart file, would be a properly formatted dart string
|
|
/// with escapes and/or interpolation. The purpose of this function is to
|
|
/// concatenate these dart strings into a single dart string which can be
|
|
/// returned in the generated localization files.
|
|
///
|
|
/// The following rules describe the kinds of string expressions that can be
|
|
/// handled:
|
|
/// 1. If [expressions] is empty, return the empty string "''".
|
|
/// 2. If [expressions] has only one [String] which is an interpolated variable,
|
|
/// it is converted to the variable itself e.g. ["'$expr'"] -> "expr".
|
|
/// 3. If one string in [expressions] is an interpolation and the next begins
|
|
/// with an alphanumeric character, then the former interpolation should be
|
|
/// wrapped in braces e.g. ["'$expr1'", "'another'"] -> "'${expr1}another'".
|
|
String generateReturnExpr(List<String> expressions, { bool isSingleStringVar = false }) {
|
|
if (expressions.isEmpty) {
|
|
return "''";
|
|
} else if (isSingleStringVar) {
|
|
// If our expression is "$varName" where varName is a String, this is equivalent to just varName.
|
|
return expressions[0].substring(1);
|
|
} else {
|
|
final String string = expressions.reversed.fold<String>('', (String string, String expression) {
|
|
if (expression[0] != r'$') {
|
|
return expression + string;
|
|
}
|
|
final RegExp alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$');
|
|
if (alphanumeric.hasMatch(expression.substring(1)) && !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) {
|
|
return '$expression$string';
|
|
} else {
|
|
return '\${${expression.substring(1)}}$string';
|
|
}
|
|
});
|
|
return "'$string'";
|
|
}
|
|
}
|
|
|
|
/// Typed configuration from the localizations config file.
|
|
class LocalizationOptions {
|
|
LocalizationOptions({
|
|
required this.arbDir,
|
|
this.outputDir,
|
|
String? templateArbFile,
|
|
String? outputLocalizationFile,
|
|
this.untranslatedMessagesFile,
|
|
String? outputClass,
|
|
this.preferredSupportedLocales,
|
|
this.header,
|
|
this.headerFile,
|
|
bool? useDeferredLoading,
|
|
this.genInputsAndOutputsList,
|
|
bool? syntheticPackage,
|
|
this.projectDir,
|
|
bool? requiredResourceAttributes,
|
|
bool? nullableGetter,
|
|
bool? format,
|
|
bool? useEscaping,
|
|
bool? suppressWarnings,
|
|
bool? relaxSyntax,
|
|
bool? useNamedParameters,
|
|
}) : templateArbFile = templateArbFile ?? 'app_en.arb',
|
|
outputLocalizationFile = outputLocalizationFile ?? 'app_localizations.dart',
|
|
outputClass = outputClass ?? 'AppLocalizations',
|
|
useDeferredLoading = useDeferredLoading ?? false,
|
|
syntheticPackage = syntheticPackage ?? true,
|
|
requiredResourceAttributes = requiredResourceAttributes ?? false,
|
|
nullableGetter = nullableGetter ?? true,
|
|
format = format ?? false,
|
|
useEscaping = useEscaping ?? false,
|
|
suppressWarnings = suppressWarnings ?? false,
|
|
relaxSyntax = relaxSyntax ?? false,
|
|
useNamedParameters = useNamedParameters ?? false;
|
|
|
|
/// The `--arb-dir` argument.
|
|
///
|
|
/// The directory where all input localization files should reside.
|
|
final String arbDir;
|
|
|
|
/// The `--output-dir` argument.
|
|
///
|
|
/// The directory where all output localization files should be generated.
|
|
final String? outputDir;
|
|
|
|
|
|
/// The `--template-arb-file` argument.
|
|
///
|
|
/// This path is relative to [arbDirectory].
|
|
final String templateArbFile;
|
|
|
|
/// The `--output-localization-file` argument.
|
|
///
|
|
/// This path is relative to [arbDir].
|
|
final String outputLocalizationFile;
|
|
|
|
/// The `--untranslated-messages-file` argument.
|
|
///
|
|
/// This path is relative to [arbDir].
|
|
final String? untranslatedMessagesFile;
|
|
|
|
/// The `--output-class` argument.
|
|
final String outputClass;
|
|
|
|
/// The `--preferred-supported-locales` argument.
|
|
final List<String>? preferredSupportedLocales;
|
|
|
|
/// The `--header` argument.
|
|
///
|
|
/// The header to prepend to the generated Dart localizations.
|
|
final String? header;
|
|
|
|
/// The `--header-file` argument.
|
|
///
|
|
/// A file containing the header to prepend to the generated
|
|
/// Dart localizations.
|
|
final String? headerFile;
|
|
|
|
/// The `--use-deferred-loading` argument.
|
|
///
|
|
/// Whether to generate the Dart localization file with locales imported
|
|
/// as deferred.
|
|
final bool useDeferredLoading;
|
|
|
|
/// The `--gen-inputs-and-outputs-list` argument.
|
|
///
|
|
/// This path is relative to [arbDir].
|
|
final String? genInputsAndOutputsList;
|
|
|
|
/// The `--synthetic-package` argument.
|
|
///
|
|
/// Whether to generate the Dart localization files in a synthetic package
|
|
/// or in a custom directory.
|
|
final bool syntheticPackage;
|
|
|
|
/// The `--project-dir` argument.
|
|
///
|
|
/// This path is relative to [arbDir].
|
|
final String? projectDir;
|
|
|
|
/// The `required-resource-attributes` argument.
|
|
///
|
|
/// Whether to require all resource ids to contain a corresponding
|
|
/// resource attribute.
|
|
final bool requiredResourceAttributes;
|
|
|
|
/// The `nullable-getter` argument.
|
|
///
|
|
/// Whether or not the localizations class getter is nullable.
|
|
final bool nullableGetter;
|
|
|
|
/// The `format` argument.
|
|
///
|
|
/// Whether or not to format the generated files.
|
|
final bool format;
|
|
|
|
/// The `use-escaping` argument.
|
|
///
|
|
/// Whether or not the ICU escaping syntax is used.
|
|
final bool useEscaping;
|
|
|
|
/// The `suppress-warnings` argument.
|
|
///
|
|
/// Whether or not to suppress warnings.
|
|
final bool suppressWarnings;
|
|
|
|
/// The `relax-syntax` argument.
|
|
///
|
|
/// Whether or not to relax the syntax. When specified, the syntax will be
|
|
/// relaxed so that the special character "{" is treated as a string if it is
|
|
/// not followed by a valid placeholder and "}" is treated as a string if it
|
|
/// does not close any previous "{" that is treated as a special character.
|
|
/// This was added in for backward compatibility and is not recommended
|
|
/// as it may mask errors.
|
|
final bool relaxSyntax;
|
|
|
|
/// The `use-named-parameters` argument.
|
|
///
|
|
/// Whether or not to use named parameters for the generated localization
|
|
/// methods.
|
|
///
|
|
/// Defaults to `false`.
|
|
final bool useNamedParameters;
|
|
}
|
|
|
|
/// Parse the localizations configuration options from [file].
|
|
///
|
|
/// Throws [Exception] if any of the contents are invalid. Returns a
|
|
/// [LocalizationOptions] with all fields as `null` if the config file exists
|
|
/// but is empty.
|
|
LocalizationOptions parseLocalizationsOptionsFromYAML({
|
|
required File file,
|
|
required Logger logger,
|
|
required String defaultArbDir,
|
|
}) {
|
|
final String contents = file.readAsStringSync();
|
|
if (contents.trim().isEmpty) {
|
|
return LocalizationOptions(arbDir: defaultArbDir);
|
|
}
|
|
final YamlNode yamlNode;
|
|
try {
|
|
yamlNode = loadYamlNode(file.readAsStringSync());
|
|
} on YamlException catch (err) {
|
|
throwToolExit(err.message);
|
|
}
|
|
if (yamlNode is! YamlMap) {
|
|
logger.printError('Expected ${file.path} to contain a map, instead was $yamlNode');
|
|
throw Exception();
|
|
}
|
|
return LocalizationOptions(
|
|
arbDir: _tryReadUri(yamlNode, 'arb-dir', logger)?.path ?? defaultArbDir,
|
|
outputDir: _tryReadUri(yamlNode, 'output-dir', logger)?.path,
|
|
templateArbFile: _tryReadUri(yamlNode, 'template-arb-file', logger)?.path,
|
|
outputLocalizationFile: _tryReadUri(yamlNode, 'output-localization-file', logger)?.path,
|
|
untranslatedMessagesFile: _tryReadUri(yamlNode, 'untranslated-messages-file', logger)?.path,
|
|
outputClass: _tryReadString(yamlNode, 'output-class', logger),
|
|
header: _tryReadString(yamlNode, 'header', logger),
|
|
headerFile: _tryReadUri(yamlNode, 'header-file', logger)?.path,
|
|
useDeferredLoading: _tryReadBool(yamlNode, 'use-deferred-loading', logger),
|
|
preferredSupportedLocales: _tryReadStringList(yamlNode, 'preferred-supported-locales', logger),
|
|
syntheticPackage: _tryReadBool(yamlNode, 'synthetic-package', logger),
|
|
requiredResourceAttributes: _tryReadBool(yamlNode, 'required-resource-attributes', logger),
|
|
nullableGetter: _tryReadBool(yamlNode, 'nullable-getter', logger),
|
|
format: _tryReadBool(yamlNode, 'format', logger),
|
|
useEscaping: _tryReadBool(yamlNode, 'use-escaping', logger),
|
|
suppressWarnings: _tryReadBool(yamlNode, 'suppress-warnings', logger),
|
|
relaxSyntax: _tryReadBool(yamlNode, 'relax-syntax', logger),
|
|
useNamedParameters: _tryReadBool(yamlNode, 'use-named-parameters', logger),
|
|
);
|
|
}
|
|
|
|
/// Parse the localizations configuration from [FlutterCommand].
|
|
LocalizationOptions parseLocalizationsOptionsFromCommand({
|
|
required FlutterCommand command,
|
|
required String defaultArbDir,
|
|
}) {
|
|
return LocalizationOptions(
|
|
arbDir: command.stringArg('arb-dir') ?? defaultArbDir,
|
|
outputDir: command.stringArg('output-dir'),
|
|
outputLocalizationFile: command.stringArg('output-localization-file'),
|
|
templateArbFile: command.stringArg('template-arb-file'),
|
|
untranslatedMessagesFile: command.stringArg('untranslated-messages-file'),
|
|
outputClass: command.stringArg('output-class'),
|
|
header: command.stringArg('header'),
|
|
headerFile: command.stringArg('header-file'),
|
|
useDeferredLoading: command.boolArg('use-deferred-loading'),
|
|
genInputsAndOutputsList: command.stringArg('gen-inputs-and-outputs-list'),
|
|
syntheticPackage: command.boolArg('synthetic-package'),
|
|
projectDir: command.stringArg('project-dir'),
|
|
requiredResourceAttributes: command.boolArg('required-resource-attributes'),
|
|
nullableGetter: command.boolArg('nullable-getter'),
|
|
format: command.boolArg('format'),
|
|
useEscaping: command.boolArg('use-escaping'),
|
|
suppressWarnings: command.boolArg('suppress-warnings'),
|
|
useNamedParameters: command.boolArg('use-named-parameters'),
|
|
);
|
|
}
|
|
|
|
// Try to read a `bool` value or null from `yamlMap`, otherwise throw.
|
|
bool? _tryReadBool(YamlMap yamlMap, String key, Logger logger) {
|
|
final Object? value = yamlMap[key];
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
if (value is! bool) {
|
|
logger.printError('Expected "$key" to have a bool value, instead was "$value"');
|
|
throw Exception();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// Try to read a `String` value or null from `yamlMap`, otherwise throw.
|
|
String? _tryReadString(YamlMap yamlMap, String key, Logger logger) {
|
|
final Object? value = yamlMap[key];
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
if (value is! String) {
|
|
logger.printError('Expected "$key" to have a String value, instead was "$value"');
|
|
throw Exception();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
List<String>? _tryReadStringList(YamlMap yamlMap, String key, Logger logger) {
|
|
final Object? value = yamlMap[key];
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
if (value is String) {
|
|
return <String>[value];
|
|
}
|
|
if (value is Iterable) {
|
|
return value.map((dynamic e) => e.toString()).toList();
|
|
}
|
|
logger.printError('"$value" must be String or List.');
|
|
throw Exception();
|
|
}
|
|
|
|
// Try to read a valid `Uri` or null from `yamlMap`, otherwise throw.
|
|
Uri? _tryReadUri(YamlMap yamlMap, String key, Logger logger) {
|
|
final String? value = _tryReadString(yamlMap, key, logger);
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
final Uri? uri = Uri.tryParse(value);
|
|
if (uri == null) {
|
|
logger.printError('"$value" must be a relative file URI');
|
|
}
|
|
return uri;
|
|
}
|