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}}