+homepage: https://docs.angel-dart.dev/packages/front-end/jael
+environment:
+ sdk: ">=2.0.0 <3.0.0"
+dependencies:
+ args: ^1.0.0
+ charcode: ^1.0.0
+ code_buffer: ^1.0.0
+ source_span: ^1.0.0
+ string_scanner: ^1.0.0
+ symbol_table: ^2.0.0
+dev_dependencies:
+ pedantic: ^1.0.0
+ test: ^1.0.0
+executables:
+ jaelfmt: jaelfmt
\ No newline at end of file
diff --git a/packages/jael/jael/test/render/custom_element_test.dart b/packages/jael/jael/test/render/custom_element_test.dart
new file mode 100644
index 00000000..303c1627
--- /dev/null
+++ b/packages/jael/jael/test/render/custom_element_test.dart
@@ -0,0 +1,113 @@
+import 'dart:math';
+import 'package:code_buffer/code_buffer.dart';
+import 'package:jael/jael.dart' as jael;
+import 'package:symbol_table/symbol_table.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('render into div', () {
+ var template = '''
+
+
+ The square root of {{ n }} is {{ sqrt(n).toInt() }}.
+
+
+
+ ''';
+
+ var html = render(template, {'sqrt': sqrt});
+ print(html);
+
+ expect(
+ html,
+ '''
+
+
+ The square root of 16 is 4.
+
+
+ '''
+ .trim());
+ });
+
+ test('render into explicit tag name', () {
+ var template = '''
+
+
+ The square root of {{ n }} is {{ sqrt(n).toInt() }}.
+
+
+
+ ''';
+
+ var html = render(template, {'sqrt': sqrt});
+ print(html);
+
+ expect(
+ html,
+ '''
+
+
+ The square root of 16 is 4.
+
+
+ '''
+ .trim());
+ });
+
+ test('pass attributes', () {
+ var template = '''
+
+
+ The square root of {{ n }} is {{ sqrt(n).toInt() }}.
+
+
+
+ ''';
+
+ var html = render(template, {'sqrt': sqrt});
+ print(html);
+
+ expect(
+ html,
+ '''
+
+
+ The square root of 16 is 4.
+
+
+ '''
+ .trim());
+ });
+
+ test('render without tag name', () {
+ var template = '''
+
+
+ The square root of {{ n }} is {{ sqrt(n).toInt() }}.
+
+
+
+ ''';
+
+ var html = render(template, {'sqrt': sqrt});
+ print(html);
+
+ expect(
+ html,
+ '''
+
+ The square root of 16 is 4.
+
+
+ '''
+ .trim());
+ });
+}
+
+String render(String template, [Map values]) {
+ var doc = jael.parseDocument(template, onError: (e) => throw e);
+ var buffer = CodeBuffer();
+ const jael.Renderer().render(doc, buffer, SymbolTable(values: values));
+ return buffer.toString();
+}
diff --git a/packages/jael/jael/test/render/dsx_test.dart b/packages/jael/jael/test/render/dsx_test.dart
new file mode 100644
index 00000000..8c47ed7f
--- /dev/null
+++ b/packages/jael/jael/test/render/dsx_test.dart
@@ -0,0 +1,43 @@
+import 'package:jael/jael.dart';
+import 'package:symbol_table/symbol_table.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('attributes', () {
+ var doc = parseDSX('''
+
+ ''');
+
+ var foo = doc.root as SelfClosingElement;
+ expect(foo.tagName.name, 'foo');
+ expect(foo.attributes, hasLength(2));
+ expect(foo.getAttribute('bar'), isNotNull);
+ expect(foo.getAttribute('yes'), isNotNull);
+ expect(foo.getAttribute('bar').value.compute(null), 'baz');
+ expect(
+ foo
+ .getAttribute('yes')
+ .value
+ .compute(SymbolTable(values: {'no': 'maybe'})),
+ 'maybe');
+ });
+
+ test('children', () {
+ var doc = parseDSX('''
+
+ {24 * 3}
+
+ ''');
+
+ var bar = doc.root.children.first as RegularElement;
+ expect(bar.tagName.name, 'bar');
+
+ var interpolation = bar.children.first as Interpolation;
+ expect(interpolation.expression.compute(null), 24 * 3);
+ });
+}
+
+Document parseDSX(String text) {
+ return parseDocument(text,
+ sourceUrl: 'test.dsx', asDSX: true, onError: (e) => throw e);
+}
diff --git a/packages/jael/jael/test/render/render_test.dart b/packages/jael/jael/test/render/render_test.dart
new file mode 100644
index 00000000..b9a60e07
--- /dev/null
+++ b/packages/jael/jael/test/render/render_test.dart
@@ -0,0 +1,357 @@
+import 'package:code_buffer/code_buffer.dart';
+import 'package:jael/jael.dart' as jael;
+import 'package:symbol_table/symbol_table.dart';
+import 'package:test/test.dart';
+
+main() {
+ test('attribute binding', () {
+ const template = '''
+
+
+ Hello
+
+
+
+
+''';
+
+ var buf = CodeBuffer();
+ jael.Document document;
+ SymbolTable scope;
+
+ try {
+ document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ scope = SymbolTable(values: {
+ 'csrf_token': 'foo',
+ 'profile': {
+ 'avatar': 'thosakwe.png',
+ }
+ });
+ } on jael.JaelError catch (e) {
+ print(e);
+ print(e.stackTrace);
+ }
+
+ expect(document, isNotNull);
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+
+ Hello
+
+
+
+
+
+ '''
+ .trim());
+ });
+
+ test('interpolation', () {
+ const template = '''
+
+
+
+ Pokémon
+ {{ pokemon.name }} - {{ pokemon.type }}
+
+
+
+''';
+
+ var buf = CodeBuffer();
+ //jael.scan(template, sourceUrl: 'test.jael').tokens.forEach(print);
+ var document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ var scope = SymbolTable(values: {
+ 'pokemon': const _Pokemon('Darkrai', 'Dark'),
+ });
+
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString().replaceAll('\n', '').replaceAll(' ', '').trim(),
+ '''
+
+
+
+
+ Pokémon
+
+ Darkrai - Dark
+
+
+
+ '''
+ .replaceAll('\n', '')
+ .replaceAll(' ', '')
+ .trim());
+ });
+
+ test('for loop', () {
+ const template = '''
+
+
+ Pokémon
+
+ #{{ idx }} {{ starter.name }} - {{ starter.type }}
+
+
+
+''';
+
+ var buf = CodeBuffer();
+ var document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ var scope = SymbolTable(values: {
+ 'starters': starters,
+ });
+
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+
+ Pokémon
+
+
+
+ #0 Bulbasaur - Grass
+
+
+ #1 Charmander - Fire
+
+
+ #2 Squirtle - Water
+
+
+
+
+ '''
+ .trim());
+ });
+
+ test('conditional', () {
+ const template = '''
+
+
+ Conditional
+ Empty
+ Not empty
+
+
+''';
+
+ var buf = CodeBuffer();
+ var document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ var scope = SymbolTable(values: {
+ 'starters': starters,
+ });
+
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+
+ Conditional
+
+
+ Not empty
+
+
+
+ '''
+ .trim());
+ });
+
+ test('declare', () {
+ const template = '''
+
+
+
+ {{one}}
+ {{two}}
+ {{three}}
+
+
+
+ {{one}}
+ {{two}}
+ {{three}}
+
+
+
+
+''';
+
+ var buf = CodeBuffer();
+ var document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ var scope = SymbolTable();
+
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+
+
+ 1
+
+
+ 2
+
+
+ 4
+
+
+
+'''
+ .trim());
+ });
+
+ test('unescaped attr/interp', () {
+ const template = '''
+
+
" />
+ {{- "
" }}
+
+''';
+
+ var buf = CodeBuffer();
+ var document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ var scope = SymbolTable();
+
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString().replaceAll('\n', '').replaceAll(' ', '').trim(),
+ '''
+
+
+
+
+'''
+ .replaceAll('\n', '')
+ .replaceAll(' ', '')
+ .trim());
+ });
+
+ test('quoted attribute name', () {
+ const template = '''
+
+''';
+
+ var buf = CodeBuffer();
+ var document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ var scope = SymbolTable();
+
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+'''
+ .trim());
+ });
+
+ test('switch', () {
+ const template = '''
+
+
+ BAN HAMMER LOLOL
+
+
+ You are in good standing.
+
+
+ Weird...
+
+
+''';
+
+ var buf = CodeBuffer();
+ var document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ var scope = SymbolTable(values: {
+ 'account': _Account(isDisabled: true),
+ });
+
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(buf.toString().trim(), 'BAN HAMMER LOLOL');
+ });
+
+ test('default', () {
+ const template = '''
+
+
+ BAN HAMMER LOLOL
+
+
+ You are in good standing.
+
+
+ Weird...
+
+
+''';
+
+ var buf = CodeBuffer();
+ var document = jael.parseDocument(template, sourceUrl: 'test.jael');
+ var scope = SymbolTable(values: {
+ 'account': _Account(isDisabled: null),
+ });
+
+ const jael.Renderer().render(document, buf, scope);
+ print(buf);
+
+ expect(buf.toString().trim(), 'Weird...');
+ });
+}
+
+const List<_Pokemon> starters = [
+ _Pokemon('Bulbasaur', 'Grass'),
+ _Pokemon('Charmander', 'Fire'),
+ _Pokemon('Squirtle', 'Water'),
+];
+
+class _Pokemon {
+ final String name, type;
+
+ const _Pokemon(this.name, this.type);
+}
+
+class _Account {
+ final bool isDisabled;
+
+ _Account({this.isDisabled});
+}
diff --git a/packages/jael/jael/test/text/common.dart b/packages/jael/jael/test/text/common.dart
new file mode 100644
index 00000000..5fb71587
--- /dev/null
+++ b/packages/jael/jael/test/text/common.dart
@@ -0,0 +1,24 @@
+import 'package:matcher/matcher.dart';
+import 'package:jael/src/ast/token.dart';
+
+Matcher isToken(TokenType type, [String text]) => _IsToken(type, text);
+
+class _IsToken extends Matcher {
+ final TokenType type;
+ final String text;
+
+ _IsToken(this.type, [this.text]);
+
+ @override
+ Description describe(Description description) {
+ if (text == null) return description.add('has type $type');
+ return description.add('has type $type and text "$text"');
+ }
+
+ @override
+ bool matches(item, Map matchState) {
+ return item is Token &&
+ item.type == type &&
+ (text == null || item.span.text == text);
+ }
+}
diff --git a/packages/jael/jael/test/text/scan_test.dart b/packages/jael/jael/test/text/scan_test.dart
new file mode 100644
index 00000000..a5fad7e6
--- /dev/null
+++ b/packages/jael/jael/test/text/scan_test.dart
@@ -0,0 +1,106 @@
+import 'package:jael/src/ast/token.dart';
+import 'package:jael/src/text/scanner.dart';
+import 'package:test/test.dart';
+import 'common.dart';
+
+main() {
+ test('plain html', () {
+ var tokens = scan(' ', sourceUrl: 'test.jael').tokens;
+ tokens.forEach(print);
+
+ expect(tokens, hasLength(7));
+ expect(tokens[0], isToken(TokenType.lt));
+ expect(tokens[1], isToken(TokenType.id, 'img'));
+ expect(tokens[2], isToken(TokenType.id, 'src'));
+ expect(tokens[3], isToken(TokenType.equals));
+ expect(tokens[4], isToken(TokenType.string, '"foo.png"'));
+ expect(tokens[5], isToken(TokenType.slash));
+ expect(tokens[6], isToken(TokenType.gt));
+ });
+
+ test('single quotes', () {
+ var tokens = scan('It\'s lit
', sourceUrl: 'test.jael').tokens;
+ tokens.forEach(print);
+
+ expect(tokens, hasLength(8));
+ expect(tokens[0], isToken(TokenType.lt));
+ expect(tokens[1], isToken(TokenType.id, 'p'));
+ expect(tokens[2], isToken(TokenType.gt));
+ expect(tokens[3], isToken(TokenType.text, 'It\'s lit'));
+ expect(tokens[4], isToken(TokenType.lt));
+ expect(tokens[5], isToken(TokenType.slash));
+ expect(tokens[6], isToken(TokenType.id, 'p'));
+ expect(tokens[7], isToken(TokenType.gt));
+ });
+
+ test('text node', () {
+ var tokens = scan('Hello\nworld
', sourceUrl: 'test.jael').tokens;
+ tokens.forEach(print);
+
+ expect(tokens, hasLength(8));
+ expect(tokens[0], isToken(TokenType.lt));
+ expect(tokens[1], isToken(TokenType.id, 'p'));
+ expect(tokens[2], isToken(TokenType.gt));
+ expect(tokens[3], isToken(TokenType.text, 'Hello\nworld'));
+ expect(tokens[4], isToken(TokenType.lt));
+ expect(tokens[5], isToken(TokenType.slash));
+ expect(tokens[6], isToken(TokenType.id, 'p'));
+ expect(tokens[7], isToken(TokenType.gt));
+ });
+
+ test('mixed', () {
+ var tokens = scan('',
+ sourceUrl: 'test.jael')
+ .tokens;
+ tokens.forEach(print);
+
+ expect(tokens, hasLength(20));
+ expect(tokens[0], isToken(TokenType.lt));
+ expect(tokens[1], isToken(TokenType.id, 'ul'));
+ expect(tokens[2], isToken(TokenType.id, 'number'));
+ expect(tokens[3], isToken(TokenType.equals));
+ expect(tokens[4], isToken(TokenType.number, '1'));
+ expect(tokens[5], isToken(TokenType.plus));
+ expect(tokens[6], isToken(TokenType.number, '2'));
+ expect(tokens[7], isToken(TokenType.gt));
+ expect(tokens[8], isToken(TokenType.text, 'three'));
+ expect(tokens[9], isToken(TokenType.lDoubleCurly));
+ expect(tokens[10], isToken(TokenType.id, 'four'));
+ expect(tokens[11], isToken(TokenType.gt));
+ expect(tokens[12], isToken(TokenType.id, 'five'));
+ expect(tokens[13], isToken(TokenType.dot));
+ expect(tokens[14], isToken(TokenType.id, 'six'));
+ expect(tokens[15], isToken(TokenType.rDoubleCurly));
+ expect(tokens[16], isToken(TokenType.lt));
+ expect(tokens[17], isToken(TokenType.slash));
+ expect(tokens[18], isToken(TokenType.id, 'ul'));
+ expect(tokens[19], isToken(TokenType.gt));
+ });
+
+ test('script tag interpolation', () {
+ var tokens = scan(
+ '''
+
+'''
+ .trim(),
+ sourceUrl: 'test.jael',
+ ).tokens;
+ tokens.forEach(print);
+
+ expect(tokens, hasLength(11));
+ expect(tokens[0], isToken(TokenType.lt));
+ expect(tokens[1], isToken(TokenType.id, 'script'));
+ expect(tokens[2], isToken(TokenType.id, 'aria-label'));
+ expect(tokens[3], isToken(TokenType.equals));
+ expect(tokens[4], isToken(TokenType.string));
+ expect(tokens[5], isToken(TokenType.gt));
+ expect(
+ tokens[6], isToken(TokenType.text, "\n window.alert('a string');\n"));
+ expect(tokens[7], isToken(TokenType.lt));
+ expect(tokens[8], isToken(TokenType.slash));
+ expect(tokens[9], isToken(TokenType.id, 'script'));
+ expect(tokens[10], isToken(TokenType.gt));
+ });
+}
diff --git a/packages/jael/jael_language_server/.gitignore b/packages/jael/jael_language_server/.gitignore
new file mode 100644
index 00000000..dbef116d
--- /dev/null
+++ b/packages/jael/jael_language_server/.gitignore
@@ -0,0 +1,21 @@
+# See https://www.dartlang.org/guides/libraries/private-files
+
+# Files and directories created by pub
+.dart_tool/
+.packages
+build/
+# If you're building an application, you may want to check-in your pubspec.lock
+pubspec.lock
+
+# Directory created by dartdoc
+# If you don't generate documentation locally you can remove this line.
+doc/api/
+
+# Avoid committing generated Javascript files:
+*.dart.js
+*.info.json # Produced by the --dump-info flag.
+*.js # When generated by dart2js. Don't specify *.js if your
+ # project includes source files written in JavaScript.
+*.js_
+*.js.deps
+*.js.map
diff --git a/packages/jael/jael_language_server/analysis_options.yaml b/packages/jael/jael_language_server/analysis_options.yaml
new file mode 100644
index 00000000..eae1e42a
--- /dev/null
+++ b/packages/jael/jael_language_server/analysis_options.yaml
@@ -0,0 +1,3 @@
+analyzer:
+ strong-mode:
+ implicit-casts: false
\ No newline at end of file
diff --git a/packages/jael/jael_language_server/bin/jael_language_server.dart b/packages/jael/jael_language_server/bin/jael_language_server.dart
new file mode 100644
index 00000000..f90548a2
--- /dev/null
+++ b/packages/jael/jael_language_server/bin/jael_language_server.dart
@@ -0,0 +1,68 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:args/args.dart';
+import 'package:io/ansi.dart';
+import 'package:io/io.dart';
+import 'package:dart_language_server/dart_language_server.dart';
+import 'package:jael_language_server/jael_language_server.dart';
+
+main(List args) async {
+ var argParser = new ArgParser()
+ ..addFlag('help',
+ abbr: 'h', negatable: false, help: 'Print this help information.')
+ ..addOption('log-file', help: 'A path to which to write a log file.');
+
+ void printUsage() {
+ print('usage: jael_language_server [options...]\n\nOptions:');
+ print(argParser.usage);
+ }
+
+ try {
+ var argResults = argParser.parse(args);
+
+ if (argResults['help'] as bool) {
+ printUsage();
+ return;
+ } else {
+ var jaelServer = new JaelLanguageServer();
+
+ if (argResults.wasParsed('log-file')) {
+ var f = new File(argResults['log-file'] as String);
+ await f.create(recursive: true);
+
+ jaelServer.logger.onRecord.listen((rec) async {
+ var sink = await f.openWrite(mode: FileMode.append);
+ sink.writeln(rec);
+ if (rec.error != null) sink.writeln(rec.error);
+ if (rec.stackTrace != null) sink.writeln(rec.stackTrace);
+ await sink.close();
+ });
+ } else {
+ jaelServer.logger.onRecord.listen((rec) async {
+ var sink = stderr;
+ sink.writeln(rec);
+ if (rec.error != null) sink.writeln(rec.error);
+ if (rec.stackTrace != null) sink.writeln(rec.stackTrace);
+ });
+ }
+
+ var spec = new ZoneSpecification(
+ handleUncaughtError: (self, parent, zone, error, stackTrace) {
+ jaelServer.logger.severe('Uncaught', error, stackTrace);
+ },
+ print: (self, parent, zone, line) {
+ jaelServer.logger.info(line);
+ },
+ );
+ var zone = Zone.current.fork(specification: spec);
+ await zone.run(() async {
+ var stdio = new StdIOLanguageServer.start(jaelServer);
+ await stdio.onDone;
+ });
+ }
+ } on ArgParserException catch (e) {
+ print('${red.wrap('error')}: ${e.message}\n');
+ printUsage();
+ exitCode = ExitCode.usage.code;
+ }
+}
diff --git a/packages/jael/jael_language_server/lib/jael_language_server.dart b/packages/jael/jael_language_server/lib/jael_language_server.dart
new file mode 100644
index 00000000..23bc99ba
--- /dev/null
+++ b/packages/jael/jael_language_server/lib/jael_language_server.dart
@@ -0,0 +1 @@
+export 'src/server.dart';
\ No newline at end of file
diff --git a/packages/jael/jael_language_server/lib/src/analyzer.dart b/packages/jael/jael_language_server/lib/src/analyzer.dart
new file mode 100644
index 00000000..3d47d057
--- /dev/null
+++ b/packages/jael/jael_language_server/lib/src/analyzer.dart
@@ -0,0 +1,153 @@
+import 'package:jael/jael.dart';
+import 'package:logging/logging.dart';
+import 'package:symbol_table/symbol_table.dart';
+import 'object.dart';
+
+class Analyzer extends Parser {
+ final Logger logger;
+ Analyzer(Scanner scanner, this.logger) : super(scanner);
+
+ final errors = [];
+ var _scope = new SymbolTable();
+ var allDefinitions = >[];
+
+ SymbolTable get parentScope =>
+ _scope.isRoot ? _scope : _scope.parent;
+
+ SymbolTable get scope => _scope;
+
+ bool ensureAttributeIsPresent(Element element, String name) {
+ if (element.getAttribute(name)?.value == null) {
+ addError(new JaelError(JaelErrorSeverity.error,
+ 'Missing required attribute `$name`.', element.span));
+ return false;
+ }
+ return true;
+ }
+
+ void addError(JaelError e) {
+ errors.add(e);
+ logger.severe(e.message, e.span.highlight());
+ }
+
+ bool ensureAttributeIsConstantString(Element element, String name) {
+ var a = element.getAttribute(name);
+ if (a?.value is! StringLiteral || a?.value == null) {
+ var e = new JaelError(
+ JaelErrorSeverity.warning,
+ "`$name` attribute should be a constant string literal.",
+ a?.span ?? element.tagName.span);
+ addError(e);
+ return false;
+ }
+
+ return true;
+ }
+
+ @override
+ Element parseElement() {
+ try {
+ _scope = _scope.createChild();
+ var element = super.parseElement();
+ if (element == null) return null;
+
+ // Check if any custom element exists.
+ _scope
+ .resolve(element.tagName.name)
+ ?.value
+ ?.usages
+ ?.add(new SymbolUsage(SymbolUsageType.read, element.span));
+
+ // Validate attrs
+ var forEach = element.getAttribute('for-each');
+ if (forEach != null) {
+ var asAttr = element.getAttribute('as');
+ if (asAttr != null) {
+ if (ensureAttributeIsConstantString(element, 'as')) {
+ var asName = asAttr.string.value;
+ _scope.create(asName,
+ value: new JaelVariable(asName, asAttr.span), constant: true);
+ }
+ }
+
+ if (forEach.value != null) {
+ addError(new JaelError(JaelErrorSeverity.error,
+ 'Missing value for `for-each` directive.', forEach.span));
+ }
+ }
+
+ var iff = element.getAttribute('if');
+ if (iff != null) {
+ if (iff.value != null) {
+ addError(new JaelError(JaelErrorSeverity.error,
+ 'Missing value for `iff` directive.', iff.span));
+ }
+ }
+
+ // Validate the tag itself
+ if (element is RegularElement) {
+ if (element.tagName.name == 'block') {
+ ensureAttributeIsConstantString(element, 'name');
+ //logger.info('Found at ${element.span.start.toolString}');
+ } else if (element.tagName.name == 'case') {
+ ensureAttributeIsPresent(element, 'value');
+ //logger.info('Found at ${element.span.start.toolString}');
+ } else if (element.tagName.name == 'declare') {
+ if (element.attributes.isEmpty) {
+ addError(new JaelError(
+ JaelErrorSeverity.warning,
+ '`declare` directive does not define any new symbols.',
+ element.tagName.span));
+ } else {
+ for (var attr in element.attributes) {
+ _scope.create(attr.name,
+ value: new JaelVariable(attr.name, attr.span));
+ }
+ }
+ } else if (element.tagName.name == 'element') {
+ if (ensureAttributeIsConstantString(element, 'name')) {
+ var nameCtx = element.getAttribute('name').value as StringLiteral;
+ var name = nameCtx.value;
+ //logger.info(
+ // 'Found custom element $name at ${element.span.start.toolString}');
+ try {
+ var symbol = parentScope.create(name,
+ value: new JaelCustomElement(name, element.tagName.span),
+ constant: true);
+ allDefinitions.add(symbol);
+ } on StateError catch (e) {
+ addError(new JaelError(
+ JaelErrorSeverity.error, e.message, element.tagName.span));
+ }
+ }
+ } else if (element.tagName.name == 'extend') {
+ ensureAttributeIsConstantString(element, 'src');
+ //logger.info('Found at ${element.span.start.toolString}');
+ }
+ } else if (element is SelfClosingElement) {
+ if (element.tagName.name == 'include') {
+ //logger.info('Found at ${element.span.start.toolString}');
+ ensureAttributeIsConstantString(element, 'src');
+ }
+ }
+
+ return element;
+ } finally {
+ _scope = _scope.parent;
+ return null;
+ }
+ }
+
+ @override
+ Expression parseExpression(int precedence) {
+ var expr = super.parseExpression(precedence);
+ if (expr == null) return null;
+
+ if (expr is Identifier) {
+ var ref = _scope.resolve(expr.name);
+ ref?.value?.usages?.add(new SymbolUsage(SymbolUsageType.read, expr.span));
+ }
+
+ return expr;
+ }
+}
diff --git a/packages/jael/jael_language_server/lib/src/object.dart b/packages/jael/jael_language_server/lib/src/object.dart
new file mode 100644
index 00000000..d91f31aa
--- /dev/null
+++ b/packages/jael/jael_language_server/lib/src/object.dart
@@ -0,0 +1,32 @@
+import 'dart:collection';
+
+import 'package:source_span/source_span.dart';
+
+abstract class JaelObject {
+ final FileSpan span;
+ final usages = [];
+ String get name;
+
+ JaelObject(this.span);
+}
+
+class JaelCustomElement extends JaelObject {
+ final String name;
+ final attributes = new SplayTreeSet();
+
+ JaelCustomElement(this.name, FileSpan span) : super(span);
+}
+
+class JaelVariable extends JaelObject {
+ final String name;
+ JaelVariable(this.name, FileSpan span) : super(span);
+}
+
+class SymbolUsage {
+ final SymbolUsageType type;
+ final FileSpan span;
+
+ SymbolUsage(this.type, this.span);
+}
+
+enum SymbolUsageType { definition, read }
diff --git a/packages/jael/jael_language_server/lib/src/server.dart b/packages/jael/jael_language_server/lib/src/server.dart
new file mode 100644
index 00000000..41de486a
--- /dev/null
+++ b/packages/jael/jael_language_server/lib/src/server.dart
@@ -0,0 +1,554 @@
+import 'dart:async';
+import 'package:dart_language_server/src/protocol/language_server/interface.dart';
+import 'package:dart_language_server/src/protocol/language_server/messages.dart';
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:file/memory.dart';
+import 'package:jael/jael.dart';
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2;
+import 'package:logging/logging.dart';
+import 'package:path/path.dart' as p;
+import 'package:source_span/source_span.dart';
+import 'package:string_scanner/string_scanner.dart';
+import 'package:symbol_table/symbol_table.dart';
+import 'analyzer.dart';
+import 'object.dart';
+
+class JaelLanguageServer extends LanguageServer {
+ var _diagnostics = new StreamController();
+ var _done = new Completer();
+ var _memFs = new MemoryFileSystem();
+ var _localFs = const LocalFileSystem();
+ Directory _localRootDir, _memRootDir;
+ var logger = new Logger('jael');
+ Uri _rootUri;
+ var _workspaceEdits = new StreamController();
+
+ @override
+ Stream get diagnostics => _diagnostics.stream;
+
+ @override
+ Future get onDone => _done.future;
+
+ @override
+ Stream get workspaceEdits => _workspaceEdits.stream;
+
+ @override
+ Future shutdown() {
+ if (!_done.isCompleted) _done.complete();
+ _diagnostics.close();
+ _workspaceEdits.close();
+ return super.shutdown();
+ }
+
+ @override
+ void setupExtraMethods(json_rpc_2.Peer peer) {
+ peer.registerMethod('textDocument/formatting',
+ (json_rpc_2.Parameters params) async {
+ var documentId =
+ new TextDocumentIdentifier.fromJson(params['textDocument'].asMap);
+ var formattingOptions =
+ new FormattingOptions.fromJson(params['options'].asMap);
+ return await textDocumentFormatting(documentId, formattingOptions);
+ });
+ }
+
+ @override
+ Future initialize(int clientPid, String rootUri,
+ ClientCapabilities clientCapabilities, String trace) async {
+ // Find our real root dir.
+ _localRootDir = _localFs.directory(_rootUri = Uri.parse(rootUri));
+ _memRootDir = _memFs.directory('/');
+ await _memRootDir.create(recursive: true);
+ _memFs.currentDirectory = _memRootDir;
+
+ // Copy all real files that end in *.jael (and *.jl for legacy) into the in-memory filesystem.
+ await for (var entity in _localRootDir.list(recursive: true)) {
+ if (entity is File && p.extension(entity.path) == '.jael') {
+ logger.info('HEY ${entity.path}');
+ var file = _memFs.file(entity.absolute.path);
+ await file.create(recursive: true);
+ await entity
+ .openRead()
+ .cast>()
+ .pipe(file.openWrite(mode: FileMode.write));
+ logger.info(
+ 'Found Jael file ${file.path}; copied to ${file.absolute.path}');
+
+ // Analyze it
+ var documentId = new TextDocumentIdentifier((b) {
+ b..uri = _rootUri.replace(path: file.path).toString();
+ });
+
+ await analyzerForId(documentId);
+ }
+ }
+
+ return new ServerCapabilities((b) {
+ b
+ ..codeActionProvider = false
+ ..completionProvider = new CompletionOptions((b) {
+ b
+ ..resolveProvider = true
+ ..triggerCharacters =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdeghijklmnopqrstuvxwyz'
+ .codeUnits
+ .map((c) => new String.fromCharCode(c))
+ .toList();
+ })
+ ..definitionProvider = true
+ ..documentHighlightProvider = true
+ ..documentRangeFormattingProvider = false
+ ..documentOnTypeFormattingProvider = null
+ ..documentSymbolProvider = true
+ ..documentFormattingProvider = true
+ ..hoverProvider = true
+ ..implementationProvider = true
+ ..referencesProvider = true
+ ..renameProvider = true
+ ..signatureHelpProvider = new SignatureHelpOptions((b) {})
+ ..textDocumentSync = new TextDocumentSyncOptions((b) {
+ b
+ ..openClose = true
+ ..change = TextDocumentSyncKind.full
+ ..save = new SaveOptions((b) {
+ b..includeText = false;
+ })
+ ..willSave = false
+ ..willSaveWaitUntil = false;
+ })
+ ..workspaceSymbolProvider = true;
+ });
+ }
+
+ Future fileForId(TextDocumentIdentifier documentId) async {
+ var uri = Uri.parse(documentId.uri);
+ var relativePath = uri.path;
+ var file = _memFs.directory('/').childFile(relativePath);
+
+ /*
+ logger.info('Searching for $relativePath. All:\n');
+
+ await for (var entity in _memFs.directory('/').list(recursive: true)) {
+ if (entity is File) print(' * ${entity.absolute.path}');
+ }
+ */
+
+ if (!await file.exists()) {
+ await file.create(recursive: true);
+ await _localFs
+ .file(uri)
+ .openRead()
+ .cast>()
+ .pipe(file.openWrite());
+ logger.info('Opened Jael file ${file.path}');
+ }
+
+ return file;
+ }
+
+ Future scannerForId(TextDocumentIdentifier documentId) async {
+ var file = await fileForId(documentId);
+ return scan(await file.readAsString(), sourceUrl: file.uri);
+ }
+
+ Future analyzerForId(TextDocumentIdentifier documentId) async {
+ var scanner = await scannerForId(documentId);
+ var analyzer = new Analyzer(scanner, logger)..errors.addAll(scanner.errors);
+ analyzer.parseDocument();
+ emitDiagnostics(documentId.uri, analyzer.errors.map(toDiagnostic).toList());
+ return analyzer;
+ }
+
+ Diagnostic toDiagnostic(JaelError e) {
+ return new Diagnostic((b) {
+ b
+ ..message = e.message
+ ..range = toRange(e.span)
+ ..severity = toSeverity(e.severity)
+ ..source = e.span.start.sourceUrl.toString();
+ });
+ }
+
+ int toSeverity(JaelErrorSeverity s) {
+ switch (s) {
+ case JaelErrorSeverity.warning:
+ return DiagnosticSeverity.warning;
+ default:
+ return DiagnosticSeverity.error;
+ }
+ }
+
+ Range toRange(FileSpan span) {
+ return new Range((b) {
+ b
+ ..start = toPosition(span.start)
+ ..end = toPosition(span.end);
+ });
+ }
+
+ Range emptyRange() {
+ return new Range((b) => b
+ ..start = b.end = new Position((b) {
+ b
+ ..character = 1
+ ..line = 0;
+ }));
+ }
+
+ Position toPosition(SourceLocation location) {
+ return new Position((b) {
+ b
+ ..line = location.line
+ ..character = location.column;
+ });
+ }
+
+ Location toLocation(String uri, FileSpan span) {
+ return new Location((b) {
+ b
+ ..range = toRange(span)
+ ..uri = uri;
+ });
+ }
+
+ bool isReachable(JaelObject obj, Position position) {
+ return obj.span.start.line <= position.line &&
+ obj.span.start.column <= position.character;
+ }
+
+ CompletionItem toCompletion(Variable symbol) {
+ var value = symbol.value;
+
+ if (value is JaelCustomElement) {
+ var name = value.name;
+ return new CompletionItem((b) {
+ b
+ ..kind = CompletionItemKind.classKind
+ ..label = symbol.name
+ ..textEdit = new TextEdit((b) {
+ b
+ ..range = emptyRange()
+ ..newText = '<$name\$1>\n \$2\n';
+ });
+ });
+ } else if (value is JaelVariable) {
+ return new CompletionItem((b) {
+ b
+ ..kind = CompletionItemKind.variable
+ ..label = symbol.name;
+ });
+ }
+
+ return null;
+ }
+
+ void emitDiagnostics(String uri, Iterable diagnostics) {
+ _diagnostics.add(new Diagnostics((b) {
+ logger.info('$uri => ${diagnostics.map((d) => d.message).toList()}');
+ b
+ ..diagnostics = diagnostics.toList()
+ ..uri = uri.toString();
+ }));
+ }
+
+ @override
+ Future textDocumentDidOpen(TextDocumentItem document) async {
+ await analyzerForId(
+ new TextDocumentIdentifier((b) => b..uri = document.uri));
+ }
+
+ @override
+ Future textDocumentDidChange(VersionedTextDocumentIdentifier documentId,
+ List changes) async {
+ var id = new TextDocumentIdentifier((b) => b..uri = documentId.uri);
+ var file = await fileForId(id);
+
+ for (var change in changes) {
+ if (change.text != null) {
+ await file.writeAsString(change.text);
+ } else if (change.range != null) {
+ var contents = await file.readAsString();
+
+ int findIndex(Position position) {
+ var lines = contents.split('\n');
+
+ // Sum the length of the previous lines.
+ int lineLength = lines
+ .take(position.line - 1)
+ .map((s) => s.length)
+ .reduce((a, b) => a + b);
+ return lineLength + position.character - 1;
+ }
+
+ if (change.range == null) {
+ contents = change.text;
+ } else {
+ var start = findIndex(change.range.start),
+ end = findIndex(change.range.end);
+ contents = contents.replaceRange(start, end, change.text);
+ }
+
+ logger.info('${file.path} => $contents');
+ await file.writeAsString(contents);
+ }
+ }
+
+ await analyzerForId(id);
+ }
+
+ @override
+ Future textDocumentCodeAction(TextDocumentIdentifier documentId,
+ Range range, CodeActionContext context) async {
+ // TODO: implement textDocumentCodeAction
+ return [];
+ }
+
+ @override
+ Future textDocumentCompletion(
+ TextDocumentIdentifier documentId, Position position) async {
+ var analyzer = await analyzerForId(documentId);
+ var symbols = analyzer.scope.allVariables;
+ var reachable = symbols.where((s) => isReachable(s.value, position));
+ return new CompletionList((b) {
+ b
+ ..isIncomplete = false
+ ..items = reachable.map(toCompletion).toList();
+ });
+ }
+
+ final RegExp _id =
+ new RegExp(r'(([A-Za-z][A-Za-z0-9_]*-)*([A-Za-z][A-Za-z0-9_]*))');
+
+ Future currentName(
+ TextDocumentIdentifier documentId, Position position) async {
+ // First, read the file.
+ var file = await fileForId(documentId);
+ var contents = await file.readAsString();
+
+ // Next, find the current index.
+ var scanner = new SpanScanner(contents);
+
+ while (!scanner.isDone &&
+ (scanner.state.line != position.line ||
+ scanner.state.column != position.character)) {
+ scanner.readChar();
+ }
+
+ // Next, just read the name.
+ if (scanner.matches(_id)) {
+ var longest = scanner.lastSpan.text;
+
+ while (scanner.matches(_id) && scanner.position > 0 && !scanner.isDone) {
+ longest = scanner.lastSpan.text;
+ scanner.position--;
+ }
+
+ return longest;
+ } else {
+ return null;
+ }
+ }
+
+ Future currentSymbol(
+ TextDocumentIdentifier documentId, Position position) async {
+ var name = await currentName(documentId, position);
+ if (name == null) return null;
+ var analyzer = await analyzerForId(documentId);
+ var symbols = analyzer.allDefinitions ?? analyzer.scope.allVariables;
+ logger
+ .info('Current symbols, seeking $name: ${symbols.map((v) => v.name)}');
+ return analyzer.scope.resolve(name)?.value;
+ }
+
+ @override
+ Future textDocumentDefinition(
+ TextDocumentIdentifier documentId, Position position) async {
+ var symbol = await currentSymbol(documentId, position);
+ if (symbol != null) {
+ return toLocation(documentId.uri, symbol.span);
+ }
+ return null;
+ }
+
+ @override
+ Future> textDocumentHighlight(
+ TextDocumentIdentifier documentId, Position position) async {
+ var symbol = await currentSymbol(documentId, position);
+ if (symbol != null) {
+ return symbol.usages.map((u) {
+ return new DocumentHighlight((b) {
+ b
+ ..range = toRange(u.span)
+ ..kind = u.type == SymbolUsageType.definition
+ ? DocumentHighlightKind.write
+ : DocumentHighlightKind.read;
+ });
+ }).toList();
+ }
+ return [];
+ }
+
+ @override
+ Future textDocumentHover(
+ TextDocumentIdentifier documentId, Position position) async {
+ var symbol = await currentSymbol(documentId, position);
+ if (symbol != null) {
+ return new Hover((b) {
+ b
+ ..contents = symbol.span.text
+ ..range = toRange(symbol.span);
+ });
+ }
+
+ return null;
+ }
+
+ @override
+ Future> textDocumentImplementation(
+ TextDocumentIdentifier documentId, Position position) async {
+ var defn = await textDocumentDefinition(documentId, position);
+ return defn == null ? [] : [defn];
+ }
+
+ @override
+ Future> textDocumentReferences(
+ TextDocumentIdentifier documentId,
+ Position position,
+ ReferenceContext context) async {
+ var symbol = await currentSymbol(documentId, position);
+ if (symbol != null) {
+ return symbol.usages.map((u) {
+ return toLocation(documentId.uri, u.span);
+ }).toList();
+ }
+
+ return [];
+ }
+
+ @override
+ Future textDocumentRename(TextDocumentIdentifier documentId,
+ Position position, String newName) async {
+ var symbol = await currentSymbol(documentId, position);
+ if (symbol != null) {
+ return new WorkspaceEdit((b) {
+ b
+ ..changes = {
+ symbol.name: symbol.usages.map((u) {
+ return new TextEdit((b) {
+ b
+ ..range = toRange(u.span)
+ ..newText = (symbol is JaelCustomElement &&
+ u.type == SymbolUsageType.definition)
+ ? '"$newName"'
+ : newName;
+ });
+ }).toList()
+ };
+ });
+ }
+ return new WorkspaceEdit((b) {
+ b..changes = {};
+ });
+ }
+
+ @override
+ Future> textDocumentSymbols(
+ TextDocumentIdentifier documentId) async {
+ var analyzer = await analyzerForId(documentId);
+ return analyzer.allDefinitions.map((symbol) {
+ return new SymbolInformation((b) {
+ b
+ ..kind = SymbolKind.classSymbol
+ ..name = symbol.name
+ ..location = toLocation(documentId.uri, symbol.value.span);
+ });
+ }).toList();
+ }
+
+ @override
+ Future workspaceExecuteCommand(String command, List arguments) async {
+ // TODO: implement workspaceExecuteCommand
+ }
+
+ @override
+ Future> workspaceSymbol(String query) async {
+ var values = [];
+
+ await for (var file in _memRootDir.list(recursive: true)) {
+ if (file is File) {
+ var id = new TextDocumentIdentifier((b) {
+ b..uri = file.uri.toString();
+ });
+ var analyzer = await analyzerForId(id);
+ values.addAll(analyzer.allDefinitions.map((v) => v.value));
+ }
+ }
+
+ return values.map((o) {
+ return new SymbolInformation((b) {
+ b
+ ..name = o.name
+ ..location = toLocation(o.span.sourceUrl.toString(), o.span)
+ ..containerName = p.basename(o.span.sourceUrl.path)
+ ..kind = o is JaelCustomElement
+ ? SymbolKind.classSymbol
+ : SymbolKind.variable;
+ });
+ }).toList();
+ }
+
+ Future> textDocumentFormatting(
+ TextDocumentIdentifier documentId,
+ FormattingOptions formattingOptions) async {
+ try {
+ var errors = [];
+ var file = await fileForId(documentId);
+ var contents = await file.readAsString();
+ var document =
+ parseDocument(contents, sourceUrl: file.uri, onError: errors.add);
+ if (errors.isNotEmpty) return null;
+ var formatter = new JaelFormatter(
+ formattingOptions.tabSize, formattingOptions.insertSpaces, 80);
+ var formatted = formatter.apply(document);
+ logger.info('Original:${contents}\nFormatted:\n$formatted');
+ if (formatted.isNotEmpty) await file.writeAsString(formatted);
+ return [
+ new TextEdit((b) {
+ b
+ ..newText = formatted
+ ..range = document == null ? emptyRange() : toRange(document.span);
+ })
+ ];
+ } catch (e, st) {
+ logger.severe('Formatter error', e, st);
+ return null;
+ }
+ }
+
+ @override
+ void initialized() {
+ // TODO: implement initialized
+ }
+
+ @override
+ // TODO: implement showMessages
+ Stream get showMessages => null;
+}
+
+abstract class DiagnosticSeverity {
+ static const int error = 0, warning = 1, information = 2, hint = 3;
+}
+
+class FormattingOptions {
+ final num tabSize;
+
+ final bool insertSpaces;
+
+ FormattingOptions(this.tabSize, this.insertSpaces);
+
+ factory FormattingOptions.fromJson(Map json) {
+ return new FormattingOptions(
+ json['tabSize'] as num, json['insertSpaces'] as bool);
+ }
+}
diff --git a/packages/jael/jael_language_server/mono_pkg.yaml b/packages/jael/jael_language_server/mono_pkg.yaml
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/jael/jael_language_server/pubspec.yaml b/packages/jael/jael_language_server/pubspec.yaml
new file mode 100644
index 00000000..d9ecb833
--- /dev/null
+++ b/packages/jael/jael_language_server/pubspec.yaml
@@ -0,0 +1,22 @@
+name: jael_language_server
+version: 0.0.0
+description: Language Server Protocol implementation for the Jael templating engine.
+author: Tobe Osakwe
+homepage: https://github.com/angel-dart/vscode
+environment:
+ sdk: ">=2.0.0-dev <3.0.0"
+dependencies:
+ args: ^1.0.0
+ dart_language_server: ^0.1.3
+ file: ^5.0.0
+ io: ^0.3.2
+ jael: ^2.0.0
+ jael_preprocessor: ^2.0.0
+ json_rpc_2: ^2.0.0
+ logging: ^0.11.3
+ path: ^1.0.0
+ source_span: ^1.0.0
+ string_scanner: ^1.0.0
+ symbol_table: ^2.0.0
+executables:
+ jael_language_server: jael_language_server
\ No newline at end of file
diff --git a/packages/jael/jael_preprocessor/.gitignore b/packages/jael/jael_preprocessor/.gitignore
new file mode 100644
index 00000000..9afb070e
--- /dev/null
+++ b/packages/jael/jael_preprocessor/.gitignore
@@ -0,0 +1,15 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Dart template
+# See https://www.dartlang.org/tools/private-files.html
+
+# Files and directories created by pub
+.packages
+.pub/
+build/
+# If you're building an application, you may want to check-in your pubspec.lock
+pubspec.lock
+
+# Directory created by dartdoc
+# If you don't generate documentation locally you can remove this line.
+doc/api/
+.dart_tool
\ No newline at end of file
diff --git a/packages/jael/jael_preprocessor/CHANGELOG.md b/packages/jael/jael_preprocessor/CHANGELOG.md
new file mode 100644
index 00000000..a4ccb70f
--- /dev/null
+++ b/packages/jael/jael_preprocessor/CHANGELOG.md
@@ -0,0 +1,12 @@
+# 2.0.1
+* Fixed a bug where failed file resolutions would not become proper errors.
+
+# 2.0.0+1
+* Homepage update for Pub.
+
+# 2.0.0
+* Dart 2 updates.
+* Fix a templating bug where multiple inheritance did not work.
+
+# 1.0.0+1
+* Minor change to `Patcher` signature for Dart 2 compatibility.
\ No newline at end of file
diff --git a/packages/jael/jael_preprocessor/LICENSE b/packages/jael/jael_preprocessor/LICENSE
new file mode 100644
index 00000000..89074fd3
--- /dev/null
+++ b/packages/jael/jael_preprocessor/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 The Angel Framework
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/jael/jael_preprocessor/README.md b/packages/jael/jael_preprocessor/README.md
new file mode 100644
index 00000000..55f86328
--- /dev/null
+++ b/packages/jael/jael_preprocessor/README.md
@@ -0,0 +1,49 @@
+# jael_preprocessor
+[![Pub](https://img.shields.io/pub/v/jael_preprocessor.svg)](https://pub.dartlang.org/packages/jael_preprocessor)
+[![build status](https://travis-ci.org/angel-dart/jael.svg)](https://travis-ci.org/angel-dart/jael)
+
+A pre-processor for resolving blocks and includes within
+[Jael](https://github.com/angel-dart/jael) templates.
+
+# Installation
+In your `pubspec.yaml`:
+
+```yaml
+dependencies:
+ jael_prepreprocessor: ^1.0.0-alpha
+```
+
+# Usage
+It is unlikely that you will directly use this package, as it is
+more of an implementation detail than a requirement. However, it
+is responsible for handling `include` and `block` directives
+(template inheritance), so you are a package maintainer and want
+to support Jael, read on.
+
+To keep things simple, just use the `resolve` function, which will
+take care of inheritance for you.
+
+```dart
+import 'package:jael_preprocessor/jael_preprocessor.dart' as jael;
+
+myFunction() async {
+ var doc = await parseTemplateSomehow();
+ var resolved = await jael.resolve(doc, dir, onError: (e) => doSomething());
+}
+```
+
+You may occasionally need to manually patch in functionality that is not
+available through the official Jael packages. To achieve this, simply
+provide an `Iterable` of `Patcher` functions:
+
+```dart
+myOtherFunction(jael.Document doc) {
+ return jael.resolve(doc, dir, onError: errorHandler, patch: [
+ syntactic(),
+ sugar(),
+ etc(),
+ ]);
+}
+```
+
+**This package uses `package:file`, rather than `dart:io`.**
\ No newline at end of file
diff --git a/packages/jael/jael_preprocessor/analysis_options.yaml b/packages/jael/jael_preprocessor/analysis_options.yaml
new file mode 100644
index 00000000..eae1e42a
--- /dev/null
+++ b/packages/jael/jael_preprocessor/analysis_options.yaml
@@ -0,0 +1,3 @@
+analyzer:
+ strong-mode:
+ implicit-casts: false
\ No newline at end of file
diff --git a/packages/jael/jael_preprocessor/example/main.dart b/packages/jael/jael_preprocessor/example/main.dart
new file mode 100644
index 00000000..5f5fb8e8
--- /dev/null
+++ b/packages/jael/jael_preprocessor/example/main.dart
@@ -0,0 +1,15 @@
+import 'dart:async';
+
+import 'package:file/file.dart';
+import 'package:jael/jael.dart' as jael;
+import 'package:jael_preprocessor/jael_preprocessor.dart' as jael;
+
+Future process(
+ jael.Document doc, Directory dir, errorHandler(jael.JaelError e)) {
+ return jael.resolve(doc, dir, onError: errorHandler, patch: [
+ (doc, dir, onError) {
+ print(doc.root.children.length);
+ return doc;
+ },
+ ]);
+}
diff --git a/packages/jael/jael_preprocessor/lib/jael_preprocessor.dart b/packages/jael/jael_preprocessor/lib/jael_preprocessor.dart
new file mode 100644
index 00000000..f554eff4
--- /dev/null
+++ b/packages/jael/jael_preprocessor/lib/jael_preprocessor.dart
@@ -0,0 +1,355 @@
+import 'dart:async';
+import 'dart:collection';
+import 'package:file/file.dart';
+import 'package:jael/jael.dart';
+import 'package:symbol_table/symbol_table.dart';
+
+/// Modifies a Jael document.
+typedef FutureOr Patcher(Document document,
+ Directory currentDirectory, void onError(JaelError error));
+
+/// Expands all `block[name]` tags within the template, replacing them with the correct content.
+///
+/// To apply additional patches to resolved documents, provide a set of [patch]
+/// functions.
+Future resolve(Document document, Directory currentDirectory,
+ {void onError(JaelError error), Iterable patch}) async {
+ onError ?? (e) => throw e;
+
+ // Resolve all includes...
+ var includesResolved =
+ await resolveIncludes(document, currentDirectory, onError);
+
+ var patched = await applyInheritance(
+ includesResolved, currentDirectory, onError, patch);
+
+ if (patch?.isNotEmpty != true) return patched;
+
+ for (var p in patch) {
+ patched = await p(patched, currentDirectory, onError);
+ }
+
+ return patched;
+}
+
+/// Folds any `extend` declarations.
+Future applyInheritance(Document document, Directory currentDirectory,
+ void onError(JaelError error), Iterable patch) async {
+ if (document.root.tagName.name != 'extend') {
+ // This is not an inherited template, so just fill in the existing blocks.
+ var root =
+ replaceChildrenOfElement(document.root, {}, onError, true, false);
+ return new Document(document.doctype, root);
+ }
+
+ var element = document.root;
+ var attr =
+ element.attributes.firstWhere((a) => a.name == 'src', orElse: () => null);
+ 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 {
+ // In theory, there exists:
+ // * A single root template with a number of blocks
+ // * Some amount of templates.
+
+ // To produce an accurate representation, we need to:
+ // 1. Find the root template, and store a copy in a variable.
+ // 2: For each template:
+ // a. Enumerate the block overrides it defines
+ // b. Replace matching blocks in the current document
+ // c. If there is no block, and this is the LAST , fill in the default block content.
+ var hierarchy = await resolveHierarchy(document, currentDirectory, onError);
+ var out = hierarchy?.root;
+
+ if (out is! RegularElement) {
+ return hierarchy.rootDocument;
+ }
+
+ Element setOut(Element out, Map definedOverrides,
+ bool anyTemplatesRemain) {
+ var children = [];
+
+ // Replace matching blocks, etc.
+ for (var c in out.children) {
+ if (c is Element) {
+ children.addAll(replaceBlocks(
+ c, definedOverrides, onError, false, anyTemplatesRemain));
+ } else {
+ children.add(c);
+ }
+ }
+
+ var root = hierarchy.root as RegularElement;
+ return new RegularElement(root.lt, root.tagName, root.attributes, root.gt,
+ children, root.lt2, root.slash, root.tagName2, root.gt2);
+ }
+
+ // Loop through all extends, filling in blocks.
+ while (hierarchy.extendsTemplates.isNotEmpty) {
+ var tmpl = hierarchy.extendsTemplates.removeFirst();
+ var definedOverrides = findBlockOverrides(tmpl, onError);
+ if (definedOverrides == null) break;
+ out =
+ setOut(out, definedOverrides, hierarchy.extendsTemplates.isNotEmpty);
+ }
+
+ // Lastly, just default-fill any remaining blocks.
+ var definedOverrides = findBlockOverrides(out, onError);
+ if (definedOverrides != null) out = setOut(out, definedOverrides, false);
+
+ // Return our processed document.
+ return new Document(document.doctype, out);
+ }
+}
+
+Map findBlockOverrides(
+ Element tmpl, void onError(JaelError e)) {
+ var out = {};
+
+ for (var child in tmpl.children) {
+ if (child is RegularElement && child.tagName?.name == 'block') {
+ var name = child.attributes
+ .firstWhere((a) => a.name == 'name', orElse: () => null)
+ ?.value
+ ?.compute(new SymbolTable()) as String;
+ if (name?.trim()?.isNotEmpty == true) {
+ out[name] = child;
+ }
+ }
+ }
+
+ return out;
+}
+
+/// Resolves the document hierarchy at a given node in the tree.
+Future resolveHierarchy(Document document,
+ Directory currentDirectory, void onError(JaelError e)) async {
+ var extendsTemplates = new Queue();
+ String parent;
+
+ while (document != null && (parent = getParent(document, onError)) != null) {
+ try {
+ extendsTemplates.addFirst(document.root);
+ var file = currentDirectory.childFile(parent);
+ var parsed = parseDocument(await file.readAsString(),
+ sourceUrl: file.uri, onError: onError);
+ document = await resolveIncludes(parsed, currentDirectory, onError);
+ } on FileSystemException catch (e) {
+ onError(new JaelError(
+ JaelErrorSeverity.error, e.message, document.root.span));
+ return null;
+ }
+ }
+
+ if (document == null) return null;
+ return new DocumentHierarchy(document, extendsTemplates);
+}
+
+class DocumentHierarchy {
+ final Document rootDocument;
+ final Queue extendsTemplates; // FIFO
+
+ DocumentHierarchy(this.rootDocument, this.extendsTemplates);
+
+ Element get root => rootDocument.root;
+}
+
+Iterable replaceBlocks(
+ Element element,
+ Map definedOverrides,
+ void onError(JaelError e),
+ bool replaceWithDefault,
+ bool anyTemplatesRemain) {
+ if (element.tagName.name == 'block') {
+ var nameAttr = element.attributes
+ .firstWhere((a) => a.name == 'name', orElse: () => null);
+ var name = nameAttr?.value?.compute(new SymbolTable());
+
+ if (name?.trim()?.isNotEmpty != true) {
+ onError(new JaelError(
+ JaelErrorSeverity.warning,
+ 'This has no `name` attribute, and will be outputted as-is.',
+ element.span));
+ return [element];
+ } else if (!definedOverrides.containsKey(name)) {
+ if (element is RegularElement) {
+ if (anyTemplatesRemain || !replaceWithDefault) {
+ // If there are still templates remaining, this current block may eventually
+ // be resolved. Keep it alive.
+
+ // We can't get rid of the block itself, but it may have blocks as children...
+ var inner = allChildrenOfRegularElement(element, definedOverrides,
+ onError, replaceWithDefault, anyTemplatesRemain);
+
+ return [
+ new RegularElement(
+ element.lt,
+ element.tagName,
+ element.attributes,
+ element.gt,
+ inner,
+ element.lt2,
+ element.slash,
+ element.tagName2,
+ element.gt2)
+ ];
+ } else {
+ // Otherwise, just return the default contents.
+ return element.children;
+ }
+ } else {
+ return [element];
+ }
+ } else {
+ return allChildrenOfRegularElement(definedOverrides[name],
+ definedOverrides, onError, replaceWithDefault, anyTemplatesRemain);
+ }
+ } else if (element is SelfClosingElement) {
+ return [element];
+ } else {
+ return [
+ replaceChildrenOfRegularElement(element as RegularElement,
+ definedOverrides, onError, replaceWithDefault, anyTemplatesRemain)
+ ];
+ }
+}
+
+Element replaceChildrenOfElement(
+ Element el,
+ Map definedOverrides,
+ void onError(JaelError e),
+ bool replaceWithDefault,
+ bool anyTemplatesRemain) {
+ if (el is RegularElement) {
+ return replaceChildrenOfRegularElement(
+ el, definedOverrides, onError, replaceWithDefault, anyTemplatesRemain);
+ } else {
+ return el;
+ }
+}
+
+RegularElement replaceChildrenOfRegularElement(
+ RegularElement el,
+ Map definedOverrides,
+ void onError(JaelError e),
+ bool replaceWithDefault,
+ bool anyTemplatesRemain) {
+ var children = allChildrenOfRegularElement(
+ el, definedOverrides, onError, replaceWithDefault, anyTemplatesRemain);
+ return new RegularElement(el.lt, el.tagName, el.attributes, el.gt, children,
+ el.lt2, el.slash, el.tagName2, el.gt2);
+}
+
+List allChildrenOfRegularElement(
+ RegularElement el,
+ Map definedOverrides,
+ void onError(JaelError e),
+ bool replaceWithDefault,
+ bool anyTemplatesRemain) {
+ var children = [];
+
+ for (var c in el.children) {
+ if (c is Element) {
+ children.addAll(replaceBlocks(c, definedOverrides, onError,
+ replaceWithDefault, anyTemplatesRemain));
+ } else {
+ children.add(c);
+ }
+ }
+
+ return children;
+}
+
+/// Finds the name of the parent template.
+String getParent(Document document, void onError(JaelError error)) {
+ var element = document.root;
+ if (element?.tagName?.name != 'extend') return null;
+
+ var attr =
+ element.attributes.firstWhere((a) => a.name == 'src', orElse: () => null);
+ 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;
+ }
+}
+
+/// Expands all `include[src]` tags within the template, and fills in the content of referenced files.
+Future resolveIncludes(Document document, Directory currentDirectory,
+ void onError(JaelError error)) async {
+ return new Document(document.doctype,
+ await _expandIncludes(document.root, currentDirectory, onError));
+}
+
+Future _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 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}');
+ }
+ }
+
+ var attr =
+ element.attributes.firstWhere((a) => a.name == 'src', orElse: () => null);
+ 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;
+ }
+}
diff --git a/packages/jael/jael_preprocessor/mono_pkg.yaml b/packages/jael/jael_preprocessor/mono_pkg.yaml
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/jael/jael_preprocessor/pubspec.yaml b/packages/jael/jael_preprocessor/pubspec.yaml
new file mode 100644
index 00000000..7da2607c
--- /dev/null
+++ b/packages/jael/jael_preprocessor/pubspec.yaml
@@ -0,0 +1,14 @@
+name: jael_preprocessor
+version: 2.0.1
+description: A pre-processor for resolving blocks and includes within Jael templates.
+author: Tobe O
+homepage: https://github.com/angel-dart/jael/tree/master/jael_preprocessor
+environment:
+ sdk: ">=2.0.0-dev <3.0.0"
+dependencies:
+ file: ^5.0.0
+ jael: ^2.0.0
+ symbol_table: ^2.0.0
+dev_dependencies:
+ code_buffer:
+ test: ^1.0.0
\ No newline at end of file
diff --git a/packages/jael/jael_preprocessor/test/block_test.dart b/packages/jael/jael_preprocessor/test/block_test.dart
new file mode 100644
index 00000000..6d7100f2
--- /dev/null
+++ b/packages/jael/jael_preprocessor/test/block_test.dart
@@ -0,0 +1,153 @@
+import 'package:code_buffer/code_buffer.dart';
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:jael/jael.dart' as jael;
+import 'package:jael_preprocessor/jael_preprocessor.dart' as jael;
+import 'package:symbol_table/symbol_table.dart';
+import 'package:test/test.dart';
+
+main() {
+ FileSystem fileSystem;
+
+ setUp(() {
+ fileSystem = new MemoryFileSystem();
+
+ // a.jl
+ fileSystem.file('a.jl').writeAsStringSync('a.jl ');
+
+ // b.jl
+ fileSystem.file('b.jl').writeAsStringSync(
+ 'Hello
');
+
+ // c.jl
+ // NOTE: This SHOULD NOT produce "yes", because the only children expanded within an
+ // are s.
+ fileSystem.file('c.jl').writeAsStringSync(
+ 'Goodbye Yes ');
+
+ // d.jl
+ // This should not output "Yes", either.
+ // It should actually produce the same as c.jl, since it doesn't define any unique blocks.
+ fileSystem.file('d.jl').writeAsStringSync(
+ 'Saluton! Yes ');
+
+ // e.jl
+ fileSystem.file('e.jl').writeAsStringSync(
+ 'Angel default ');
+
+ // fox.jl
+ fileSystem.file('fox.jl').writeAsStringSync(
+ 'The name is default-name
');
+
+ // trot.jl
+ fileSystem.file('trot.jl').writeAsStringSync(
+ 'CONGA YEAH ');
+
+ // foxtrot.jl
+ fileSystem.file('foxtrot.jl').writeAsStringSync(
+ 'framework ');
+ });
+
+ test('blocks are replaced or kept', () async {
+ var file = fileSystem.file('c.jl');
+ var original = jael.parseDocument(await file.readAsString(),
+ sourceUrl: file.uri, onError: (e) => throw e);
+ var processed = await jael.resolve(
+ original, fileSystem.directory(fileSystem.currentDirectory),
+ onError: (e) => throw e);
+ var buf = new CodeBuffer();
+ var scope = new SymbolTable();
+ const jael.Renderer().render(processed, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+ a.jl
+
+ Goodbye
+
+ '''
+ .trim());
+ });
+
+ test('block defaults are emitted', () async {
+ var file = fileSystem.file('b.jl');
+ var original = jael.parseDocument(await file.readAsString(),
+ sourceUrl: file.uri, onError: (e) => throw e);
+ var processed = await jael.resolve(
+ original, fileSystem.directory(fileSystem.currentDirectory),
+ onError: (e) => throw e);
+ var buf = new CodeBuffer();
+ var scope = new SymbolTable();
+ const jael.Renderer().render(processed, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+ a.jl
+
+
+ Hello
+
+
+ '''
+ .trim());
+ });
+
+ test('block resolution only redefines blocks at one level at a time',
+ () async {
+ var file = fileSystem.file('d.jl');
+ var original = jael.parseDocument(await file.readAsString(),
+ sourceUrl: file.uri, onError: (e) => throw e);
+ var processed = await jael.resolve(
+ original, fileSystem.directory(fileSystem.currentDirectory),
+ onError: (e) => throw e);
+ var buf = new CodeBuffer();
+ var scope = new SymbolTable();
+ const jael.Renderer().render(processed, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+ a.jl
+
+ Goodbye
+
+ '''
+ .trim());
+ });
+
+ test('blocks within blocks', () async {
+ var file = fileSystem.file('foxtrot.jl');
+ var original = jael.parseDocument(await file.readAsString(),
+ sourceUrl: file.uri, onError: (e) => throw e);
+ var processed = await jael.resolve(
+ original, fileSystem.directory(fileSystem.currentDirectory),
+ onError: (e) => throw e);
+ var buf = new CodeBuffer();
+ var scope = new SymbolTable();
+ const jael.Renderer().render(processed, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+ The name is CONGA
+
+ framework
+
+
+ '''
+ .trim());
+ });
+}
diff --git a/packages/jael/jael_preprocessor/test/include_test.dart b/packages/jael/jael_preprocessor/test/include_test.dart
new file mode 100644
index 00000000..4b23bb11
--- /dev/null
+++ b/packages/jael/jael_preprocessor/test/include_test.dart
@@ -0,0 +1,49 @@
+import 'package:code_buffer/code_buffer.dart';
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:jael/jael.dart' as jael;
+import 'package:jael_preprocessor/jael_preprocessor.dart' as jael;
+import 'package:symbol_table/symbol_table.dart';
+import 'package:test/test.dart';
+
+main() {
+ FileSystem fileSystem;
+
+ setUp(() {
+ fileSystem = new MemoryFileSystem();
+
+ // a.jl
+ fileSystem.file('a.jl').writeAsStringSync('a.jl ');
+
+ // b.jl
+ fileSystem.file('b.jl').writeAsStringSync(' ');
+
+ // c.jl
+ fileSystem.file('c.jl').writeAsStringSync(' ');
+ });
+
+ test('includes are expanded', () async {
+ var file = fileSystem.file('c.jl');
+ var original = jael.parseDocument(await file.readAsString(),
+ sourceUrl: file.uri, onError: (e) => throw e);
+ var processed = await jael.resolveIncludes(original,
+ fileSystem.directory(fileSystem.currentDirectory), (e) => throw e);
+ var buf = new CodeBuffer();
+ var scope = new SymbolTable();
+ const jael.Renderer().render(processed, buf, scope);
+ print(buf);
+
+ expect(
+ buf.toString(),
+ '''
+
+
+
+ a.jl
+
+
+
+'''
+ .trim());
+ });
+}
diff --git a/packages/jael/jael_web/.gitignore b/packages/jael/jael_web/.gitignore
new file mode 100644
index 00000000..cab9622e
--- /dev/null
+++ b/packages/jael/jael_web/.gitignore
@@ -0,0 +1,16 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Dart template
+# See https://www.dartlang.org/tools/private-files.html
+
+# Files and directories created by pub
+.packages
+.pub/
+build/
+# If you're building an application, you may want to check-in your pubspec.lock
+pubspec.lock
+
+# Directory created by dartdoc
+# If you don't generate documentation locally you can remove this line.
+doc/api/
+
+.dart_tool
\ No newline at end of file
diff --git a/packages/jael/jael_web/LICENSE b/packages/jael/jael_web/LICENSE
new file mode 100644
index 00000000..89074fd3
--- /dev/null
+++ b/packages/jael/jael_web/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 The Angel Framework
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/jael/jael_web/README.md b/packages/jael/jael_web/README.md
new file mode 100644
index 00000000..bd31ee48
--- /dev/null
+++ b/packages/jael/jael_web/README.md
@@ -0,0 +1,6 @@
+# jael_web
+[![Pub](https://img.shields.io/pub/v/jael_web.svg)](https://pub.dartlang.org/packages/jael_web)
+[![build status](https://travis-ci.org/angel-dart/jael_web.svg)](https://travis-ci.org/angel-dart/jael)
+
+Experimental virtual DOM/SPA engine built on Jael. Supports SSR.
+**Not ready for production use.**
\ No newline at end of file
diff --git a/packages/jael/jael_web/analysis_options.yaml b/packages/jael/jael_web/analysis_options.yaml
new file mode 100644
index 00000000..eae1e42a
--- /dev/null
+++ b/packages/jael/jael_web/analysis_options.yaml
@@ -0,0 +1,3 @@
+analyzer:
+ strong-mode:
+ implicit-casts: false
\ No newline at end of file
diff --git a/packages/jael/jael_web/build.yaml b/packages/jael/jael_web/build.yaml
new file mode 100644
index 00000000..9082d3c2
--- /dev/null
+++ b/packages/jael/jael_web/build.yaml
@@ -0,0 +1,10 @@
+builders:
+ jael_web:
+ import: "package:jael_web/builder.dart"
+ builder_factories:
+ - jaelComponentBuilder
+ build_extensions:
+ .dart:
+ - .jael_web_cmp.g.part
+ auto_apply: root_package
+ applies_builders: ["source_gen|combining_builder", "source_gen|part_cleanup"]
\ No newline at end of file
diff --git a/packages/jael/jael_web/example/main.dart b/packages/jael/jael_web/example/main.dart
new file mode 100644
index 00000000..4520daeb
--- /dev/null
+++ b/packages/jael/jael_web/example/main.dart
@@ -0,0 +1,30 @@
+import 'package:jael_web/jael_web.dart';
+import 'package:jael_web/elements.dart';
+part 'main.g.dart';
+
+@Jael(template: '''
+
+
Hello, Jael!
+ Current time: {{now}}
+
+''')
+class Hello extends Component with _HelloJaelTemplate {
+ DateTime get now => DateTime.now();
+}
+
+// Could also have been:
+class Hello2 extends Component {
+ DateTime get now => DateTime.now();
+
+ @override
+ DomNode render() {
+ return div(c: [
+ h1(c: [
+ text('Hello, Jael!'),
+ ]),
+ i(c: [
+ text('Current time: $now'),
+ ]),
+ ]);
+ }
+}
diff --git a/packages/jael/jael_web/example/main.g.dart b/packages/jael/jael_web/example/main.g.dart
new file mode 100644
index 00000000..6a208fa6
--- /dev/null
+++ b/packages/jael/jael_web/example/main.g.dart
@@ -0,0 +1,18 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'main.dart';
+
+// **************************************************************************
+// JaelComponentGenerator
+// **************************************************************************
+
+abstract class _HelloJaelTemplate implements Component {
+ DateTime get now;
+ @override
+ DomNode render() {
+ return h('div', {}, [
+ h('h1', {}, [text('Hello, Jael!')]),
+ h('i', {}, [text('Current time: '), text(now.toString())])
+ ]);
+ }
+}
diff --git a/packages/jael/jael_web/example/stateful.dart b/packages/jael/jael_web/example/stateful.dart
new file mode 100644
index 00000000..617c7bd2
--- /dev/null
+++ b/packages/jael/jael_web/example/stateful.dart
@@ -0,0 +1,32 @@
+import 'dart:async';
+import 'package:jael_web/jael_web.dart';
+part 'stateful.g.dart';
+
+void main() {}
+
+class _AppState {
+ final int ticks;
+
+ _AppState({this.ticks});
+
+ _AppState copyWith({int ticks}) {
+ return _AppState(ticks: ticks ?? this.ticks);
+ }
+}
+
+@Jael(template: 'Tick count: {{state.ticks}}
')
+class StatefulApp extends Component<_AppState> with _StatefulAppJaelTemplate {
+ Timer _timer;
+
+ StatefulApp() {
+ state = _AppState(ticks: 0);
+ _timer = Timer.periodic(Duration(seconds: 1), (t) {
+ setState(state.copyWith(ticks: t.tick));
+ });
+ }
+
+ @override
+ void beforeDestroy() {
+ _timer.cancel();
+ }
+}
diff --git a/packages/jael/jael_web/example/stateful.g.dart b/packages/jael/jael_web/example/stateful.g.dart
new file mode 100644
index 00000000..398897a8
--- /dev/null
+++ b/packages/jael/jael_web/example/stateful.g.dart
@@ -0,0 +1,16 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'stateful.dart';
+
+// **************************************************************************
+// JaelComponentGenerator
+// **************************************************************************
+
+abstract class _StatefulAppJaelTemplate implements Component<_AppState> {
+ Timer get _timer;
+ void beforeDestroy();
+ @override
+ DomNode render() {
+ return h('div', {}, [text('Tick count: '), text(state.ticks.toString())]);
+ }
+}
diff --git a/packages/jael/jael_web/example/using_components.dart b/packages/jael/jael_web/example/using_components.dart
new file mode 100644
index 00000000..50ccbfa5
--- /dev/null
+++ b/packages/jael/jael_web/example/using_components.dart
@@ -0,0 +1,25 @@
+import 'package:jael_web/jael_web.dart';
+part 'using_components.g.dart';
+
+@Jael(template: '''
+
+
Welcome to my app
+
+
+''')
+class MyApp extends Component with _MyAppJaelTemplate {}
+
+@Jael(template: '''
+
+
+ {{name}}:
+
+
+
+
+''')
+class LabeledInput extends Component with _LabeledInputJaelTemplate {
+ final String name;
+
+ LabeledInput({this.name});
+}
diff --git a/packages/jael/jael_web/example/using_components.g.dart b/packages/jael/jael_web/example/using_components.g.dart
new file mode 100644
index 00000000..43a16ceb
--- /dev/null
+++ b/packages/jael/jael_web/example/using_components.g.dart
@@ -0,0 +1,35 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'using_components.dart';
+
+// **************************************************************************
+// JaelComponentGenerator
+// **************************************************************************
+
+abstract class _MyAppJaelTemplate implements Component {
+ @override
+ DomNode render() {
+ return h('div', {}, [
+ h('h1', {}, [text('Welcome to my app')]),
+ LabeledInput(name: "username")
+ ]);
+ }
+}
+
+abstract class _LabeledInputJaelTemplate implements Component {
+ String get name;
+ @override
+ DomNode render() {
+ return h('div', {}, [
+ h('label', {}, [
+ h('b', {}, [text(name.toString()), text(':')])
+ ]),
+ h('br', {}, []),
+ h('input', {
+ 'name': name,
+ 'placeholder': "Enter " + name + "...",
+ 'type': "text"
+ }, [])
+ ]);
+ }
+}
diff --git a/packages/jael/jael_web/lib/builder.dart b/packages/jael/jael_web/lib/builder.dart
new file mode 100644
index 00000000..1d848d2e
--- /dev/null
+++ b/packages/jael/jael_web/lib/builder.dart
@@ -0,0 +1 @@
+export 'src/builder/builder.dart';
diff --git a/packages/jael/jael_web/lib/elements.dart b/packages/jael/jael_web/lib/elements.dart
new file mode 100644
index 00000000..c97cfa6d
--- /dev/null
+++ b/packages/jael/jael_web/lib/elements.dart
@@ -0,0 +1 @@
+export 'src/elements.dart';
diff --git a/packages/jael/jael_web/lib/jael_web.dart b/packages/jael/jael_web/lib/jael_web.dart
new file mode 100644
index 00000000..f8f56f7f
--- /dev/null
+++ b/packages/jael/jael_web/lib/jael_web.dart
@@ -0,0 +1,6 @@
+export 'src/builder_node.dart';
+export 'src/component.dart';
+export 'src/dom_builder.dart';
+export 'src/dom_node.dart';
+export 'src/fn.dart';
+export 'src/jael_component.dart';
diff --git a/packages/jael/jael_web/lib/src/builder/builder.dart b/packages/jael/jael_web/lib/src/builder/builder.dart
new file mode 100644
index 00000000..618909c7
--- /dev/null
+++ b/packages/jael/jael_web/lib/src/builder/builder.dart
@@ -0,0 +1,145 @@
+import 'dart:async';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:build/build.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:jael/jael.dart' as jael;
+import 'package:jael_preprocessor/jael_preprocessor.dart' as jael;
+import 'package:jael_web/jael_web.dart';
+import 'package:path/path.dart' as p;
+import 'package:source_gen/source_gen.dart';
+import 'util.dart';
+
+var _upper = RegExp(r'^[A-Z]');
+
+Builder jaelComponentBuilder(_) {
+ return SharedPartBuilder([JaelComponentGenerator()], 'jael_web_cmp');
+}
+
+class JaelComponentGenerator extends GeneratorForAnnotation {
+ @override
+ Future generateForAnnotatedElement(
+ Element element, ConstantReader annotation, BuildStep buildStep) async {
+ if (element is ClassElement) {
+ // Load the template
+ String templateString;
+ var inputId = buildStep.inputId;
+ var ann = Jael(
+ template: annotation.peek('template')?.stringValue,
+ templateUrl: annotation.peek('templateUrl')?.stringValue,
+ asDsx: annotation.peek('asDsx')?.boolValue ?? false,
+ );
+
+ if (ann.template == null && ann.templateUrl == null) {
+ throw 'Both `template` and `templateUrl` cannot be null.';
+ }
+
+ if (ann.template != null)
+ templateString = ann.template;
+ else {
+ var dir = p.dirname(inputId.path);
+ var assetId = AssetId(inputId.package, p.join(dir, ann.templateUrl));
+ if (!await buildStep.canRead(assetId)) {
+ throw 'Cannot find template "${assetId.uri}"';
+ } else {
+ templateString = await buildStep.readAsString(assetId);
+ }
+ }
+
+ var fs = BuildFileSystem(buildStep, inputId.package);
+ var errors = [];
+ var doc = await jael.parseDocument(templateString,
+ sourceUrl: inputId.uri, asDSX: ann.asDsx, onError: errors.add);
+ if (errors.isEmpty) {
+ doc = await jael.resolve(doc, fs.file(inputId.uri).parent,
+ onError: errors.add);
+ }
+
+ if (errors.isNotEmpty) {
+ errors.forEach(log.severe);
+ throw 'Jael processing finished with ${errors.length} error(s).';
+ }
+
+ // Generate a _XJaelTemplate mixin class
+ var clazz = Class((b) {
+ b
+ ..abstract = true
+ ..name = '_${element.name}JaelTemplate'
+ ..implements.add(convertTypeReference(element.supertype));
+
+ // Add fields corresponding to each of the class's fields.
+ for (var field in element.fields) {
+ b.methods.add(Method((b) {
+ b
+ ..name = field.name
+ ..type = MethodType.getter
+ ..returns = convertTypeReference(field.type);
+ }));
+ }
+
+ // ... And methods too.
+ for (var method in element.methods) {
+ b.methods.add(Method((b) {
+ b
+ ..name = method.name
+ ..returns = convertTypeReference(method.returnType)
+ ..requiredParameters.addAll(method.parameters
+ .where(isRequiredParameter)
+ .map(convertParameter))
+ ..optionalParameters.addAll(method.parameters
+ .where(isOptionalParameter)
+ .map(convertParameter));
+ }));
+ }
+
+ // Add a render() stub
+ b.methods.add(Method((b) {
+ b
+ ..name = 'render'
+ ..returns = refer('DomNode')
+ ..annotations.add(refer('override'))
+ ..body = Block((b) {
+ var result = compileElementChild(doc.root);
+ b.addExpression(result.returned);
+ });
+ }));
+ });
+
+ return clazz.accept(DartEmitter()).toString();
+ } else {
+ throw '@Jael() is only supported for classes.';
+ }
+ }
+
+ Expression compileElementChild(jael.ElementChild child) {
+ if (child is jael.TextNode || child is jael.Text) {
+ return refer('text').call([literalString(child.span.text)]);
+ } else if (child is jael.Interpolation) {
+ Expression expr = CodeExpression(Code(child.expression.span.text));
+ expr = expr.property('toString').call([]);
+ return refer('text').call([expr]);
+ } else if (child is jael.Element) {
+ // TODO: Handle strict resolution
+ var attrs = {};
+ for (var attr in child.attributes) {
+ attrs[attr.name] = attr.value == null
+ ? literalTrue
+ : CodeExpression(Code(attr.value.span.text));
+ }
+
+ var tagName = child.tagName.name;
+ if (!_upper.hasMatch(tagName)) {
+ return refer('h').call([
+ literalString(tagName),
+ literalMap(attrs),
+ literalList(child.children.map(compileElementChild)),
+ ]);
+ } else {
+ // TODO: How to pass children?
+ return refer(tagName).newInstance([], attrs);
+ }
+ // return refer(child.tagName.name).newInstance([]);
+ } else {
+ throw 'Unsupported: $child';
+ }
+ }
+}
diff --git a/packages/jael/jael_web/lib/src/builder/util.dart b/packages/jael/jael_web/lib/src/builder/util.dart
new file mode 100644
index 00000000..2796cec4
--- /dev/null
+++ b/packages/jael/jael_web/lib/src/builder/util.dart
@@ -0,0 +1,395 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:build/build.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:file/file.dart';
+import 'package:path/src/context.dart';
+
+/// Converts a [DartType] to a [TypeReference].
+TypeReference convertTypeReference(DartType t) {
+ return new TypeReference((b) {
+ b..symbol = t.name;
+
+ if (t is InterfaceType) {
+ b.types.addAll(t.typeArguments.map(convertTypeReference));
+ }
+ });
+}
+
+bool isRequiredParameter(ParameterElement e) {
+ return e.isNotOptional;
+}
+
+bool isOptionalParameter(ParameterElement e) {
+ return e.isOptional;
+}
+
+Parameter convertParameter(ParameterElement e) {
+ return Parameter((b) {
+ b
+ ..name = e.name
+ ..type = convertTypeReference(e.type)
+ ..named = e.isNamed
+ ..defaultTo =
+ e.defaultValueCode == null ? null : Code(e.defaultValueCode);
+ });
+}
+
+UnsupportedError _unsupported() =>
+ UnsupportedError('Not support in R/O build file system.');
+
+class BuildFileSystem extends FileSystem {
+ final AssetReader reader;
+ final String package;
+ Context _path = Context();
+
+ BuildFileSystem(this.reader, this.package);
+
+ Context get path => _path;
+
+ @override
+ Directory get currentDirectory {
+ return BuildSystemDirectory(this, reader, package, _path.current);
+ }
+
+ set currentDirectory(value) {
+ if (value is Directory) {
+ _path = Context(current: value.path);
+ } else if (value is String) {
+ _path = Context(current: value);
+ } else {
+ throw ArgumentError();
+ }
+ }
+
+ @override
+ Directory directory(path) {
+ String p;
+ if (path is String)
+ p = path;
+ else if (path is Uri)
+ p = p.toString();
+ else if (path is FileSystemEntity)
+ p = path.path;
+ else
+ throw ArgumentError();
+ return BuildSystemDirectory(this, reader, package, p);
+ }
+
+ @override
+ File file(path) {
+ String p;
+ if (path is String)
+ p = path;
+ else if (path is Uri)
+ p = p.toString();
+ else if (path is FileSystemEntity)
+ p = path.path;
+ else
+ throw ArgumentError();
+ return BuildSystemFile(this, reader, package, p);
+ }
+
+ @override
+ Future identical(String path1, String path2) => throw _unsupported();
+
+ @override
+ bool identicalSync(String path1, String path2) => throw _unsupported();
+
+ @override
+ bool get isWatchSupported => false;
+
+ @override
+ Link link(path) => throw _unsupported();
+
+ @override
+ Future stat(String path) => throw _unsupported();
+
+ @override
+ FileStat statSync(String path) => throw _unsupported();
+
+ @override
+ Directory get systemTempDirectory => throw _unsupported();
+
+ @override
+ Future type(String path, {bool followLinks = true}) =>
+ throw _unsupported();
+
+ @override
+ FileSystemEntityType typeSync(String path, {bool followLinks = true}) =>
+ throw _unsupported();
+}
+
+class BuildSystemFile extends File {
+ final BuildFileSystem fileSystem;
+ final AssetReader reader;
+ final String package;
+ final String path;
+
+ BuildSystemFile(this.fileSystem, this.reader, this.package, this.path);
+
+ Uri get uri => fileSystem.path.toUri(path);
+
+ @override
+ File get absolute => this;
+
+ @override
+ String get basename => fileSystem.path.basename(path);
+
+ @override
+ Future copy(String newPath) => throw _unsupported();
+
+ @override
+ File copySync(String newPath) => throw _unsupported();
+
+ @override
+ Future create({bool recursive = false}) => throw _unsupported();
+
+ @override
+ void createSync({bool recursive = false}) => throw _unsupported();
+
+ @override
+ Future delete({bool recursive = false}) =>
+ throw _unsupported();
+
+ @override
+ void deleteSync({bool recursive = false}) => throw _unsupported();
+
+ @override
+ String get dirname => fileSystem.path.dirname(path);
+
+ @override
+ Future exists() => throw _unsupported();
+ @override
+ bool existsSync() => throw _unsupported();
+
+ @override
+ bool get isAbsolute => true;
+
+ @override
+ Future lastAccessed() => throw _unsupported();
+
+ @override
+ DateTime lastAccessedSync() => throw _unsupported();
+
+ @override
+ Future lastModified() => throw _unsupported();
+
+ @override
+ DateTime lastModifiedSync() => throw _unsupported();
+
+ @override
+ Future length() => throw _unsupported();
+ @override
+ int lengthSync() => throw _unsupported();
+
+ @override
+ Future open({FileMode mode = FileMode.read}) =>
+ throw _unsupported();
+
+ @override
+ Stream> openRead([int start, int end]) => throw _unsupported();
+
+ @override
+ RandomAccessFile openSync({FileMode mode = FileMode.read}) =>
+ throw _unsupported();
+
+ @override
+ IOSink openWrite(
+ {FileMode mode = FileMode.write, Encoding encoding = utf8}) =>
+ throw _unsupported();
+
+ @override
+ Directory get parent => BuildSystemDirectory(
+ fileSystem, reader, package, fileSystem.path.dirname(path));
+
+ @override
+ Future> readAsBytes() {
+ var assetId = AssetId(package, path);
+ return reader.readAsBytes(assetId);
+ }
+
+ @override
+ List readAsBytesSync() => throw _unsupported();
+ @override
+ Future> readAsLines({Encoding encoding = utf8}) =>
+ throw _unsupported();
+
+ @override
+ List readAsLinesSync({Encoding encoding = utf8}) =>
+ throw _unsupported();
+
+ @override
+ Future readAsString({Encoding encoding = utf8}) {
+ var assetId = AssetId(package, path);
+ return reader.readAsString(assetId);
+ }
+
+ @override
+ String readAsStringSync({Encoding encoding = utf8}) => throw _unsupported();
+
+ @override
+ Future rename(String newPath) => throw _unsupported();
+
+ @override
+ File renameSync(String newPath) => throw _unsupported();
+
+ @override
+ Future resolveSymbolicLinks() => throw _unsupported();
+
+ @override
+ String resolveSymbolicLinksSync() => throw _unsupported();
+
+ @override
+ Future setLastAccessed(DateTime time) => throw _unsupported();
+
+ @override
+ void setLastAccessedSync(DateTime time) => throw _unsupported();
+
+ @override
+ Future setLastModified(DateTime time) => throw _unsupported();
+
+ @override
+ void setLastModifiedSync(DateTime time) => throw _unsupported();
+
+ @override
+ Future stat() => throw _unsupported();
+
+ @override
+ FileStat statSync() => throw _unsupported();
+
+ @override
+ Stream watch(
+ {int events = FileSystemEvent.all, bool recursive = false}) =>
+ throw _unsupported();
+
+ @override
+ Future writeAsBytes(List bytes,
+ {FileMode mode = FileMode.write, bool flush = false}) =>
+ throw _unsupported();
+
+ @override
+ void writeAsBytesSync(List bytes,
+ {FileMode mode = FileMode.write, bool flush = false}) =>
+ throw _unsupported();
+
+ @override
+ Future writeAsString(String contents,
+ {FileMode mode = FileMode.write,
+ Encoding encoding = utf8,
+ bool flush = false}) =>
+ throw _unsupported();
+
+ @override
+ void writeAsStringSync(String contents,
+ {FileMode mode = FileMode.write,
+ Encoding encoding = utf8,
+ bool flush = false}) =>
+ throw _unsupported();
+}
+
+class BuildSystemDirectory extends Directory {
+ final BuildFileSystem fileSystem;
+ final AssetReader reader;
+ final String package;
+ final String path;
+
+ BuildSystemDirectory(this.fileSystem, this.reader, this.package, this.path);
+
+ @override
+ Directory get absolute => this;
+
+ @override
+ String get basename => fileSystem.path.basename(path);
+
+ @override
+ Directory childDirectory(String basename) {
+ return BuildSystemDirectory(
+ fileSystem, reader, package, fileSystem.path.join(path, basename));
+ }
+
+ @override
+ File childFile(String basename) {
+ return BuildSystemFile(
+ fileSystem, reader, package, fileSystem.path.join(path, basename));
+ }
+
+ @override
+ Link childLink(String basename) => throw _unsupported();
+
+ @override
+ Future create({bool recursive = false}) => throw _unsupported();
+
+ @override
+ void createSync({bool recursive = false}) => throw _unsupported();
+
+ @override
+ Future createTemp([String prefix]) => throw _unsupported();
+
+ @override
+ Directory createTempSync([String prefix]) => throw _unsupported();
+
+ @override
+ Future delete({bool recursive = false}) =>
+ throw _unsupported();
+
+ @override
+ void deleteSync({bool recursive = false}) => throw _unsupported();
+
+ @override
+ String get dirname => fileSystem.path.dirname(path);
+
+ @override
+ Future exists() => throw _unsupported();
+
+ @override
+ bool existsSync() => throw _unsupported();
+
+ @override
+ bool get isAbsolute => true;
+
+ @override
+ Stream list(
+ {bool recursive = false, bool followLinks = true}) =>
+ throw _unsupported();
+
+ @override
+ List listSync(
+ {bool recursive = false, bool followLinks = true}) =>
+ throw _unsupported();
+
+ @override
+ Directory get parent {
+ return BuildSystemDirectory(
+ fileSystem, reader, package, fileSystem.path.dirname(path));
+ }
+
+ @override
+ Future rename(String newPath) => throw _unsupported();
+
+ @override
+ Directory renameSync(String newPath) => throw _unsupported();
+
+ @override
+ Future resolveSymbolicLinks() => throw _unsupported();
+
+ @override
+ String resolveSymbolicLinksSync() => throw _unsupported();
+
+ @override
+ Future stat() => throw _unsupported();
+
+ @override
+ FileStat statSync() => throw _unsupported();
+
+ @override
+ Uri get uri => fileSystem.path.toUri(path);
+
+ @override
+ Stream watch(
+ {int events = FileSystemEvent.all, bool recursive = false}) =>
+ throw _unsupported();
+}
diff --git a/packages/jael/jael_web/lib/src/builder_node.dart b/packages/jael/jael_web/lib/src/builder_node.dart
new file mode 100644
index 00000000..91c5970e
--- /dev/null
+++ b/packages/jael/jael_web/lib/src/builder_node.dart
@@ -0,0 +1,53 @@
+import 'dom_builder.dart';
+import 'dom_node.dart';
+
+abstract class BuilderNode extends DomNode {
+ DomBuilderElement build(DomBuilder dom);
+
+ void destroy(DomBuilderElement el);
+}
+
+DomNode h(String tagName,
+ [Map props = const {},
+ Iterable children = const []]) {
+ return _H(tagName, props, children);
+}
+
+DomNode text(String value) => _Text(value);
+
+class _Text extends BuilderNode {
+ final String text;
+
+ _Text(this.text);
+
+ @override
+ DomBuilderElement build(DomBuilder dom) {
+ dom.text(text);
+ // TODO: implement build
+ return null;
+ }
+
+ @override
+ void destroy(DomBuilderElement el) {
+ // TODO: implement destroy
+ }
+}
+
+class _H extends BuilderNode {
+ final String tagName;
+ final Map props;
+ final Iterable children;
+
+ _H(this.tagName, this.props, this.children);
+
+ @override
+ DomBuilderElement build(DomBuilder dom) {
+ // TODO: implement build
+ return null;
+ }
+
+ @override
+ void destroy(DomBuilderElement el) {
+ // TODO: implement destroy
+ }
+}
diff --git a/packages/jael/jael_web/lib/src/component.dart b/packages/jael/jael_web/lib/src/component.dart
new file mode 100644
index 00000000..4faf6901
--- /dev/null
+++ b/packages/jael/jael_web/lib/src/component.dart
@@ -0,0 +1,15 @@
+import 'dom_node.dart';
+
+abstract class Component extends DomNode {
+ State state;
+
+ DomNode render();
+
+ void afterMount() {}
+
+ void beforeDestroy() {}
+
+ void setState(State newState) {
+ // TODO:
+ }
+}
diff --git a/packages/jael/jael_web/lib/src/dom_builder.dart b/packages/jael/jael_web/lib/src/dom_builder.dart
new file mode 100644
index 00000000..6800529b
--- /dev/null
+++ b/packages/jael/jael_web/lib/src/dom_builder.dart
@@ -0,0 +1,14 @@
+abstract class DomBuilder {
+ DomBuilderElement append(
+ String tagName, void Function(DomBuilderElement) f);
+
+ void text(String value);
+}
+
+abstract class DomBuilderElement extends DomBuilder {
+ void attr(String name, [String value]);
+
+ void attrs(Map map);
+
+ T close();
+}
diff --git a/packages/jael/jael_web/lib/src/dom_node.dart b/packages/jael/jael_web/lib/src/dom_node.dart
new file mode 100644
index 00000000..e35e6ff9
--- /dev/null
+++ b/packages/jael/jael_web/lib/src/dom_node.dart
@@ -0,0 +1 @@
+abstract class DomNode {}
diff --git a/packages/jael/jael_web/lib/src/elements.dart b/packages/jael/jael_web/lib/src/elements.dart
new file mode 100644
index 00000000..c304842d
--- /dev/null
+++ b/packages/jael/jael_web/lib/src/elements.dart
@@ -0,0 +1,2036 @@
+import 'builder_node.dart';
+import 'dom_node.dart';
+
+Map _apply(Iterable> props,
+ [Map attrs]) {
+ var map = {};
+ attrs?.forEach((k, attr) {
+ if (attr is String && attr?.isNotEmpty == true) {
+ map[k] = attr;
+ } else if (attr is Iterable && attr?.isNotEmpty == true) {
+ map[k] = attr.toList();
+ } else if (attr != null) {
+ map[k] = attr;
+ }
+ });
+
+ for (var p in props) {
+ map.addAll(p ?? {});
+ }
+
+ return map.cast();
+}
+
+DomNode a(
+ {String href,
+ String rel,
+ String target,
+ String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable c: const [],
+ @deprecated Iterable children: const []}) =>
+ h(
+ 'a',
+ _apply([
+ p,
+ props
+ ], {
+ 'href': href,
+ 'rel': rel,
+ 'target': target,
+ 'id': id,
+ 'class': className,
+ 'style': style,
+ }),
+ []..addAll(c ?? [])..addAll(children ?? []));
+
+DomNode abbr(
+ {String title,
+ String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable c: const [],
+ @deprecated Iterable children: const []}) =>
+ h(
+ 'addr',
+ _apply([p, props],
+ {'title': title, 'id': id, 'class': className, 'style': style}),
+ []..addAll(c ?? [])..addAll(children ?? []));
+
+DomNode address(
+ {String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable c: const [],
+ @deprecated Iterable children: const []}) =>
+ h(
+ 'address',
+ _apply([p, props], {'id': id, 'class': className, 'style': style}),
+ []..addAll(c ?? [])..addAll(children ?? []));
+
+DomNode area(
+ {String alt,
+ Iterable coordinates,
+ String download,
+ String href,
+ String hreflang,
+ String media,
+ String nohref,
+ String rel,
+ String shape,
+ String target,
+ String type,
+ String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {}}) =>
+ h(
+ 'area',
+ _apply([
+ p,
+ props
+ ], {
+ 'alt': alt,
+ 'coordinates': coordinates,
+ 'download': download,
+ 'href': href,
+ 'hreflang': hreflang,
+ 'media': media,
+ 'nohref': nohref,
+ 'rel': rel,
+ 'shape': shape,
+ 'target': target,
+ 'type': type,
+ 'id': id,
+ 'class': className,
+ 'style': style
+ }));
+
+DomNode article(
+ {className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable c: const [],
+ @deprecated Iterable children: const []}) =>
+ h('article', _apply([p, props], {'class': className, 'style': style}),
+ []..addAll(c ?? [])..addAll(children ?? []));
+
+DomNode aside(
+ {String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable c: const [],
+ @deprecated Iterable children: const []}) =>
+ h(
+ 'aside',
+ _apply([p, props], {'id': id, 'class': className, 'style': style}),
+ []..addAll(c ?? [])..addAll(children ?? []));
+
+DomNode audio(
+ {bool autoplay,
+ bool controls,
+ bool loop,
+ bool muted,
+ String preload,
+ String src,
+ String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable c: const [],
+ @deprecated Iterable children: const []}) =>
+ h(
+ 'audio',
+ _apply([
+ p,
+ props
+ ], {
+ 'autoplay': autoplay,
+ 'controls': controls,
+ 'loop': loop,
+ 'muted': muted,
+ 'preload': preload,
+ 'src': src,
+ 'id': id,
+ 'class': className,
+ 'style': style
+ }),
+ []..addAll(c ?? [])..addAll(children ?? []));
+
+DomNode b(
+ {String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable c: const [],
+ @deprecated Iterable children: const []}) =>
+ h('b', _apply([p, props], {'id': id, 'class': className, 'style': style}),
+ []..addAll(c ?? [])..addAll(children ?? []));
+
+DomNode base(
+ {String href,
+ String target,
+ String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {}}) =>
+ h(
+ 'base',
+ _apply([
+ p,
+ props
+ ], {
+ 'href': href,
+ 'target': target,
+ 'id': id,
+ 'class': className,
+ 'style': style
+ }));
+
+DomNode bdi(
+ {String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable c: const [],
+ @deprecated Iterable children: const []}) =>
+ h('bdi', _apply([p, props], {'id': id, 'class': className, 'style': style}),
+ []..addAll(c ?? [])..addAll(children ?? []));
+
+DomNode bdo(
+ {String dir,
+ String id,
+ className,
+ style,
+ Map p: const {},
+ @deprecated Map props: const {},
+ Iterable