Refactor html generator into frontend and backend (#2115)

* Create generator frontend

* Start html generator backend

* rename file

* Init single generator instead of list

* Wire up html backend

* Generator frontend/backend fixes

* Change FileWriter back to typedef

* dartfmt and misc fixes

* Fix typo in parameter name

* Use whereType

* Restructure FileWriter usage

Make FileWriter an abstract class and give it the responsibility for
recording written filenames. This removes the FileWriter wrapping in
GeneratorFrontEnd.

* Fix duplicate file test
diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart
index 66a8a8a..721bcec 100644
--- a/lib/dartdoc.dart
+++ b/lib/dartdoc.dart
@@ -44,10 +44,58 @@
       : 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 List<Generator> generators;
+  final Generator generator;
   final Set<String> writtenFiles = Set();
   Directory outputDir;
 
@@ -55,29 +103,20 @@
   final StreamController<String> _onCheckProgress =
       StreamController(sync: true);
 
-  Dartdoc._(DartdocOptionContext config, this.generators) : super(config) {
+  Dartdoc._(DartdocOptionContext config, this.generator) : 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 {
-    List<Generator> generators = await initHtmlGenerators(config);
-    return Dartdoc._(config, generators);
+    return Dartdoc._(config, await initHtmlGenerator(config));
   }
 
   /// An asynchronous factory method that builds
   static Future<Dartdoc> withEmptyGenerator(DartdocOptionContext config) async {
-    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, []);
+    return Dartdoc._(config, await initEmptyGenerator(config));
   }
 
   Stream<String> get onCheckProgress => _onCheckProgress.stream;
@@ -94,19 +133,20 @@
     double seconds;
     packageGraph = await buildPackageGraph();
     seconds = _stopwatch.elapsedMilliseconds / 1000.0;
-    logInfo(
-        "Initialized dartdoc with ${packageGraph.libraries.length} librar${packageGraph.libraries.length == 1 ? 'y' : 'ies'} "
+    int libs = packageGraph.libraries.length;
+    logInfo("Initialized dartdoc with ${libs} librar${libs == 1 ? 'y' : 'ies'} "
         "in ${seconds.toStringAsFixed(1)} seconds");
     _stopwatch.reset();
 
-    if (generators.isNotEmpty) {
+    final generator = this.generator;
+    if (generator != null) {
       // Create the out directory.
       if (!outputDir.existsSync()) outputDir.createSync(recursive: true);
 
-      for (var generator in generators) {
-        await generator.generate(packageGraph, outputDir.path);
-        writtenFiles.addAll(generator.writtenFiles.keys.map(path.normalize));
-      }
+      DartdocFileWriter writer = DartdocFileWriter(outputDir.path);
+      await generator.generate(packageGraph, writer);
+
+      writtenFiles.addAll(writer.writtenFiles);
       if (config.validateLinks && writtenFiles.isNotEmpty) {
         validateLinks(packageGraph, outputDir.path);
       }
@@ -122,8 +162,8 @@
     }
 
     seconds = _stopwatch.elapsedMilliseconds / 1000.0;
-    logInfo(
-        "Documented ${packageGraph.localPublicLibraries.length} public librar${packageGraph.localPublicLibraries.length == 1 ? 'y' : 'ies'} "
+    libs = packageGraph.localPublicLibraries.length;
+    logInfo("Documented ${libs} public librar${libs == 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 666df06..595a666 100644
--- a/lib/src/empty_generator.dart
+++ b/lib/src/empty_generator.dart
@@ -4,6 +4,7 @@
 
 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';
@@ -13,34 +14,23 @@
 /// it were.
 class EmptyGenerator extends Generator {
   @override
-  Future generate(PackageGraph _packageGraph, String outputDirectoryPath) {
-    _onFileCreated.add(_packageGraph.defaultPackage.documentationAsHtml);
+  Future generate(PackageGraph _packageGraph, FileWriter writer) {
+    logProgress(_packageGraph.defaultPackage.documentationAsHtml);
     for (var package in Set.from([_packageGraph.defaultPackage])
       ..addAll(_packageGraph.localPackages)) {
       for (var category in filterNonDocumented(package.categories)) {
-        _onFileCreated.add(category.documentationAsHtml);
+        logProgress(category.documentationAsHtml);
       }
 
       for (Library lib in filterNonDocumented(package.libraries)) {
         filterNonDocumented(lib.allModelElements)
-            .forEach((m) => _onFileCreated.add(m.documentationAsHtml));
+            .forEach((m) => logProgress(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<List<Generator>> initEmptyGenerators(DartdocOptionContext config) async {
-  return [EmptyGenerator()];
+Future<Generator> initEmptyGenerator(DartdocOptionContext config) async {
+  return EmptyGenerator();
 }
diff --git a/lib/src/generator.dart b/lib/src/generator.dart
index 6ea61e1..28a0e8e 100644
--- a/lib/src/generator.dart
+++ b/lib/src/generator.dart
@@ -15,20 +15,23 @@
 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 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;
+  /// Generate the documentation for the given package using the specified
+  /// writer. Completes the returned future when done.
+  Future generate(PackageGraph packageGraph, FileWriter writer);
 }
 
 /// Dartdoc options related to generators generally.
diff --git a/lib/src/generator_frontend.dart b/lib/src/generator_frontend.dart
new file mode 100644
index 0000000..ff65f1e
--- /dev/null
+++ b/lib/src/generator_frontend.dart
@@ -0,0 +1,343 @@
+// 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
new file mode 100644
index 0000000..59fa128
--- /dev/null
+++ b/lib/src/generator_utils.dart
@@ -0,0 +1,76 @@
+// 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 29be73c..1224005 100644
--- a/lib/src/html/html_generator.dart
+++ b/lib/src/html/html_generator.dart
@@ -4,173 +4,14 @@
 
 library dartdoc.html_generator;
 
-import 'dart:async' show Future, StreamController, Stream;
-import 'dart:io' show Directory, File;
-import 'dart:isolate';
+import 'dart:async' show Future;
 
 import 'package:dartdoc/dartdoc.dart';
-import 'package:dartdoc/src/empty_generator.dart';
 import 'package:dartdoc/src/generator.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;
+import 'package:dartdoc/src/generator_frontend.dart';
+import 'package:dartdoc/src/html/html_generator_backend.dart';
 
-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,
-    )
-  ];
+Future<Generator> initHtmlGenerator(GeneratorContext context) async {
+  var backend = await HtmlGeneratorBackend.fromContext(context);
+  return GeneratorFrontEnd(backend);
 }
diff --git a/lib/src/html/html_generator_backend.dart b/lib/src/html/html_generator_backend.dart
new file mode 100644
index 0000000..da0dfb1
--- /dev/null
+++ b/lib/src/html/html_generator_backend.dart
@@ -0,0 +1,237 @@
+// 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
deleted file mode 100644
index e82fe1b..0000000
--- a/lib/src/html/html_generator_instance.dart
+++ /dev/null
@@ -1,413 +0,0 @@
-// 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 a47efde..2b929c3 100644
--- a/lib/src/html/templates.dart
+++ b/lib/src/html/templates.dart
@@ -158,6 +158,21 @@
   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 460c5dc..db62526 100644
--- a/test/html_generator_test.dart
+++ b/test/html_generator_test.dart
@@ -6,7 +6,9 @@
 
 import 'dart:io' show File, Directory;
 
-import 'package:dartdoc/src/html/html_generator.dart';
+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/templates.dart';
 import 'package:dartdoc/src/html/resources.g.dart';
 import 'package:dartdoc/src/model/package_graph.dart';
@@ -16,6 +18,12 @@
 
 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;
@@ -68,13 +76,15 @@
   group('HtmlGenerator', () {
     // TODO: Run the HtmlGenerator and validate important constraints.
     group('for a null package', () {
-      HtmlGenerator generator;
+      Generator generator;
       Directory tempOutput;
+      FileWriter writer;
 
       setUp(() async {
-        generator = await HtmlGenerator.create();
+        generator = await _initGeneratorForTest();
         tempOutput = Directory.systemTemp.createTempSync('doc_test_temp');
-        return generator.generate(null, tempOutput.path);
+        writer = DartdocFileWriter(tempOutput.path);
+        return generator.generate(null, writer);
       });
 
       tearDown(() {
@@ -96,15 +106,17 @@
     });
 
     group('for a package that causes duplicate files', () {
-      HtmlGenerator generator;
+      Generator generator;
       PackageGraph packageGraph;
       Directory tempOutput;
+      FileWriter writer;
 
       setUp(() async {
-        generator = await HtmlGenerator.create();
+        generator = await _initGeneratorForTest();
         packageGraph = await utils
             .bootBasicPackage(utils.testPackageDuplicateDir.path, []);
         tempOutput = await Directory.systemTemp.createTemp('doc_test_temp');
+        writer = DartdocFileWriter(tempOutput.path);
       });
 
       tearDown(() {
@@ -114,7 +126,7 @@
       });
 
       test('run generator and verify duplicate file error', () async {
-        await generator.generate(packageGraph, tempOutput.path);
+        await generator.generate(packageGraph, writer);
         expect(generator, isNotNull);
         expect(tempOutput, isNotNull);
         String expectedPath =