// 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; default: newStatus = oldStatus; } 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); _FileReadStatus? 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( {required String sourceDirectoryPath, String? targetFilePath, required 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; }