Support custom templates directory (#2006)

Adds a (hidden) command-line flag to supply a directory with custom
html templates to use. If not specified, dartdoc defaults to using the
packaged templates.

Change-Id: I4148ff388947b1e0e24aa63b598be00cb01ae220
diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart
index 2439fe6..6fd89ed 100644
--- a/lib/src/dartdoc_options.dart
+++ b/lib/src/dartdoc_options.dart
@@ -1419,6 +1419,9 @@
 
   bool isPackageExcluded(String name) =>
       excludePackages.any((pattern) => name == pattern);
+
+  String get templatesDir =>
+      optionSet['templatesDir'].valueAt(context);
 }
 
 /// Instantiate dartdoc's configuration file and options parser with the
@@ -1621,6 +1624,13 @@
             'exist. Executables for different platforms are specified by '
             'giving the platform name as a key, and a list of strings as the '
             'command.'),
+    DartdocOptionArgOnly<String>("templatesDir", null, isDir: true, mustExist: true, hide: true,
+        help: 'Path to a directory containing templates to use instead of the default ones. '
+            'Directory must contain an html file for each of the following: 404error, category, '
+            'class, constant, constructor, enum, function, index, library, method, mixin, '
+            'property, top_level_constant, top_level_property, typedef. Partial templates are '
+            'supported; they must begin with an underscore, and references to them must omit the '
+            'leading underscore (e.g. use {{>foo}} to reference the partial template _foo.html).'),
     // TODO(jcollins-g): refactor so there is a single static "create" for
     // each DartdocOptionContext that traverses the inheritance tree itself.
   ]
diff --git a/lib/src/html/html_generator.dart b/lib/src/html/html_generator.dart
index c4c18ee..a0ecb00 100644
--- a/lib/src/html/html_generator.dart
+++ b/lib/src/html/html_generator.dart
@@ -55,10 +55,21 @@
       List<String> headers,
       List<String> footers,
       List<String> footerTexts}) async {
-    var templates = await Templates.create(
-        headerPaths: headers,
-        footerPaths: footers,
-        footerTextPaths: footerTexts);
+    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);
   }
@@ -114,6 +125,7 @@
   final String url;
   final String faviconPath;
   final bool prettyIndexJson;
+  final String templatesDir;
 
   @override
   final String relCanonicalPrefix;
@@ -126,7 +138,8 @@
       this.relCanonicalPrefix,
       this.faviconPath,
       String toolVersion,
-      this.prettyIndexJson = false})
+      this.prettyIndexJson = false,
+      this.templatesDir})
       : this.toolVersion = toolVersion ?? 'unknown';
 }
 
@@ -143,7 +156,8 @@
       relCanonicalPrefix: config.relCanonicalPrefix,
       toolVersion: dartdocVersion,
       faviconPath: config.favicon,
-      prettyIndexJson: config.prettyIndexJson);
+      prettyIndexJson: config.prettyIndexJson,
+      templatesDir: config.templatesDir);
 
   return [
     await HtmlGenerator.create(
diff --git a/lib/src/html/templates.dart b/lib/src/html/templates.dart
index bcd90c2..ca8e31b 100644
--- a/lib/src/html/templates.dart
+++ b/lib/src/html/templates.dart
@@ -5,10 +5,12 @@
 library dartdoc.templates;
 
 import 'dart:async' show Future;
-import 'dart:io' show File;
+import 'dart:io' show File, Directory;
 
+import 'package:dartdoc/dartdoc.dart';
 import 'package:dartdoc/src/html/resource_loader.dart' as loader;
 import 'package:mustache/mustache.dart';
+import 'package:path/path.dart' as path;
 
 const _partials = <String>[
   'callable',
@@ -36,50 +38,104 @@
   'accessor_setter',
 ];
 
-Future<Map<String, String>> _loadPartials(List<String> headerPaths,
-    List<String> footerPaths, List<String> footerTextPaths) async {
-  final String headerPlaceholder = '<!-- header placeholder -->';
-  final String footerPlaceholder = '<!-- footer placeholder -->';
-  final String footerTextPlaceholder = '<!-- footer-text placeholder -->';
+const _requiredTemplates = <String>[
+  '404error.html',
+  'category.html',
+  'class.html',
+  'constant.html',
+  'constructor.html',
+  'enum.html',
+  'function.html',
+  'index.html',
+  'library.html',
+  'method.html',
+  'mixin.html',
+  'property.html',
+  'top_level_constant.html',
+  'top_level_property.html',
+  'typedef.html',
+];
+
+const String _headerPlaceholder = '<!-- header placeholder -->';
+const String _footerPlaceholder = '<!-- footer placeholder -->';
+const String _footerTextPlaceholder = '<!-- footer-text placeholder -->';
+
+Future<Map<String, String>> _loadPartials(
+    _TemplatesLoader templatesLoader,
+    List<String> headerPaths,
+    List<String> footerPaths,
+    List<String> footerTextPaths) async {
 
   headerPaths ??= [];
   footerPaths ??= [];
   footerTextPaths ??= [];
 
-  var partials = <String, String>{};
+  var partials = await templatesLoader.loadPartials();
 
-  Future<String> _loadPartial(String templatePath) async {
-    String template = await _getTemplateFile(templatePath);
-
-    if (templatePath.contains('_head')) {
-      String headerValue =
-          headerPaths.map((path) => File(path).readAsStringSync()).join('\n');
-      template = template.replaceAll(headerPlaceholder, headerValue);
-    }
-
-    if (templatePath.contains('_footer')) {
-      String footerValue =
-          footerPaths.map((path) => File(path).readAsStringSync()).join('\n');
-      template = template.replaceAll(footerPlaceholder, footerValue);
-
-      String footerTextValue = footerTextPaths
-          .map((path) => File(path).readAsStringSync())
+  void replacePlaceholder(String key, String placeholder, List<String> paths) {
+    var template = partials[key];
+    if (template != null && paths != null && paths.isNotEmpty) {
+      String replacement = paths.map((p) => File(p).readAsStringSync())
           .join('\n');
-      template = template.replaceAll(footerTextPlaceholder, footerTextValue);
+      template = template.replaceAll(placeholder, replacement);
+      partials[key] = template;
     }
-
-    return template;
   }
 
-  for (String partial in _partials) {
-    partials[partial] = await _loadPartial('_$partial.html');
-  }
+  replacePlaceholder('head', _headerPlaceholder, headerPaths);
+  replacePlaceholder('footer', _footerPlaceholder, footerPaths);
+  replacePlaceholder('footer', _footerTextPlaceholder, footerTextPaths);
 
   return partials;
 }
 
-Future<String> _getTemplateFile(String templateFileName) =>
-    loader.loadAsString('package:dartdoc/templates/$templateFileName');
+abstract class _TemplatesLoader {
+  Future<Map<String, String>> loadPartials();
+  Future<String> loadTemplate(String name);
+}
+
+class _DefaultTemplatesLoader extends _TemplatesLoader {
+  @override
+  Future<Map<String, String>> loadPartials() async {
+    var partials = <String, String>{};
+    for (String partial in _partials) {
+      var uri = 'package:dartdoc/templates/_$partial.html';
+      partials[partial] = await loader.loadAsString(uri);
+    }
+    return partials;
+  }
+
+  @override
+  Future<String> loadTemplate(String name) =>
+      loader.loadAsString('package:dartdoc/templates/$name');
+}
+
+class _DirectoryTemplatesLoader extends _TemplatesLoader {
+  final Directory _directory;
+
+  _DirectoryTemplatesLoader(this._directory);
+
+  @override
+  Future<Map<String, String>> loadPartials() async {
+    var partials = <String, String>{};
+
+    for (File file in _directory.listSync().whereType<File>()) {
+      var basename = path.basename(file.path);
+      if (basename.startsWith('_') && basename.endsWith('.html')) {
+        var content = file.readAsString();
+        var partialName = basename.substring(1, basename.lastIndexOf('.'));
+        partials[partialName] = await content;
+      }
+    }
+    return partials;
+  }
+
+  @override
+  Future<String> loadTemplate(String name) {
+    var file = File(path.join(_directory.path, name));
+    return file.readAsString();
+  }
+}
 
 class Templates {
   final Template categoryTemplate;
@@ -98,12 +154,44 @@
   final Template topLevelPropertyTemplate;
   final Template typeDefTemplate;
 
-  static Future<Templates> create(
+  static Future<Templates> createDefault(
+      {List<String> headerPaths,
+      List<String> footerPaths,
+      List<String> footerTextPaths}) async {
+    return _create(_DefaultTemplatesLoader(),
+        headerPaths: headerPaths,
+        footerPaths: footerPaths,
+        footerTextPaths: footerTextPaths);
+  }
+
+  static Future<Templates> fromDirectory(
+      Directory dir,
+      {List<String> headerPaths,
+      List<String> footerPaths,
+      List<String> footerTextPaths}) async {
+    await _checkRequiredTemplatesExist(dir);
+    return _create(_DirectoryTemplatesLoader(dir),
+        headerPaths: headerPaths,
+        footerPaths: footerPaths,
+        footerTextPaths: footerTextPaths);
+  }
+
+  static void _checkRequiredTemplatesExist(Directory dir) {
+    for (var name in _requiredTemplates) {
+      var file = File(path.join(dir.path, name));
+      if (!file.existsSync()) {
+        throw DartdocFailure('Missing required template file: "$name"');
+      }
+    }
+  }
+
+  static Future<Templates> _create(
+      _TemplatesLoader templatesLoader,
       {List<String> headerPaths,
       List<String> footerPaths,
       List<String> footerTextPaths}) async {
     var partials =
-        await _loadPartials(headerPaths, footerPaths, footerTextPaths);
+        await _loadPartials(templatesLoader, headerPaths, footerPaths, footerTextPaths);
 
     Template _partial(String name) {
       String partial = partials[name];
@@ -114,7 +202,7 @@
     }
 
     Future<Template> _loadTemplate(String templatePath) async {
-      String templateContents = await _getTemplateFile(templatePath);
+      String templateContents = await templatesLoader.loadTemplate(templatePath);
       return Template(templateContents, partialResolver: _partial);
     }
 
diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart
index ce1f348..66bdbde 100644
--- a/test/dartdoc_test.dart
+++ b/test/dartdoc_test.dart
@@ -363,5 +363,40 @@
           dart_bear.allClasses.map((cls) => cls.name).contains('Bear'), isTrue);
       expect(p.packageMap["Dart"].publicLibraries, hasLength(3));
     });
+
+    test('generate docs with custom templates', () async {
+      String templatesDir = path.join(testPackageCustomTemplates.path, 'templates');
+      Dartdoc dartdoc =
+          await buildDartdoc(['--templates-dir', templatesDir],
+              testPackageCustomTemplates, tempDir);
+
+      DartdocResults results = await dartdoc.generateDocs();
+      expect(results.packageGraph, isNotNull);
+
+      PackageGraph p = results.packageGraph;
+      expect(p.defaultPackage.name, 'test_package_custom_templates');
+      expect(p.localPublicLibraries, hasLength(1));
+    });
+
+    test('generate docs with missing required template fails', () async {
+      var templatesDir = path.join(path.current, 'test/templates');
+      try {
+        await buildDartdoc(['--templates-dir', templatesDir], testPackageCustomTemplates, tempDir);
+        fail('dartdoc should fail with missing required template');
+      } catch (e) {
+        expect(e is DartdocFailure, isTrue);
+        expect((e as DartdocFailure).message, startsWith('Missing required template file'));
+      }
+    });
+
+    test('generate docs with bad templatesDir path fails', () async {
+      String badPath = path.join(tempDir.path, 'BAD');
+      try {
+        await buildDartdoc(['--templates-dir', badPath], testPackageCustomTemplates, tempDir);
+        fail('dartdoc should fail with bad templatesDir path');
+      } catch (e) {
+        expect(e is DartdocFailure, isTrue);
+      }
+    });
   }, timeout: Timeout.factor(8));
 }
diff --git a/test/html_generator_test.dart b/test/html_generator_test.dart
index e5cb79b..cda62d7 100644
--- a/test/html_generator_test.dart
+++ b/test/html_generator_test.dart
@@ -17,7 +17,7 @@
     Templates templates;
 
     setUp(() async {
-      templates = await Templates.create();
+      templates = await Templates.createDefault();
     });
 
     test('index html', () {
diff --git a/test/src/utils.dart b/test/src/utils.dart
index 8655f8f..9167896 100644
--- a/test/src/utils.dart
+++ b/test/src/utils.dart
@@ -46,6 +46,8 @@
     Directory('testing/test_package_options_importer');
 final Directory testPackageToolError =
     Directory('testing/test_package_tool_error');
+final Directory testPackageCustomTemplates =
+    Directory('testing/test_package_custom_templates');
 
 /// Convenience factory to build a [DartdocGeneratorOptionContext] and associate
 /// it with a [DartdocOptionSet] based on the current working directory and/or
diff --git a/testing/test_package_custom_templates/lib/main.dart b/testing/test_package_custom_templates/lib/main.dart
new file mode 100644
index 0000000..b98134f
--- /dev/null
+++ b/testing/test_package_custom_templates/lib/main.dart
@@ -0,0 +1,12 @@
+/// The main function. It does the main thing.
+main(List<String> args) {
+  new HelloPrinter().sayHello();
+}
+
+/// A class that prints 'Hello'.
+class HelloPrinter {
+  /// A method that prints 'Hello'
+  void sayHello() {
+    print('hello');
+  }
+}
diff --git a/testing/test_package_custom_templates/pubspec.yaml b/testing/test_package_custom_templates/pubspec.yaml
new file mode 100644
index 0000000..d7f23e0
--- /dev/null
+++ b/testing/test_package_custom_templates/pubspec.yaml
@@ -0,0 +1,5 @@
+name: test_package_custom_templates
+version: 0.0.1
+description: A simple console application.
+#dependencies:
+#  foo_bar: '>=1.0.0 <2.0.0'
diff --git a/testing/test_package_custom_templates/templates/404error.html b/testing/test_package_custom_templates/templates/404error.html
new file mode 100644
index 0000000..c88dea0
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/404error.html
@@ -0,0 +1,7 @@
+{{>head}}
+
+  <div>
+    <h1>Oops, seems there's a problem...</h1>
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/_documentation.html b/testing/test_package_custom_templates/templates/_documentation.html
new file mode 100644
index 0000000..7b62be4
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/_documentation.html
@@ -0,0 +1,5 @@
+{{#hasDocumentation}}
+<section>
+  {{{ documentationAsHtml }}}
+</section>
+{{/hasDocumentation}}
diff --git a/testing/test_package_custom_templates/templates/_footer.html b/testing/test_package_custom_templates/templates/_footer.html
new file mode 100644
index 0000000..23ef725
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/_footer.html
@@ -0,0 +1,14 @@
+</main>
+
+<footer>
+  <p>(FOOT)</p>
+  <p>
+    {{packageGraph.defaultPackage.name}} {{packageGraph.defaultPackage.version}}
+  </p>
+
+  <!-- footer-text placeholder -->
+</footer>
+
+<!-- footer placeholder -->
+</body>
+</html>
diff --git a/testing/test_package_custom_templates/templates/_head.html b/testing/test_package_custom_templates/templates/_head.html
new file mode 100644
index 0000000..4c6f1e8
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/_head.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>{{ title }}</title>
+  <!-- header placeholder -->
+</head>
+
+<body>
+
+<header id="title">
+  <div>{{self.name}}</div>
+</header>
+
+<main>
diff --git a/testing/test_package_custom_templates/templates/category.html b/testing/test_package_custom_templates/templates/category.html
new file mode 100644
index 0000000..51f097d
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/category.html
@@ -0,0 +1,9 @@
+{{>head}}
+
+<div>
+  {{#self}}
+  <h1>{{name}} {{kind}}</h1>
+  {{/self}}
+</div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/class.html b/testing/test_package_custom_templates/templates/class.html
new file mode 100644
index 0000000..76cb435
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/class.html
@@ -0,0 +1,42 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{nameWithGenerics}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#clazz}}
+    {{>documentation}}
+
+    {{#hasPublicConstructors}}
+    <section>
+      <h2>Constructors</h2>
+
+      <dl>
+        {{#publicConstructors}}
+        <dt>
+          {{{linkedName}}}({{{ linkedParams }}})
+        </dt>
+        <dd>
+          {{{ oneLineDoc }}}
+        </dd>
+        {{/publicConstructors}}
+      </dl>
+    </section>
+    {{/hasPublicConstructors}}
+
+    {{#hasPublicMethods}}
+    <section>
+      <h2>Methods</h2>
+      <dl>
+        {{#allPublicInstanceMethods}}
+        {{{ oneLineDoc }}}
+        {{/allPublicInstanceMethods}}
+      </dl>
+    </section>
+    {{/hasPublicMethods}}
+
+    {{/clazz}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/constant.html b/testing/test_package_custom_templates/templates/constant.html
new file mode 100644
index 0000000..5f5d8fe
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/constant.html
@@ -0,0 +1,13 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{name}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#property}}
+    {{>documentation}}
+    {{/property}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/constructor.html b/testing/test_package_custom_templates/templates/constructor.html
new file mode 100644
index 0000000..6c8ad54
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/constructor.html
@@ -0,0 +1,13 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{nameWithGenerics}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#constructor}}
+    {{>documentation}}
+    {{/constructor}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/enum.html b/testing/test_package_custom_templates/templates/enum.html
new file mode 100644
index 0000000..ff59427
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/enum.html
@@ -0,0 +1,13 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{name}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#eNum}}
+    {{>documentation}}
+    {{/eNum}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/function.html b/testing/test_package_custom_templates/templates/function.html
new file mode 100644
index 0000000..5f486ce
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/function.html
@@ -0,0 +1,13 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+    <div><h1>{{{nameWithGenerics}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#function}}
+    {{>documentation}}
+    {{/function}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/index.html b/testing/test_package_custom_templates/templates/index.html
new file mode 100644
index 0000000..4c570d8
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/index.html
@@ -0,0 +1,43 @@
+{{>head}}
+
+  <div>
+    <h5>{{self.name}} {{self.kind}}</h5>
+  </div>
+
+  <div>
+    {{#packageGraph.defaultPackage}}
+      {{>documentation}}
+    {{/packageGraph.defaultPackage}}
+
+    {{#packageGraph}}
+      {{#localPackages}}
+        <section>
+          {{#isFirstPackage}}
+            <h2>Libraries</h2>
+          {{/isFirstPackage}}
+          {{^isFirstPackage}}
+            <h2>{{name}}</h2>
+          {{/isFirstPackage}}
+          <dl>
+          {{#defaultCategory.publicLibraries}}
+            {{{ oneLineDoc }}}
+          {{/defaultCategory.publicLibraries}}
+          {{#categoriesWithPublicLibraries}}
+            <h3>{{name}}</h3>
+            {{#publicLibraries}}
+              <dt>
+                {{{ linkedName }}}
+              </dt>
+              <dd>
+                {{#isDocumented}}{{{ oneLineDoc }}}{{/isDocumented}}
+              </dd>
+            {{/publicLibraries}}
+          {{/categoriesWithPublicLibraries}}
+          </dl>
+        </section>
+      {{/localPackages}}
+    {{/packageGraph}}
+
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/library.html b/testing/test_package_custom_templates/templates/library.html
new file mode 100644
index 0000000..cffa0ad
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/library.html
@@ -0,0 +1,44 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{name}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#library}}
+    {{>documentation}}
+    {{/library}}
+
+    {{#library.hasPublicClasses}}
+    <div>
+      <h2>Classes</h2>
+
+      <dl>
+        {{#library.publicClasses}}
+        <dt>
+          {{{linkedName}}}{{{linkedGenericParameters}}}
+        </dt>
+        <dd>
+          {{{ oneLineDoc }}}
+        </dd>
+        {{/library.publicClasses}}
+      </dl>
+    </div>
+    {{/library.hasPublicClasses}}
+
+    {{#library.hasPublicFunctions}}
+    <div>
+      <h2>Functions</h2>
+
+      <dl>
+        {{#library.publicFunctions}}
+        {{{linkedName}}}{{{linkedGenericParameters}}}
+        {{{ oneLineDoc }}}
+        {{/library.publicFunctions}}
+      </dl>
+    </div>
+    {{/library.hasPublicFunctions}}
+
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/method.html b/testing/test_package_custom_templates/templates/method.html
new file mode 100644
index 0000000..249912d
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/method.html
@@ -0,0 +1,13 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{nameWithGenerics}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#method}}
+    {{>documentation}}
+    {{/method}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/mixin.html b/testing/test_package_custom_templates/templates/mixin.html
new file mode 100644
index 0000000..cbe91a6
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/mixin.html
@@ -0,0 +1,13 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{nameWithGenerics}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#mixin}}
+    {{>documentation}}
+    {{/mixin}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/property.html b/testing/test_package_custom_templates/templates/property.html
new file mode 100644
index 0000000..bce5e5f
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/property.html
@@ -0,0 +1,25 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{name}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#self}}
+      {{#hasNoGetterSetter}}
+        {{>documentation}}
+      {{/hasNoGetterSetter}}
+
+      {{#hasGetterOrSetter}}
+        {{#hasGetter}}
+        <div>Has getter</div>
+        {{/hasGetter}}
+
+        {{#hasSetter}}
+        <div>Has setter</div>
+        {{/hasSetter}}
+      {{/hasGetterOrSetter}}
+    {{/self}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/top_level_constant.html b/testing/test_package_custom_templates/templates/top_level_constant.html
new file mode 100644
index 0000000..1ed7a02
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/top_level_constant.html
@@ -0,0 +1,15 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{name}}} {{kind}}</h1></div>
+
+      <div>
+        = {{{ constantValue }}}
+      </div>
+      {{>documentation}}
+    {{/self}}
+
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/top_level_property.html b/testing/test_package_custom_templates/templates/top_level_property.html
new file mode 100644
index 0000000..5bc442b
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/top_level_property.html
@@ -0,0 +1,21 @@
+{{>head}}
+
+  <div>
+    {{#self}}
+      <div><h1>{{{name}}} {{kind}}</h1></div>
+
+      {{#hasNoGetterSetter}}
+        {{>documentation}}
+      {{/hasNoGetterSetter}}
+
+      {{#hasExplicitGetter}}
+        <div>Has getter</div>
+      {{/hasExplicitGetter}}
+
+      {{#hasExplicitSetter}}
+        <div>Has setter</div>
+      {{/hasExplicitSetter}}
+    {{/self}}
+  </div>
+
+{{>footer}}
diff --git a/testing/test_package_custom_templates/templates/typedef.html b/testing/test_package_custom_templates/templates/typedef.html
new file mode 100644
index 0000000..01d4c29
--- /dev/null
+++ b/testing/test_package_custom_templates/templates/typedef.html
@@ -0,0 +1,14 @@
+{{>head}}
+
+<div>
+    {{#self}}
+      <div><h1>{{{nameWithGenerics}}} {{kind}}</h1></div>
+    {{/self}}
+
+    {{#typeDef}}
+    {{>documentation}}
+    {{/typeDef}}
+
+  </div>
+
+{{>footer}}