diff --git a/.gitignore b/.gitignore index 1985397..2fed51d 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ build/ !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 +code_segments.dart diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3f730fc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "ansu" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index fa7294d..0f227dc 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ ## Getting Started +### 生成关键代码 + +```bash +flutter pub run grinder update-code-segments +``` + ### 安装ansu_ui #### Android @@ -16,6 +22,27 @@ Change the minimum Android sdk version to 21 (or higher) in your `android/app/bu minSdkVersion 21 ``` +## ROAD MAP + +* [ ] Auto Code generate + * [x] Scaffold + * [x] Button + * [ ] Extension + * [ ] Badge + * [ ] Bars + * [ ] Box + * [ ] Dialog + * [ ] Divider + * [ ] Drawer + * [ ] ListTile + * [ ] Pickers + * [ ] PopUpMenu + * [ ] Refresh + * [ ] Tag + * [ ] TextField + * [ ] Toast + * [ ] Utils + ## 贡献 [@laiiihz](http://192.168.2.201:8099/u/laiiihz) diff --git a/example/lib/codeviewer/code_style.dart b/example/lib/codeviewer/code_style.dart new file mode 100644 index 0000000..7297bc4 --- /dev/null +++ b/example/lib/codeviewer/code_style.dart @@ -0,0 +1,43 @@ +// Copyright 2019 The Flutter team. 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:flutter/material.dart'; + +class CodeStyle extends InheritedWidget { + const CodeStyle({ + this.baseStyle, + this.numberStyle, + this.commentStyle, + this.keywordStyle, + this.stringStyle, + this.punctuationStyle, + this.classStyle, + this.constantStyle, + @required Widget child, + }) : super(child: child); + + final TextStyle baseStyle; + final TextStyle numberStyle; + final TextStyle commentStyle; + final TextStyle keywordStyle; + final TextStyle stringStyle; + final TextStyle punctuationStyle; + final TextStyle classStyle; + final TextStyle constantStyle; + + static CodeStyle of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(CodeStyle oldWidget) => + oldWidget.baseStyle != baseStyle || + oldWidget.numberStyle != numberStyle || + oldWidget.commentStyle != commentStyle || + oldWidget.keywordStyle != keywordStyle || + oldWidget.stringStyle != stringStyle || + oldWidget.punctuationStyle != punctuationStyle || + oldWidget.classStyle != classStyle || + oldWidget.constantStyle != constantStyle; +} \ No newline at end of file diff --git a/example/lib/common/base_page.dart b/example/lib/common/base_page.dart new file mode 100644 index 0000000..5d1bf53 --- /dev/null +++ b/example/lib/common/base_page.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'code_view.dart'; + +class BasePage extends StatelessWidget { + final String title; + final Widget body; + final List actions; + final CodeBuilder codeBuilder; + const BasePage( + {Key key, + @required this.title, + @required this.body, + this.actions, + this.codeBuilder}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(title), + actions: actions ?? + [ + IconButton( + icon: Icon(Icons.code), + onPressed: () => Get.to(CodeView(text: codeBuilder)), + ), + ], + ), + body: body, + ); + } +} diff --git a/example/lib/common/code_tile.dart b/example/lib/common/code_tile.dart new file mode 100644 index 0000000..e5e411f --- /dev/null +++ b/example/lib/common/code_tile.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'code_view.dart'; + +class CodeTile extends StatelessWidget { + final Widget title; + final Widget subTitle; + final CodeBuilder builder; + const CodeTile({Key key, this.title, this.subTitle, this.builder}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + title: title, + subtitle: subTitle, + trailing: IconButton( + icon: Icon(Icons.code), + onPressed: () => Get.to( + CodeView( + text: builder, + ), + ), + ), + ); + } +} diff --git a/example/lib/common/code_view.dart b/example/lib/common/code_view.dart new file mode 100644 index 0000000..ab14b04 --- /dev/null +++ b/example/lib/common/code_view.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../codeviewer/code_style.dart'; + +typedef CodeBuilder = TextSpan Function(BuildContext context); + +class CodeView extends StatefulWidget { + final CodeBuilder text; + final String title; + CodeView({Key key, @required this.text, this.title = 'Code'}) + : super(key: key); + + @override + _CodeViewState createState() => _CodeViewState(); +} + +class _CodeViewState extends State { + TextStyle codeTheme = TextStyle(fontSize: 14); + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black87, + appBar: AppBar(title: Text(widget.title)), + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: CodeStyle( + baseStyle: codeTheme.copyWith(color: Colors.white70), + numberStyle: codeTheme.copyWith(color: const Color(0xFFBD93F9)), + commentStyle: codeTheme.copyWith(color: const Color(0xFF808080)), + keywordStyle: codeTheme.copyWith(color: const Color(0xFF1CDEC9)), + stringStyle: codeTheme.copyWith(color: const Color(0xFFFFA65C)), + punctuationStyle: + codeTheme.copyWith(color: const Color(0xFF8BE9FD)), + classStyle: codeTheme.copyWith(color: const Color(0xFFD65BAD)), + constantStyle: codeTheme.copyWith(color: const Color(0xFFFF8383)), + child: + Builder(builder: (context) => Text.rich(widget.text(context))), + )), + ); + } +} diff --git a/example/lib/example_button.dart b/example/lib/example_button.dart index 1fbc4f8..e8e3edb 100644 --- a/example/lib/example_button.dart +++ b/example/lib/example_button.dart @@ -1,6 +1,10 @@ +// BEGIN button import 'package:ansu_ui/ansu_ui.dart'; +import 'package:example/codeviewer/code_segments.dart'; +import 'package:example/common/code_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; class ExampleButton extends StatefulWidget { ExampleButton({Key key}) : super(key: key); @@ -15,6 +19,14 @@ class _ExampleButtonState extends State { Widget build(BuildContext context) { return ASScaffold( title: 'ASButton', + actions: [ + IconButton( + icon: Icon(Icons.code, color: Colors.black54), + onPressed: () => Get.to(CodeView( + text: (context) => CodeSegments.button(context), + )), + ), + ], body: ListView( children: [ ListTile( @@ -162,3 +174,5 @@ class _ExampleButtonState extends State { ); } } + +// END diff --git a/example/lib/example_scaffold.dart b/example/lib/example_scaffold.dart index c691fb9..e938d1d 100644 --- a/example/lib/example_scaffold.dart +++ b/example/lib/example_scaffold.dart @@ -1,4 +1,7 @@ +// BEGIN scaffold import 'package:ansu_ui/ansu_ui.dart'; +import 'package:example/codeviewer/code_segments.dart'; +import 'package:example/common/code_view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -28,6 +31,19 @@ class _ExampleScaffoldState extends State Widget build(BuildContext context) { return ASScaffold( title: '框架 Scaffold', + actions: [ + IconButton( + icon: Icon( + Icons.code, + color: Colors.black54, + ), + onPressed: () => Get.to( + CodeView( + text: (context) => CodeSegments.scaffold(context), + ), + ), + ) + ], appBarBottom: ASTabBar( items: tabs, isScrollable: true, @@ -66,3 +82,5 @@ class _ExampleScaffoldState extends State ); } } + +// END diff --git a/example/pubspec.lock b/example/pubspec.lock index f267400..44e7304 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -8,6 +8,20 @@ packages: relative: true source: path version: "0.0.4" + args: + dependency: "direct main" + description: + name: args + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" bot_toast: dependency: transitive description: @@ -29,6 +43,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0-nullsafety.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" collection: dependency: transitive description: @@ -43,6 +64,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" flutter: dependency: "direct main" description: flutter @@ -81,6 +109,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.17.1" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + grinder: + dependency: "direct main" + description: + name: grinder + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.6" http: dependency: transitive description: @@ -116,6 +158,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.16.1" + js: + dependency: transitive + description: + name: js + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.2" lpinyin: dependency: transitive description: @@ -130,6 +179,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0-nullsafety.3" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" path: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6f282f7..104325d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -publish_to: "none" +publish_to: "none" version: 1.0.0+1 environment: @@ -18,6 +18,8 @@ dependencies: ansu_ui: path: ../ get: + grinder: + args: flutter: uses-material-design: true diff --git a/example/tool/codeviewer_cli/main.dart b/example/tool/codeviewer_cli/main.dart new file mode 100644 index 0000000..f292289 --- /dev/null +++ b/example/tool/codeviewer_cli/main.dart @@ -0,0 +1,31 @@ +// Copyright 2019 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as path; + +import 'segment_generator.dart'; + +void main(List arguments) { + final parser = ArgParser() + ..addOption( + 'target', + help: 'The file path for the output target file.', + defaultsTo: path.join( + Directory.current.path, 'lib', 'codeviewer', 'code_segments.dart'), + ) + ..addFlag( + 'dry-run', + help: 'Write the output to stdout.', + ); + final argResults = parser.parse(arguments); + + writeSegments( + sourceDirectoryPath: path.join(Directory.current.path, 'lib'), + targetFilePath: argResults['target'] as String, + isDryRun: argResults['dry-run'] as bool, + ); +} diff --git a/example/tool/codeviewer_cli/prehighlighter.dart b/example/tool/codeviewer_cli/prehighlighter.dart new file mode 100644 index 0000000..df9bd6b --- /dev/null +++ b/example/tool/codeviewer_cli/prehighlighter.dart @@ -0,0 +1,412 @@ +// Copyright 2019 The Flutter team. 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:string_scanner/string_scanner.dart'; + +abstract class SyntaxPrehighlighter { + List format(String src); +} + +class DartSyntaxPrehighlighter extends SyntaxPrehighlighter { + DartSyntaxPrehighlighter() { + _spans = <_HighlightSpan>[]; + } + + static const List _keywords = [ + 'abstract', + 'as', + 'assert', + 'async', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'default', + 'deferred', + 'do', + 'dynamic', + 'else', + 'enum', + 'export', + 'external', + 'extends', + 'factory', + 'false', + 'final', + 'finally', + 'for', + 'get', + 'if', + 'implements', + 'import', + 'in', + 'is', + 'library', + 'new', + 'null', + 'operator', + 'part', + 'rethrow', + 'return', + 'set', + 'static', + 'super', + 'switch', + 'sync', + 'this', + 'throw', + 'true', + 'try', + 'typedef', + 'var', + 'void', + 'while', + 'with', + 'yield', + ]; + + static const List _builtInTypes = [ + 'int', + 'double', + 'num', + 'bool', + ]; + + String _src; + StringScanner _scanner; + + List<_HighlightSpan> _spans; + + @override + List format(String src) { + _src = src; + _scanner = StringScanner(_src); + + if (_generateSpans()) { + // Successfully parsed the code + final formattedText = []; + var currentPosition = 0; + + for (final span in _spans) { + if (currentPosition != span.start) { + formattedText + .add(CodeSpan(text: _src.substring(currentPosition, span.start))); + } + + formattedText + .add(CodeSpan(type: span.type, text: span.textForSpan(_src))); + + currentPosition = span.end; + } + + if (currentPosition != _src.length) { + formattedText + .add(CodeSpan(text: _src.substring(currentPosition, _src.length))); + } + + return formattedText; + } else { + // Parsing failed, return with only basic formatting + return [CodeSpan(type: _HighlightType.base, text: src)]; + } + } + + bool _generateSpans() { + var lastLoopPosition = _scanner.position; + + while (!_scanner.isDone) { + // Skip White space + _scanner.scan(RegExp(r'\s+')); + + // Block comments + if (_scanner.scan(RegExp(r'/\*(.|\n)*\*/'))) { + _spans.add(_HighlightSpan( + _HighlightType.comment, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // Line comments + if (_scanner.scan('//')) { + final startComment = _scanner.lastMatch.start; + + var eof = false; + int endComment; + if (_scanner.scan(RegExp(r'.*\n'))) { + endComment = _scanner.lastMatch.end - 1; + } else { + eof = true; + endComment = _src.length; + } + + _spans.add(_HighlightSpan( + _HighlightType.comment, + startComment, + endComment, + )); + + if (eof) { + break; + } + + continue; + } + + // Raw r"String" + if (_scanner.scan(RegExp(r'r".*"'))) { + _spans.add(_HighlightSpan( + _HighlightType.string, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // Raw r'String' + if (_scanner.scan(RegExp(r"r'.*'"))) { + _spans.add(_HighlightSpan( + _HighlightType.string, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // Multiline """String""" + if (_scanner.scan(RegExp(r'"""(?:[^"\\]|\\(.|\n))*"""'))) { + _spans.add(_HighlightSpan( + _HighlightType.string, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // Multiline '''String''' + if (_scanner.scan(RegExp(r"'''(?:[^'\\]|\\(.|\n))*'''"))) { + _spans.add(_HighlightSpan( + _HighlightType.string, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // "String" + if (_scanner.scan(RegExp(r'"(?:[^"\\]|\\.)*"'))) { + _spans.add(_HighlightSpan( + _HighlightType.string, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // 'String' + if (_scanner.scan(RegExp(r"'(?:[^'\\]|\\.)*'"))) { + _spans.add(_HighlightSpan( + _HighlightType.string, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // Double + if (_scanner.scan(RegExp(r'\d+\.\d+'))) { + _spans.add(_HighlightSpan( + _HighlightType.number, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // Integer + if (_scanner.scan(RegExp(r'\d+'))) { + _spans.add(_HighlightSpan(_HighlightType.number, + _scanner.lastMatch.start, _scanner.lastMatch.end)); + continue; + } + + // Punctuation + if (_scanner.scan(RegExp(r'[\[\]{}().!=<>&\|\?\+\-\*/%\^~;:,]'))) { + _spans.add(_HighlightSpan( + _HighlightType.punctuation, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // Meta data + if (_scanner.scan(RegExp(r'@\w+'))) { + _spans.add(_HighlightSpan( + _HighlightType.keyword, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + continue; + } + + // Words + if (_scanner.scan(RegExp(r'\w+'))) { + _HighlightType type; + + var word = _scanner.lastMatch[0]; + if (word.startsWith('_')) { + word = word.substring(1); + } + + if (_keywords.contains(word)) { + type = _HighlightType.keyword; + } else if (_builtInTypes.contains(word)) { + type = _HighlightType.keyword; + } else if (_firstLetterIsUpperCase(word)) { + type = _HighlightType.klass; + } else if (word.length >= 2 && + word.startsWith('k') && + _firstLetterIsUpperCase(word.substring(1))) { + type = _HighlightType.constant; + } + + if (type != null) { + _spans.add(_HighlightSpan( + type, + _scanner.lastMatch.start, + _scanner.lastMatch.end, + )); + } + } + + // Check if this loop did anything + if (lastLoopPosition == _scanner.position) { + // Failed to parse this file, abort gracefully + return false; + } + lastLoopPosition = _scanner.position; + } + + _simplify(); + return true; + } + + void _simplify() { + for (var i = _spans.length - 2; i >= 0; i -= 1) { + if (_spans[i].type == _spans[i + 1].type && + _spans[i].end == _spans[i + 1].start) { + _spans[i] = _HighlightSpan( + _spans[i].type, + _spans[i].start, + _spans[i + 1].end, + ); + _spans.removeAt(i + 1); + } + } + } + + bool _firstLetterIsUpperCase(String str) { + if (str.isNotEmpty) { + final first = str.substring(0, 1); + return first == first.toUpperCase(); + } + return false; + } +} + +enum _HighlightType { + number, + comment, + keyword, + string, + punctuation, + klass, + constant, + base, +} + +class _HighlightSpan { + _HighlightSpan(this.type, this.start, this.end); + final _HighlightType type; + final int start; + final int end; + + String textForSpan(String src) { + return src.substring(start, end); + } +} + +class CodeSpan { + CodeSpan({this.type = _HighlightType.base, this.text}); + + final _HighlightType type; + final String text; + + @override + String toString() { + return 'TextSpan(' + 'style: codeStyle.${_styleNameOf(type)}, ' + "text: '${_escape(text)}'" + ')'; + } +} + +String _styleNameOf(_HighlightType type) { + switch (type) { + case _HighlightType.number: + return 'numberStyle'; + case _HighlightType.comment: + return 'commentStyle'; + case _HighlightType.keyword: + return 'keywordStyle'; + case _HighlightType.string: + return 'stringStyle'; + case _HighlightType.punctuation: + return 'punctuationStyle'; + case _HighlightType.klass: + return 'classStyle'; + case _HighlightType.constant: + return 'constantStyle'; + case _HighlightType.base: + return 'baseStyle'; + } + return ''; +} + +String _escape(String text) { + final escapedText = StringBuffer(); + + for (final char in text.runes) { + if (char < 0x20 || + char >= 0x7F || + char == 0x22 || + char == 0x24 || + char == 0x27 || + char == 0x5C) { + if (char <= 0xffff) { + escapedText.write('\\u${_encodeAndPad(char)}'); + } else { + escapedText.write('\\u{${_encode(char)}}'); + } + } else { + escapedText.write(String.fromCharCode(char)); + } + } + + return escapedText.toString(); +} + +String _encode(int charCode) { + return charCode.toRadixString(16); +} + +String _encodeAndPad(int charCode) { + final encoded = _encode(charCode); + return '0' * (4 - encoded.length) + encoded; +} \ No newline at end of file diff --git a/example/tool/codeviewer_cli/segment_generator.dart b/example/tool/codeviewer_cli/segment_generator.dart new file mode 100644 index 0000000..e7bd35d --- /dev/null +++ b/example/tool/codeviewer_cli/segment_generator.dart @@ -0,0 +1,266 @@ +// Copyright 2019 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'prehighlighter.dart'; + +const _globalPrologue = + '''// This file is automatically generated by codeviewer_cli. +// Do not edit this file. +// Copyright 2019 The Flutter team. 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:flutter/material.dart'; +import 'package:example/codeviewer/code_style.dart'; +class CodeSegments { +'''; + +const _globalEpilogue = '}\n'; + +final Pattern beginSubsegment = RegExp(r'//\s+BEGIN'); +final Pattern endSubsegment = RegExp(r'//\s+END'); + +enum _FileReadStatus { + comments, + imports, + finished, +} + +/// Returns the new status of the scanner whose previous status was +/// [oldStatus], after scanning the line [line]. +_FileReadStatus _updatedStatus(_FileReadStatus oldStatus, String line) { + _FileReadStatus lineStatus; + if (line.trim().startsWith('//')) { + lineStatus = _FileReadStatus.comments; + } else if (line.trim().startsWith('import')) { + lineStatus = _FileReadStatus.imports; + } else { + lineStatus = _FileReadStatus.finished; + } + + _FileReadStatus newStatus; + switch (oldStatus) { + case _FileReadStatus.comments: + newStatus = + (line.trim().isEmpty || lineStatus == _FileReadStatus.comments) + ? _FileReadStatus.comments + : lineStatus; + break; + case _FileReadStatus.imports: + newStatus = (line.trim().isEmpty || lineStatus == _FileReadStatus.imports) + ? _FileReadStatus.imports + : _FileReadStatus.finished; + break; + case _FileReadStatus.finished: + newStatus = oldStatus; + break; + } + return newStatus; +} + +Map _createSegments(String sourceDirectoryPath) { + final files = Directory(sourceDirectoryPath) + .listSync(recursive: true) + .whereType() + .toList(); + + var subsegments = {}; + var subsegmentPrologues = {}; + + var appearedSubsegments = {}; + + for (final file in files) { + // Process file. + + final content = file.readAsStringSync(); + final lines = const LineSplitter().convert(content); + + var status = _FileReadStatus.comments; + + final prologue = StringBuffer(); + + final activeSubsegments = {}; + + for (final line in lines) { + // Update status. + + status = _updatedStatus(status, line); + + if (status != _FileReadStatus.finished) { + prologue.writeln(line); + } + + // Process run commands. + + if (line.trim().startsWith(beginSubsegment)) { + final argumentString = line.replaceFirst(beginSubsegment, '').trim(); + var arguments = argumentString.isEmpty + ? [] + : argumentString.split(RegExp(r'\s+')); + + for (final argument in arguments) { + if (activeSubsegments.contains(argument)) { + throw PreformatterException( + 'BEGIN $argument is used twice in file ${file.path}'); + } else if (appearedSubsegments.contains(argument)) { + throw PreformatterException('BEGIN $argument is used twice'); + } else { + activeSubsegments.add(argument); + appearedSubsegments.add(argument); + subsegments[argument] = StringBuffer(); + subsegmentPrologues[argument] = prologue.toString(); + } + } + } else if (line.trim().startsWith(endSubsegment)) { + final argumentString = line.replaceFirst(endSubsegment, '').trim(); + final arguments = argumentString.isEmpty + ? [] + : argumentString.split(RegExp(r'\s+')); + + if (arguments.isEmpty && activeSubsegments.length == 1) { + arguments.add(activeSubsegments.first); + } + + for (final argument in arguments) { + if (activeSubsegments.contains(argument)) { + activeSubsegments.remove(argument); + } else { + throw PreformatterException( + 'END $argument is used without a paired BEGIN in ${file.path}'); + } + } + } else { + // Simple line. + + for (final name in activeSubsegments) { + subsegments[name].writeln(line); + } + } + } + + if (activeSubsegments.isNotEmpty) { + throw PreformatterException('File ${file.path} has unpaired BEGIN'); + } + } + + var segments = >{}; + var segmentPrologues = {}; + + // Sometimes a code segment is made up of subsegments. They are marked by + // names with a "#" symbol in it, such as "bottomSheetDemoModal#1" and + // "bottomSheetDemoModal#2". + // The following code groups the subsegments by order into segments. + subsegments.forEach((key, value) { + String name; + double order; + + if (key.contains('#')) { + var parts = key.split('#'); + name = parts[0]; + order = double.parse(parts[1]); + } else { + name = key; + order = 0; + } + + if (!segments.containsKey(name)) { + segments[name] = []; + } + segments[name].add( + TaggedString( + text: value.toString(), + order: order, + ), + ); + + segmentPrologues[name] = subsegmentPrologues[key]; + }); + + segments.forEach((key, value) { + value.sort((ts1, ts2) => (ts1.order - ts2.order).sign.round()); + }); + + var answer = {}; + + for (final name in segments.keys) { + final buffer = StringBuffer(); + + buffer.write(segmentPrologues[name].trim()); + buffer.write('\n\n'); + + for (final ts in segments[name]) { + buffer.write(ts.text.trim()); + buffer.write('\n\n'); + } + + answer[name] = buffer.toString(); + } + + return answer; +} + +/// A string [text] together with a number [order], for sorting purposes. +/// Used to store different subsegments of a code segment. +/// The [order] of each subsegment is tagged with the code in order to be +/// sorted in the desired order. +class TaggedString { + TaggedString({this.text, this.order}); + + final String text; + final double order; +} + +void _formatSegments(Map segments, IOSink output) { + output.write(_globalPrologue); + + final sortedNames = segments.keys.toList()..sort(); + for (final name in sortedNames) { + final code = segments[name]; + + output.writeln(' static TextSpan $name (BuildContext context) {'); + output.writeln(' final codeStyle = CodeStyle.of(context);'); + output.writeln(' return TextSpan(children: ['); + + final codeSpans = DartSyntaxPrehighlighter().format(code); + + for (final span in codeSpans) { + output.write(' '); + output.write(span.toString()); + output.write(',\n'); + } + + output.write(' ]); }\n'); + } + + output.write(_globalEpilogue); + + output.close(); +} + +/// Collect code segments, highlight, and write to file. +/// +/// [writeSegments] walks through the directory specified by +/// [sourceDirectoryPath] and reads every file in it, +/// collects code segments marked by "// BEGIN " and "// END", +/// highlights them, and writes to the file specified by +/// [targetFilePath]. If [isDryRun] is true, the output will +/// be written to stdout. +/// +/// The output file is a dart source file with a class "CodeSegments" and +/// static methods of type TextSpan(BuildContext context). +/// Each method generates a widget that displays a segment of code. +/// +/// The target file is overwritten. +void writeSegments( + {String sourceDirectoryPath, String targetFilePath, bool isDryRun}) { + final segments = _createSegments(sourceDirectoryPath); + final output = isDryRun ? stdout : File(targetFilePath).openWrite(); + _formatSegments(segments, output); +} + +class PreformatterException implements Exception { + PreformatterException(this.cause); + String cause; +} \ No newline at end of file diff --git a/example/tool/grind.dart b/example/tool/grind.dart new file mode 100644 index 0000000..cd2bc2f --- /dev/null +++ b/example/tool/grind.dart @@ -0,0 +1,87 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:grinder/grinder.dart'; +import 'package:path/path.dart' as path; + +void main(List args) => grind(args); + + +@Task('Format dart files') +Future format({String path = '.'}) async { + await _runProcess('flutter', ['format', path]); +} + + +@Task('Update code segments') +Future updateCodeSegments() async { + final codeviewerPath = + path.join(Directory.current.path, 'tool', 'codeviewer_cli', 'main.dart'); + + Dart.run(codeviewerPath); + final codeSegmentsPath = path.join('lib', 'codeviewer', 'code_segments.dart'); + await format(path: codeSegmentsPath); +} + +@Task('Verify code segments') +Future verifyCodeSegments() async { + final codeviewerPath = + path.join(Directory.current.path, 'tool', 'codeviewer_cli', 'main.dart'); + + // We use stdin and stdout to write and format the code segments, to avoid + // creating any files. + final codeSegmentsUnformatted = + Dart.run(codeviewerPath, arguments: ['--dry-run'], quiet: true); + final codeSegmentsFormatted = await _startProcess( + path.normalize(path.join(dartVM.path, '../dartfmt')), + input: codeSegmentsUnformatted, + ); + + // Read the original code segments file. + final codeSegmentsPath = path.join( + Directory.current.path, 'lib', 'codeviewer', 'code_segments.dart'); + final expectedCodeSegmentsOutput = + await File(codeSegmentsPath).readAsString(); + + if (codeSegmentsFormatted.trim() != expectedCodeSegmentsOutput.trim()) { + stderr.writeln( + 'The contents of $codeSegmentsPath are different from that produced by ' + 'codeviewer_cli. Did you forget to run `flutter pub run grinder ' + 'update-code-segments` after updating a demo?', + ); + exit(1); + } +} + +Future _runProcess(String executable, List arguments) async { + final result = await Process.run(executable, arguments); + stdout.write(result.stdout); + stderr.write(result.stderr); +} + +// Function to make sure we capture all of the stdout. +// Reference: https://github.com/dart-lang/sdk/issues/31666 +Future _startProcess(String executable, + {List arguments = const [], String input}) async { + final output = []; + final completer = Completer(); + final process = await Process.start(executable, arguments); + process.stdin.writeln(input); + process.stdout.listen( + (event) { + output.addAll(event); + }, + onDone: () async => completer.complete(await process.exitCode), + ); + await process.stdin.close(); + + final exitCode = await completer.future; + if (exitCode != 0) { + stderr.write( + 'Running "$executable ${arguments.join(' ')}" failed with $exitCode.\n', + ); + exit(exitCode); + } + return Future.value(utf8.decoder.convert(output)); +} \ No newline at end of file diff --git a/lib/scaffold/as_scaffold.dart b/lib/scaffold/as_scaffold.dart index 6bf3cc9..3a165a3 100644 --- a/lib/scaffold/as_scaffold.dart +++ b/lib/scaffold/as_scaffold.dart @@ -24,6 +24,9 @@ class ASScaffold extends StatefulWidget { /// `Scaffold` leading final Widget leading; + /// `Scaffold` actions + final List actions; + /// `Scaffold` bottomNavigationBar final Widget bottomNavigationBar; @@ -50,6 +53,7 @@ class ASScaffold extends StatefulWidget { this.endDrawer, this.appBar, this.backgroundColor = kBackgroundColor, + this.actions, }) : super(key: key); @override @@ -74,6 +78,7 @@ class _ASScaffoldState extends State { backgroundColor: kForegroundColor, elevation: 0, leading: widget.leading ?? ASBackButton(), + actions: widget.actions ?? [], centerTitle: true, title: DefaultTextStyle( style: TextStyle(