Move templates.dart to use ResourceProvider. (#2481)

Move templates.dart to use ResourceProvider.

* **Breaking Change**: Templates.fromDirectory is made private and now requires
  a ResourceLoader.
* Templates.createDefault is made `@visibleForTesting` and now requires a
  ResourceLoader.

This includes a few minor cleanups as well.
diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart
index cfc2a0e..d1f1002 100644
--- a/lib/dartdoc.dart
+++ b/lib/dartdoc.dart
@@ -50,7 +50,8 @@
 
 class DartdocFileWriter implements FileWriter {
   final String outputDir;
-  ResourceProvider resourceProvider;
+  @override
+  final ResourceProvider resourceProvider;
   final Map<String, Warnable> _fileElementMap = {};
   @override
   final Set<String> writtenFiles = {};
diff --git a/lib/src/generator/generator.dart b/lib/src/generator/generator.dart
index 4258342..79258ec 100644
--- a/lib/src/generator/generator.dart
+++ b/lib/src/generator/generator.dart
@@ -5,12 +5,15 @@
 /// A library containing an abstract documentation generator.
 library dartdoc.generator;
 
+import 'package:analyzer/file_system/file_system.dart';
 import 'package:dartdoc/src/dartdoc_options.dart';
 import 'package:dartdoc/src/model/model.dart' show PackageGraph;
 import 'package:dartdoc/src/package_meta.dart';
 import 'package:dartdoc/src/warnings.dart';
 
 abstract class FileWriter {
+  ResourceProvider get resourceProvider;
+
   /// All filenames written by this generator.
   Set<String> get writtenFiles;
 
diff --git a/lib/src/generator/html_generator.dart b/lib/src/generator/html_generator.dart
index d13c86c..17be599 100644
--- a/lib/src/generator/html_generator.dart
+++ b/lib/src/generator/html_generator.dart
@@ -9,10 +9,9 @@
 import 'package:dartdoc/src/generator/generator.dart';
 import 'package:dartdoc/src/generator/generator_frontend.dart';
 import 'package:dartdoc/src/generator/html_resources.g.dart' as resources;
-import 'package:dartdoc/src/generator/resource_loader.dart' as resource_loader;
+import 'package:dartdoc/src/generator/resource_loader.dart';
 import 'package:dartdoc/src/generator/template_data.dart';
 import 'package:dartdoc/src/generator/templates.dart';
-import 'package:path/path.dart' as path;
 
 Future<Generator> initHtmlGenerator(
     DartdocGeneratorOptionContext context) async {
@@ -43,21 +42,28 @@
       // Allow overwrite of favicon.
       var bytes =
           graph.resourceProvider.getFile(options.favicon).readAsBytesSync();
-      writer.write(path.join('static-assets', 'favicon.png'), bytes,
+      writer.write(
+          graph.resourceProvider.pathContext
+              .join('static-assets', 'favicon.png'),
+          bytes,
           allowOverwrite: true);
     }
   }
 
   Future<void> _copyResources(FileWriter writer) async {
-    final prefix = 'package:dartdoc/resources/';
+    var resourceLoader = ResourceLoader(writer.resourceProvider);
     for (var resourcePath in resources.resource_names) {
-      if (!resourcePath.startsWith(prefix)) {
-        throw StateError('Resource paths must start with $prefix, '
-            'encountered $resourcePath');
+      if (!resourcePath.startsWith(_dartdocResourcePrefix)) {
+        throw StateError('Resource paths must start with '
+            '$_dartdocResourcePrefix, encountered $resourcePath');
       }
-      var destFileName = resourcePath.substring(prefix.length);
-      writer.write(path.join('static-assets', destFileName),
-          await resource_loader.loadAsBytes(resourcePath));
+      var destFileName = resourcePath.substring(_dartdocResourcePrefix.length);
+      var destFilePath = writer.resourceProvider.pathContext
+          .join('static-assets', destFileName);
+      writer.write(
+          destFilePath, await resourceLoader.loadAsBytes(resourcePath));
     }
   }
+
+  static const _dartdocResourcePrefix = 'package:dartdoc/resources/';
 }
diff --git a/lib/src/generator/resource_loader.dart b/lib/src/generator/resource_loader.dart
index 2e4990a..2959699 100644
--- a/lib/src/generator/resource_loader.dart
+++ b/lib/src/generator/resource_loader.dart
@@ -6,35 +6,43 @@
 library dartdoc.resource_loader;
 
 import 'dart:convert' show utf8;
-import 'dart:io' show File;
 import 'dart:isolate' show Isolate;
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:meta/meta.dart';
 
-/// Loads a `package:` resource as a String.
-Future<String> loadAsString(String path) async {
-  var bytes = await loadAsBytes(path);
+class ResourceLoader {
+  final ResourceProvider provider;
 
-  return utf8.decode(bytes);
-}
+  ResourceLoader(this.provider);
 
-/// Loads a `package:` resource as an [List<int>].
-Future<List<int>> loadAsBytes(String path) async {
-  if (!path.startsWith('package:')) {
-    throw ArgumentError('path must begin with package:');
+  /// Loads a `package:` resource as a String.
+  Future<String> loadAsString(String path) async {
+    var bytes = await loadAsBytes(path);
+
+    return utf8.decode(bytes);
   }
 
-  var uri = await _resolveUri(Uri.parse(path));
-  return File.fromUri(uri).readAsBytes();
-}
+  /// Loads a `package:` resource as an [List<int>].
+  Future<List<int>> loadAsBytes(String path) async {
+    if (!path.startsWith('package:')) {
+      throw ArgumentError('path must begin with package:');
+    }
 
-/// Helper function for resolving to a non-relative, non-package URI.
-Future<Uri> _resolveUri(Uri uri) {
-  if (uri.scheme == 'package') {
-    return Isolate.resolvePackageUri(uri).then((resolvedUri) {
-      if (resolvedUri == null) {
-        throw ArgumentError.value(uri, 'uri', 'Unknown package');
-      }
-      return resolvedUri;
-    });
+    var uri = await resolveUri(Uri.parse(path));
+    return provider.getFile(uri.toFilePath()).readAsBytesSync();
   }
-  return Future<Uri>.value(Uri.base.resolveUri(uri));
+
+  /// Helper function for resolving to a non-relative, non-package URI.
+  @visibleForTesting
+  Future<Uri> resolveUri(Uri uri) {
+    if (uri.scheme == 'package') {
+      return Isolate.resolvePackageUri(uri).then((resolvedUri) {
+        if (resolvedUri == null) {
+          throw ArgumentError.value(uri, 'uri', 'Unknown package');
+        }
+        return resolvedUri;
+      });
+    }
+    return Future<Uri>.value(Uri.base.resolveUri(uri));
+  }
 }
diff --git a/lib/src/generator/templates.dart b/lib/src/generator/templates.dart
index 572edb1..17cc496 100644
--- a/lib/src/generator/templates.dart
+++ b/lib/src/generator/templates.dart
@@ -7,12 +7,12 @@
 @Renderer(#renderIndex, Context<PackageTemplateData>())
 library dartdoc.templates;
 
-import 'dart:io' show File, Directory;
-
+import 'package:analyzer/file_system/file_system.dart';
 import 'package:dartdoc/dartdoc.dart';
-import 'package:dartdoc/src/generator/resource_loader.dart' as loader;
+import 'package:dartdoc/src/generator/resource_loader.dart';
 import 'package:dartdoc/src/generator/template_data.dart';
 import 'package:dartdoc/src/mustachio/annotations.dart';
+import 'package:meta/meta.dart';
 import 'package:mustache/mustache.dart';
 import 'package:path/path.dart' as path;
 
@@ -58,7 +58,6 @@
   'features',
   'feature_set',
   'footer',
-  'footer',
   'head',
   'library',
   'mixin',
@@ -77,16 +76,15 @@
     List<String> headerPaths,
     List<String> footerPaths,
     List<String> footerTextPaths) async {
-  headerPaths ??= [];
-  footerPaths ??= [];
-  footerTextPaths ??= [];
-
   var partials = await templatesLoader.loadPartials();
 
   void replacePlaceholder(String key, String placeholder, List<String> paths) {
     var template = partials[key];
     if (template != null && paths != null && paths.isNotEmpty) {
-      var replacement = paths.map((p) => File(p).readAsStringSync()).join('\n');
+      var replacement = paths
+          .map((p) =>
+              templatesLoader.loader.provider.getFile(p).readAsStringSync())
+          .join('\n');
       template = template.replaceAll(placeholder, replacement);
       partials[key] = template;
     }
@@ -100,6 +98,8 @@
 }
 
 abstract class _TemplatesLoader {
+  ResourceLoader get loader;
+
   Future<Map<String, String>> loadPartials();
 
   Future<String> loadTemplate(String name);
@@ -110,7 +110,10 @@
   final String _format;
   final List<String> _partials;
 
-  factory _DefaultTemplatesLoader.create(String format) {
+  @override
+  final ResourceLoader loader;
+
+  factory _DefaultTemplatesLoader.create(String format, ResourceLoader loader) {
     List<String> partials;
     switch (format) {
       case 'html':
@@ -122,10 +125,10 @@
       default:
         partials = [];
     }
-    return _DefaultTemplatesLoader(format, partials);
+    return _DefaultTemplatesLoader(format, partials, loader);
   }
 
-  _DefaultTemplatesLoader(this._format, this._partials);
+  _DefaultTemplatesLoader(this._format, this._partials, this.loader);
 
   @override
   Future<Map<String, String>> loadPartials() async {
@@ -144,33 +147,38 @@
 
 /// Loads templates from a specified Directory.
 class _DirectoryTemplatesLoader extends _TemplatesLoader {
-  final Directory _directory;
+  final Folder _directory;
   final String _format;
 
-  _DirectoryTemplatesLoader(this._directory, this._format);
+  @override
+  final ResourceLoader loader;
+
+  _DirectoryTemplatesLoader(this._directory, this._format, this.loader);
+
+  path.Context get pathContext => _directory.provider.pathContext;
 
   @override
   Future<Map<String, String>> loadPartials() async {
     var partials = <String, String>{};
 
-    for (var file in _directory.listSync().whereType<File>()) {
-      var basename = path.basename(file.path);
+    for (var file in _directory.getChildren().whereType<File>()) {
+      var basename = pathContext.basename(file.path);
       if (basename.startsWith('_') && basename.endsWith('.$_format')) {
-        var content = file.readAsString();
+        var content = file.readAsStringSync();
         var partialName = basename.substring(1, basename.lastIndexOf('.'));
-        partials[partialName] = await content;
+        partials[partialName] = content;
       }
     }
     return partials;
   }
 
   @override
-  Future<String> loadTemplate(String name) {
-    var file = File(path.join(_directory.path, '$name.$_format'));
-    if (!file.existsSync()) {
+  Future<String> loadTemplate(String name) async {
+    var file = _directory.getChildAssumingFile('$name.$_format');
+    if (!file.exists) {
       throw DartdocFailure('Missing required template file: $name.$_format');
     }
-    return file.readAsString();
+    return file.readAsStringSync();
   }
 }
 
@@ -197,44 +205,51 @@
     var templatesDir = context.templatesDir;
     var format = context.format;
     var footerTextPaths = context.footerText;
+    var resourceLoader = ResourceLoader(context.resourceProvider);
 
     if (templatesDir != null) {
-      return fromDirectory(Directory(templatesDir), format,
+      return _fromDirectory(
+          context.resourceProvider.getFolder(templatesDir), format,
+          loader: resourceLoader,
           headerPaths: context.header,
           footerPaths: context.footer,
           footerTextPaths: footerTextPaths);
     } else {
       return createDefault(format,
+          loader: resourceLoader,
           headerPaths: context.header,
           footerPaths: context.footer,
           footerTextPaths: footerTextPaths);
     }
   }
 
+  @visibleForTesting
   static Future<Templates> createDefault(String format,
-      {List<String> headerPaths,
-      List<String> footerPaths,
-      List<String> footerTextPaths}) async {
-    return _create(_DefaultTemplatesLoader.create(format),
+      {@required ResourceLoader loader,
+      List<String> headerPaths = const <String>[],
+      List<String> footerPaths = const <String>[],
+      List<String> footerTextPaths = const <String>[]}) async {
+    return _create(_DefaultTemplatesLoader.create(format, loader),
         headerPaths: headerPaths,
         footerPaths: footerPaths,
         footerTextPaths: footerTextPaths);
   }
 
-  static Future<Templates> fromDirectory(Directory dir, String format,
-      {List<String> headerPaths,
-      List<String> footerPaths,
-      List<String> footerTextPaths}) async {
-    return _create(_DirectoryTemplatesLoader(dir, format),
+  static Future<Templates> _fromDirectory(Folder dir, String format,
+      {@required ResourceLoader loader,
+      @required List<String> headerPaths,
+      @required List<String> footerPaths,
+      @required List<String> footerTextPaths}) async {
+    return _create(_DirectoryTemplatesLoader(dir, format, loader),
         headerPaths: headerPaths,
         footerPaths: footerPaths,
         footerTextPaths: footerTextPaths);
   }
 
   static Future<Templates> _create(_TemplatesLoader templatesLoader,
-      {List<String> headerPaths,
-      List<String> footerPaths,
-      List<String> footerTextPaths}) async {
+      {@required List<String> headerPaths,
+      @required List<String> footerPaths,
+      @required List<String> footerTextPaths}) async {
     var partials = await _loadPartials(
         templatesLoader, headerPaths, footerPaths, footerTextPaths);
 
diff --git a/test/html_generator_test.dart b/test/html_generator_test.dart
index e95f44f..5b0d8f8 100644
--- a/test/html_generator_test.dart
+++ b/test/html_generator_test.dart
@@ -8,6 +8,7 @@
 import 'package:dartdoc/src/generator/generator_frontend.dart';
 import 'package:dartdoc/src/generator/html_generator.dart';
 import 'package:dartdoc/src/generator/html_resources.g.dart';
+import 'package:dartdoc/src/generator/resource_loader.dart';
 import 'package:dartdoc/src/generator/templates.dart';
 import 'package:dartdoc/src/package_config_provider.dart';
 import 'package:dartdoc/src/package_meta.dart';
@@ -37,8 +38,66 @@
     pathContext = resourceProvider.pathContext;
     packageConfigProvider = utils
         .getTestPackageConfigProvider(packageMetaProvider.defaultSdkDir.path);
+    var resourceLoader = ResourceLoader(resourceProvider);
+    for (var template in [
+      '_accessor_getter',
+      '_accessor_setter',
+      '_callable',
+      '_callable_multiline',
+      '_categorization',
+      '_class',
+      '_constant',
+      '_documentation',
+      '_extension',
+      '_features',
+      '_feature_set',
+      '_footer',
+      '_head',
+      '_library',
+      '_mixin',
+      '_name_summary',
+      '_packages',
+      '_property',
+      '_search_sidebar',
+      '_sidebar_for_category',
+      '_sidebar_for_container',
+      '_sidebar_for_library',
+      '_source_code',
+      '_source_link',
+      '404error',
+      'category',
+      'class',
+      'constructor',
+      'enum',
+      'extension',
+      'function',
+      'index',
+      'library',
+      'method',
+      'mixin',
+      'property',
+      'top_level_property',
+      'typedef',
+    ]) {
+      await resourceLoader.writeDartdocResource(
+          'templates/html/$template.html', 'CONTENT');
+    }
 
-    templates = await Templates.createDefault('html');
+    for (var resource in [
+      'favicon.png',
+      'github.css',
+      'highlight.pack.js',
+      'play_button.svg',
+      'readme.md',
+      'script.js',
+      'styles.css',
+      'typeahead.bundle.min.js',
+    ]) {
+      await resourceLoader.writeDartdocResource(
+          'resources/$resource', 'CONTENT');
+    }
+
+    templates = await Templates.createDefault('html', loader: resourceLoader);
     generator = GeneratorFrontEnd(HtmlGeneratorBackend(null, templates));
 
     projectRoot = utils.writePackage(
@@ -127,3 +186,12 @@
     }
   }
 }
+
+/// Extension methods just for tests.
+extension on ResourceLoader {
+  Future<void> writeDartdocResource(String path, String content) async {
+    var filePath =
+        (await resolveUri(Uri.parse('package:dartdoc/$path'))).toFilePath();
+    provider.getFile(filePath).writeAsStringSync(content);
+  }
+}
diff --git a/test/resource_loader_test.dart b/test/resource_loader_test.dart
index 1670aab..6607f22 100644
--- a/test/resource_loader_test.dart
+++ b/test/resource_loader_test.dart
@@ -4,11 +4,18 @@
 
 library dartdoc.resource_loader_test;
 
-import 'package:dartdoc/src/generator/resource_loader.dart' as loader;
+import 'package:analyzer/file_system/physical_file_system.dart';
+import 'package:dartdoc/src/generator/resource_loader.dart';
 import 'package:test/test.dart';
 
 void main() {
   group('Resource Loader', () {
+    ResourceLoader loader;
+
+    setUp(() {
+      loader = ResourceLoader(PhysicalResourceProvider());
+    });
+
     test('load from packages', () async {
       var contents = await loader
           .loadAsString('package:dartdoc/templates/html/index.html');