diff --git a/jael_web/build.yaml b/jael_web/build.yaml
new file mode 100644
index 00000000..9082d3c2
--- /dev/null
+++ b/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/jael_web/example/main.dart b/jael_web/example/main.dart
index b82b7e86..55bfc068 100644
--- a/jael_web/example/main.dart
+++ b/jael_web/example/main.dart
@@ -1,3 +1,30 @@
import 'package:jael_web/jael_web.dart';
+import 'package:jael_web/elements.dart';
+part 'main.g.dart';
-void main() {}
\ No newline at end of file
+@Dsx(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/jael_web/example/main.g.dart b/jael_web/example/main.g.dart
new file mode 100644
index 00000000..6a208fa6
--- /dev/null
+++ b/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/jael_web/example/stateful.dart b/jael_web/example/stateful.dart
new file mode 100644
index 00000000..664a1fe0
--- /dev/null
+++ b/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);
+ }
+}
+
+@Dsx(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/jael_web/example/stateful.g.dart b/jael_web/example/stateful.g.dart
new file mode 100644
index 00000000..5d26147d
--- /dev/null
+++ b/jael_web/example/stateful.g.dart
@@ -0,0 +1,15 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'stateful.dart';
+
+// **************************************************************************
+// JaelComponentGenerator
+// **************************************************************************
+
+abstract class _StatefulAppJaelTemplate implements Component<_AppState> {
+ Timer get _timer;
+ @override
+ DomNode render() {
+ return h('div', {}, [text('Tick count: '), text(state.ticks.toString())]);
+ }
+}
diff --git a/jael_web/lib/builder.dart b/jael_web/lib/builder.dart
new file mode 100644
index 00000000..0c9f4b71
--- /dev/null
+++ b/jael_web/lib/builder.dart
@@ -0,0 +1 @@
+export 'src/builder/builder.dart';
\ No newline at end of file
diff --git a/jael_web/lib/elements.dart b/jael_web/lib/elements.dart
new file mode 100644
index 00000000..01ea2e52
--- /dev/null
+++ b/jael_web/lib/elements.dart
@@ -0,0 +1 @@
+export 'src/elements.dart';
\ No newline at end of file
diff --git a/jael_web/lib/jael_web.dart b/jael_web/lib/jael_web.dart
index 98c7b7f7..f8f56f7f 100644
--- a/jael_web/lib/jael_web.dart
+++ b/jael_web/lib/jael_web.dart
@@ -3,3 +3,4 @@ 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/jael_web/lib/src/builder/builder.dart b/jael_web/lib/src/builder/builder.dart
new file mode 100644
index 00000000..fc5fe05a
--- /dev/null
+++ b/jael_web/lib/src/builder/builder.dart
@@ -0,0 +1,123 @@
+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';
+
+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);
+ }));
+ }
+
+ // 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));
+ }
+
+ return refer('h').call([
+ literalString(child.tagName.name),
+ literalMap(attrs),
+ literalList(child.children.map(compileElementChild)),
+ ]);
+ // return refer(child.tagName.name).newInstance([]);
+ } else {
+ throw 'Unsupported: $child';
+ }
+ }
+}
diff --git a/jael_web/lib/src/builder/util.dart b/jael_web/lib/src/builder/util.dart
new file mode 100644
index 00000000..72ac0974
--- /dev/null
+++ b/jael_web/lib/src/builder/util.dart
@@ -0,0 +1,375 @@
+import 'dart:async';
+import 'dart:convert';
+
+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));
+ }
+ });
+}
+
+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/jael_web/lib/src/builder_node.dart b/jael_web/lib/src/builder_node.dart
index 555187ff..91c5970e 100644
--- a/jael_web/lib/src/builder_node.dart
+++ b/jael_web/lib/src/builder_node.dart
@@ -6,3 +6,48 @@ abstract class BuilderNode extends DomNode {
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/jael_web/lib/src/elements.dart b/jael_web/lib/src/elements.dart
new file mode 100644
index 00000000..c20a99f3
--- /dev/null
+++ b/jael_web/lib/src/elements.dart
@@ -0,0 +1,2036 @@
+import 'builder_node.dart';
+import 'dom_node.dart';
+
+Map _apply(Iterable