Skip to content

Support custom templates directory #2006

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/src/dartdoc_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,9 @@ class DartdocOptionContext extends DartdocOptionContextBase

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
Expand Down Expand Up @@ -1617,6 +1620,13 @@ Future<List<DartdocOption>> createDartdocOptions() async {
'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).'),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have a check to see if all the expected templates are in the directory, and fail with the a message that lets the user know what's missing. The template files can change (working on one such change now), and it will be helpful to flag this with a check rather than a failure/stack trace.

Copy link
Contributor Author

@jdkoren jdkoren Aug 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check in templates.dart (and a test), but let me know if you want me to go about it in a different way.

// TODO(jcollins-g): refactor so there is a single static "create" for
// each DartdocOptionContext that traverses the inheritance tree itself.
]
Expand Down
26 changes: 20 additions & 6 deletions lib/src/html/html_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,21 @@ class HtmlGenerator extends Generator {
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);
}
Expand Down Expand Up @@ -114,6 +125,7 @@ class HtmlGeneratorOptions implements HtmlOptions {
final String url;
final String faviconPath;
final bool prettyIndexJson;
final String templatesDir;

@override
final String relCanonicalPrefix;
Expand All @@ -126,7 +138,8 @@ class HtmlGeneratorOptions implements HtmlOptions {
this.relCanonicalPrefix,
this.faviconPath,
String toolVersion,
this.prettyIndexJson = false})
this.prettyIndexJson = false,
this.templatesDir})
: this.toolVersion = toolVersion ?? 'unknown';
}

Expand All @@ -143,7 +156,8 @@ Future<List<Generator>> initGenerators(GeneratorContext config) async {
relCanonicalPrefix: config.relCanonicalPrefix,
toolVersion: dartdocVersion,
faviconPath: config.favicon,
prettyIndexJson: config.prettyIndexJson);
prettyIndexJson: config.prettyIndexJson,
templatesDir: config.templatesDir);

return [
await HtmlGenerator.create(
Expand Down
154 changes: 121 additions & 33 deletions lib/src/html/templates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -36,50 +38,104 @@ const _partials = <String>[
'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);
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(placeholder, replacement);
partials[key] = template;
}
}

if (templatePath.contains('_footer')) {
String footerValue =
footerPaths.map((path) => File(path).readAsStringSync()).join('\n');
template = template.replaceAll(footerPlaceholder, footerValue);
replacePlaceholder('head', _headerPlaceholder, headerPaths);
replacePlaceholder('footer', _footerPlaceholder, footerPaths);
replacePlaceholder('footer', _footerTextPlaceholder, footerTextPaths);

String footerTextValue = footerTextPaths
.map((path) => File(path).readAsStringSync())
.join('\n');
template = template.replaceAll(footerTextPlaceholder, footerTextValue);
}
return partials;
}

return template;
}
abstract class _TemplatesLoader {
Future<Map<String, String>> loadPartials();
Future<String> loadTemplate(String name);
}

for (String partial in _partials) {
partials[partial] = await _loadPartial('_$partial.html');
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;
}

return partials;
@override
Future<String> loadTemplate(String name) =>
loader.loadAsString('package:dartdoc/templates/$name');
}

Future<String> _getTemplateFile(String templateFileName) =>
loader.loadAsString('package:dartdoc/templates/$templateFileName');
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;
Expand All @@ -98,12 +154,44 @@ class Templates {
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];
Expand All @@ -114,7 +202,7 @@ class Templates {
}

Future<Template> _loadTemplate(String templatePath) async {
String templateContents = await _getTemplateFile(templatePath);
String templateContents = await templatesLoader.loadTemplate(templatePath);
return Template(templateContents, partialResolver: _partial);
}

Expand Down
35 changes: 35 additions & 0 deletions test/dartdoc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -363,5 +363,40 @@ void main() {
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));
}
2 changes: 1 addition & 1 deletion test/html_generator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void main() {
Templates templates;

setUp(() async {
templates = await Templates.create();
templates = await Templates.createDefault();
});

test('index html', () {
Expand Down
2 changes: 2 additions & 0 deletions test/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ final Directory testPackageOptionsImporter =
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
Expand Down
12 changes: 12 additions & 0 deletions testing/test_package_custom_templates/lib/main.dart
Original file line number Diff line number Diff line change
@@ -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');
}
}
5 changes: 5 additions & 0 deletions testing/test_package_custom_templates/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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'
7 changes: 7 additions & 0 deletions testing/test_package_custom_templates/templates/404error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{>head}}

<div>
<h1>Oops, seems there's a problem...</h1>
</div>

{{>footer}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{#hasDocumentation}}
<section>
{{{ documentationAsHtml }}}
</section>
{{/hasDocumentation}}
14 changes: 14 additions & 0 deletions testing/test_package_custom_templates/templates/_footer.html
Original file line number Diff line number Diff line change
@@ -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>
Loading