2017-09-30 05:27:31 +00:00
|
|
|
import 'dart:async';
|
2017-10-01 03:14:44 +00:00
|
|
|
import 'dart:collection';
|
2018-11-10 17:43:39 +00:00
|
|
|
|
2017-09-30 05:27:31 +00:00
|
|
|
import 'package:file/file.dart';
|
|
|
|
import 'package:jael/jael.dart';
|
2018-11-03 00:16:03 +00:00
|
|
|
import 'package:symbol_table/symbol_table.dart';
|
2017-09-30 05:27:31 +00:00
|
|
|
|
2017-10-02 15:46:00 +00:00
|
|
|
/// Modifies a Jael document.
|
2018-04-03 05:04:34 +00:00
|
|
|
typedef FutureOr<Document> Patcher(Document document,
|
|
|
|
Directory currentDirectory, void onError(JaelError error));
|
2017-10-02 15:46:00 +00:00
|
|
|
|
2017-09-30 05:27:31 +00:00
|
|
|
/// Expands all `block[name]` tags within the template, replacing them with the correct content.
|
2017-10-02 15:46:00 +00:00
|
|
|
///
|
|
|
|
/// To apply additional patches to resolved documents, provide a set of [patch]
|
|
|
|
/// functions.
|
2017-09-30 05:27:31 +00:00
|
|
|
Future<Document> resolve(Document document, Directory currentDirectory,
|
2017-10-02 15:46:00 +00:00
|
|
|
{void onError(JaelError error), Iterable<Patcher> patch}) async {
|
2017-09-30 05:27:31 +00:00
|
|
|
onError ?? (e) => throw e;
|
|
|
|
|
|
|
|
// Resolve all includes...
|
|
|
|
var includesResolved =
|
|
|
|
await resolveIncludes(document, currentDirectory, onError);
|
2018-11-03 00:16:03 +00:00
|
|
|
|
2018-11-10 17:43:39 +00:00
|
|
|
var patched = await applyInheritance(
|
|
|
|
includesResolved, currentDirectory, onError, patch);
|
2017-10-02 15:46:00 +00:00
|
|
|
|
|
|
|
if (patch?.isNotEmpty != true) return patched;
|
|
|
|
|
|
|
|
for (var p in patch) {
|
|
|
|
patched = await p(patched, currentDirectory, onError);
|
|
|
|
}
|
|
|
|
|
|
|
|
return patched;
|
2017-10-01 03:14:44 +00:00
|
|
|
}
|
2017-09-30 05:27:31 +00:00
|
|
|
|
2017-10-01 03:14:44 +00:00
|
|
|
/// Folds any `extend` declarations.
|
|
|
|
Future<Document> applyInheritance(Document document, Directory currentDirectory,
|
2018-11-10 17:43:39 +00:00
|
|
|
void onError(JaelError error), Iterable<Patcher> patch) async {
|
2017-10-01 03:14:44 +00:00
|
|
|
if (document.root.tagName.name != 'extend') return document;
|
2017-09-30 05:27:31 +00:00
|
|
|
|
2017-10-01 03:14:44 +00:00
|
|
|
var element = document.root;
|
2018-04-03 05:04:34 +00:00
|
|
|
var attr =
|
|
|
|
element.attributes.firstWhere((a) => a.name == 'src', orElse: () => null);
|
2017-09-30 05:27:31 +00:00
|
|
|
if (attr == null) {
|
|
|
|
onError(new JaelError(JaelErrorSeverity.warning,
|
|
|
|
'Missing "src" attribute in "extend" tag.', element.tagName.span));
|
|
|
|
return null;
|
|
|
|
} else if (attr.value is! StringLiteral) {
|
|
|
|
onError(new JaelError(
|
|
|
|
JaelErrorSeverity.warning,
|
|
|
|
'The "src" attribute in an "extend" tag must be a string literal.',
|
|
|
|
element.tagName.span));
|
|
|
|
return null;
|
|
|
|
} else {
|
2017-10-01 03:14:44 +00:00
|
|
|
// First, we need to identify the root template.
|
|
|
|
var chain = new Queue<Document>();
|
|
|
|
|
|
|
|
while (document != null) {
|
|
|
|
chain.addFirst(document);
|
|
|
|
var parent = getParent(document, onError);
|
|
|
|
if (parent == null) break;
|
|
|
|
var file = currentDirectory.fileSystem
|
|
|
|
.file(currentDirectory.uri.resolve(parent));
|
|
|
|
var contents = await file.readAsString();
|
|
|
|
document = parseDocument(contents, sourceUrl: file.uri, onError: onError);
|
2018-11-10 17:43:39 +00:00
|
|
|
//document = await resolveIncludes(document, file.parent, onError);
|
|
|
|
document = await resolve(document, currentDirectory,
|
|
|
|
onError: onError, patch: patch);
|
2017-10-01 03:14:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Then, for each referenced template, in order, transform the last template
|
|
|
|
// by filling in blocks.
|
|
|
|
document = chain.removeFirst();
|
|
|
|
|
2018-11-03 00:16:03 +00:00
|
|
|
var blocks = new SymbolTable<Element>();
|
|
|
|
|
2017-10-01 03:14:44 +00:00
|
|
|
while (chain.isNotEmpty) {
|
|
|
|
var child = chain.removeFirst();
|
2018-11-03 00:16:03 +00:00
|
|
|
var scope = blocks;
|
|
|
|
extractBlockDeclarations(scope, child.root, onError);
|
2018-11-10 17:43:39 +00:00
|
|
|
|
2017-10-01 03:14:44 +00:00
|
|
|
var blocksExpanded =
|
|
|
|
await expandBlocks(document.root, blocks, currentDirectory, onError);
|
2018-11-03 00:16:03 +00:00
|
|
|
|
|
|
|
if (blocksExpanded == null) {
|
2018-11-10 17:43:39 +00:00
|
|
|
// When this returns null, we've reached a block declaration.
|
|
|
|
// Just continue.
|
|
|
|
continue;
|
2018-11-03 00:16:03 +00:00
|
|
|
}
|
|
|
|
|
2017-10-01 03:14:44 +00:00
|
|
|
document =
|
|
|
|
new Document(child.doctype ?? document.doctype, blocksExpanded);
|
2017-09-30 05:27:31 +00:00
|
|
|
}
|
|
|
|
|
2017-10-01 03:14:44 +00:00
|
|
|
// Fill in any remaining blocks
|
2017-09-30 05:27:31 +00:00
|
|
|
var blocksExpanded =
|
2018-11-03 00:16:03 +00:00
|
|
|
await expandBlocks(document.root, blocks, currentDirectory, onError);
|
2017-10-01 03:14:44 +00:00
|
|
|
return new Document(document.doctype, blocksExpanded);
|
2017-09-30 05:27:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-03 00:16:03 +00:00
|
|
|
List<Element> allBlocksRecursive(Element element) {
|
|
|
|
var out = <Element>[];
|
|
|
|
|
|
|
|
for (var e in element.children) {
|
|
|
|
if (e is Element && e.tagName.name == 'block') {
|
|
|
|
out.add(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var child in element.children) {
|
|
|
|
if (child is Element) {
|
|
|
|
var childBlocks = allBlocksRecursive(child);
|
|
|
|
out.addAll(childBlocks);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out; //out.reversed.toList();
|
|
|
|
}
|
|
|
|
|
2017-10-01 03:14:44 +00:00
|
|
|
/// Extracts any `block` declarations.
|
2018-11-03 00:16:03 +00:00
|
|
|
void extractBlockDeclarations(SymbolTable<Element> blocks, Element element,
|
|
|
|
void onError(JaelError error)) {
|
|
|
|
//var blockElements = allBlocksRecursive(element);
|
2017-10-01 03:14:44 +00:00
|
|
|
var blockElements =
|
|
|
|
element.children.where((e) => e is Element && e.tagName.name == 'block');
|
|
|
|
|
|
|
|
for (Element blockElement in blockElements) {
|
|
|
|
var nameAttr = blockElement.attributes
|
2017-10-02 16:08:02 +00:00
|
|
|
.firstWhere((a) => a.name == 'name', orElse: () => null);
|
2017-10-01 03:14:44 +00:00
|
|
|
if (nameAttr == null) {
|
|
|
|
onError(new JaelError(JaelErrorSeverity.warning,
|
|
|
|
'Missing "name" attribute in "block" tag.', blockElement.span));
|
|
|
|
} else if (nameAttr.value is! StringLiteral) {
|
|
|
|
onError(new JaelError(
|
|
|
|
JaelErrorSeverity.warning,
|
|
|
|
'The "name" attribute in an "block" tag must be a string literal.',
|
|
|
|
nameAttr.span));
|
|
|
|
} else {
|
|
|
|
var name = (nameAttr.value as StringLiteral).value;
|
2018-11-03 00:16:03 +00:00
|
|
|
blocks.assign(name, blockElement);
|
2017-10-01 03:14:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Finds the name of the parent template.
|
|
|
|
String getParent(Document document, void onError(JaelError error)) {
|
|
|
|
var element = document.root;
|
2018-11-10 17:43:39 +00:00
|
|
|
if (element?.tagName?.name != 'extend') return null;
|
2017-10-01 03:14:44 +00:00
|
|
|
|
2018-04-03 05:04:34 +00:00
|
|
|
var attr =
|
|
|
|
element.attributes.firstWhere((a) => a.name == 'src', orElse: () => null);
|
2017-10-01 03:14:44 +00:00
|
|
|
if (attr == null) {
|
|
|
|
onError(new JaelError(JaelErrorSeverity.warning,
|
|
|
|
'Missing "src" attribute in "extend" tag.', element.tagName.span));
|
|
|
|
return null;
|
|
|
|
} else if (attr.value is! StringLiteral) {
|
|
|
|
onError(new JaelError(
|
|
|
|
JaelErrorSeverity.warning,
|
|
|
|
'The "src" attribute in an "extend" tag must be a string literal.',
|
|
|
|
element.tagName.span));
|
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
return (attr.value as StringLiteral).value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Replaces any `block` tags within the element.
|
2018-11-10 17:43:39 +00:00
|
|
|
Future<Element> expandBlocks(Element element, SymbolTable<Element> outerScope,
|
2017-09-30 05:27:31 +00:00
|
|
|
Directory currentDirectory, void onError(JaelError error)) async {
|
2018-11-10 17:43:39 +00:00
|
|
|
var innerScope = outerScope.createChild();
|
2018-11-03 00:16:03 +00:00
|
|
|
|
2017-09-30 05:27:31 +00:00
|
|
|
if (element is SelfClosingElement)
|
|
|
|
return element;
|
|
|
|
else if (element is RegularElement) {
|
|
|
|
if (element.children.isEmpty) return element;
|
|
|
|
|
2018-11-10 17:43:39 +00:00
|
|
|
var expanded = new Set<ElementChild>();
|
|
|
|
|
|
|
|
element.children.forEach((e) => print(e.span.highlight()));
|
2017-09-30 05:27:31 +00:00
|
|
|
|
|
|
|
for (var child in element.children) {
|
|
|
|
if (child is Element) {
|
|
|
|
if (child is SelfClosingElement)
|
|
|
|
expanded.add(child);
|
|
|
|
else if (child is RegularElement) {
|
|
|
|
if (child.tagName.name != 'block') {
|
|
|
|
expanded.add(child);
|
|
|
|
} else {
|
2017-10-01 03:14:44 +00:00
|
|
|
var nameAttr = child.attributes
|
2017-10-02 16:08:02 +00:00
|
|
|
.firstWhere((a) => a.name == 'name', orElse: () => null);
|
2017-09-30 05:27:31 +00:00
|
|
|
if (nameAttr == null) {
|
2017-10-01 03:14:44 +00:00
|
|
|
onError(new JaelError(JaelErrorSeverity.warning,
|
|
|
|
'Missing "name" attribute in "block" tag.', child.span));
|
2017-09-30 05:27:31 +00:00
|
|
|
} else if (nameAttr.value is! StringLiteral) {
|
|
|
|
onError(new JaelError(
|
|
|
|
JaelErrorSeverity.warning,
|
|
|
|
'The "name" attribute in an "block" tag must be a string literal.',
|
|
|
|
nameAttr.span));
|
|
|
|
}
|
|
|
|
|
|
|
|
var name = (nameAttr.value as StringLiteral).value;
|
|
|
|
Iterable<ElementChild> children;
|
|
|
|
|
2018-11-10 17:43:39 +00:00
|
|
|
if (innerScope.resolve(name) == null) {
|
|
|
|
print('Why is $name not defined?');
|
|
|
|
children = []; //child.children;
|
2017-09-30 05:27:31 +00:00
|
|
|
} else {
|
2018-11-10 17:43:39 +00:00
|
|
|
children = innerScope.resolve(name).value.children;
|
2017-09-30 05:27:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
expanded.addAll(children);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new UnsupportedError(
|
|
|
|
'Unsupported element type: ${element.runtimeType}');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
expanded.add(child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resolve all includes...
|
2018-11-03 00:16:03 +00:00
|
|
|
var out = <ElementChild>[];
|
|
|
|
|
|
|
|
for (var c in expanded) {
|
|
|
|
if (c is Element) {
|
|
|
|
var blocksExpanded =
|
2018-11-10 17:43:39 +00:00
|
|
|
await expandBlocks(c, innerScope, currentDirectory, onError);
|
2018-11-03 00:16:03 +00:00
|
|
|
var nameAttr = c.attributes
|
|
|
|
.firstWhere((a) => a.name == 'name', orElse: () => null);
|
|
|
|
var name = (nameAttr?.value is StringLiteral)
|
|
|
|
? ((nameAttr.value as StringLiteral).value)
|
|
|
|
: null;
|
|
|
|
|
|
|
|
if (name == null) {
|
|
|
|
out.add(blocksExpanded);
|
|
|
|
} else {
|
|
|
|
// This element itself resolved to a block; expand it.
|
2018-11-10 17:43:39 +00:00
|
|
|
out.addAll(innerScope.resolve(name)?.value?.children ?? <ElementChild>[]);
|
2018-11-03 00:16:03 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
out.add(c);
|
|
|
|
}
|
|
|
|
}
|
2017-09-30 05:27:31 +00:00
|
|
|
|
2018-11-03 00:16:03 +00:00
|
|
|
var finalElement = new RegularElement(
|
2017-09-30 05:27:31 +00:00
|
|
|
element.lt,
|
|
|
|
element.tagName,
|
|
|
|
element.attributes,
|
|
|
|
element.gt,
|
2018-11-03 00:16:03 +00:00
|
|
|
out,
|
2017-09-30 05:27:31 +00:00
|
|
|
element.lt2,
|
|
|
|
element.slash,
|
|
|
|
element.tagName2,
|
|
|
|
element.gt2);
|
2018-11-03 00:16:03 +00:00
|
|
|
|
|
|
|
// If THIS element is a block, it needs to be expanded as well.
|
|
|
|
if (element.tagName.name == 'block') {
|
|
|
|
var nameAttr = element.attributes
|
|
|
|
.firstWhere((a) => a.name == 'name', orElse: () => null);
|
|
|
|
if (nameAttr == null) {
|
|
|
|
onError(new JaelError(JaelErrorSeverity.warning,
|
|
|
|
'Missing "name" attribute in "block" tag.', element.span));
|
|
|
|
} else if (nameAttr.value is! StringLiteral) {
|
|
|
|
onError(new JaelError(
|
|
|
|
JaelErrorSeverity.warning,
|
|
|
|
'The "name" attribute in an "block" tag must be a string literal.',
|
|
|
|
nameAttr.span));
|
|
|
|
}
|
|
|
|
|
|
|
|
var name = (nameAttr.value as StringLiteral).value;
|
2018-11-10 17:43:39 +00:00
|
|
|
outerScope.assign(name, finalElement);
|
|
|
|
throw outerScope.allVariables.map((v) => v.name);
|
2018-11-03 00:16:03 +00:00
|
|
|
} else {
|
|
|
|
return finalElement;
|
|
|
|
}
|
2017-09-30 05:27:31 +00:00
|
|
|
} else {
|
|
|
|
throw new UnsupportedError(
|
|
|
|
'Unsupported element type: ${element.runtimeType}');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Expands all `include[src]` tags within the template, and fills in the content of referenced files.
|
|
|
|
Future<Document> resolveIncludes(Document document, Directory currentDirectory,
|
|
|
|
void onError(JaelError error)) async {
|
|
|
|
return new Document(document.doctype,
|
|
|
|
await _expandIncludes(document.root, currentDirectory, onError));
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Element> _expandIncludes(Element element, Directory currentDirectory,
|
|
|
|
void onError(JaelError error)) async {
|
|
|
|
if (element.tagName.name != 'include') {
|
|
|
|
if (element is SelfClosingElement)
|
|
|
|
return element;
|
|
|
|
else if (element is RegularElement) {
|
|
|
|
List<ElementChild> expanded = [];
|
|
|
|
|
|
|
|
for (var child in element.children) {
|
|
|
|
if (child is Element) {
|
|
|
|
expanded.add(await _expandIncludes(child, currentDirectory, onError));
|
|
|
|
} else {
|
|
|
|
expanded.add(child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new RegularElement(
|
|
|
|
element.lt,
|
|
|
|
element.tagName,
|
|
|
|
element.attributes,
|
|
|
|
element.gt,
|
|
|
|
expanded,
|
|
|
|
element.lt2,
|
|
|
|
element.slash,
|
|
|
|
element.tagName2,
|
|
|
|
element.gt2);
|
|
|
|
} else {
|
|
|
|
throw new UnsupportedError(
|
|
|
|
'Unsupported element type: ${element.runtimeType}');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-03 05:04:34 +00:00
|
|
|
var attr =
|
|
|
|
element.attributes.firstWhere((a) => a.name == 'src', orElse: () => null);
|
2017-09-30 05:27:31 +00:00
|
|
|
if (attr == null) {
|
|
|
|
onError(new JaelError(JaelErrorSeverity.warning,
|
|
|
|
'Missing "src" attribute in "include" tag.', element.tagName.span));
|
|
|
|
return null;
|
|
|
|
} else if (attr.value is! StringLiteral) {
|
|
|
|
onError(new JaelError(
|
|
|
|
JaelErrorSeverity.warning,
|
|
|
|
'The "src" attribute in an "include" tag must be a string literal.',
|
|
|
|
element.tagName.span));
|
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
var src = (attr.value as StringLiteral).value;
|
|
|
|
var file =
|
|
|
|
currentDirectory.fileSystem.file(currentDirectory.uri.resolve(src));
|
|
|
|
var contents = await file.readAsString();
|
|
|
|
var doc = parseDocument(contents, sourceUrl: file.uri, onError: onError);
|
|
|
|
var processed = await resolve(
|
|
|
|
doc, currentDirectory.fileSystem.directory(file.dirname),
|
|
|
|
onError: onError);
|
|
|
|
return processed.root;
|
|
|
|
}
|
|
|
|
}
|