Skip to content

Commit 3a16c11

Browse files
authored
Mustachio: Introduce Template class for pre-parsing template files. (#2484)
Mustachio: Introduce Template class for pre-parsing template files. Second, change the signature of a partial resolver to return a Future<File>. This allows for dartdoc's current partial parsing, which requires use of an asynchronous Isolate API to get the path of a "package:" URI asset. Before this change, Mustachio required re-parsing a template file for every object which was to be rendered with that file. Horribly inefficient. This change makes Mustachio much more similar to other Mustache parsers which allow the user to handle a Template object which can be used to render multiple context objects. When parsing a Mustache template into a Template object, all partials (recursive) will also be resolved, read, and parsed into Template objects. This change has broad and beneficial consequences across the APIs which previously passesd around Files and PartialResolvers: * A RenderFunction now takes a Template instead of a File and a PartialResolver. * RenderBase() now takes a Template instead of a File and a PartialResolver, and never concerns itself with partial resolution. * RenderBase.partial now just retreives the pre-parsed partial instead of parsing inline. * SimpleRenderer and renderSimple now take a Template instead of a File and a PartialResolver. Also, the meta package is bumped in order to use `@internal`.
1 parent 29db621 commit 3a16c11

File tree

7 files changed

+316
-239
lines changed

7 files changed

+316
-239
lines changed

lib/src/generator/templates.renderers.dart

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,14 @@ String _simpleResolveErrorMessage(List<String> key, String type) =>
1616
'expose the properties of $type by adding it to the @Renderer '
1717
"annotation's 'visibleTypes' list";
1818

19-
String renderIndex(PackageTemplateData context, File file,
20-
{PartialResolver partialResolver}) {
21-
try {
22-
var parser = MustachioParser(file.readAsStringSync());
23-
return _render_PackageTemplateData(context, parser.parse(), file,
24-
partialResolver: partialResolver);
25-
} on FileSystemException catch (e) {
26-
throw MustachioResolutionError(
27-
'FileSystemException when reading template "${file.path}": ${e.message}');
28-
}
19+
String renderIndex(PackageTemplateData context, Template template) {
20+
return _render_PackageTemplateData(context, template.ast, template);
2921
}
3022

3123
String _render_PackageTemplateData(
32-
PackageTemplateData context, List<MustachioNode> ast, File file,
33-
{RendererBase<Object> parent, PartialResolver partialResolver}) {
34-
var renderer = _Renderer_PackageTemplateData(context, parent, file,
35-
partialResolver: partialResolver);
24+
PackageTemplateData context, List<MustachioNode> ast, Template template,
25+
{RendererBase<Object> parent}) {
26+
var renderer = _Renderer_PackageTemplateData(context, parent, template);
3627
renderer.renderBlock(ast);
3728
return renderer.buffer.toString();
3829
}
@@ -203,10 +194,9 @@ class _Renderer_PackageTemplateData extends RendererBase<PackageTemplateData> {
203194
..._Renderer_TemplateData.propertyMap<Package, CT_>(),
204195
};
205196

206-
_Renderer_PackageTemplateData(
207-
PackageTemplateData context, RendererBase<Object> parent, File file,
208-
{PartialResolver partialResolver})
209-
: super(context, parent, file, partialResolver: partialResolver);
197+
_Renderer_PackageTemplateData(PackageTemplateData context,
198+
RendererBase<Object> parent, Template template)
199+
: super(context, parent, template);
210200

211201
@override
212202
Property<PackageTemplateData> getProperty(String key) {
@@ -218,10 +208,10 @@ class _Renderer_PackageTemplateData extends RendererBase<PackageTemplateData> {
218208
}
219209
}
220210

221-
String _render_Object(Object context, List<MustachioNode> ast, File file,
222-
{RendererBase<Object> parent, PartialResolver partialResolver}) {
223-
var renderer =
224-
_Renderer_Object(context, parent, file, partialResolver: partialResolver);
211+
String _render_Object(
212+
Object context, List<MustachioNode> ast, Template template,
213+
{RendererBase<Object> parent}) {
214+
var renderer = _Renderer_Object(context, parent, template);
225215
renderer.renderBlock(ast);
226216
return renderer.buffer.toString();
227217
}
@@ -246,9 +236,9 @@ class _Renderer_Object extends RendererBase<Object> {
246236
),
247237
};
248238

249-
_Renderer_Object(Object context, RendererBase<Object> parent, File file,
250-
{PartialResolver partialResolver})
251-
: super(context, parent, file, partialResolver: partialResolver);
239+
_Renderer_Object(
240+
Object context, RendererBase<Object> parent, Template template)
241+
: super(context, parent, template);
252242

253243
@override
254244
Property<Object> getProperty(String key) {
@@ -261,10 +251,9 @@ class _Renderer_Object extends RendererBase<Object> {
261251
}
262252

263253
String _render_TemplateData<T extends Documentable>(
264-
TemplateData<T> context, List<MustachioNode> ast, File file,
265-
{RendererBase<Object> parent, PartialResolver partialResolver}) {
266-
var renderer = _Renderer_TemplateData(context, parent, file,
267-
partialResolver: partialResolver);
254+
TemplateData<T> context, List<MustachioNode> ast, Template template,
255+
{RendererBase<Object> parent}) {
256+
var renderer = _Renderer_TemplateData(context, parent, template);
268257
renderer.renderBlock(ast);
269258
return renderer.buffer.toString();
270259
}
@@ -570,9 +559,8 @@ class _Renderer_TemplateData<T extends Documentable>
570559
};
571560

572561
_Renderer_TemplateData(
573-
TemplateData<T> context, RendererBase<Object> parent, File file,
574-
{PartialResolver partialResolver})
575-
: super(context, parent, file, partialResolver: partialResolver);
562+
TemplateData<T> context, RendererBase<Object> parent, Template template)
563+
: super(context, parent, template);
576564

577565
@override
578566
Property<TemplateData<T>> getProperty(String key) {

lib/src/mustachio/renderer_base.dart

Lines changed: 124 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,131 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:collection';
6+
57
import 'package:analyzer/file_system/file_system.dart';
68
import 'package:meta/meta.dart';
7-
import 'package:path/path.dart' as path;
89
import 'parser.dart';
910

1011
/// The signature of a partial resolver function.
11-
typedef PartialResolver = String Function(String path);
12+
typedef PartialResolver = Future<File> Function(String uri);
1213

1314
/// The signature of a generated render function.
14-
typedef Renderer<T> = String Function(T context, File file,
15-
{PartialResolver partialResolver});
15+
typedef RenderFunction<T> = String Function(T context, Template template);
16+
17+
/// A parsed Mustache template.
18+
///
19+
/// This container includes Templates for all partials parsed in the template
20+
/// file.
21+
class Template {
22+
/// The parsed Mustache syntax tree.
23+
final List<MustachioNode> ast;
24+
25+
/// A mapping of partial keys to partial [File]s.
26+
///
27+
/// This may appear redundant with [partialTemplates], the mapping of partial
28+
/// files to partial templates. The reason for two mappings is that
29+
/// [partialTemplates] functions as a cache, so that each [File] containing a
30+
/// partial template is only parsed once, and also so that partial templates
31+
/// can reference themselves, or otherwise recurse. The cache is passed down
32+
/// from template to partial. Mapping a partial key to a partial [File],
33+
/// however, cannot be shared between templates and partials because they
34+
/// typically contain relative file paths. Each partial file path is given
35+
/// relative to the template or partial making the reference.
36+
// TODO(srawlins): Resolving partials from the Parser rather than
37+
// [Template.parse] would allow the [File] to be stored on the [Partial] node,
38+
// removing the need for this mapping.
39+
final Map<String, File> partials;
40+
41+
/// A mapping of partial [File]s to parsed Mustache templates.
42+
final Map<File, Template> partialTemplates;
43+
44+
Template._(
45+
{@required this.ast,
46+
@required this.partials,
47+
@required this.partialTemplates});
48+
49+
/// Parses [file] as a Mustache template, returning a [Template].
50+
///
51+
/// A [partialResolver] can be passed if custom partial resolution is required
52+
/// (for example, to load any partial from a single directory, and to append a
53+
/// particular suffix).
54+
///
55+
/// By default, a partial key is resolved as a file path which is either
56+
/// absolute, or relative to the directory containing the template which
57+
/// references the partial.
58+
///
59+
/// For example, if the Mustache template at `/foo/template.html` includes a
60+
/// Partial node, `{{>partials/p1.html}}`, then the partial file path is
61+
/// resolved as `/foo/partials/p1.html`. If this template includes a Partial
62+
/// node, `{{>p2.html}}`, then this partial file path is resolved (relative to
63+
/// the directory containing `p1.html`, not relative to the top-level
64+
/// template), as `/foo/partials/p2.html`.
65+
static Future<Template> parse(File file,
66+
{PartialResolver partialResolver,
67+
@internal Map<File, Template> partialTemplates}) async {
68+
partialTemplates ??= <File, Template>{};
69+
if (partialResolver == null) {
70+
var pathContext = file.provider.pathContext;
71+
// By default, resolve partials as absolute file paths, or as relative
72+
// file paths, relative to the template directory from which they are
73+
// referenced.
74+
partialResolver = (String path) async {
75+
var partialPath = pathContext.isAbsolute(path)
76+
? path
77+
: pathContext.join(file.parent.path, path);
78+
var partialFile =
79+
file.provider.getFile(pathContext.normalize(partialPath));
80+
return partialFile;
81+
};
82+
}
83+
84+
// If we fail to read [file], and an exception is thrown, one of two
85+
// things happen:
86+
// 1) In the case of a reference from a partial, the exception is caught
87+
// below, when parsing partials, and rethrown with information about the
88+
// template with the reference.
89+
// 2) In the case of a reference from a top-level template, user code has
90+
// called [Template.parse], and the user is responsible for handling the
91+
// exception.
92+
var ast = MustachioParser(file.readAsStringSync()).parse();
93+
var nodeQueue = Queue.of(ast);
94+
var partials = <String, File>{};
95+
96+
// Walk the Mustache syntax tree, looking for Partial nodes.
97+
while (nodeQueue.isNotEmpty) {
98+
var node = nodeQueue.removeFirst();
99+
if (node is Text) {
100+
// Nothing to do.
101+
} else if (node is Variable) {
102+
// Nothing to do.
103+
} else if (node is Section) {
104+
nodeQueue.addAll(node.children);
105+
} else if (node is Partial) {
106+
var key = node.key;
107+
if (!partials.containsKey(key)) {
108+
partials[key] = await partialResolver(key);
109+
}
110+
var partialFile = partials[key];
111+
if (!partialTemplates.containsKey(partialFile)) {
112+
try {
113+
var partialTemplate = await Template.parse(partialFile,
114+
partialResolver: partialResolver,
115+
partialTemplates: {...partialTemplates});
116+
partialTemplates[partialFile] = partialTemplate;
117+
} on FileSystemException catch (e) {
118+
throw MustachioResolutionError(
119+
'FileSystemException when reading partial "$key" found in '
120+
'template "${file.path}": ${e.message}');
121+
}
122+
}
123+
}
124+
}
125+
126+
return Template._(
127+
ast: ast, partials: partials, partialTemplates: partialTemplates);
128+
}
129+
}
16130

17131
/// The base class for a generated Mustache renderer.
18132
abstract class RendererBase<T> {
@@ -22,25 +136,12 @@ abstract class RendererBase<T> {
22136
/// The renderer of the parent context, if any, otherwise `null`.
23137
final RendererBase parent;
24138

25-
final File template;
26-
27-
final PartialResolver partialResolver;
139+
final Template template;
28140

29141
/// The output buffer into which [context] is rendered, using a template.
30142
final buffer = StringBuffer();
31143

32-
RendererBase(this.context, this.parent, this.template,
33-
{this.partialResolver});
34-
35-
path.Context get pathContext => template.provider.pathContext;
36-
37-
String _defaultResolver(String path) {
38-
var partialPath = pathContext.isAbsolute(path)
39-
? path
40-
: pathContext.join(template.parent.path, path);
41-
var file = template.provider.getFile(pathContext.normalize(partialPath));
42-
return file.readAsStringSync();
43-
}
144+
RendererBase(this.context, this.parent, this.template);
44145

45146
void write(String text) => buffer.write(text);
46147

@@ -143,28 +244,20 @@ abstract class RendererBase<T> {
143244

144245
void partial(Partial node) {
145246
var key = node.key;
146-
try {
147-
var partial = (partialResolver ?? _defaultResolver)(key);
148-
var parser = MustachioParser(partial);
149-
var ast = parser.parse();
150-
renderBlock(ast);
151-
} on FileSystemException catch (e) {
152-
throw MustachioResolutionError(
153-
'FileSystemException when reading partial "$key" found in template '
154-
'"${template.path}": ${e.message}');
155-
}
247+
var partialFile = template.partials[key];
248+
renderBlock(template.partialTemplates[partialFile].ast);
156249
}
157250
}
158251

159-
String renderSimple(Object context, List<MustachioNode> ast, File template,
252+
String renderSimple(Object context, List<MustachioNode> ast, Template template,
160253
{RendererBase parent}) {
161254
var renderer = SimpleRenderer(context, parent, template);
162255
renderer.renderBlock(ast);
163256
return renderer.buffer.toString();
164257
}
165258

166259
class SimpleRenderer extends RendererBase<Object> {
167-
SimpleRenderer(Object context, RendererBase<Object> parent, File template)
260+
SimpleRenderer(Object context, RendererBase<Object> parent, Template template)
168261
: super(context, parent, template);
169262

170263
@override

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies:
1717
html: '>=0.12.1 <0.15.0'
1818
logging: ^0.11.3+1
1919
markdown: '>=2.1.5 <4.0.0'
20-
meta: ^1.1.8
20+
meta: ^1.2.4
2121
mustache: ^1.1.0
2222
package_config: '>=0.1.5 <2.0.0'
2323
path: ^1.3.0

test/mustachio/builder_test.dart

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ class Bar {}
9292
// The render function for Foo
9393
expect(
9494
generatedContent,
95-
contains(
96-
'String _render_FooBase(FooBase context, List<MustachioNode> ast,'));
95+
contains('String _render_FooBase(\n'
96+
' FooBase context, List<MustachioNode> ast, Template template,'));
9797
// The renderer class for Foo
9898
expect(generatedContent,
9999
contains('class _Renderer_FooBase extends RendererBase<FooBase>'));
@@ -103,8 +103,8 @@ class Bar {}
103103
// The render function for Object
104104
expect(
105105
generatedContent,
106-
contains(
107-
'String _render_Object(Object context, List<MustachioNode> ast,'));
106+
contains('String _render_Object(\n'
107+
' Object context, List<MustachioNode> ast, Template template,'));
108108
// The renderer class for Object
109109
expect(generatedContent,
110110
contains('class _Renderer_Object extends RendererBase<Object> {'));
@@ -241,15 +241,14 @@ import 'package:mustachio/annotations.dart';
241241

242242
test('with a corresponding public API function', () async {
243243
expect(generatedContent,
244-
contains('String renderFoo<T>(Foo<T> context, File file,'));
245-
expect(generatedContent, contains('{PartialResolver partialResolver})'));
244+
contains('String renderFoo<T>(Foo<T> context, Template template)'));
246245
});
247246

248247
test('with a corresponding render function', () async {
249248
expect(
250249
generatedContent,
251-
contains(
252-
'String _render_Foo<T>(Foo<T> context, List<MustachioNode> ast, File file,'));
250+
contains('String _render_Foo<T>(\n'
251+
' Foo<T> context, List<MustachioNode> ast, Template template,'));
253252
});
254253

255254
test('with a generic supertype type argument', () async {

0 commit comments

Comments
 (0)