Revert "Refactor html generator into frontend and backend (#2115)" (#2120)

This reverts commit 983b3d6d9d2210cd80733c0b617816bd85a25937.
diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart
index 721bcec..66a8a8a 100644
--- a/lib/dartdoc.dart
+++ b/lib/dartdoc.dart
@@ -44,58 +44,10 @@
       : super(optionSet, dir);
 }
 
-class DartdocFileWriter implements FileWriter {
-  final String outputDir;
-  final Map<String, Warnable> _fileElementMap = {};
-  @override
-  final Set<String> writtenFiles = Set();
-
-  DartdocFileWriter(this.outputDir);
-
-  @override
-  void write(String filePath, Object content,
-      {bool allowOverwrite, Warnable element}) {
-    // Replace '/' separators with proper separators for the platform.
-    String outFile = path.joinAll(filePath.split('/'));
-
-    allowOverwrite ??= false;
-    if (!allowOverwrite) {
-      if (_fileElementMap.containsKey(outFile)) {
-        assert(element != null,
-            'Attempted overwrite of ${outFile} without corresponding element');
-        Warnable originalElement = _fileElementMap[outFile];
-        Iterable<Warnable> referredFrom =
-            originalElement != null ? [originalElement] : null;
-        element?.warn(PackageWarning.duplicateFile,
-            message: outFile, referredFrom: referredFrom);
-      }
-    }
-    _fileElementMap[outFile] = element;
-
-    var file = File(path.join(outputDir, outFile));
-    var parent = file.parent;
-    if (!parent.existsSync()) {
-      parent.createSync(recursive: true);
-    }
-
-    if (content is String) {
-      file.writeAsStringSync(content);
-    } else if (content is List<int>) {
-      file.writeAsBytesSync(content);
-    } else {
-      throw ArgumentError.value(
-          content, 'content', '`content` must be `String` or `List<int>`.');
-    }
-
-    writtenFiles.add(outFile);
-    logProgress(outFile);
-  }
-}
-
 /// Generates Dart documentation for all public Dart libraries in the given
 /// directory.
 class Dartdoc extends PackageBuilder {
-  final Generator generator;
+  final List<Generator> generators;
   final Set<String> writtenFiles = Set();
   Directory outputDir;
 
@@ -103,20 +55,29 @@
   final StreamController<String> _onCheckProgress =
       StreamController(sync: true);
 
-  Dartdoc._(DartdocOptionContext config, this.generator) : super(config) {
+  Dartdoc._(DartdocOptionContext config, this.generators) : super(config) {
     outputDir = Directory(config.output)..createSync(recursive: true);
+    generators.forEach((g) => g.onFileCreated.listen(logProgress));
   }
 
   /// An asynchronous factory method that builds Dartdoc's file writers
   /// and returns a Dartdoc object with them.
   static Future<Dartdoc> withDefaultGenerators(
       DartdocGeneratorOptionContext config) async {
-    return Dartdoc._(config, await initHtmlGenerator(config));
+    List<Generator> generators = await initHtmlGenerators(config);
+    return Dartdoc._(config, generators);
   }
 
   /// An asynchronous factory method that builds
   static Future<Dartdoc> withEmptyGenerator(DartdocOptionContext config) async {
-    return Dartdoc._(config, await initEmptyGenerator(config));
+    List<Generator> generators = await initEmptyGenerators(config);
+    return Dartdoc._(config, generators);
+  }
+
+  /// Basic synchronous factory that gives a stripped down Dartdoc that won't
+  /// use generators.  Useful for testing.
+  factory Dartdoc.withoutGenerators(DartdocOptionContext config) {
+    return Dartdoc._(config, []);
   }
 
   Stream<String> get onCheckProgress => _onCheckProgress.stream;
@@ -133,20 +94,19 @@
     double seconds;
     packageGraph = await buildPackageGraph();
     seconds = _stopwatch.elapsedMilliseconds / 1000.0;
-    int libs = packageGraph.libraries.length;
-    logInfo("Initialized dartdoc with ${libs} librar${libs == 1 ? 'y' : 'ies'} "
+    logInfo(
+        "Initialized dartdoc with ${packageGraph.libraries.length} librar${packageGraph.libraries.length == 1 ? 'y' : 'ies'} "
         "in ${seconds.toStringAsFixed(1)} seconds");
     _stopwatch.reset();
 
-    final generator = this.generator;
-    if (generator != null) {
+    if (generators.isNotEmpty) {
       // Create the out directory.
       if (!outputDir.existsSync()) outputDir.createSync(recursive: true);
 
-      DartdocFileWriter writer = DartdocFileWriter(outputDir.path);
-      await generator.generate(packageGraph, writer);
-
-      writtenFiles.addAll(writer.writtenFiles);
+      for (var generator in generators) {
+        await generator.generate(packageGraph, outputDir.path);
+        writtenFiles.addAll(generator.writtenFiles.keys.map(path.normalize));
+      }
       if (config.validateLinks && writtenFiles.isNotEmpty) {
         validateLinks(packageGraph, outputDir.path);
       }
@@ -162,8 +122,8 @@
     }
 
     seconds = _stopwatch.elapsedMilliseconds / 1000.0;
-    libs = packageGraph.localPublicLibraries.length;
-    logInfo("Documented ${libs} public librar${libs == 1 ? 'y' : 'ies'} "
+    logInfo(
+        "Documented ${packageGraph.localPublicLibraries.length} public librar${packageGraph.localPublicLibraries.length == 1 ? 'y' : 'ies'} "
         "in ${seconds.toStringAsFixed(1)} seconds");
     return DartdocResults(config.topLevelPackageMeta, packageGraph, outputDir);
   }
diff --git a/lib/src/empty_generator.dart b/lib/src/empty_generator.dart
index 595a666..666df06 100644
--- a/lib/src/empty_generator.dart
+++ b/lib/src/empty_generator.dart
@@ -4,7 +4,6 @@
 
 import 'package:dartdoc/src/dartdoc_options.dart';
 import 'package:dartdoc/src/generator.dart';
-import 'package:dartdoc/src/logging.dart';
 import 'package:dartdoc/src/model/model.dart';
 import 'package:dartdoc/src/model_utils.dart';
 import 'package:dartdoc/src/warnings.dart';
@@ -14,23 +13,34 @@
 /// it were.
 class EmptyGenerator extends Generator {
   @override
-  Future generate(PackageGraph _packageGraph, FileWriter writer) {
-    logProgress(_packageGraph.defaultPackage.documentationAsHtml);
+  Future generate(PackageGraph _packageGraph, String outputDirectoryPath) {
+    _onFileCreated.add(_packageGraph.defaultPackage.documentationAsHtml);
     for (var package in Set.from([_packageGraph.defaultPackage])
       ..addAll(_packageGraph.localPackages)) {
       for (var category in filterNonDocumented(package.categories)) {
-        logProgress(category.documentationAsHtml);
+        _onFileCreated.add(category.documentationAsHtml);
       }
 
       for (Library lib in filterNonDocumented(package.libraries)) {
         filterNonDocumented(lib.allModelElements)
-            .forEach((m) => logProgress(m.documentationAsHtml));
+            .forEach((m) => _onFileCreated.add(m.documentationAsHtml));
       }
     }
     return null;
   }
+
+  final StreamController<void> _onFileCreated = StreamController(sync: true);
+
+  @override
+
+  /// Implementation fires on each model element processed rather than
+  /// file creation.
+  Stream<void> get onFileCreated => _onFileCreated.stream;
+
+  @override
+  final Map<String, Warnable> writtenFiles = {};
 }
 
-Future<Generator> initEmptyGenerator(DartdocOptionContext config) async {
-  return EmptyGenerator();
+Future<List<Generator>> initEmptyGenerators(DartdocOptionContext config) async {
+  return [EmptyGenerator()];
 }
diff --git a/lib/src/generator.dart b/lib/src/generator.dart
index 28a0e8e..6ea61e1 100644
--- a/lib/src/generator.dart
+++ b/lib/src/generator.dart
@@ -15,23 +15,20 @@
 import 'package:dartdoc/src/warnings.dart';
 import 'package:path/path.dart' as path;
 
-abstract class FileWriter {
-  /// All filenames written by this generator.
-  Set<String> get writtenFiles;
-
-  /// Write [content] to a file at [filePath].
-  void write(String filePath, Object content,
-      {bool allowOverwrite, Warnable element});
-}
-
 /// An abstract class that defines a generator that generates documentation for
 /// a given package.
 ///
 /// Generators can generate documentation in different formats: html, json etc.
 abstract class Generator {
-  /// Generate the documentation for the given package using the specified
-  /// writer. Completes the returned future when done.
-  Future generate(PackageGraph packageGraph, FileWriter writer);
+  /// Generate the documentation for the given package in the specified
+  /// directory. Completes the returned future when done.
+  Future generate(PackageGraph packageGraph, String outputDirectoryPath);
+
+  /// Fires when a file is created.
+  Stream<void> get onFileCreated;
+
+  /// Fetches all filenames written by this generator.
+  Map<String, Warnable> get writtenFiles;
 }
 
 /// Dartdoc options related to generators generally.
diff --git a/lib/src/generator_frontend.dart b/lib/src/generator_frontend.dart
deleted file mode 100644
index ff65f1e..0000000
--- a/lib/src/generator_frontend.dart
+++ /dev/null
@@ -1,343 +0,0 @@
-// Copyright (c) 2019, 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' show File;
-
-import 'package:dartdoc/src/generator.dart';
-import 'package:dartdoc/src/logging.dart';
-import 'package:dartdoc/src/model/model.dart';
-import 'package:dartdoc/src/model_utils.dart';
-import 'package:dartdoc/src/warnings.dart';
-import 'package:path/path.dart' as path;
-
-/// [Generator] that delegates rendering to a [GeneratorBackend] and delegates
-/// file creation to a [FileWriter].
-class GeneratorFrontEnd implements Generator {
-  final GeneratorBackend _generatorBackend;
-
-  GeneratorFrontEnd(this._generatorBackend);
-
-  @override
-  Future generate(PackageGraph packageGraph, FileWriter writer) async {
-    List<Indexable> indexElements = <Indexable>[];
-    _generateDocs(packageGraph, writer, indexElements);
-    await _generatorBackend.generateAdditionalFiles(writer, packageGraph);
-
-    List<Categorization> categories = indexElements
-        .whereType<Categorization>()
-        .where((e) => e.hasCategorization)
-        .toList();
-    _generatorBackend.generateCategoryJson(writer, categories);
-    _generatorBackend.generateSearchIndex(writer, indexElements);
-  }
-
-  // Traverses the package graph and collects elements for the search index.
-  void _generateDocs(PackageGraph packageGraph, FileWriter writer,
-      List<Indexable> indexAccumulator) {
-    if (packageGraph == null) return;
-
-    logInfo('documenting ${packageGraph.defaultPackage.name}');
-    _generatorBackend.generatePackage(
-        writer, packageGraph, packageGraph.defaultPackage);
-
-    for (var package in packageGraph.localPackages) {
-      for (var category in filterNonDocumented(package.categories)) {
-        logInfo('Generating docs for category ${category.name} from '
-            '${category.package.fullyQualifiedName}...');
-        indexAccumulator.add(category);
-        _generatorBackend.generateCategory(writer, packageGraph, category);
-      }
-
-      for (var lib in filterNonDocumented(package.libraries)) {
-        logInfo('Generating docs for library ${lib.name} from '
-            '${lib.element.source.uri}...');
-        if (!lib.isAnonymous && !lib.hasDocumentation) {
-          packageGraph.warnOnElement(lib, PackageWarning.noLibraryLevelDocs);
-        }
-        indexAccumulator.add(lib);
-        _generatorBackend.generateLibrary(writer, packageGraph, lib);
-
-        for (var clazz in filterNonDocumented(lib.allClasses)) {
-          indexAccumulator.add(clazz);
-          _generatorBackend.generateClass(writer, packageGraph, lib, clazz);
-
-          for (var constructor in filterNonDocumented(clazz.constructors)) {
-            if (!constructor.isCanonical) continue;
-
-            indexAccumulator.add(constructor);
-            _generatorBackend.generateConstructor(
-                writer, packageGraph, lib, clazz, constructor);
-          }
-
-          for (var constant in filterNonDocumented(clazz.constants)) {
-            if (!constant.isCanonical) continue;
-
-            indexAccumulator.add(constant);
-            _generatorBackend.generateConstant(
-                writer, packageGraph, lib, clazz, constant);
-          }
-
-          for (var property in filterNonDocumented(clazz.staticProperties)) {
-            if (!property.isCanonical) continue;
-
-            indexAccumulator.add(property);
-            _generatorBackend.generateProperty(
-                writer, packageGraph, lib, clazz, property);
-          }
-
-          for (var property in filterNonDocumented(clazz.allInstanceFields)) {
-            if (!property.isCanonical) continue;
-
-            indexAccumulator.add(property);
-            _generatorBackend.generateProperty(
-                writer, packageGraph, lib, clazz, property);
-          }
-
-          for (var method in filterNonDocumented(clazz.allInstanceMethods)) {
-            if (!method.isCanonical) continue;
-
-            indexAccumulator.add(method);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, clazz, method);
-          }
-
-          for (var operator in filterNonDocumented(clazz.allOperators)) {
-            if (!operator.isCanonical) continue;
-
-            indexAccumulator.add(operator);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, clazz, operator);
-          }
-
-          for (var method in filterNonDocumented(clazz.staticMethods)) {
-            if (!method.isCanonical) continue;
-
-            indexAccumulator.add(method);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, clazz, method);
-          }
-        }
-
-        for (var extension in filterNonDocumented(lib.extensions)) {
-          indexAccumulator.add(extension);
-          _generatorBackend.generateExtension(
-              writer, packageGraph, lib, extension);
-
-          for (var constant in filterNonDocumented(extension.constants)) {
-            indexAccumulator.add(constant);
-            _generatorBackend.generateConstant(
-                writer, packageGraph, lib, extension, constant);
-          }
-
-          for (var property
-              in filterNonDocumented(extension.staticProperties)) {
-            indexAccumulator.add(property);
-            _generatorBackend.generateProperty(
-                writer, packageGraph, lib, extension, property);
-          }
-
-          for (var method
-              in filterNonDocumented(extension.allPublicInstanceMethods)) {
-            indexAccumulator.add(method);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, extension, method);
-          }
-
-          for (var method in filterNonDocumented(extension.staticMethods)) {
-            indexAccumulator.add(method);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, extension, method);
-          }
-
-          for (var operator in filterNonDocumented(extension.allOperators)) {
-            indexAccumulator.add(operator);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, extension, operator);
-          }
-
-          for (var property
-              in filterNonDocumented(extension.allInstanceFields)) {
-            indexAccumulator.add(property);
-            _generatorBackend.generateProperty(
-                writer, packageGraph, lib, extension, property);
-          }
-        }
-
-        for (var mixin in filterNonDocumented(lib.mixins)) {
-          indexAccumulator.add(mixin);
-          _generatorBackend.generateMixin(writer, packageGraph, lib, mixin);
-
-          for (var constructor in filterNonDocumented(mixin.constructors)) {
-            if (!constructor.isCanonical) continue;
-
-            indexAccumulator.add(constructor);
-            _generatorBackend.generateConstructor(
-                writer, packageGraph, lib, mixin, constructor);
-          }
-
-          for (var constant in filterNonDocumented(mixin.constants)) {
-            if (!constant.isCanonical) continue;
-            indexAccumulator.add(constant);
-            _generatorBackend.generateConstant(
-                writer, packageGraph, lib, mixin, constant);
-          }
-
-          for (var property in filterNonDocumented(mixin.staticProperties)) {
-            if (!property.isCanonical) continue;
-
-            indexAccumulator.add(property);
-            _generatorBackend.generateConstant(
-                writer, packageGraph, lib, mixin, property);
-          }
-
-          for (var property in filterNonDocumented(mixin.allInstanceFields)) {
-            if (!property.isCanonical) continue;
-
-            indexAccumulator.add(property);
-            _generatorBackend.generateConstant(
-                writer, packageGraph, lib, mixin, property);
-          }
-
-          for (var method in filterNonDocumented(mixin.allInstanceMethods)) {
-            if (!method.isCanonical) continue;
-
-            indexAccumulator.add(method);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, mixin, method);
-          }
-
-          for (var operator in filterNonDocumented(mixin.allOperators)) {
-            if (!operator.isCanonical) continue;
-
-            indexAccumulator.add(operator);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, mixin, operator);
-          }
-
-          for (var method in filterNonDocumented(mixin.staticMethods)) {
-            if (!method.isCanonical) continue;
-
-            indexAccumulator.add(method);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, mixin, method);
-          }
-        }
-
-        for (var eNum in filterNonDocumented(lib.enums)) {
-          indexAccumulator.add(eNum);
-          _generatorBackend.generateEnum(writer, packageGraph, lib, eNum);
-
-          for (var property in filterNonDocumented(eNum.allInstanceFields)) {
-            indexAccumulator.add(property);
-            _generatorBackend.generateConstant(
-                writer, packageGraph, lib, eNum, property);
-          }
-          for (var operator in filterNonDocumented(eNum.allOperators)) {
-            indexAccumulator.add(operator);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, eNum, operator);
-          }
-          for (var method in filterNonDocumented(eNum.allInstanceMethods)) {
-            indexAccumulator.add(method);
-            _generatorBackend.generateMethod(
-                writer, packageGraph, lib, eNum, method);
-          }
-        }
-
-        for (var constant in filterNonDocumented(lib.constants)) {
-          indexAccumulator.add(constant);
-          _generatorBackend.generateTopLevelConstant(
-              writer, packageGraph, lib, constant);
-        }
-
-        for (var property in filterNonDocumented(lib.properties)) {
-          indexAccumulator.add(property);
-          _generatorBackend.generateTopLevelProperty(
-              writer, packageGraph, lib, property);
-        }
-
-        for (var function in filterNonDocumented(lib.functions)) {
-          indexAccumulator.add(function);
-          _generatorBackend.generateFunction(
-              writer, packageGraph, lib, function);
-        }
-
-        for (var typeDef in filterNonDocumented(lib.typedefs)) {
-          indexAccumulator.add(typeDef);
-          _generatorBackend.generateTypeDef(writer, packageGraph, lib, typeDef);
-        }
-      }
-    }
-  }
-}
-
-abstract class GeneratorBackend {
-  /// Emit json describing the [categories] defined by the package.
-  void generateCategoryJson(FileWriter writer, List<Categorization> categories);
-
-  /// Emit json catalog of [indexedElements] for use with a search index.
-  void generateSearchIndex(FileWriter writer, List<Indexable> indexedElements);
-
-  /// Emit documentation content for the [package].
-  void generatePackage(FileWriter writer, PackageGraph graph, Package package);
-
-  /// Emit documentation content for the [category].
-  void generateCategory(
-      FileWriter writer, PackageGraph graph, Category category);
-
-  /// Emit documentation content for the [library].
-  void generateLibrary(FileWriter writer, PackageGraph graph, Library library);
-
-  /// Emit documentation content for the [clazz].
-  void generateClass(
-      FileWriter writer, PackageGraph graph, Library library, Class clazz);
-
-  /// Emit documentation content for the [eNum].
-  void generateEnum(
-      FileWriter writer, PackageGraph graph, Library library, Enum eNum);
-
-  /// Emit documentation content for the [mixin].
-  void generateMixin(
-      FileWriter writer, PackageGraph graph, Library library, Mixin mixin);
-
-  /// Emit documentation content for the [constructor].
-  void generateConstructor(FileWriter writer, PackageGraph graph,
-      Library library, Class clazz, Constructor constructor);
-
-  /// Emit documentation content for the [field].
-  void generateConstant(FileWriter writer, PackageGraph graph, Library library,
-      Container clazz, Field field);
-
-  /// Emit documentation content for the [field].
-  void generateProperty(FileWriter writer, PackageGraph graph, Library library,
-      Container clazz, Field field);
-
-  /// Emit documentation content for the [method].
-  void generateMethod(FileWriter writer, PackageGraph graph, Library library,
-      Container clazz, Method method);
-
-  /// Emit documentation content for the [extension].
-  void generateExtension(FileWriter writer, PackageGraph graph, Library library,
-      Extension extension);
-
-  /// Emit documentation content for the [function].
-  void generateFunction(FileWriter writer, PackageGraph graph, Library library,
-      ModelFunction function);
-
-  /// Emit documentation content for the [constant].
-  void generateTopLevelConstant(FileWriter writer, PackageGraph graph,
-      Library library, TopLevelVariable constant);
-
-  /// Emit documentation content for the [property].
-  void generateTopLevelProperty(FileWriter writer, PackageGraph graph,
-      Library library, TopLevelVariable property);
-
-  /// Emit documentation content for the [typedef].
-  void generateTypeDef(
-      FileWriter writer, PackageGraph graph, Library library, Typedef typedef);
-
-  /// Emit files not specific to a Dart language element.
-  void generateAdditionalFiles(FileWriter writer, PackageGraph graph);
-}
diff --git a/lib/src/generator_utils.dart b/lib/src/generator_utils.dart
deleted file mode 100644
index 59fa128..0000000
--- a/lib/src/generator_utils.dart
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) 2019, 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 'package:collection/collection.dart';
-import 'package:dartdoc/src/model/categorization.dart';
-import 'package:dartdoc/src/model/enclosed_element.dart';
-import 'package:dartdoc/src/model/indexable.dart';
-
-/// Convenience function to generate category JSON since different generators
-/// will likely want the same content for this.
-String generateCategoryJson(Iterable<Categorization> categories, bool pretty) {
-  var encoder = pretty ? JsonEncoder.withIndent(' ') : JsonEncoder();
-  final List<Map> indexItems = categories.map((Categorization e) {
-    Map data = {
-      'name': e.name,
-      'qualifiedName': e.fullyQualifiedName,
-      'href': e.href,
-      'type': e.kind,
-    };
-
-    if (e.hasCategoryNames) data['categories'] = e.categoryNames;
-    if (e.hasSubCategoryNames) data['subcategories'] = e.subCategoryNames;
-    if (e.hasImage) data['image'] = e.image;
-    if (e.hasSamples) data['samples'] = e.samples;
-    return data;
-  }).toList();
-
-  indexItems.sort((a, b) {
-    var value = compareNatural(a['qualifiedName'], b['qualifiedName']);
-    if (value == 0) {
-      value = compareNatural(a['type'], b['type']);
-    }
-    return value;
-  });
-
-  return encoder.convert(indexItems);
-}
-
-/// Convenience function to generate search index JSON since different
-/// generators will likely want the same content for this.
-String generateSearchIndexJson(
-    Iterable<Indexable> indexedElements, bool pretty) {
-  var encoder = pretty ? JsonEncoder.withIndent(' ') : JsonEncoder();
-  final List<Map> indexItems = indexedElements.map((Indexable e) {
-    Map data = {
-      'name': e.name,
-      'qualifiedName': e.fullyQualifiedName,
-      'href': e.href,
-      'type': e.kind,
-      'overriddenDepth': e.overriddenDepth,
-    };
-    if (e is EnclosedElement) {
-      EnclosedElement ee = e as EnclosedElement;
-      data['enclosedBy'] = {
-        'name': ee.enclosingElement.name,
-        'type': ee.enclosingElement.kind
-      };
-
-      data['qualifiedName'] = e.fullyQualifiedName;
-    }
-    return data;
-  }).toList();
-
-  indexItems.sort((a, b) {
-    var value = compareNatural(a['qualifiedName'], b['qualifiedName']);
-    if (value == 0) {
-      value = compareNatural(a['type'], b['type']);
-    }
-    return value;
-  });
-
-  return encoder.convert(indexItems);
-}
diff --git a/lib/src/html/html_generator.dart b/lib/src/html/html_generator.dart
index 1224005..29be73c 100644
--- a/lib/src/html/html_generator.dart
+++ b/lib/src/html/html_generator.dart
@@ -4,14 +4,173 @@
 
 library dartdoc.html_generator;
 
-import 'dart:async' show Future;
+import 'dart:async' show Future, StreamController, Stream;
+import 'dart:io' show Directory, File;
+import 'dart:isolate';
 
 import 'package:dartdoc/dartdoc.dart';
+import 'package:dartdoc/src/empty_generator.dart';
 import 'package:dartdoc/src/generator.dart';
-import 'package:dartdoc/src/generator_frontend.dart';
-import 'package:dartdoc/src/html/html_generator_backend.dart';
+import 'package:dartdoc/src/html/html_generator_instance.dart';
+import 'package:dartdoc/src/html/template_data.dart';
+import 'package:dartdoc/src/html/templates.dart';
+import 'package:dartdoc/src/model/model.dart';
+import 'package:dartdoc/src/warnings.dart';
+import 'package:path/path.dart' as path;
 
-Future<Generator> initHtmlGenerator(GeneratorContext context) async {
-  var backend = await HtmlGeneratorBackend.fromContext(context);
-  return GeneratorFrontEnd(backend);
+typedef Renderer = String Function(String input);
+
+// Generation order for libraries:
+//   constants
+//   typedefs
+//   properties
+//   functions
+//   enums
+//   classes
+//   exceptions
+//
+// Generation order for classes:
+//   constants
+//   static properties
+//   static methods
+//   properties
+//   constructors
+//   operators
+//   methods
+
+class HtmlGenerator extends Generator {
+  final Templates _templates;
+  final HtmlGeneratorOptions _options;
+  HtmlGeneratorInstance _instance;
+
+  final StreamController<void> _onFileCreated = StreamController(sync: true);
+
+  @override
+  Stream<void> get onFileCreated => _onFileCreated.stream;
+
+  @override
+  final Map<String, Warnable> writtenFiles = {};
+
+  static Future<HtmlGenerator> create(
+      {HtmlGeneratorOptions options,
+      List<String> headers,
+      List<String> footers,
+      List<String> footerTexts}) async {
+    var templates;
+    String dirname = options?.templatesDir;
+    if (dirname != null) {
+      Directory templateDir = Directory(dirname);
+      templates = await Templates.fromDirectory(templateDir,
+          headerPaths: headers,
+          footerPaths: footers,
+          footerTextPaths: footerTexts);
+    } else {
+      templates = await Templates.createDefault(
+          headerPaths: headers,
+          footerPaths: footers,
+          footerTextPaths: footerTexts);
+    }
+
+    return HtmlGenerator._(options ?? HtmlGeneratorOptions(), templates);
+  }
+
+  HtmlGenerator._(this._options, this._templates);
+
+  @override
+
+  /// Actually write out the documentation for [packageGraph].
+  /// Stores the HtmlGeneratorInstance so we can access it in [writtenFiles].
+  Future generate(PackageGraph packageGraph, String outputDirectoryPath) async {
+    assert(_instance == null);
+
+    var enabled = true;
+    void write(String filePath, Object content,
+        {bool allowOverwrite, Warnable element}) {
+      allowOverwrite ??= false;
+      if (!enabled) {
+        throw StateError('`write` was called after `generate` completed.');
+      }
+      if (!allowOverwrite) {
+        if (writtenFiles.containsKey(filePath)) {
+          assert(element != null,
+              'Attempted overwrite of ${filePath} without corresponding element');
+          Warnable originalElement = writtenFiles[filePath];
+          Iterable<Warnable> referredFrom =
+              originalElement != null ? [originalElement] : null;
+          element?.warn(PackageWarning.duplicateFile,
+              message: filePath, referredFrom: referredFrom);
+        }
+      }
+
+      var file = File(path.join(outputDirectoryPath, filePath));
+      var parent = file.parent;
+      if (!parent.existsSync()) {
+        parent.createSync(recursive: true);
+      }
+
+      if (content is String) {
+        file.writeAsStringSync(content);
+      } else if (content is List<int>) {
+        file.writeAsBytesSync(content);
+      } else {
+        throw ArgumentError.value(
+            content, 'content', '`content` must be `String` or `List<int>`.');
+      }
+      _onFileCreated.add(file);
+      writtenFiles[filePath] = element;
+    }
+
+    try {
+      _instance =
+          HtmlGeneratorInstance(_options, _templates, packageGraph, write);
+      await _instance.generate();
+    } finally {
+      enabled = false;
+    }
+  }
+}
+
+class HtmlGeneratorOptions implements HtmlOptions {
+  final String faviconPath;
+  final bool prettyIndexJson;
+  final String templatesDir;
+
+  @override
+  final String relCanonicalPrefix;
+
+  @override
+  final String toolVersion;
+
+  @override
+  final bool useBaseHref;
+
+  HtmlGeneratorOptions(
+      {this.relCanonicalPrefix,
+      this.faviconPath,
+      String toolVersion,
+      this.prettyIndexJson = false,
+      this.templatesDir,
+      this.useBaseHref = false})
+      : this.toolVersion = toolVersion ?? 'unknown';
+}
+
+/// Initialize and setup the generators.
+Future<List<Generator>> initHtmlGenerators(GeneratorContext context) async {
+  // TODO(jcollins-g): Rationalize based on GeneratorContext all the way down
+  // through the generators.
+  HtmlGeneratorOptions options = HtmlGeneratorOptions(
+      relCanonicalPrefix: context.relCanonicalPrefix,
+      toolVersion: dartdocVersion,
+      faviconPath: context.favicon,
+      prettyIndexJson: context.prettyIndexJson,
+      templatesDir: context.templatesDir,
+      useBaseHref: context.useBaseHref);
+  return [
+    await HtmlGenerator.create(
+      options: options,
+      headers: context.header,
+      footers: context.footer,
+      footerTexts: context.footerTextPaths,
+    )
+  ];
 }
diff --git a/lib/src/html/html_generator_backend.dart b/lib/src/html/html_generator_backend.dart
deleted file mode 100644
index da0dfb1..0000000
--- a/lib/src/html/html_generator_backend.dart
+++ /dev/null
@@ -1,237 +0,0 @@
-// Copyright (c) 2019, 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:dartdoc/dartdoc.dart';
-import 'package:dartdoc/src/generator_frontend.dart';
-import 'package:dartdoc/src/generator_utils.dart' as generator_util;
-import 'package:dartdoc/src/html/resource_loader.dart' as loader;
-import 'package:dartdoc/src/html/resources.g.dart' as resources;
-import 'package:dartdoc/src/html/template_data.dart';
-import 'package:dartdoc/src/html/templates.dart';
-import 'package:dartdoc/src/model/model.dart';
-import 'package:dartdoc/src/model/package.dart';
-import 'package:dartdoc/src/model/package_graph.dart';
-import 'package:dartdoc/src/warnings.dart';
-import 'package:mustache/mustache.dart';
-import 'package:path/path.dart' as path;
-
-/// Configuration options for the html backend.
-class HtmlBackendOptions implements HtmlOptions {
-  @override
-  final String relCanonicalPrefix;
-  @override
-  final String toolVersion;
-
-  final String favicon;
-
-  final bool prettyIndexJson;
-
-  @override
-  final bool useBaseHref;
-
-  HtmlBackendOptions(
-      {this.relCanonicalPrefix,
-      this.toolVersion,
-      this.favicon,
-      this.prettyIndexJson = false,
-      this.useBaseHref = false});
-}
-
-/// GeneratorBackend for html output.
-class HtmlGeneratorBackend implements GeneratorBackend {
-  final HtmlBackendOptions _options;
-  final Templates _templates;
-
-  static Future<HtmlGeneratorBackend> fromContext(
-      GeneratorContext context) async {
-    Templates templates = await Templates.fromContext(context);
-    // TODO(jcollins-g): Rationalize based on GeneratorContext all the way down
-    // through the generators.
-    HtmlOptions options = HtmlBackendOptions(
-      relCanonicalPrefix: context.relCanonicalPrefix,
-      toolVersion: dartdocVersion,
-      favicon: context.favicon,
-      prettyIndexJson: context.prettyIndexJson,
-      useBaseHref: context.useBaseHref,
-    );
-    return HtmlGeneratorBackend(options, templates);
-  }
-
-  HtmlGeneratorBackend(HtmlBackendOptions options, this._templates)
-      : this._options = (options ?? HtmlBackendOptions());
-
-  /// Helper method to bind template data and emit the content to the writer.
-  void _render(FileWriter writer, String filename, Template template,
-      TemplateData data) {
-    String content = template.renderString(data);
-    if (!_options.useBaseHref) {
-      content = content.replaceAll(HTMLBASE_PLACEHOLDER, data.htmlBase);
-    }
-    writer.write(filename, content,
-        element: data.self is Warnable ? data.self : null);
-  }
-
-  @override
-  void generateCategoryJson(
-      FileWriter writer, List<Categorization> categories) {
-    String json = generator_util.generateCategoryJson(
-        categories, _options.prettyIndexJson);
-    if (!_options.useBaseHref) {
-      json = json.replaceAll(HTMLBASE_PLACEHOLDER, '');
-    }
-    writer.write(path.join('categories.json'), '${json}\n');
-  }
-
-  @override
-  void generateSearchIndex(FileWriter writer, List<Indexable> indexedElements) {
-    String json = generator_util.generateSearchIndexJson(
-        indexedElements, _options.prettyIndexJson);
-    if (!_options.useBaseHref) {
-      json = json.replaceAll(HTMLBASE_PLACEHOLDER, '');
-    }
-    writer.write(path.join('index.json'), '${json}\n');
-  }
-
-  @override
-  void generatePackage(FileWriter writer, PackageGraph graph, Package package) {
-    TemplateData data = PackageTemplateData(_options, graph, package);
-    _render(writer, package.filePath, _templates.indexTemplate, data);
-    _render(writer, '__404error.html', _templates.errorTemplate, data);
-  }
-
-  @override
-  void generateCategory(
-      FileWriter writer, PackageGraph packageGraph, Category category) {
-    TemplateData data = CategoryTemplateData(_options, packageGraph, category);
-    _render(writer, category.filePath, _templates.categoryTemplate, data);
-  }
-
-  @override
-  void generateLibrary(
-      FileWriter writer, PackageGraph packageGraph, Library lib) {
-    TemplateData data = LibraryTemplateData(_options, packageGraph, lib);
-    _render(writer, lib.filePath, _templates.libraryTemplate, data);
-  }
-
-  @override
-  void generateClass(
-      FileWriter writer, PackageGraph packageGraph, Library lib, Class clazz) {
-    TemplateData data = ClassTemplateData(_options, packageGraph, lib, clazz);
-    _render(writer, clazz.filePath, _templates.classTemplate, data);
-  }
-
-  @override
-  void generateExtension(FileWriter writer, PackageGraph packageGraph,
-      Library lib, Extension extension) {
-    TemplateData data =
-        ExtensionTemplateData(_options, packageGraph, lib, extension);
-    _render(writer, extension.filePath, _templates.extensionTemplate, data);
-  }
-
-  @override
-  void generateMixin(
-      FileWriter writer, PackageGraph packageGraph, Library lib, Mixin mixin) {
-    TemplateData data = MixinTemplateData(_options, packageGraph, lib, mixin);
-    _render(writer, mixin.filePath, _templates.mixinTemplate, data);
-  }
-
-  @override
-  void generateConstructor(FileWriter writer, PackageGraph packageGraph,
-      Library lib, Class clazz, Constructor constructor) {
-    TemplateData data = ConstructorTemplateData(
-        _options, packageGraph, lib, clazz, constructor);
-
-    _render(writer, constructor.filePath, _templates.constructorTemplate, data);
-  }
-
-  @override
-  void generateEnum(
-      FileWriter writer, PackageGraph packageGraph, Library lib, Enum eNum) {
-    TemplateData data = EnumTemplateData(_options, packageGraph, lib, eNum);
-
-    _render(writer, eNum.filePath, _templates.enumTemplate, data);
-  }
-
-  @override
-  void generateFunction(FileWriter writer, PackageGraph packageGraph,
-      Library lib, ModelFunction function) {
-    TemplateData data =
-        FunctionTemplateData(_options, packageGraph, lib, function);
-
-    _render(writer, function.filePath, _templates.functionTemplate, data);
-  }
-
-  @override
-  void generateMethod(FileWriter writer, PackageGraph packageGraph, Library lib,
-      Container clazz, Method method) {
-    TemplateData data =
-        MethodTemplateData(_options, packageGraph, lib, clazz, method);
-
-    _render(writer, method.filePath, _templates.methodTemplate, data);
-  }
-
-  @override
-  void generateConstant(FileWriter writer, PackageGraph packageGraph,
-          Library lib, Container clazz, Field property) =>
-      generateProperty(writer, packageGraph, lib, clazz, property);
-
-  @override
-  void generateProperty(FileWriter writer, PackageGraph packageGraph,
-      Library lib, Container clazz, Field property) {
-    TemplateData data =
-        PropertyTemplateData(_options, packageGraph, lib, clazz, property);
-
-    _render(writer, property.filePath, _templates.propertyTemplate, data);
-  }
-
-  @override
-  void generateTopLevelProperty(FileWriter writer, PackageGraph packageGraph,
-      Library lib, TopLevelVariable property) {
-    TemplateData data =
-        TopLevelPropertyTemplateData(_options, packageGraph, lib, property);
-
-    _render(
-        writer, property.filePath, _templates.topLevelPropertyTemplate, data);
-  }
-
-  @override
-  void generateTopLevelConstant(FileWriter writer, PackageGraph packageGraph,
-          Library lib, TopLevelVariable property) =>
-      generateTopLevelProperty(writer, packageGraph, lib, property);
-
-  @override
-  void generateTypeDef(FileWriter writer, PackageGraph packageGraph,
-      Library lib, Typedef typeDef) {
-    TemplateData data =
-        TypedefTemplateData(_options, packageGraph, lib, typeDef);
-
-    _render(writer, typeDef.filePath, _templates.typeDefTemplate, data);
-  }
-
-  @override
-  void generateAdditionalFiles(FileWriter writer, PackageGraph graph) async {
-    await _copyResources(writer);
-    if (_options.favicon != null) {
-      // Allow overwrite of favicon.
-      var bytes = File(_options.favicon).readAsBytesSync();
-      writer.write(path.join('static-assets', 'favicon.png'), bytes,
-          allowOverwrite: true);
-    }
-  }
-
-  Future _copyResources(FileWriter writer) async {
-    final prefix = 'package:dartdoc/resources/';
-    for (String resourcePath in resources.resource_names) {
-      if (!resourcePath.startsWith(prefix)) {
-        throw StateError('Resource paths must start with $prefix, '
-            'encountered $resourcePath');
-      }
-      String destFileName = resourcePath.substring(prefix.length);
-      writer.write(path.join('static-assets', destFileName),
-          await loader.loadAsBytes(resourcePath));
-    }
-  }
-}
diff --git a/lib/src/html/html_generator_instance.dart b/lib/src/html/html_generator_instance.dart
new file mode 100644
index 0000000..e82fe1b
--- /dev/null
+++ b/lib/src/html/html_generator_instance.dart
@@ -0,0 +1,413 @@
+// Copyright (c) 2014, 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' show Future;
+import 'dart:convert' show JsonEncoder;
+import 'dart:io' show File;
+
+import 'package:collection/collection.dart' show compareNatural;
+import 'package:dartdoc/src/html/html_generator.dart' show HtmlGeneratorOptions;
+import 'package:dartdoc/src/html/resource_loader.dart' as loader;
+import 'package:dartdoc/src/html/resources.g.dart' as resources;
+import 'package:dartdoc/src/html/template_data.dart';
+import 'package:dartdoc/src/html/templates.dart';
+import 'package:dartdoc/src/logging.dart';
+import 'package:dartdoc/src/model/model.dart';
+import 'package:dartdoc/src/model_utils.dart';
+import 'package:dartdoc/src/warnings.dart';
+import 'package:mustache/mustache.dart';
+import 'package:path/path.dart' as path;
+
+typedef FileWriter = void Function(String path, Object content,
+    {bool allowOverwrite, Warnable element});
+
+class HtmlGeneratorInstance {
+  final HtmlGeneratorOptions _options;
+  final Templates _templates;
+  final PackageGraph _packageGraph;
+  final List<Indexable> _indexedElements = <Indexable>[];
+  final FileWriter _writer;
+
+  HtmlGeneratorInstance(
+      this._options, this._templates, this._packageGraph, this._writer);
+
+  Future generate() async {
+    if (_packageGraph != null) {
+      _generateDocs();
+      _generateSearchIndex();
+      _generateCategoryJson();
+    }
+
+    await _copyResources();
+    if (_options.faviconPath != null) {
+      var bytes = File(_options.faviconPath).readAsBytesSync();
+      // Allow overwrite of favicon.
+      _writer(path.join('static-assets', 'favicon.png'), bytes,
+          allowOverwrite: true);
+    }
+  }
+
+  void _generateCategoryJson() {
+    var encoder = JsonEncoder.withIndent('  ');
+    final List<Map> indexItems = _categorizationItems.map((Categorization e) {
+      Map data = {
+        'name': e.name,
+        'qualifiedName': e.fullyQualifiedName,
+        'href': e.href,
+        'type': e.kind,
+      };
+
+      if (e.hasCategoryNames) data['categories'] = e.categoryNames;
+      if (e.hasSubCategoryNames) data['subcategories'] = e.subCategoryNames;
+      if (e.hasImage) data['image'] = e.image;
+      if (e.hasSamples) data['samples'] = e.samples;
+      return data;
+    }).toList();
+
+    indexItems.sort((a, b) {
+      var value = compareNatural(a['qualifiedName'], b['qualifiedName']);
+      if (value == 0) {
+        value = compareNatural(a['type'], b['type']);
+      }
+      return value;
+    });
+
+    String json = encoder.convert(indexItems);
+    if (!_options.useBaseHref) {
+      json = json.replaceAll(HTMLBASE_PLACEHOLDER, '');
+    }
+    _writer(path.join('categories.json'), '${json}\n');
+  }
+
+  List<Categorization> _categorizationItems;
+
+  void _generateSearchIndex() {
+    var encoder =
+        _options.prettyIndexJson ? JsonEncoder.withIndent(' ') : JsonEncoder();
+    _categorizationItems = [];
+
+    final List<Map> indexItems = _indexedElements.map((Indexable e) {
+      if (e is Categorization && e.hasCategorization) {
+        _categorizationItems.add(e);
+      }
+      Map data = {
+        'name': e.name,
+        'qualifiedName': e.fullyQualifiedName,
+        'href': e.href,
+        'type': e.kind,
+        'overriddenDepth': e.overriddenDepth,
+      };
+      if (e is EnclosedElement) {
+        EnclosedElement ee = e as EnclosedElement;
+        data['enclosedBy'] = {
+          'name': ee.enclosingElement.name,
+          'type': ee.enclosingElement.kind
+        };
+
+        data['qualifiedName'] = e.fullyQualifiedName;
+      }
+      return data;
+    }).toList();
+
+    indexItems.sort((a, b) {
+      var value = compareNatural(a['qualifiedName'], b['qualifiedName']);
+      if (value == 0) {
+        value = compareNatural(a['type'], b['type']);
+      }
+      return value;
+    });
+
+    String json = encoder.convert(indexItems);
+    if (!_options.useBaseHref) {
+      json = json.replaceAll(HTMLBASE_PLACEHOLDER, '');
+    }
+    _writer(path.join('index.json'), '${json}\n');
+  }
+
+  void _generateDocs() {
+    if (_packageGraph == null) return;
+
+    generatePackage(_packageGraph, _packageGraph.defaultPackage);
+
+    for (var package in _packageGraph.localPackages) {
+      for (var category in filterNonDocumented(package.categories)) {
+        generateCategory(_packageGraph, category);
+      }
+
+      for (var lib in filterNonDocumented(package.libraries)) {
+        generateLibrary(_packageGraph, lib);
+
+        for (var clazz in filterNonDocumented(lib.allClasses)) {
+          generateClass(_packageGraph, lib, clazz);
+
+          for (var constructor in filterNonDocumented(clazz.constructors)) {
+            if (!constructor.isCanonical) continue;
+            generateConstructor(_packageGraph, lib, clazz, constructor);
+          }
+
+          for (var constant in filterNonDocumented(clazz.constants)) {
+            if (!constant.isCanonical) continue;
+            generateConstant(_packageGraph, lib, clazz, constant);
+          }
+
+          for (var property in filterNonDocumented(clazz.staticProperties)) {
+            if (!property.isCanonical) continue;
+            generateProperty(_packageGraph, lib, clazz, property);
+          }
+
+          for (var property in filterNonDocumented(clazz.allInstanceFields)) {
+            if (!property.isCanonical) continue;
+            generateProperty(_packageGraph, lib, clazz, property);
+          }
+
+          for (var method in filterNonDocumented(clazz.allInstanceMethods)) {
+            if (!method.isCanonical) continue;
+            generateMethod(_packageGraph, lib, clazz, method);
+          }
+
+          for (var operator in filterNonDocumented(clazz.allOperators)) {
+            if (!operator.isCanonical) continue;
+            generateMethod(_packageGraph, lib, clazz, operator);
+          }
+
+          for (var method in filterNonDocumented(clazz.staticMethods)) {
+            if (!method.isCanonical) continue;
+            generateMethod(_packageGraph, lib, clazz, method);
+          }
+        }
+
+        for (var extension in filterNonDocumented(lib.extensions)) {
+          generateExtension(_packageGraph, lib, extension);
+
+          for (var constant in filterNonDocumented(extension.constants)) {
+            generateConstant(_packageGraph, lib, extension, constant);
+          }
+
+          for (var property
+              in filterNonDocumented(extension.staticProperties)) {
+            generateProperty(_packageGraph, lib, extension, property);
+          }
+
+          for (var method
+              in filterNonDocumented(extension.allPublicInstanceMethods)) {
+            generateMethod(_packageGraph, lib, extension, method);
+          }
+
+          for (var method in filterNonDocumented(extension.staticMethods)) {
+            generateMethod(_packageGraph, lib, extension, method);
+          }
+
+          for (var operator in filterNonDocumented(extension.allOperators)) {
+            generateMethod(_packageGraph, lib, extension, operator);
+          }
+
+          for (var property
+              in filterNonDocumented(extension.allInstanceFields)) {
+            generateProperty(_packageGraph, lib, extension, property);
+          }
+        }
+
+        for (var mixin in filterNonDocumented(lib.mixins)) {
+          generateMixins(_packageGraph, lib, mixin);
+          for (var constructor in filterNonDocumented(mixin.constructors)) {
+            if (!constructor.isCanonical) continue;
+            generateConstructor(_packageGraph, lib, mixin, constructor);
+          }
+
+          for (var constant in filterNonDocumented(mixin.constants)) {
+            if (!constant.isCanonical) continue;
+            generateConstant(_packageGraph, lib, mixin, constant);
+          }
+
+          for (var property in filterNonDocumented(mixin.staticProperties)) {
+            if (!property.isCanonical) continue;
+            generateProperty(_packageGraph, lib, mixin, property);
+          }
+
+          for (var property in filterNonDocumented(mixin.allInstanceFields)) {
+            if (!property.isCanonical) continue;
+            generateProperty(_packageGraph, lib, mixin, property);
+          }
+
+          for (var method in filterNonDocumented(mixin.allInstanceMethods)) {
+            if (!method.isCanonical) continue;
+            generateMethod(_packageGraph, lib, mixin, method);
+          }
+
+          for (var operator in filterNonDocumented(mixin.allOperators)) {
+            if (!operator.isCanonical) continue;
+            generateMethod(_packageGraph, lib, mixin, operator);
+          }
+
+          for (var method in filterNonDocumented(mixin.staticMethods)) {
+            if (!method.isCanonical) continue;
+            generateMethod(_packageGraph, lib, mixin, method);
+          }
+        }
+
+        for (var eNum in filterNonDocumented(lib.enums)) {
+          generateEnum(_packageGraph, lib, eNum);
+          for (var property in filterNonDocumented(eNum.allInstanceFields)) {
+            generateProperty(_packageGraph, lib, eNum, property);
+          }
+          for (var operator in filterNonDocumented(eNum.allOperators)) {
+            generateMethod(_packageGraph, lib, eNum, operator);
+          }
+          for (var method in filterNonDocumented(eNum.allInstanceMethods)) {
+            generateMethod(_packageGraph, lib, eNum, method);
+          }
+        }
+
+        for (var constant in filterNonDocumented(lib.constants)) {
+          generateTopLevelConstant(_packageGraph, lib, constant);
+        }
+
+        for (var property in filterNonDocumented(lib.properties)) {
+          generateTopLevelProperty(_packageGraph, lib, property);
+        }
+
+        for (var function in filterNonDocumented(lib.functions)) {
+          generateFunction(_packageGraph, lib, function);
+        }
+
+        for (var typeDef in filterNonDocumented(lib.typedefs)) {
+          generateTypeDef(_packageGraph, lib, typeDef);
+        }
+      }
+    }
+  }
+
+  void generatePackage(PackageGraph packageGraph, Package package) {
+    TemplateData data = PackageTemplateData(_options, packageGraph, package);
+    logInfo('documenting ${package.name}');
+
+    _build(package.filePath, _templates.indexTemplate, data);
+    _build('__404error.html', _templates.errorTemplate, data);
+  }
+
+  void generateCategory(PackageGraph packageGraph, Category category) {
+    logInfo(
+        'Generating docs for category ${category.name} from ${category.package.fullyQualifiedName}...');
+    TemplateData data = CategoryTemplateData(_options, packageGraph, category);
+
+    _build(category.filePath, _templates.categoryTemplate, data);
+  }
+
+  void generateLibrary(PackageGraph packageGraph, Library lib) {
+    logInfo(
+        'Generating docs for library ${lib.name} from ${lib.element.source.uri}...');
+    if (!lib.isAnonymous && !lib.hasDocumentation) {
+      packageGraph.warnOnElement(lib, PackageWarning.noLibraryLevelDocs);
+    }
+    TemplateData data = LibraryTemplateData(_options, packageGraph, lib);
+
+    _build(lib.filePath, _templates.libraryTemplate, data);
+  }
+
+  void generateClass(PackageGraph packageGraph, Library lib, Class clazz) {
+    TemplateData data = ClassTemplateData(_options, packageGraph, lib, clazz);
+    _build(clazz.filePath, _templates.classTemplate, data);
+  }
+
+  void generateExtension(
+      PackageGraph packageGraph, Library lib, Extension extension) {
+    TemplateData data =
+        ExtensionTemplateData(_options, packageGraph, lib, extension);
+    _build(extension.filePath, _templates.extensionTemplate, data);
+  }
+
+  void generateMixins(PackageGraph packageGraph, Library lib, Mixin mixin) {
+    TemplateData data = MixinTemplateData(_options, packageGraph, lib, mixin);
+    _build(mixin.filePath, _templates.mixinTemplate, data);
+  }
+
+  void generateConstructor(PackageGraph packageGraph, Library lib, Class clazz,
+      Constructor constructor) {
+    TemplateData data = ConstructorTemplateData(
+        _options, packageGraph, lib, clazz, constructor);
+
+    _build(constructor.filePath, _templates.constructorTemplate, data);
+  }
+
+  void generateEnum(PackageGraph packageGraph, Library lib, Enum eNum) {
+    TemplateData data = EnumTemplateData(_options, packageGraph, lib, eNum);
+
+    _build(eNum.filePath, _templates.enumTemplate, data);
+  }
+
+  void generateFunction(
+      PackageGraph packageGraph, Library lib, ModelFunction function) {
+    TemplateData data =
+        FunctionTemplateData(_options, packageGraph, lib, function);
+
+    _build(function.filePath, _templates.functionTemplate, data);
+  }
+
+  void generateMethod(
+      PackageGraph packageGraph, Library lib, Container clazz, Method method) {
+    TemplateData data =
+        MethodTemplateData(_options, packageGraph, lib, clazz, method);
+
+    _build(method.filePath, _templates.methodTemplate, data);
+  }
+
+  void generateConstant(PackageGraph packageGraph, Library lib, Container clazz,
+          Field property) =>
+      generateProperty(packageGraph, lib, clazz, property);
+
+  void generateProperty(
+      PackageGraph packageGraph, Library lib, Container clazz, Field property) {
+    TemplateData data =
+        PropertyTemplateData(_options, packageGraph, lib, clazz, property);
+
+    _build(property.filePath, _templates.propertyTemplate, data);
+  }
+
+  void generateTopLevelProperty(
+      PackageGraph packageGraph, Library lib, TopLevelVariable property) {
+    TemplateData data =
+        TopLevelPropertyTemplateData(_options, packageGraph, lib, property);
+
+    _build(property.filePath, _templates.topLevelPropertyTemplate, data);
+  }
+
+  void generateTopLevelConstant(
+          PackageGraph packageGraph, Library lib, TopLevelVariable property) =>
+      generateTopLevelProperty(packageGraph, lib, property);
+
+  void generateTypeDef(
+      PackageGraph packageGraph, Library lib, Typedef typeDef) {
+    TemplateData data =
+        TypedefTemplateData(_options, packageGraph, lib, typeDef);
+
+    _build(typeDef.filePath, _templates.typeDefTemplate, data);
+  }
+
+  // TODO: change this to use resource_loader
+  Future _copyResources() async {
+    final prefix = 'package:dartdoc/resources/';
+    for (String resourcePath in resources.resource_names) {
+      if (!resourcePath.startsWith(prefix)) {
+        throw StateError('Resource paths must start with $prefix, '
+            'encountered $resourcePath');
+      }
+      String destFileName = resourcePath.substring(prefix.length);
+      _writer(path.join('static-assets', destFileName),
+          await loader.loadAsBytes(resourcePath));
+    }
+  }
+
+  void _build(String filename, Template template, TemplateData data) {
+    // Replaces '/' separators with proper separators for the platform.
+    String outFile = path.joinAll(filename.split('/'));
+    String content = template.renderString(data);
+
+    if (!_options.useBaseHref) {
+      content = content.replaceAll(HTMLBASE_PLACEHOLDER, data.htmlBase);
+    }
+    _writer(outFile, content,
+        element: data.self is Warnable ? data.self : null);
+    if (data.self is Indexable) _indexedElements.add(data.self as Indexable);
+  }
+}
diff --git a/lib/src/html/templates.dart b/lib/src/html/templates.dart
index 2b929c3..a47efde 100644
--- a/lib/src/html/templates.dart
+++ b/lib/src/html/templates.dart
@@ -158,21 +158,6 @@
   final Template topLevelPropertyTemplate;
   final Template typeDefTemplate;
 
-  static Future<Templates> fromContext(GeneratorContext context) {
-    String templatesDir = context.templatesDir;
-    if (templatesDir != null) {
-      return fromDirectory(Directory(templatesDir),
-          headerPaths: context.header,
-          footerPaths: context.footer,
-          footerTextPaths: context.footerTextPaths);
-    } else {
-      return createDefault(
-          headerPaths: context.header,
-          footerPaths: context.footer,
-          footerTextPaths: context.footerTextPaths);
-    }
-  }
-
   static Future<Templates> createDefault(
       {List<String> headerPaths,
       List<String> footerPaths,
diff --git a/test/html_generator_test.dart b/test/html_generator_test.dart
index db62526..460c5dc 100644
--- a/test/html_generator_test.dart
+++ b/test/html_generator_test.dart
@@ -6,9 +6,7 @@
 
 import 'dart:io' show File, Directory;
 
-import 'package:dartdoc/dartdoc.dart';
-import 'package:dartdoc/src/generator_frontend.dart';
-import 'package:dartdoc/src/html/html_generator_backend.dart';
+import 'package:dartdoc/src/html/html_generator.dart';
 import 'package:dartdoc/src/html/templates.dart';
 import 'package:dartdoc/src/html/resources.g.dart';
 import 'package:dartdoc/src/model/package_graph.dart';
@@ -18,12 +16,6 @@
 
 import 'src/utils.dart' as utils;
 
-// Init a generator without a GeneratorContext and with the default file writer.
-Future<Generator> _initGeneratorForTest() async {
-  var backend = HtmlGeneratorBackend(null, await Templates.createDefault());
-  return GeneratorFrontEnd(backend);
-}
-
 void main() {
   group('Templates', () {
     Templates templates;
@@ -76,15 +68,13 @@
   group('HtmlGenerator', () {
     // TODO: Run the HtmlGenerator and validate important constraints.
     group('for a null package', () {
-      Generator generator;
+      HtmlGenerator generator;
       Directory tempOutput;
-      FileWriter writer;
 
       setUp(() async {
-        generator = await _initGeneratorForTest();
+        generator = await HtmlGenerator.create();
         tempOutput = Directory.systemTemp.createTempSync('doc_test_temp');
-        writer = DartdocFileWriter(tempOutput.path);
-        return generator.generate(null, writer);
+        return generator.generate(null, tempOutput.path);
       });
 
       tearDown(() {
@@ -106,17 +96,15 @@
     });
 
     group('for a package that causes duplicate files', () {
-      Generator generator;
+      HtmlGenerator generator;
       PackageGraph packageGraph;
       Directory tempOutput;
-      FileWriter writer;
 
       setUp(() async {
-        generator = await _initGeneratorForTest();
+        generator = await HtmlGenerator.create();
         packageGraph = await utils
             .bootBasicPackage(utils.testPackageDuplicateDir.path, []);
         tempOutput = await Directory.systemTemp.createTemp('doc_test_temp');
-        writer = DartdocFileWriter(tempOutput.path);
       });
 
       tearDown(() {
@@ -126,7 +114,7 @@
       });
 
       test('run generator and verify duplicate file error', () async {
-        await generator.generate(packageGraph, writer);
+        await generator.generate(packageGraph, tempOutput.path);
         expect(generator, isNotNull);
         expect(tempOutput, isNotNull);
         String expectedPath =