张萌 4 years ago
commit c9745829ce

1
.gitignore vendored

@ -72,3 +72,4 @@ build/
!**/ios/**/default.mode2v3 !**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser !**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3 !**/ios/**/default.perspectivev3
code_segments.dart

@ -0,0 +1,5 @@
{
"cSpell.words": [
"ansu"
]
}

@ -6,6 +6,12 @@
## Getting Started ## Getting Started
### 生成关键代码
```bash
flutter pub run grinder update-code-segments
```
### 安装ansu_ui ### 安装ansu_ui
#### Android #### Android
@ -16,6 +22,27 @@ Change the minimum Android sdk version to 21 (or higher) in your `android/app/bu
minSdkVersion 21 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) [@laiiihz](http://192.168.2.201:8099/u/laiiihz)

@ -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<CodeStyle>();
}
@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;
}

@ -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<Widget> 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,
);
}
}

@ -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,
),
),
),
);
}
}

@ -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<CodeView> {
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))),
)),
);
}
}

@ -1,6 +1,10 @@
// BEGIN button
import 'package:ansu_ui/ansu_ui.dart'; 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/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class ExampleButton extends StatefulWidget { class ExampleButton extends StatefulWidget {
ExampleButton({Key key}) : super(key: key); ExampleButton({Key key}) : super(key: key);
@ -15,6 +19,14 @@ class _ExampleButtonState extends State<ExampleButton> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ASScaffold( return ASScaffold(
title: 'ASButton', title: 'ASButton',
actions: [
IconButton(
icon: Icon(Icons.code, color: Colors.black54),
onPressed: () => Get.to(CodeView(
text: (context) => CodeSegments.button(context),
)),
),
],
body: ListView( body: ListView(
children: [ children: [
ListTile( ListTile(
@ -162,3 +174,5 @@ class _ExampleButtonState extends State<ExampleButton> {
); );
} }
} }
// END

@ -1,4 +1,7 @@
// BEGIN scaffold
import 'package:ansu_ui/ansu_ui.dart'; 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/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -28,6 +31,19 @@ class _ExampleScaffoldState extends State<ExampleScaffold>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ASScaffold( return ASScaffold(
title: '框架 Scaffold', title: '框架 Scaffold',
actions: [
IconButton(
icon: Icon(
Icons.code,
color: Colors.black54,
),
onPressed: () => Get.to(
CodeView(
text: (context) => CodeSegments.scaffold(context),
),
),
)
],
appBarBottom: ASTabBar( appBarBottom: ASTabBar(
items: tabs, items: tabs,
isScrollable: true, isScrollable: true,
@ -66,3 +82,5 @@ class _ExampleScaffoldState extends State<ExampleScaffold>
); );
} }
} }
// END

@ -8,6 +8,20 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.4" 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: bot_toast:
dependency: transitive dependency: transitive
description: description:
@ -29,6 +43,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.2.0-nullsafety.1" 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: collection:
dependency: transitive dependency: transitive
description: description:
@ -43,6 +64,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
file:
dependency: transitive
description:
name: file
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.2.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -81,6 +109,20 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.17.1" 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: http:
dependency: transitive dependency: transitive
description: description:
@ -116,6 +158,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.16.1" version: "0.16.1"
js:
dependency: transitive
description:
name: js
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.2"
lpinyin: lpinyin:
dependency: transitive dependency: transitive
description: description:
@ -130,6 +179,20 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.3.0-nullsafety.3" 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: path:
dependency: transitive dependency: transitive
description: description:

@ -18,6 +18,8 @@ dependencies:
ansu_ui: ansu_ui:
path: ../ path: ../
get: get:
grinder:
args:
flutter: flutter:
uses-material-design: true uses-material-design: true

@ -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<String> 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,
);
}

@ -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<CodeSpan> format(String src);
}
class DartSyntaxPrehighlighter extends SyntaxPrehighlighter {
DartSyntaxPrehighlighter() {
_spans = <_HighlightSpan>[];
}
static const List<String> _keywords = <String>[
'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<String> _builtInTypes = <String>[
'int',
'double',
'num',
'bool',
];
String _src;
StringScanner _scanner;
List<_HighlightSpan> _spans;
@override
List<CodeSpan> format(String src) {
_src = src;
_scanner = StringScanner(_src);
if (_generateSpans()) {
// Successfully parsed the code
final formattedText = <CodeSpan>[];
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;
}

@ -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<String, String> _createSegments(String sourceDirectoryPath) {
final files = Directory(sourceDirectoryPath)
.listSync(recursive: true)
.whereType<File>()
.toList();
var subsegments = <String, StringBuffer>{};
var subsegmentPrologues = <String, String>{};
var appearedSubsegments = <String>{};
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 = <String>{};
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
? <String>[]
: 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
? <String>[]
: 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 = <String, List<TaggedString>>{};
var segmentPrologues = <String, String>{};
// 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 = <String, String>{};
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<String, String> 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 <segment_name>" 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;
}

@ -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<String> args) => grind(args);
@Task('Format dart files')
Future<void> format({String path = '.'}) async {
await _runProcess('flutter', ['format', path]);
}
@Task('Update code segments')
Future<void> 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<void> 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<void> _runProcess(String executable, List<String> 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<String> _startProcess(String executable,
{List<String> arguments = const [], String input}) async {
final output = <int>[];
final completer = Completer<int>();
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<String>.value(utf8.decoder.convert(output));
}

@ -24,6 +24,9 @@ class ASScaffold extends StatefulWidget {
/// `Scaffold` leading /// `Scaffold` leading
final Widget leading; final Widget leading;
/// `Scaffold` actions
final List<Widget> actions;
/// `Scaffold` bottomNavigationBar /// `Scaffold` bottomNavigationBar
final Widget bottomNavigationBar; final Widget bottomNavigationBar;
@ -50,6 +53,7 @@ class ASScaffold extends StatefulWidget {
this.endDrawer, this.endDrawer,
this.appBar, this.appBar,
this.backgroundColor = kBackgroundColor, this.backgroundColor = kBackgroundColor,
this.actions,
}) : super(key: key); }) : super(key: key);
@override @override
@ -74,6 +78,7 @@ class _ASScaffoldState extends State<ASScaffold> {
backgroundColor: kForegroundColor, backgroundColor: kForegroundColor,
elevation: 0, elevation: 0,
leading: widget.leading ?? ASBackButton(), leading: widget.leading ?? ASBackButton(),
actions: widget.actions ?? [],
centerTitle: true, centerTitle: true,
title: DefaultTextStyle( title: DefaultTextStyle(
style: TextStyle( style: TextStyle(

Loading…
Cancel
Save