Skip to content

Commit a93e4bb

Browse files
jdkorenkeertip
authored andcommitted
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
1 parent 043bd79 commit a93e4bb

26 files changed

+537
-40
lines changed

lib/src/dartdoc_options.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,6 +1419,9 @@ class DartdocOptionContext extends DartdocOptionContextBase
14191419

14201420
bool isPackageExcluded(String name) =>
14211421
excludePackages.any((pattern) => name == pattern);
1422+
1423+
String get templatesDir =>
1424+
optionSet['templatesDir'].valueAt(context);
14221425
}
14231426

14241427
/// Instantiate dartdoc's configuration file and options parser with the
@@ -1621,6 +1624,13 @@ Future<List<DartdocOption>> createDartdocOptions() async {
16211624
'exist. Executables for different platforms are specified by '
16221625
'giving the platform name as a key, and a list of strings as the '
16231626
'command.'),
1627+
DartdocOptionArgOnly<String>("templatesDir", null, isDir: true, mustExist: true, hide: true,
1628+
help: 'Path to a directory containing templates to use instead of the default ones. '
1629+
'Directory must contain an html file for each of the following: 404error, category, '
1630+
'class, constant, constructor, enum, function, index, library, method, mixin, '
1631+
'property, top_level_constant, top_level_property, typedef. Partial templates are '
1632+
'supported; they must begin with an underscore, and references to them must omit the '
1633+
'leading underscore (e.g. use {{>foo}} to reference the partial template _foo.html).'),
16241634
// TODO(jcollins-g): refactor so there is a single static "create" for
16251635
// each DartdocOptionContext that traverses the inheritance tree itself.
16261636
]

lib/src/html/html_generator.dart

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,21 @@ class HtmlGenerator extends Generator {
5555
List<String> headers,
5656
List<String> footers,
5757
List<String> footerTexts}) async {
58-
var templates = await Templates.create(
59-
headerPaths: headers,
60-
footerPaths: footers,
61-
footerTextPaths: footerTexts);
58+
var templates;
59+
String dirname = options?.templatesDir;
60+
if (dirname != null) {
61+
Directory templateDir = Directory(dirname);
62+
templates = await Templates.fromDirectory(
63+
templateDir,
64+
headerPaths: headers,
65+
footerPaths: footers,
66+
footerTextPaths: footerTexts);
67+
} else {
68+
templates = await Templates.createDefault(
69+
headerPaths: headers,
70+
footerPaths: footers,
71+
footerTextPaths: footerTexts);
72+
}
6273

6374
return HtmlGenerator._(options ?? HtmlGeneratorOptions(), templates);
6475
}
@@ -114,6 +125,7 @@ class HtmlGeneratorOptions implements HtmlOptions {
114125
final String url;
115126
final String faviconPath;
116127
final bool prettyIndexJson;
128+
final String templatesDir;
117129

118130
@override
119131
final String relCanonicalPrefix;
@@ -126,7 +138,8 @@ class HtmlGeneratorOptions implements HtmlOptions {
126138
this.relCanonicalPrefix,
127139
this.faviconPath,
128140
String toolVersion,
129-
this.prettyIndexJson = false})
141+
this.prettyIndexJson = false,
142+
this.templatesDir})
130143
: this.toolVersion = toolVersion ?? 'unknown';
131144
}
132145

@@ -143,7 +156,8 @@ Future<List<Generator>> initGenerators(GeneratorContext config) async {
143156
relCanonicalPrefix: config.relCanonicalPrefix,
144157
toolVersion: dartdocVersion,
145158
faviconPath: config.favicon,
146-
prettyIndexJson: config.prettyIndexJson);
159+
prettyIndexJson: config.prettyIndexJson,
160+
templatesDir: config.templatesDir);
147161

148162
return [
149163
await HtmlGenerator.create(

lib/src/html/templates.dart

Lines changed: 121 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
library dartdoc.templates;
66

77
import 'dart:async' show Future;
8-
import 'dart:io' show File;
8+
import 'dart:io' show File, Directory;
99

10+
import 'package:dartdoc/dartdoc.dart';
1011
import 'package:dartdoc/src/html/resource_loader.dart' as loader;
1112
import 'package:mustache/mustache.dart';
13+
import 'package:path/path.dart' as path;
1214

1315
const _partials = <String>[
1416
'callable',
@@ -36,50 +38,104 @@ const _partials = <String>[
3638
'accessor_setter',
3739
];
3840

39-
Future<Map<String, String>> _loadPartials(List<String> headerPaths,
40-
List<String> footerPaths, List<String> footerTextPaths) async {
41-
final String headerPlaceholder = '<!-- header placeholder -->';
42-
final String footerPlaceholder = '<!-- footer placeholder -->';
43-
final String footerTextPlaceholder = '<!-- footer-text placeholder -->';
41+
const _requiredTemplates = <String>[
42+
'404error.html',
43+
'category.html',
44+
'class.html',
45+
'constant.html',
46+
'constructor.html',
47+
'enum.html',
48+
'function.html',
49+
'index.html',
50+
'library.html',
51+
'method.html',
52+
'mixin.html',
53+
'property.html',
54+
'top_level_constant.html',
55+
'top_level_property.html',
56+
'typedef.html',
57+
];
58+
59+
const String _headerPlaceholder = '<!-- header placeholder -->';
60+
const String _footerPlaceholder = '<!-- footer placeholder -->';
61+
const String _footerTextPlaceholder = '<!-- footer-text placeholder -->';
62+
63+
Future<Map<String, String>> _loadPartials(
64+
_TemplatesLoader templatesLoader,
65+
List<String> headerPaths,
66+
List<String> footerPaths,
67+
List<String> footerTextPaths) async {
4468

4569
headerPaths ??= [];
4670
footerPaths ??= [];
4771
footerTextPaths ??= [];
4872

49-
var partials = <String, String>{};
73+
var partials = await templatesLoader.loadPartials();
5074

51-
Future<String> _loadPartial(String templatePath) async {
52-
String template = await _getTemplateFile(templatePath);
53-
54-
if (templatePath.contains('_head')) {
55-
String headerValue =
56-
headerPaths.map((path) => File(path).readAsStringSync()).join('\n');
57-
template = template.replaceAll(headerPlaceholder, headerValue);
75+
void replacePlaceholder(String key, String placeholder, List<String> paths) {
76+
var template = partials[key];
77+
if (template != null && paths != null && paths.isNotEmpty) {
78+
String replacement = paths.map((p) => File(p).readAsStringSync())
79+
.join('\n');
80+
template = template.replaceAll(placeholder, replacement);
81+
partials[key] = template;
5882
}
83+
}
5984

60-
if (templatePath.contains('_footer')) {
61-
String footerValue =
62-
footerPaths.map((path) => File(path).readAsStringSync()).join('\n');
63-
template = template.replaceAll(footerPlaceholder, footerValue);
85+
replacePlaceholder('head', _headerPlaceholder, headerPaths);
86+
replacePlaceholder('footer', _footerPlaceholder, footerPaths);
87+
replacePlaceholder('footer', _footerTextPlaceholder, footerTextPaths);
6488

65-
String footerTextValue = footerTextPaths
66-
.map((path) => File(path).readAsStringSync())
67-
.join('\n');
68-
template = template.replaceAll(footerTextPlaceholder, footerTextValue);
69-
}
89+
return partials;
90+
}
7091

71-
return template;
72-
}
92+
abstract class _TemplatesLoader {
93+
Future<Map<String, String>> loadPartials();
94+
Future<String> loadTemplate(String name);
95+
}
7396

74-
for (String partial in _partials) {
75-
partials[partial] = await _loadPartial('_$partial.html');
97+
class _DefaultTemplatesLoader extends _TemplatesLoader {
98+
@override
99+
Future<Map<String, String>> loadPartials() async {
100+
var partials = <String, String>{};
101+
for (String partial in _partials) {
102+
var uri = 'package:dartdoc/templates/_$partial.html';
103+
partials[partial] = await loader.loadAsString(uri);
104+
}
105+
return partials;
76106
}
77107

78-
return partials;
108+
@override
109+
Future<String> loadTemplate(String name) =>
110+
loader.loadAsString('package:dartdoc/templates/$name');
79111
}
80112

81-
Future<String> _getTemplateFile(String templateFileName) =>
82-
loader.loadAsString('package:dartdoc/templates/$templateFileName');
113+
class _DirectoryTemplatesLoader extends _TemplatesLoader {
114+
final Directory _directory;
115+
116+
_DirectoryTemplatesLoader(this._directory);
117+
118+
@override
119+
Future<Map<String, String>> loadPartials() async {
120+
var partials = <String, String>{};
121+
122+
for (File file in _directory.listSync().whereType<File>()) {
123+
var basename = path.basename(file.path);
124+
if (basename.startsWith('_') && basename.endsWith('.html')) {
125+
var content = file.readAsString();
126+
var partialName = basename.substring(1, basename.lastIndexOf('.'));
127+
partials[partialName] = await content;
128+
}
129+
}
130+
return partials;
131+
}
132+
133+
@override
134+
Future<String> loadTemplate(String name) {
135+
var file = File(path.join(_directory.path, name));
136+
return file.readAsString();
137+
}
138+
}
83139

84140
class Templates {
85141
final Template categoryTemplate;
@@ -98,12 +154,44 @@ class Templates {
98154
final Template topLevelPropertyTemplate;
99155
final Template typeDefTemplate;
100156

101-
static Future<Templates> create(
157+
static Future<Templates> createDefault(
158+
{List<String> headerPaths,
159+
List<String> footerPaths,
160+
List<String> footerTextPaths}) async {
161+
return _create(_DefaultTemplatesLoader(),
162+
headerPaths: headerPaths,
163+
footerPaths: footerPaths,
164+
footerTextPaths: footerTextPaths);
165+
}
166+
167+
static Future<Templates> fromDirectory(
168+
Directory dir,
169+
{List<String> headerPaths,
170+
List<String> footerPaths,
171+
List<String> footerTextPaths}) async {
172+
await _checkRequiredTemplatesExist(dir);
173+
return _create(_DirectoryTemplatesLoader(dir),
174+
headerPaths: headerPaths,
175+
footerPaths: footerPaths,
176+
footerTextPaths: footerTextPaths);
177+
}
178+
179+
static void _checkRequiredTemplatesExist(Directory dir) {
180+
for (var name in _requiredTemplates) {
181+
var file = File(path.join(dir.path, name));
182+
if (!file.existsSync()) {
183+
throw DartdocFailure('Missing required template file: "$name"');
184+
}
185+
}
186+
}
187+
188+
static Future<Templates> _create(
189+
_TemplatesLoader templatesLoader,
102190
{List<String> headerPaths,
103191
List<String> footerPaths,
104192
List<String> footerTextPaths}) async {
105193
var partials =
106-
await _loadPartials(headerPaths, footerPaths, footerTextPaths);
194+
await _loadPartials(templatesLoader, headerPaths, footerPaths, footerTextPaths);
107195

108196
Template _partial(String name) {
109197
String partial = partials[name];
@@ -114,7 +202,7 @@ class Templates {
114202
}
115203

116204
Future<Template> _loadTemplate(String templatePath) async {
117-
String templateContents = await _getTemplateFile(templatePath);
205+
String templateContents = await templatesLoader.loadTemplate(templatePath);
118206
return Template(templateContents, partialResolver: _partial);
119207
}
120208

test/dartdoc_test.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,5 +363,40 @@ void main() {
363363
dart_bear.allClasses.map((cls) => cls.name).contains('Bear'), isTrue);
364364
expect(p.packageMap["Dart"].publicLibraries, hasLength(3));
365365
});
366+
367+
test('generate docs with custom templates', () async {
368+
String templatesDir = path.join(testPackageCustomTemplates.path, 'templates');
369+
Dartdoc dartdoc =
370+
await buildDartdoc(['--templates-dir', templatesDir],
371+
testPackageCustomTemplates, tempDir);
372+
373+
DartdocResults results = await dartdoc.generateDocs();
374+
expect(results.packageGraph, isNotNull);
375+
376+
PackageGraph p = results.packageGraph;
377+
expect(p.defaultPackage.name, 'test_package_custom_templates');
378+
expect(p.localPublicLibraries, hasLength(1));
379+
});
380+
381+
test('generate docs with missing required template fails', () async {
382+
var templatesDir = path.join(path.current, 'test/templates');
383+
try {
384+
await buildDartdoc(['--templates-dir', templatesDir], testPackageCustomTemplates, tempDir);
385+
fail('dartdoc should fail with missing required template');
386+
} catch (e) {
387+
expect(e is DartdocFailure, isTrue);
388+
expect((e as DartdocFailure).message, startsWith('Missing required template file'));
389+
}
390+
});
391+
392+
test('generate docs with bad templatesDir path fails', () async {
393+
String badPath = path.join(tempDir.path, 'BAD');
394+
try {
395+
await buildDartdoc(['--templates-dir', badPath], testPackageCustomTemplates, tempDir);
396+
fail('dartdoc should fail with bad templatesDir path');
397+
} catch (e) {
398+
expect(e is DartdocFailure, isTrue);
399+
}
400+
});
366401
}, timeout: Timeout.factor(8));
367402
}

test/html_generator_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ void main() {
1717
Templates templates;
1818

1919
setUp(() async {
20-
templates = await Templates.create();
20+
templates = await Templates.createDefault();
2121
});
2222

2323
test('index html', () {

test/src/utils.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ final Directory testPackageOptionsImporter =
4646
Directory('testing/test_package_options_importer');
4747
final Directory testPackageToolError =
4848
Directory('testing/test_package_tool_error');
49+
final Directory testPackageCustomTemplates =
50+
Directory('testing/test_package_custom_templates');
4951

5052
/// Convenience factory to build a [DartdocGeneratorOptionContext] and associate
5153
/// it with a [DartdocOptionSet] based on the current working directory and/or
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// The main function. It does the main thing.
2+
main(List<String> args) {
3+
new HelloPrinter().sayHello();
4+
}
5+
6+
/// A class that prints 'Hello'.
7+
class HelloPrinter {
8+
/// A method that prints 'Hello'
9+
void sayHello() {
10+
print('hello');
11+
}
12+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name: test_package_custom_templates
2+
version: 0.0.1
3+
description: A simple console application.
4+
#dependencies:
5+
# foo_bar: '>=1.0.0 <2.0.0'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{{>head}}
2+
3+
<div>
4+
<h1>Oops, seems there's a problem...</h1>
5+
</div>
6+
7+
{{>footer}}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{{#hasDocumentation}}
2+
<section>
3+
{{{ documentationAsHtml }}}
4+
</section>
5+
{{/hasDocumentation}}

0 commit comments

Comments
 (0)