diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..0d92f55 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1695137929883 + + + + + + + + file://$USER_HOME$/.pub-cache/hosted/pub.dev/args-2.4.2/lib/src/arg_results.dart + 64 + + + + + \ No newline at end of file diff --git a/bin/fontify.dart b/bin/fontify.dart index d67f8e2..5207b4b 100644 --- a/bin/fontify.dart +++ b/bin/fontify.dart @@ -3,8 +3,9 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:fontify/src/cli/arguments.dart'; import 'package:fontify/src/cli/options.dart'; -import 'package:fontify/src/common/api.dart'; +import 'package:fontify/src/common.dart'; import 'package:fontify/src/otf/io.dart'; +import 'package:fontify/src/utils/flutter_class_gen.dart'; import 'package:fontify/src/utils/logger.dart'; import 'package:path/path.dart' as p; import 'package:yaml/yaml.dart'; @@ -46,14 +47,12 @@ void _run(CliArguments parsedArgs) { } if (parsedArgs.classFile?.existsSync() ?? false) { - logger.v( - 'Output file for a Flutter class already exists (${parsedArgs.classFile!.path}) - ' + logger.v('Output file for a Flutter class already exists (${parsedArgs.classFile!.path}) - ' 'overwriting it'); } if (!parsedArgs.fontFile.existsSync()) { - logger.v( - 'Output file for a font file already exists (${parsedArgs.fontFile.path}) - ' + logger.v('Output file for a font file already exists (${parsedArgs.fontFile.path}) - ' 'overwriting it'); } @@ -63,13 +62,11 @@ void _run(CliArguments parsedArgs) { .toList(); if (svgFileList.isEmpty) { - logger.w( - "The input directory doesn't contain any SVG file (${parsedArgs.svgDir.path})."); + logger.w("The input directory doesn't contain any SVG file (${parsedArgs.svgDir.path})."); } final svgMap = { - for (final f in svgFileList) - p.basenameWithoutExtension(f.path): File(f.path).readAsStringSync(), + for (final f in svgFileList) p.basenameWithoutExtension(f.path): File(f.path).readAsStringSync(), }; final otfResult = svgToOtf( @@ -88,13 +85,13 @@ void _run(CliArguments parsedArgs) { final fontFileName = p.basename(parsedArgs.fontFile.path); final classString = generateFlutterClass( - glyphList: otfResult.glyphList, - className: parsedArgs.className, - indent: parsedArgs.indent, - fontFileName: fontFileName, - familyName: otfResult.font.familyName, - package: parsedArgs.fontPackage, - ); + glyphList: otfResult.glyphList, + className: parsedArgs.className, + indent: parsedArgs.indent, + fontFileName: fontFileName, + familyName: otfResult.font.familyName, + package: parsedArgs.fontPackage, + variableNameCase: VariableNameCase.values.firstWhere((e) => e.option == parsedArgs.variableNameCase)); parsedArgs.classFile!.writeAsStringSync(classString); } @@ -124,8 +121,7 @@ ${_argParser.usage} exit(64); } -const _kAbout = - 'Converts .svg icons to an OpenType font and generates Flutter-compatible class.'; +const _kAbout = 'Converts .svg icons to an OpenType font and generates Flutter-compatible class.'; const _kUsage = ''' Usage: fontify [options] diff --git a/example/example.dart b/example/example.dart index 4f52e99..8c04c8e 100644 --- a/example/example.dart +++ b/example/example.dart @@ -24,6 +24,7 @@ void main() { familyName: svgToOtfResult.font.familyName, className: 'MyIcons', fontFileName: fontFileName, + variableNameCase: VariableNameCase.camel, ); // Writing class content to a file diff --git a/lib/src/cli/arguments.dart b/lib/src/cli/arguments.dart index ab74fd6..5330094 100644 --- a/lib/src/cli/arguments.dart +++ b/lib/src/cli/arguments.dart @@ -25,6 +25,7 @@ const _kArgAllowedTypes = >{ CliArgument.verbose: [bool], CliArgument.help: [bool], CliArgument.configFile: [String], + CliArgument.variableNameCase: [String] }; const kDefaultVerbose = false; @@ -44,6 +45,7 @@ const kOptionNames = EnumClass({ CliArgument.recursive: 'recursive', CliArgument.verbose: 'verbose', + CliArgument.variableNameCase: 'variable-name-case', CliArgument.help: 'help', CliArgument.configFile: 'config-file', @@ -61,6 +63,7 @@ const kConfigKeys = EnumClass({ CliArgument.fontName: 'font_name', CliArgument.normalize: 'normalize', CliArgument.ignoreShapes: 'ignore_shapes', + CliArgument.variableNameCase: 'variable_name_case', CliArgument.recursive: 'recursive', CliArgument.verbose: 'verbose', @@ -92,6 +95,7 @@ enum CliArgument { // Others recursive, verbose, + variableNameCase, // Only in CLI help, @@ -113,6 +117,7 @@ class CliArguments { this.normalize, this.verbose, this.configFile, + this.variableNameCase, ); /// Creates [CliArguments] for a map of raw values. @@ -135,6 +140,7 @@ class CliArguments { map[CliArgument.normalize] as bool?, map[CliArgument.verbose] as bool?, map[CliArgument.configFile] as File?, + map[CliArgument.variableNameCase] as String?, ); } @@ -150,6 +156,7 @@ class CliArguments { final bool? normalize; final bool? verbose; final File? configFile; + final String? variableNameCase; } /// Parses argument list. @@ -157,8 +164,7 @@ class CliArguments { /// Throws [CliHelpException], if 'help' option is present. /// /// Returns an instance of [CliArguments] containing all parsed data. -Map parseArguments( - ArgParser argParser, List args) { +Map parseArguments(ArgParser argParser, List args) { late final ArgResults argResults; try { argResults = argParser.parse(args); @@ -170,13 +176,11 @@ Map parseArguments( throw CliHelpException(); } - final posArgsLength = - math.min(_kPositionalArguments.length, argResults.rest.length); + final posArgsLength = math.min(_kPositionalArguments.length, argResults.rest.length); final rawArgMap = { for (final e in kOptionNames.entries) e.key: argResults[e.value], - for (var i = 0; i < posArgsLength; i++) - _kPositionalArguments[i]: argResults.rest[i], + for (var i = 0; i < posArgsLength; i++) _kPositionalArguments[i]: argResults.rest[i], }; return rawArgMap; @@ -219,9 +223,7 @@ Map? parseConfig(String config) { return null; } - final entries = fontifyYamlMap.entries - .map(_mapConfigKeyEntry) - .whereType>(); + final entries = fontifyYamlMap.entries.map(_mapConfigKeyEntry).whereType>(); return Map.fromEntries(entries); } @@ -235,10 +237,7 @@ CliArguments parseArgsAndConfig(ArgParser argParser, List args) { var parsedArgs = parseArguments(argParser, args); final dynamic configFile = parsedArgs[CliArgument.configFile]; - final configList = [ - if (configFile is String) configFile, - ..._kDefaultConfigPathList - ].map((e) => File(e)); + final configList = [if (configFile is String) configFile, ..._kDefaultConfigPathList].map((e) => File(e)); for (final configFile in configList) { if (configFile.existsSync()) { @@ -304,13 +303,11 @@ extension CliArgumentMapExtension on Map { } if (svgDir.statSync().type != FileSystemEntityType.directory) { - throw CliArgumentException( - "The input directory is not a directory or it doesn't exist."); + throw CliArgumentException("The input directory is not a directory or it doesn't exist."); } if (indent != null && indent < 0) { - throw CliArgumentException( - 'indent must be a non-negative integer, was $indent.'); + throw CliArgumentException('indent must be a non-negative integer, was $indent.'); } } diff --git a/lib/src/cli/options.dart b/lib/src/cli/options.dart index 8ef3427..03d0340 100644 --- a/lib/src/cli/options.dart +++ b/lib/src/cli/options.dart @@ -1,5 +1,6 @@ import 'package:args/args.dart'; +import '../../fontify.dart'; import 'arguments.dart'; void defineOptions(ArgParser argParser) { @@ -8,8 +9,7 @@ void defineOptions(ArgParser argParser) { ..addOption( kOptionNames[CliArgument.classFile]!, abbr: 'o', - help: - 'Output path for Flutter-compatible class that contains identifiers for the icons.', + help: 'Output path for Flutter-compatible class that contains identifiers for the icons.', valueHelp: 'path', ) ..addOption( @@ -28,10 +28,17 @@ void defineOptions(ArgParser argParser) { ..addOption( kOptionNames[CliArgument.fontPackage]!, abbr: 'p', - help: - 'Name of a package that provides a font. Used to provide a font through package dependency.', + help: 'Name of a package that provides a font. Used to provide a font through package dependency.', valueHelp: 'name', ) + ..addOption( + kOptionNames[CliArgument.variableNameCase]!, + abbr: 'n', + help: 'The case to use when generating variable names.', + valueHelp: 'name', + allowed: VariableNameCase.values.map((e) => e.option), + defaultsTo: VariableNameCase.camel.option, + ) ..addSeparator('Font options:') ..addOption( kOptionNames[CliArgument.fontName]!, @@ -41,8 +48,7 @@ void defineOptions(ArgParser argParser) { ) ..addFlag( kOptionNames[CliArgument.normalize]!, - help: - 'Enables glyph normalization for the font. Disable this if every icon has the same size and positioning.', + help: 'Enables glyph normalization for the font. Disable this if every icon has the same size and positioning.', defaultsTo: true, ) ..addFlag( @@ -54,8 +60,7 @@ void defineOptions(ArgParser argParser) { ..addOption( kOptionNames[CliArgument.configFile]!, abbr: 'z', - help: - 'Path to Fontify yaml configuration file. pubspec.yaml and fontify.yaml files are used by default.', + help: 'Path to Fontify yaml configuration file. pubspec.yaml and fontify.yaml files are used by default.', valueHelp: 'path', ) ..addFlag( diff --git a/lib/src/common/api.dart b/lib/src/common/api.dart index 4e976f9..d1b94f9 100644 --- a/lib/src/common/api.dart +++ b/lib/src/common/api.dart @@ -35,10 +35,7 @@ SvgToOtfResult svgToOtf({ }) { normalize ??= true; - final svgList = [ - for (final e in svgMap.entries) - Svg.parse(e.key, e.value, ignoreShapes: ignoreShapes) - ]; + final svgList = [for (final e in svgMap.entries) Svg.parse(e.key, e.value, ignoreShapes: ignoreShapes)]; if (!normalize) { for (var i = 1; i < svgList.length; i++) { @@ -74,6 +71,7 @@ SvgToOtfResult svgToOtf({ /// * [package] is the name of a font package. Used to provide a font through package dependency. /// * [fontFileName] is font file's name. Used in generated docs for class. /// * [indent] is a number of spaces in leading indentation for class' members. Defaults to 2. +/// * [variableNameCase] is the generated variable name case (e.g. camelCase, snake_case) /// /// Returns content of a class file. String generateFlutterClass({ @@ -83,6 +81,7 @@ String generateFlutterClass({ String? fontFileName, String? package, int? indent, + VariableNameCase variableNameCase = VariableNameCase.camel, }) { final generator = FlutterClassGenerator( glyphList, @@ -91,6 +90,7 @@ String generateFlutterClass({ fontFileName: fontFileName, familyName: familyName, package: package, + variableNameCase: variableNameCase, ); return generator.generate(); diff --git a/lib/src/otf/table/maxp.dart b/lib/src/otf/table/maxp.dart index 61c6d8a..8d4d03b 100644 --- a/lib/src/otf/table/maxp.dart +++ b/lib/src/otf/table/maxp.dart @@ -2,7 +2,6 @@ import 'dart:typed_data'; import '../../utils/otf.dart'; import '../debugger.dart'; - import 'abstract.dart'; import 'glyf.dart'; import 'table_record_entry.dart'; @@ -62,7 +61,7 @@ class MaximumProfileTable extends FontTable { return MaximumProfileTable.v1( null, numGlyphs, - glyf!.maxPoints, + glyf.maxPoints, glyf.maxContours, 0, // Composite glyphs are not supported 0, // Composite glyphs are not supported @@ -79,8 +78,7 @@ class MaximumProfileTable extends FontTable { 0); } - static MaximumProfileTable? fromByteData( - ByteData data, TableRecordEntry entry) { + static MaximumProfileTable? fromByteData(ByteData data, TableRecordEntry entry) { final version = data.getInt32(entry.offset); if (version == _kVersion0) { diff --git a/lib/src/utils/flutter_class_gen.dart b/lib/src/utils/flutter_class_gen.dart index 9527e81..2c8b509 100644 --- a/lib/src/utils/flutter_class_gen.dart +++ b/lib/src/utils/flutter_class_gen.dart @@ -1,5 +1,5 @@ -import 'package:recase/recase.dart'; import 'package:path/path.dart' as p; +import 'package:recase/recase.dart'; import '../common/constant.dart'; import '../common/generic_glyph.dart'; @@ -26,6 +26,7 @@ class FlutterClassGenerator { /// * [package] is the name of a font package. Used to provide a font through package dependency. /// * [fontFileName] is font file's name. Used in generated docs for class. /// * [indent] is a number of spaces in leading indentation for class' members. Defaults to 2. + /// * [variableNameCase] is the generated variable name case (e.g. camelCase, snake_case) FlutterClassGenerator( this.glyphList, { String? className, @@ -33,11 +34,12 @@ class FlutterClassGenerator { String? fontFileName, String? package, int? indent, + VariableNameCase variableNameCase = VariableNameCase.camel, }) : _indent = ' ' * (indent ?? _kDefaultIndent), _className = _getVarName(className ?? _kDefaultClassName), _familyName = familyName ?? kDefaultFontFamily, _fontFileName = fontFileName ?? _kDefaultFontFileName, - _iconVarNames = _generateVariableNames(glyphList), + _iconVarNames = _generateVariableNames(glyphList, variableNameCase), _package = package?.isEmpty ?? true ? null : package; final List glyphList; @@ -48,12 +50,12 @@ class FlutterClassGenerator { final String? _package; final List _iconVarNames; - static List _generateVariableNames(List glyphList) { + static List _generateVariableNames(List glyphList, VariableNameCase variableNameCase) { final iconNameSet = {}; return glyphList.map((g) { - final baseName = - _getVarName(p.basenameWithoutExtension(g.metadata.name!)).snakeCase; + final rc = ReCase(_getVarName(p.basenameWithoutExtension(g.metadata.name!))); + final baseName = variableNameCase == VariableNameCase.camel ? rc.camelCase : rc.snakeCase; final usingDefaultName = baseName.isEmpty; var variableName = usingDefaultName ? _kUnnamedIconName : baseName; @@ -74,8 +76,7 @@ class FlutterClassGenerator { String variableNameWithCount; do { - variableNameWithCount = - '${variableWithoutCount}_${++variableNameCount}'; + variableNameWithCount = '${variableWithoutCount}_${++variableNameCount}'; } while (iconNameSet.contains(variableNameWithCount)); variableName = variableNameWithCount; @@ -89,8 +90,7 @@ class FlutterClassGenerator { bool get _hasPackage => _package != null; - String get _fontFamilyConst => - "static const iconFontFamily = '$_familyName';"; + String get _fontFamilyConst => "static const iconFontFamily = '$_familyName';"; String get _fontPackageConst => "static const iconFontPackage = '$_package';"; @@ -103,18 +103,11 @@ class FlutterClassGenerator { final varName = _iconVarNames[index]; final hexCode = charCode.toRadixString(16); - final posParamList = [ - 'fontFamily: iconFontFamily', - if (_hasPackage) 'fontPackage: iconFontPackage' - ]; + final posParamList = ['fontFamily: iconFontFamily', if (_hasPackage) 'fontPackage: iconFontPackage']; final posParamString = posParamList.join(', '); - return [ - '', - '/// $iconName', - 'static const IconData $varName = IconData(0x$hexCode, $posParamString);' - ]; + return ['', '/// $iconName', 'static const IconData $varName = IconData(0x$hexCode, $posParamString);']; } /// Generates content for a class' file. @@ -127,8 +120,15 @@ class FlutterClassGenerator { for (var i = 0; i < glyphList.length; i++) ..._generateIconConst(i), ]; - final classContentString = - classContent.map((e) => e.isEmpty ? '' : '$_indent$e').join('\n'); + var classContentString = classContent.map((e) => e.isEmpty ? '' : '$_indent$e').join('\n'); + + classContentString += '\n\n${_indent}static const Map allIcons = {\n'; + + for (final name in _iconVarNames) { + classContentString += "${_indent}${_indent}'$name': $name,\n"; + } + + classContentString += '$_indent};'; return ''' // Generated code: do not hand-edit. @@ -162,3 +162,12 @@ $classContentString '''; } } + +/// Represents the case of the variable names. +enum VariableNameCase { + camel(option: 'camel'), + snake(option: 'snake'); + + const VariableNameCase({required this.option}); + final String option; +} diff --git a/pubspec.yaml b/pubspec.yaml index 1ca1ef4..d033d93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,22 +6,21 @@ description: >- homepage: https://github.com/westracer/fontify environment: - sdk: ">=2.12.0 <3.0.0" + sdk: '>=3.0.0 <4.0.0' dependencies: args: ^2.0.0 collection: ^1.15.0 - logger: ^1.0.0 + logger: ^1.3.0 meta: ^1.3.0 path: ^1.8.0 - path_parsing: ^0.2.0 + path_parsing: ^1.0.1 recase: ^4.0.0 vector_math: ^2.1.0 - xml: ^5.1.0 + xml: ^6.3.0 yaml: ^3.1.0 dev_dependencies: - pedantic: ^1.11.0 test: ^1.17.0 executables: diff --git a/test/cli_test.dart b/test/cli_test.dart index 0da70fd..245e0b0 100644 --- a/test/cli_test.dart +++ b/test/cli_test.dart @@ -12,8 +12,7 @@ void main() { defineOptions(_argParser); void expectCliArgumentException(List args) { - expect(() => parseArgsAndConfig(_argParser, args), - throwsA(const TypeMatcher())); + expect(() => parseArgsAndConfig(_argParser, args), throwsA(const TypeMatcher())); } test('No positional args', () { @@ -52,6 +51,7 @@ void main() { '--verbose', '--config-file=test/config.yaml', '--package=test_package', + '--variable-name-case=snake' ]; final parsedArgs = parseArgsAndConfig(_argParser, args); @@ -68,6 +68,7 @@ void main() { expect(parsedArgs.verbose, isTrue); expect(parsedArgs.configFile?.path, 'test/config.yaml'); expect(parsedArgs.fontPackage, 'test_package'); + expect(parsedArgs.variableNameCase, 'snake'); }); test('All arguments with defaults', () { @@ -96,8 +97,7 @@ void main() { test('Help', () { void expectCliHelpException(List args) { - expect(() => parseArgsAndConfig(_argParser, args), - throwsA(const TypeMatcher())); + expect(() => parseArgsAndConfig(_argParser, args), throwsA(const TypeMatcher())); } expectCliHelpException(['-h']); @@ -186,8 +186,7 @@ void main() { } void expectCliArgumentException(String cfg) { - expect(() => _parseConfig(cfg), - throwsA(const TypeMatcher())); + expect(() => _parseConfig(cfg), throwsA(const TypeMatcher())); } test('No required', () {