",
+ );
+
+ for (var sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
+ final section = sections[sectionIndex];
+ if (sectionIndex > 0) {
+ if (sectionIndex.isEven) {
+ newOutput.writeln("
");
+ }
+ newOutput.writeln(
+ "
\n",
+ );
+ }
+ _writeSection(newOutput, section);
+ newOutput.writeln('\n
');
+ }
+
+ newOutput.writeln("
");
+
+ final tocFile = File(path.join(dirPath, '_toc.md'));
+ try {
+ final oldContents = tocFile.readAsStringSync();
+
+ if (oldContents != newOutput.toString()) {
+ if (justCheck) {
+ stderr.writeln(
+ 'Error: The Effective Dart TOC needs to be regenerated!',
+ );
+ return 1;
+ } else {
+ tocFile.writeAsStringSync(newOutput.toString());
+ print('Successfully updated the Effective Dart TOC.');
+ }
+ } else {
+ print('The Effective Dart TOC is up to date!');
+ }
+ } catch (e, stackTrace) {
+ stderr.writeln('Error: Failed to read or write the TOC file.');
+ stderr.writeln(e);
+ stderr.writeln(stackTrace);
+ return 1;
+ }
+
+ return 0;
+}
+
+void _writeSection(StringSink out, _Section section) {
+ out.writeln('\n### ${section.name}\n');
+ for (final subsection in section.subsections) {
+ out.writeln('\n**${subsection.name}**\n');
+ for (final rule in subsection.rules) {
+ final link = section.uri.resolve('#${rule.fragment}');
+ out.writeln("*
${rule.html}");
+ }
+ }
+}
+
+class _Rule {
+ final String html;
+ final String fragment;
+
+ factory _Rule(md.Element element) {
+ var name = _concatenatedText(element);
+ var html = md.renderToHtml(element.children ?? const []);
+
+ // Handle headers with an explicit "{#anchor-text}" anchor.
+ var match = _anchorPattern.firstMatch(name);
+
+ final String fragment;
+ if (match != null) {
+ // Pull the anchor from the name.
+ name = (match[1] ?? '').trim();
+ fragment = match[2] ?? '';
+
+ // Strip it from the HTML too.
+ match = _anchorPattern.firstMatch(html);
+ if (match != null) {
+ html = (match[1] ?? '').trim();
+ }
+ } else {
+ fragment = _generateAnchorHash(name);
+ }
+
+ if (html.endsWith('.')) {
+ throw Exception(
+ "Effective Dart rule '$name' ends with a period when it shouldn't.",
+ );
+ }
+
+ html += '.';
+
+ return _Rule._(html, fragment);
+ }
+
+ _Rule._(this.html, this.fragment);
+}
+
+class _Section {
+ final Uri uri;
+ final File file;
+ final String name;
+ final List<_Subsection> subsections = [];
+
+ _Section(String dirPath, String filename)
+ : file = File(path.join(dirPath, filename)),
+ uri = Uri.parse('/effective-dart/').resolve(filename.split('.').first),
+ name = '${filename[0].toUpperCase()}'
+ "${filename.substring(1).split('.').first}";
+}
+
+class _Subsection {
+ final String name;
+ final String fragment;
+ final List<_Rule> rules = [];
+
+ _Subsection(md.Element element)
+ : name = _concatenatedText(element),
+ fragment = _generateAnchorHash(_concatenatedText(element));
+}
+
+/// Generates a valid HTML anchor from [text].
+String _generateAnchorHash(String text) => text
+ .toLowerCase()
+ .trim()
+ .replaceFirst(RegExp(r'^[^a-z]+'), '')
+ .replaceAll(RegExp(r'[^a-z0-9 _-]'), '')
+ .replaceAll(RegExp(r'\s'), '-');
+
+/// Concatenates the text found in all the children of [element].
+String _concatenatedText(md.Element element) {
+ final children = element.children;
+
+ if (children == null) {
+ return '';
+ }
+
+ return children
+ .map((child) => (child is md.Text)
+ ? _unescape.convert(child.text)
+ : (child is md.Element)
+ ? _concatenatedText(child)
+ : _unescape.convert(child.textContent))
+ .join('');
+}
diff --git a/tool/dart_site/lib/src/commands/refresh_excerpts.dart b/tool/dart_site/lib/src/commands/refresh_excerpts.dart
new file mode 100644
index 0000000000..9dcc3f7114
--- /dev/null
+++ b/tool/dart_site/lib/src/commands/refresh_excerpts.dart
@@ -0,0 +1,186 @@
+// Copyright (c) 2024, 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:async';
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart' as path;
+import '../utils.dart';
+
+final class RefreshExcerptsCommand extends Command
{
+ static const String _verboseFlag = 'verbose';
+ static const String _deleteCacheFlag = 'delete-cache';
+ static const String _failOnUpdateFlag = 'fail-on-update';
+
+ RefreshExcerptsCommand() {
+ argParser.addFlag(
+ _verboseFlag,
+ defaultsTo: false,
+ help: 'Show verbose logging.',
+ );
+ argParser.addFlag(
+ _deleteCacheFlag,
+ defaultsTo: false,
+ help: 'Delete dart build tooling and cache files after running.',
+ );
+ argParser.addFlag(
+ _failOnUpdateFlag,
+ defaultsTo: false,
+ help: 'Fails if updates were needed.',
+ );
+ }
+
+ @override
+ String get description => 'Updates all code excerpts on the site.';
+
+ @override
+ String get name => 'refresh-excerpts';
+
+ @override
+ Future run() async => _refreshExcerpts(
+ verboseLogging: argResults.get(_verboseFlag, false),
+ deleteCache: argResults.get(_deleteCacheFlag, false),
+ failOnUpdate: argResults.get(_failOnUpdateFlag, false));
+}
+
+Future _refreshExcerpts({
+ bool verboseLogging = false,
+ bool deleteCache = false,
+ bool failOnUpdate = false,
+}) async {
+ final repositoryRoot = Directory.current.path;
+ final temporaryRoot = Directory.systemTemp.path;
+ final fragments = path.join(temporaryRoot, '_excerpter_fragments');
+
+ // Delete any existing fragments.
+ final fragmentsDirectory = Directory(fragments);
+ if (fragmentsDirectory.existsSync()) {
+ if (verboseLogging) {
+ print('Deleting previously generated $fragments.');
+ }
+ fragmentsDirectory.deleteSync(recursive: true);
+ }
+
+ print('Running the code excerpt fragment generator...');
+
+ // Run the code excerpter tool to generate the fragments used for updates.
+ final excerptsGenerated = Process.runSync(Platform.resolvedExecutable, [
+ 'run',
+ 'build_runner',
+ 'build',
+ '--delete-conflicting-outputs',
+ '--config',
+ 'excerpt',
+ '--output',
+ fragments,
+ ]);
+
+ if (verboseLogging) {
+ print(excerptsGenerated.stdout);
+ }
+
+ // If the excerpt fragments were not generated successfully,
+ // then output the error log and return 1 to indicate failure.
+ if (excerptsGenerated.exitCode != 0) {
+ stderr.writeln('Error: Excerpt generation failed:');
+ stderr.writeln(excerptsGenerated.stderr);
+ return 1;
+ }
+
+ print('Code excerpt fragments generated successfully.');
+
+ // Verify the fragments directory for the /examples was generated properly.
+ if (!Directory(path.join(fragments, 'examples')).existsSync()) {
+ stderr.writeln(
+ 'Error: The examples fragments folder was not generated!',
+ );
+ return 1;
+ }
+
+ // A collection of replacements for the code excerpt updater tool
+ // to run by default.
+ // They must not contain (unencoded/unescaped) spaces.
+ const replacements = [
+ // Allows use of //!
to force a line break (against dart format)
+ r'/\/\/!
//g;',
+ // Replace the word ellipsis, with optional parentheses.
+ r'/ellipsis(<\w+>)?(\(\))?;?/.../g;',
+ // Replace commented out ellipses: /*...*/ --> ...
+ r'/\/\*(\s*\.\.\.\s*)\*\//$1/g;',
+ // Replace brackets with commented out ellipses: {/*-...-*/} --> ...
+ r'/\{\/\*-(\s*\.\.\.\s*)-\*\/\}/$1/g;',
+ // Remove markers declaring an analysis issue or runtime error.
+ r'/\/\/!(analysis-issue|runtime-error)[^\n]*//g;',
+ ];
+
+ final srcDirectoryPath = path.join(repositoryRoot, 'src');
+ final updaterArguments = [
+ '--fragment-dir-path',
+ path.join(fragments, 'examples'),
+ '--src-dir-path',
+ 'examples',
+ if (verboseLogging) '--log-fine',
+ '--yaml',
+ '--no-escape-ng-interpolation',
+ '--replace=${replacements.join('')}',
+ '--write-in-place',
+ srcDirectoryPath,
+ ];
+
+ print('Running the code excerpt updater...');
+
+ // Open a try block so we can guarantee
+ // any temporary files are deleted.
+ try {
+ // Run the code excerpt updater tool to update the code excerpts
+ // in the /src directory.
+ final excerptsUpdated = Process.runSync(Platform.resolvedExecutable, [
+ 'run',
+ 'code_excerpt_updater',
+ ...updaterArguments,
+ ]);
+
+ final updateOutput = excerptsUpdated.stdout.toString();
+ final updateErrors = excerptsUpdated.stderr.toString();
+
+ final bool success;
+
+ // Inform the user if the updater failed, didn't need to make any updates,
+ // or successfully refreshed each excerpt.
+ if (excerptsUpdated.exitCode != 0 || updateErrors.contains('Error')) {
+ stderr.writeln('Error: Excerpt generation failed:');
+ stderr.write(updateErrors);
+ success = false;
+ } else if (updateOutput.contains('0 out of')) {
+ if (verboseLogging) {
+ print(updateOutput);
+ }
+ print('All code excerpts are already up to date!');
+ success = true;
+ } else {
+ stdout.write(updateOutput);
+
+ if (failOnUpdate) {
+ stderr.writeln('Error: Some code excerpts needed to be updated!');
+ success = false;
+ } else {
+ print('Code excerpts successfully refreshed!');
+ success = true;
+ }
+ }
+ return success ? 0 : 1;
+ } finally {
+ // Clean up Dart build cache files if desired.
+ if (deleteCache) {
+ if (verboseLogging) {
+ print('Removing cached build files.');
+ }
+ final dartBuildCache = Directory(path.join('.dart_tool', 'build'));
+ if (dartBuildCache.existsSync()) {
+ dartBuildCache.deleteSync(recursive: true);
+ }
+ }
+ }
+}
diff --git a/tool/dart_site/lib/src/commands/test_dart.dart b/tool/dart_site/lib/src/commands/test_dart.dart
new file mode 100644
index 0000000000..9da973424f
--- /dev/null
+++ b/tool/dart_site/lib/src/commands/test_dart.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2024, 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:io';
+
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart' as path;
+
+import '../utils.dart';
+
+final class TestDartCommand extends Command {
+ static const String _verboseFlag = 'verbose';
+
+ TestDartCommand() {
+ argParser.addFlag(
+ _verboseFlag,
+ defaultsTo: false,
+ help: 'Show verbose logging.',
+ );
+ }
+
+ @override
+ String get description => 'Run tests on the site infra and examples.';
+
+ @override
+ String get name => 'test-dart';
+
+ @override
+ Future run() async => _testDart(
+ verboseLogging: argResults.get(_verboseFlag, false),
+ );
+}
+
+int _testDart({
+ bool verboseLogging = false,
+}) {
+ final directoriesToTest = [
+ path.join('tool', 'dart_site'),
+ ...dartProjectExampleDirectories,
+ ];
+
+ print('Testing code...');
+
+ for (final directory in directoriesToTest) {
+ if (verboseLogging) {
+ print('Testing code in $directory...');
+ }
+
+ if (runPubGetIfNecessary(directory) case final pubGetResult
+ when pubGetResult != 0) {
+ return pubGetResult;
+ }
+
+ final dartTestOutput = Process.runSync(
+ Platform.executable,
+ const [
+ 'test',
+ '--reporter',
+ 'expanded', // Non-animated expanded output looks better in CI and logs.
+ ],
+ workingDirectory: directory,
+ );
+
+ if (dartTestOutput.exitCode != 0) {
+ final normalOutput = dartTestOutput.stdout.toString();
+ final errorOutput = dartTestOutput.stderr.toString();
+
+ // It's ok if the test directory is not found.
+ if (!errorOutput.contains('No test') &&
+ !normalOutput.contains('Could not find package `test`') &&
+ !normalOutput.contains('No tests were')) {
+ stderr.write(normalOutput);
+ stderr.writeln('Error: Tests in $directory failed:');
+ stderr.write(errorOutput);
+ return 1;
+ }
+
+ if (verboseLogging) {
+ print('No tests found or ran in $directory.');
+ }
+ } else {
+ if (verboseLogging) {
+ print('All tests passed in $directory.');
+ }
+ }
+ }
+
+ print('All tests passed successfully!');
+
+ return 0;
+}
diff --git a/tool/dart_site/lib/src/commands/verify_firebase_json.dart b/tool/dart_site/lib/src/commands/verify_firebase_json.dart
new file mode 100644
index 0000000000..c799496534
--- /dev/null
+++ b/tool/dart_site/lib/src/commands/verify_firebase_json.dart
@@ -0,0 +1,156 @@
+// Copyright (c) 2024, 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';
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+
+final class VerifyFirebaseJsonCommand extends Command {
+ @override
+ String get description => 'Verify the firebase.json file is valid and '
+ 'meets the site standards.';
+
+ @override
+ String get name => 'verify-firebase-json';
+
+ @override
+ Future run() async => _verifyFirebaseJson();
+}
+
+int _verifyFirebaseJson() {
+ final firebaseFile = File('firebase.json');
+
+ if (!firebaseFile.existsSync()) {
+ stderr.writeln(
+ 'Cannot find the firebase.json file in the current directory.',
+ );
+ return 1;
+ }
+
+ try {
+ final firebaseConfigString = firebaseFile.readAsStringSync();
+ final firebaseConfig =
+ jsonDecode(firebaseConfigString) as Map;
+
+ final hostingConfig = firebaseConfig['hosting'] as Map?;
+
+ if (hostingConfig == null) {
+ stderr.writeln(
+ "Error: The firebase.json file is missing a top-level 'hosting' entry.",
+ );
+ return 1;
+ }
+
+ final redirects = hostingConfig['redirects'];
+
+ if (redirects == null) {
+ stdout.writeln(
+ 'There are no redirects specified within the firebase.json file.',
+ );
+ return 0;
+ }
+
+ if (redirects is! List