blob: 7f1e0c0cd596b8be3fbeff116ca43862e89ee36b [file] [edit]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. 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' as convert;
import 'dart:io';
import 'package:path/path.dart' as p;
String stripDartdoc(String str) {
str = str.trim();
if (str.startsWith('/**')) {
str = str.substring('/**'.length);
if (str.endsWith('*/')) str = str.substring(0, str.length - '*/'.length);
return str
.split('\n')
.map((line) => line.trimLeft())
.map((line) {
if (line.startsWith('* ')) return line.substring(2);
if (line.startsWith('*')) return line.substring(1);
return line;
})
.map((line) => line.trimRight())
.skipWhile((l) => l.trim().isEmpty)
.join('\n');
} else {
return str
.split('\n')
.map((line) => line.trimLeft())
.map((line) {
if (line.startsWith('/// ')) return line.substring(4);
if (line.startsWith('///')) return line.substring(3);
return line;
})
.map((line) => line.trimRight())
.join('\n');
}
}
String titleCase(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
String htmlEscape(String text) => convert.htmlEscape.convert(text);
/// Returns an adjusted lexical compare.
///
/// Adjustments include sorting case-insensitive.
int adjustedLexicalCompare(String a, String b) {
if (a.isEmpty || b.isEmpty) return a.compareTo(b);
var lowerA = a.toLowerCase();
var lowerB = b.toLowerCase();
return lowerA.compareTo(lowerB);
}
String stringToAnchorId(String str) => Uri.encodeQueryComponent(str);
typedef _JsonType = Map<String, dynamic>;
String? getPackageRoot(Directory fromDir, String packageName) {
var packageFile = _findPackageConfig(fromDir);
if (packageFile == null) return null;
var packageInfo =
convert.jsonDecode(packageFile.readAsStringSync()) as _JsonType;
for (var info in packageInfo['packages'] as List<_JsonType>) {
if (info['name'] == packageName) {
return File.fromUri(Uri.parse(info['rootUri'] as String)).path;
}
}
return null;
}
File? _findPackageConfig(Directory dir) {
var file = File(p.join(dir.path, '.dart_tool', 'package_config.json'));
if (file.existsSync()) return file;
return dir.parent == dir ? null : _findPackageConfig(dir.parent);
}
extension DirectoryExtension on Directory {
List<FileSystemEntity> listSyncSorted({bool recursive = false}) {
var entites = listSync(recursive: recursive);
entites.sort((a, b) => adjustedLexicalCompare(a.path, b.path));
return entites;
}
}
extension FileSystemEntityExtension on FileSystemEntity {
String get name => p.basename(path);
}
extension FileExtension on File {
bool get publicMarkdownFile =>
!name.startsWith('_') && p.extension(path) == '.md';
}
class Outline {
final List<Heading> items = [];
void add(Heading heading) {
if (items.isEmpty) {
items.add(heading);
} else if (items.last.level >= heading.level) {
items.add(heading);
} else {
items.last.add(heading);
}
}
String get asHtml {
var buf = StringBuffer(
'<ul class="table-of-contents table-of-contents__left-border">\n',
);
for (var item in items) {
buf.writeln(item.asHtml);
}
buf.write('</ul>');
return buf.toString();
}
}
class Heading {
final String label;
final String? id;
final int level;
final List<Heading> children = [];
Heading(this.label, {this.id, this.level = 2});
void add(Heading heading) {
if (children.isEmpty) {
children.add(heading);
} else if (children.last.level >= heading.level) {
children.add(heading);
} else {
children.last.add(heading);
}
}
String get asHtml {
var buf = StringBuffer('<li>');
var href = id == null ? '' : 'href="#$id"';
buf.write(
'<a class="table-of-contents__link toc-highlight" $href>$label</a>',
);
if (children.isNotEmpty) {
buf.writeln();
buf.writeln('<ul>');
for (var child in children) {
buf.writeln(child.asHtml);
}
buf.writeln('</ul>');
}
buf.write('</li>');
return buf.toString();
}
@override
String toString() => id == null ? '$label h$level' : '$label h$level ($id)';
}
class Stats {
final Stopwatch timer = Stopwatch();
int fileCount = 0;
int byteCount = 0;
void start() {
timer.start();
}
void genFile(File file) {
fileCount++;
byteCount += file.lengthSync();
}
String get elapsedSeconds =>
(timer.elapsedMilliseconds / 1000.0).toStringAsFixed(1);
String get sizeDesc {
const bytesPerMB = 1024.0 * 1024;
if (byteCount >= bytesPerMB) {
return '${(byteCount / (1024.0 * 1024.0)).toStringAsFixed(1)}MB';
} else {
return '${byteCount ~/ 1024.0}k';
}
}
void stop() {
timer.stop();
}
}
extension StringExtension on String {
/// A simple implementation of `package:path`'s `relative` function.
///
/// The implementation from `package:path` ends up calling `Directory.current`
/// enough that that call along can account for 60% of this tool's run time.
String pathRelative({required String fromDir}) {
if (fromDir.isEmpty || fromDir == '.') {
return this;
}
var paths = split('/');
var froms = fromDir.split('/');
while (paths.isNotEmpty && froms.isNotEmpty && paths.first == froms.first) {
paths = paths.sublist(1);
froms = froms.sublist(1);
}
for (var _ in froms) {
paths.insert(0, '..');
}
return paths.join('/');
}
}