Add 'packages/jael/' from commit 'af168281d94cda98a8fd333618696e92f4e035c5'

git-subtree-dir: packages/jael
git-subtree-mainline: 834de0300f
git-subtree-split: af168281d9
This commit is contained in:
Tobe O 2020-02-15 18:22:11 -05:00
commit edfd785dfe
113 changed files with 8435 additions and 0 deletions

51
packages/jael/.gitignore vendored Normal file
View file

@ -0,0 +1,51 @@
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/jael.iml" filepath="$PROJECT_DIR$/jael.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="blocks within blocks in block_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/jael_preprocessor/test/block_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="blocks within blocks" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="for loop in render_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/jael/test/render/render_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="for loop" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="jael::example" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/jael/example/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/jael" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/angel_jael/example/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/angel_jael/example" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in dsx_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/jael/test/render/dsx_test.dart" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in jael" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/jael" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,2 @@
language: dart
script: bash ./travis.sh

21
packages/jael/LICENSE Normal file
View file

@ -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.

34
packages/jael/README.md Normal file
View file

@ -0,0 +1,34 @@
# jael
[![Pub](https://img.shields.io/pub/v/jael.svg)](https://pub.dartlang.org/packages/jael)
[![build status](https://travis-ci.org/angel-dart/jael.svg)](https://travis-ci.org/angel-dart/jael)
A simple server-side HTML templating engine for Dart.
Though its syntax is but a superset of HTML, it supports features such as:
* **Custom elements**
* Loops
* Conditionals
* Template inheritance
* Block scoping
* `switch` syntax
* Interpolation of any Dart expression
Jael is a good choice for applications of any scale, especially when the development team is small,
or the time invested in building an SPA would be too much.
## Documentation
Each of the [packages within this repository](#this-repository) contains
some sort of documentation.
Documentation for Jael syntax and directives has been
**moved** to the
[Angel framework wiki](https://docs.angel-dart.dev/packages/front-end/jael).
## This Repository
Within this repository are three packages:
* `package:jael` - Contains the Jael parser, AST, and HTML renderer.
* `package:jael_preprocessor` - Handles template inheritance, and facilitates the use of "compile-time" constructs.
* `package:build_jael` - Uses `package:build` to compile Jael templates, therefore allowing speedy incremental builds to HTML files.
* `package:angel_jael` - [Angel](https://angel-dart.github.io) support for Jael. Angel contains other
facilities to speed up application development, so something like Jael is right at home.

16
packages/jael/angel_jael/.gitignore vendored Normal file
View file

@ -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

View file

@ -0,0 +1,13 @@
# 2.0.0
* Angel 2 and Dart 2 updates.
* Default to `.jael` instead of `.jl`.
# 1.0.3
* Update for annoying map casting bug.
# 1.0.2
* Update for DSX support.
* Clear the buffer on errors.
# 1.0.1
* Use `Renderer.errorDocument`.

View file

@ -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.

View file

@ -0,0 +1,83 @@
# jael
[![Pub](https://img.shields.io/pub/v/angel_jael.svg)](https://pub.dartlang.org/packages/angel_jael)
[![build status](https://travis-ci.org/angel-dart/jael.svg)](https://travis-ci.org/angel-dart/jael)
[Angel](https://angel-dart.github.io)
support for
[Jael](https://github.com/angel-dart/jael).
# Installation
In your `pubspec.yaml`:
```yaml
dependencies:
angel_jael: ^1.0.0-alpha
```
# Usage
Just like `mustache` and other renderers, configuring Angel to use
Jael is as simple as calling `app.configure`:
```dart
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_jael/angel_jael.dart';
import 'package:file/file.dart';
AngelConfigurer myPlugin(FileSystem fileSystem) {
return (Angel app) async {
// Connect Jael to your server...
await app.configure(
jael(fileSystem.directory('views')),
);
};
}
```
`package:angel_jael` supports caching views, to improve server performance.
You might not want to enable this in development, so consider setting
the flag to `app.isProduction`:
```
jael(viewsDirectory, cacheViews: app.isProduction);
```
Keep in mind that this package uses `package:file`, rather than
`dart:io`.
The following is a basic example of a server setup that can render Jael
templates from a directory named `views`:
```dart
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_jael/angel_jael.dart';
import 'package:file/local.dart';
import 'package:logging/logging.dart';
main() async {
var app = new Angel();
var fileSystem = const LocalFileSystem();
await app.configure(
jael(fileSystem.directory('views')),
);
// Render the contents of views/index.jael
app.get('/', (res) => res.render('index', {'title': 'ESKETTIT'}));
app.use(() => throw new AngelHttpException.notFound());
app.logger = new Logger('angel')
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
var server = await app.startServer(null, 3000);
print('Listening at http://${server.address.address}:${server.port}');
}
```
To apply additional transforms to parsed documents, provide a
set of `patch` functions, like in `package:jael_preprocessor`.

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,44 @@
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_jael/angel_jael.dart';
import 'package:file/local.dart';
import 'package:logging/logging.dart';
main() async {
var app = new Angel();
var http = new AngelHttp(app);
var fileSystem = const LocalFileSystem();
await app.configure(
jael(fileSystem.directory('views')),
);
app.get(
'/',
(req, res) =>
res.render('index', {'title': 'Sample App', 'message': null}));
app.post('/', (req, res) async {
var body = await req.parseBody().then((_) => req.bodyAsMap);
print('Body: $body');
var msg = body['message'] ?? '<unknown>';
return await res.render('index', {
'title': 'Form Submission',
'message': msg,
'json_message': json.encode(msg),
});
});
app.fallback((req, res) => throw new AngelHttpException.notFound());
app.logger = new Logger('angel')
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
var server = await http.startServer('127.0.0.1', 3000);
print('Listening at http://${server.address.address}:${server.port}');
}

View file

@ -0,0 +1,15 @@
<extend src="layout.jael">
<block name="content">
<i if=message != null>
<script>
window.alert({{- json_message }});
</script>
You said: {{ message }}
</i>
<form action="/" method="post">
<input name="message" placeholder="Say something..." type="text" value=message>
<br>
<input type="submit" value="Submit">
</form>
</block>
</extend>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>{{title}}</title>
</head>
<body>
<h1>
{{title}}
</h1>
<block name="content">
<i>Content goes here.</i>
</block>
<script>
console.info('JAEL :)');
</script>
</body>
</html>

View file

@ -0,0 +1,71 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:code_buffer/code_buffer.dart';
import 'package:file/file.dart';
import 'package:jael/jael.dart';
import 'package:jael_preprocessor/jael_preprocessor.dart';
import 'package:symbol_table/symbol_table.dart';
/// Configures an Angel server to use Jael to render templates.
///
/// To enable "minified" output, you need to override the [createBuffer] function,
/// to instantiate a [CodeBuffer] that emits no spaces or line breaks.
///
/// To apply additional transforms to parsed documents, provide a set of [patch] functions.
AngelConfigurer jael(Directory viewsDirectory,
{String fileExtension,
bool strictResolution: false,
bool cacheViews: false,
Iterable<Patcher> patch,
bool asDSX: false,
CodeBuffer createBuffer()}) {
var cache = <String, Document>{};
fileExtension ??= '.jael';
createBuffer ??= () => new CodeBuffer();
return (Angel app) async {
app.viewGenerator = (String name, [Map locals]) async {
var errors = <JaelError>[];
Document processed;
if (cacheViews == true && cache.containsKey(name)) {
processed = cache[name];
} else {
var file = viewsDirectory.childFile(name + fileExtension);
var contents = await file.readAsString();
var doc = parseDocument(contents,
sourceUrl: file.uri, asDSX: asDSX == true, onError: errors.add);
processed = doc;
try {
processed = await resolve(doc, viewsDirectory,
patch: patch, onError: errors.add);
} catch (_) {
// Ignore these errors, so that we can show syntax errors.
}
if (cacheViews == true) {
cache[name] = processed;
}
}
var buf = createBuffer();
var scope = new SymbolTable(
values: locals?.keys?.fold<Map<String, dynamic>>(<String, dynamic>{},
(out, k) => out..[k.toString()] = locals[k]) ??
<String, dynamic>{});
if (errors.isEmpty) {
try {
const Renderer().render(processed, buf, scope,
strictResolution: strictResolution == true);
return buf.toString();
} on JaelError catch (e) {
errors.add(e);
}
}
Renderer.errorDocument(errors, buf..clear());
return buf.toString();
};
};
}

View file

View file

@ -0,0 +1,19 @@
name: angel_jael
version: 2.0.0
description: Angel support for the Jael templating engine, similar to Blade or Liquid.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/jael/tree/master/jael
environment:
sdk: ">=2.0.0-dev <=3.0.0"
dependencies:
angel_framework: ^2.0.0-alpha
code_buffer: ^1.0.0
file: ^5.0.0
jael: ^2.0.0
jael_preprocessor: #^2.0.0
path: ../jael_preprocessor
symbol_table: ^2.0.0
dev_dependencies:
angel_test: ^2.0.0-alpha
html:
test: ^1.0.0

View file

@ -0,0 +1,84 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_jael/angel_jael.dart';
import 'package:angel_test/angel_test.dart';
import 'package:file/memory.dart';
import 'package:html/parser.dart' as html;
import 'package:logging/logging.dart';
import 'package:test/test.dart';
main() {
// These tests need not actually test that the preprocessor or renderer works,
// because those packages are already tested.
//
// Instead, just test that we can render at all.
TestClient client;
setUp(() async {
var app = new Angel();
app.configuration['properties'] = app.configuration;
var fileSystem = new MemoryFileSystem();
var viewsDirectory = fileSystem.directory('views')..createSync();
viewsDirectory.childFile('layout.jael').writeAsStringSync('''
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
</head>
<body>
<block name="content">
Fallback content
</block>
</body>
</html>
''');
viewsDirectory.childFile('github.jael').writeAsStringSync('''
<extend src="layout.jael">
<block name="content">{{username}}</block>
</extend>
''');
app.get('/github/:username', (req, res) {
var username = req.params['username'];
return res.render('github', {'username': username});
});
await app.configure(
jael(viewsDirectory),
);
app.fallback((req, res) => throw new AngelHttpException.notFound());
app.logger = new Logger('angel')
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
client = await connectTo(app);
});
test('can render', () async {
var response = await client.get('/github/thosakwe');
print('Body:\n${response.body}');
expect(
html.parse(response.body).outerHtml,
html
.parse('''
<html>
<head>
<title>
Hello
</title>
</head>
<body>
thosakwe
</body>
</html>'''
.trim())
.outerHtml);
});
}

39
packages/jael/jael.iml Normal file
View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/angel_jael">
<excludeFolder url="file://$MODULE_DIR$/angel_jael/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/angel_jael/.pub" />
<excludeFolder url="file://$MODULE_DIR$/angel_jael/build" />
</content>
<content url="file://$MODULE_DIR$/build_jael">
<excludeFolder url="file://$MODULE_DIR$/build_jael/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/build_jael/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build_jael/build" />
</content>
<content url="file://$MODULE_DIR$/dsx">
<excludeFolder url="file://$MODULE_DIR$/dsx/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/dsx/.pub" />
<excludeFolder url="file://$MODULE_DIR$/dsx/build" />
</content>
<content url="file://$MODULE_DIR$/dsx_generator">
<excludeFolder url="file://$MODULE_DIR$/dsx_generator/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/dsx_generator/.pub" />
<excludeFolder url="file://$MODULE_DIR$/dsx_generator/build" />
</content>
<content url="file://$MODULE_DIR$/jael">
<excludeFolder url="file://$MODULE_DIR$/jael/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/jael/.pub" />
<excludeFolder url="file://$MODULE_DIR$/jael/build" />
</content>
<content url="file://$MODULE_DIR$/jael_preprocessor">
<excludeFolder url="file://$MODULE_DIR$/jael_preprocessor/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/jael_preprocessor/.pub" />
<excludeFolder url="file://$MODULE_DIR$/jael_preprocessor/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

16
packages/jael/jael/.gitignore vendored Normal file
View file

@ -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

View file

@ -0,0 +1,42 @@
# 2.0.2
* Fixed handling of `if` in non-strict mode.
* Roll `JaelFormatter` and `jaelfmt`.
# 2.0.1
* Fixed bug where the `textarea` name check would never return `true`.
# 2.0.0+1
* Meta-update for Pub score.
# 2.0.0
* Dart 2 updates.
* Remove usage of `package:dart2_constant`.
# 1.0.6+1
* Ensure `<element>` passes attributes.
# 1.0.6
* Add `index-as` to `for-each`.
* Support registering + rendering custom elements.
* Improve handling of booleans in non-strict mode.
# 1.0.5
* Add support for DSX, a port of JSX to Dart.
# 1.0.4
* Skip HTML comments in free text.
# 1.0.3
* Fix a scanner bug that prevented proper parsing of HTML nodes
followed by free text.
* Don't trim `<textarea>` content.
# 1.0.2
* Use `package:dart2_constant`.
* Upgrade `package:symbol_table`.
* Added `Renderer.errorDocument`.
# 1.0.1
* Reworked the scanner; thereby fixing an extremely pesky bug
that prevented successful parsing of Jael files containing
JavaScript.

View file

@ -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.

View file

@ -0,0 +1,50 @@
# jael
[![Pub](https://img.shields.io/pub/v/jael.svg)](https://pub.dartlang.org/packages/jael)
[![build status](https://travis-ci.org/angel-dart/jael.svg)](https://travis-ci.org/angel-dart/jael)
A simple server-side HTML templating engine for Dart.
[See documentation.](https://docs.angel-dart.dev/packages/front-end/jael)
# Installation
In your `pubspec.yaml`:
```yaml
dependencies:
jael: ^2.0.0
```
# API
The core `jael` package exports classes for parsing Jael templates,
an AST library, and a `Renderer` class that generates HTML on-the-fly.
```dart
import 'package:code_buffer/code_buffer.dart';
import 'package:jael/jael.dart' as jael;
import 'package:symbol_table/symbol_table.dart';
void myFunction() {
const template = '''
<html>
<body>
<h1>Hello</h1>
<img src=profile['avatar']>
</body>
</html>
''';
var buf = CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jael', asDSX: false);
var scope = SymbolTable(values: {
'profile': {
'avatar': 'thosakwe.png',
}
});
const jael.Renderer().render(document, buf, scope);
print(buf);
}
```
Pre-processing (i.e. handling of blocks and includes) is handled
by `package:jael_preprocessor.`.

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,124 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:jael/jael.dart';
var argParser = ArgParser()
..addOption('line-length',
abbr: 'l',
help: 'The maximum length of a single line. Longer lines will wrap.',
defaultsTo: '80')
..addOption('stdin-name',
help: 'The filename to print when an error occurs in standard input.',
defaultsTo: '<stdin>')
..addOption('tab-size',
help: 'The number of spaces to output where a TAB would be inserted.',
defaultsTo: '2')
..addFlag('dry-run',
abbr: 'n',
help:
'Print the names of files that would be changed, without actually overwriting them.',
negatable: false)
..addFlag('help',
abbr: 'h', help: 'Print this usage information.', negatable: false)
..addFlag('insert-spaces',
help: 'Insert spaces instead of TAB character.', defaultsTo: true)
..addFlag('overwrite',
abbr: 'w',
help: 'Overwrite input files with formatted output.',
negatable: false);
main(List<String> args) async {
try {
var argResults = argParser.parse(args);
if (argResults['help'] as bool) {
stdout..writeln('Formatter for Jael templates.')..writeln();
printUsage(stdout);
return;
}
if (argResults.rest.isEmpty) {
var text = await stdin.transform(utf8.decoder).join();
var result =
await format(argResults['stdin-name'] as String, text, argResults);
if (result != null) print(result);
} else {
for (var arg in argResults.rest) {
await formatPath(arg, argResults);
}
}
} on ArgParserException catch (e) {
stderr..writeln(e.message)..writeln();
printUsage(stderr);
exitCode = 65;
}
}
void printUsage(IOSink sink) {
sink
..writeln('Usage: jaelfmt [options...] [files or directories...]')
..writeln()
..writeln('Options:')
..writeln(argParser.usage);
}
Future<void> formatPath(String path, ArgResults argResults) async {
var stat = await FileStat.stat(path);
await formatStat(stat, path, argResults);
}
Future<void> formatStat(
FileStat stat, String path, ArgResults argResults) async {
switch (stat.type) {
case FileSystemEntityType.directory:
await for (var entity in Directory(path).list()) {
await formatStat(await entity.stat(), entity.path, argResults);
}
break;
case FileSystemEntityType.file:
if (path.endsWith('.jael')) await formatFile(File(path), argResults);
break;
case FileSystemEntityType.link:
var link = await Link(path).resolveSymbolicLinks();
await formatPath(link, argResults);
break;
default:
throw 'No file or directory found at "$path".';
break;
}
}
Future<void> formatFile(File file, ArgResults argResults) async {
var content = await file.readAsString();
var formatted = await format(file.path, content, argResults);
if (formatted == null) return;
if (argResults['overwrite'] as bool) {
if (formatted != content) {
if (argResults['dry-run'] as bool) {
print('Would have formatted ${file.path}');
} else {
await file.writeAsStringSync(formatted);
print('Formatted ${file.path}');
}
} else {
print('Unchanged ${file.path}');
}
} else {
print(formatted);
}
}
String format(String filename, String content, ArgResults argResults) {
var errored = false;
var doc = parseDocument(content, sourceUrl: filename, onError: (e) {
stderr.writeln(e);
errored = true;
});
if (errored) return null;
var fmt = JaelFormatter(
int.parse(argResults['tab-size'] as String),
argResults['insert-spaces'] as bool,
int.parse(argResults['line-length'] as String));
return fmt.apply(doc);
}

View file

@ -0,0 +1,37 @@
import 'dart:io';
import 'package:charcode/charcode.dart';
import 'package:code_buffer/code_buffer.dart';
import 'package:jael/jael.dart' as jael;
import 'package:symbol_table/symbol_table.dart';
main() {
while (true) {
var buf = StringBuffer();
int ch;
print('Enter lines of Jael text, terminated by CTRL^D.');
print('All environment variables are injected into the template scope.');
while ((ch = stdin.readByteSync()) != $eot && ch != -1) {
buf.writeCharCode(ch);
}
var document = jael.parseDocument(
buf.toString(),
sourceUrl: 'stdin',
onError: stderr.writeln,
);
if (document == null) {
stderr.writeln('Could not parse the given text.');
} else {
var output = CodeBuffer();
const jael.Renderer().render(
document,
output,
SymbolTable(values: Platform.environment),
strictResolution: false,
);
print('GENERATED HTML:\n$output');
}
}
}

View file

@ -0,0 +1,5 @@
export 'src/ast/ast.dart';
export 'src/text/parser.dart';
export 'src/text/scanner.dart';
export 'src/formatter.dart';
export 'src/renderer.dart';

View file

@ -0,0 +1,41 @@
import 'package:source_span/source_span.dart';
import 'expression.dart';
import 'token.dart';
class Array extends Expression {
final Token lBracket, rBracket;
final List<Expression> items;
Array(this.lBracket, this.rBracket, this.items);
@override
compute(scope) => items.map((e) => e.compute(scope)).toList();
@override
FileSpan get span {
return items
.fold<FileSpan>(lBracket.span, (out, i) => out.expand(i.span))
.expand(rBracket.span);
}
}
class IndexerExpression extends Expression {
final Expression target, indexer;
final Token lBracket, rBracket;
IndexerExpression(this.target, this.lBracket, this.indexer, this.rBracket);
@override
FileSpan get span {
return target.span
.expand(lBracket.span)
.expand(indexer.span)
.expand(rBracket.span);
}
@override
compute(scope) {
var a = target.compute(scope), b = indexer.compute(scope);
return a[b];
}
}

View file

@ -0,0 +1,18 @@
export 'array.dart';
export 'ast_node.dart';
export 'attribute.dart';
export 'binary.dart';
export 'call.dart';
export 'conditional.dart';
export 'document.dart';
export 'element.dart';
export 'error.dart';
export 'expression.dart';
export 'identifier.dart';
export 'interpolation.dart';
export 'map.dart';
export 'member.dart';
export 'new.dart';
export 'number.dart';
export 'string.dart';
export 'token.dart';

View file

@ -0,0 +1,5 @@
import 'package:source_span/source_span.dart';
abstract class AstNode {
FileSpan get span;
}

View file

@ -0,0 +1,27 @@
import 'package:source_span/source_span.dart';
import 'ast_node.dart';
import 'expression.dart';
import 'identifier.dart';
import 'string.dart';
import 'token.dart';
class Attribute extends AstNode {
final Identifier id;
final StringLiteral string;
final Token equals, nequ;
final Expression value;
Attribute(this.id, this.string, this.equals, this.nequ, this.value);
bool get isRaw => nequ != null;
Expression get nameNode => id ?? string;
String get name => string?.value ?? id.name;
@override
FileSpan get span {
if (equals == null) return nameNode.span;
return nameNode.span.expand(equals?.span ?? nequ.span).expand(value.span);
}
}

View file

@ -0,0 +1,47 @@
import 'package:source_span/source_span.dart';
import 'expression.dart';
import 'token.dart';
class BinaryExpression extends Expression {
final Expression left, right;
final Token operator;
BinaryExpression(this.left, this.operator, this.right);
@override
compute(scope) {
var l = left.compute(scope), r = right.compute(scope);
switch (operator?.type) {
case TokenType.asterisk:
return l * r;
case TokenType.slash:
return l / r;
case TokenType.plus:
if (l is String || r is String) return l.toString() + r.toString();
return l + r;
case TokenType.minus:
return l - r;
case TokenType.lt:
return l < r;
case TokenType.gt:
return l > r;
case TokenType.lte:
return l <= r;
case TokenType.gte:
return l >= r;
case TokenType.equ:
return l == r;
case TokenType.nequ:
return l != r;
case TokenType.elvis:
return l ?? r;
default:
throw UnsupportedError(
'Unsupported binary operator: "${operator?.span?.text ?? "<null>"}".');
}
}
@override
FileSpan get span => left.span.expand(operator.span).expand(right.span);
}

View file

@ -0,0 +1,56 @@
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import 'ast_node.dart';
import 'expression.dart';
import 'identifier.dart';
import 'token.dart';
class Call extends Expression {
final Expression target;
final Token lParen, rParen;
final List<Expression> arguments;
final List<NamedArgument> namedArguments;
Call(this.target, this.lParen, this.rParen, this.arguments,
this.namedArguments);
@override
FileSpan get span {
return arguments
.fold<FileSpan>(target.span, (out, a) => out.expand(a.span))
.expand(namedArguments.fold<FileSpan>(
lParen.span, (out, a) => out.expand(a.span)))
.expand(rParen.span);
}
List computePositional(SymbolTable scope) =>
arguments.map((e) => e.compute(scope)).toList();
Map<Symbol, dynamic> computeNamed(SymbolTable scope) {
return namedArguments.fold<Map<Symbol, dynamic>>({}, (out, a) {
return out..[Symbol(a.name.name)] = a.value.compute(scope);
});
}
@override
compute(scope) {
var callee = target.compute(scope);
var args = computePositional(scope);
var named = computeNamed(scope);
return Function.apply(callee as Function, args, named);
}
}
class NamedArgument extends AstNode {
final Identifier name;
final Token colon;
final Expression value;
NamedArgument(this.name, this.colon, this.value);
@override
FileSpan get span {
return name.span.expand(colon.span).expand(value.span);
}
}

View file

@ -0,0 +1,31 @@
import 'package:source_span/source_span.dart';
import 'expression.dart';
import 'token.dart';
class Conditional extends Expression {
final Expression condition, ifTrue, ifFalse;
final Token question, colon;
Conditional(
this.condition, this.question, this.ifTrue, this.colon, this.ifFalse);
@override
FileSpan get span {
return condition.span
.expand(question.span)
.expand(ifTrue.span)
.expand(colon.span)
.expand(ifFalse.span);
}
@override
compute(scope) {
var v = condition.compute(scope) as bool;
if (scope.resolve('!strict!')?.value == false) {
v = v == true;
}
return v ? ifTrue.compute(scope) : ifFalse.compute(scope);
}
}

View file

@ -0,0 +1,60 @@
import 'package:source_span/source_span.dart';
import 'ast_node.dart';
import 'element.dart';
import 'identifier.dart';
import 'string.dart';
import 'token.dart';
class Document extends AstNode {
final Doctype doctype;
final Element root;
Document(this.doctype, this.root);
@override
FileSpan get span {
if (doctype == null) return root.span;
return doctype.span.expand(root.span);
}
}
class HtmlComment extends ElementChild {
final Token htmlComment;
HtmlComment(this.htmlComment);
@override
FileSpan get span => htmlComment.span;
}
class Text extends ElementChild {
final Token text;
Text(this.text);
@override
FileSpan get span => text.span;
}
class Doctype extends AstNode {
final Token lt, doctype, gt;
final Identifier html, public;
final StringLiteral name, url;
Doctype(this.lt, this.doctype, this.html, this.public, this.name, this.url,
this.gt);
@override
FileSpan get span {
if (public == null) {
return lt.span.expand(doctype.span).expand(html.span).expand(gt.span);
}
return lt.span
.expand(doctype.span)
.expand(html.span)
.expand(public.span)
.expand(name.span)
.expand(url.span)
.expand(gt.span);
}
}

View file

@ -0,0 +1,96 @@
import 'package:source_span/source_span.dart';
import 'ast_node.dart';
import 'attribute.dart';
import 'identifier.dart';
import 'token.dart';
abstract class ElementChild extends AstNode {}
class TextNode extends ElementChild {
final Token text;
TextNode(this.text);
@override
FileSpan get span => text.span;
}
abstract class Element extends ElementChild {
static const List<String> selfClosing = [
'include',
'base',
'basefont',
'frame',
'link',
'meta',
'area',
'br',
'col',
'hr',
'img',
'input',
'param',
];
Identifier get tagName;
Iterable<Attribute> get attributes;
Iterable<ElementChild> get children;
Attribute getAttribute(String name) =>
attributes.firstWhere((a) => a.name == name, orElse: () => null);
}
class SelfClosingElement extends Element {
final Token lt, slash, gt;
final Identifier tagName;
final Iterable<Attribute> attributes;
@override
Iterable<ElementChild> get children => [];
SelfClosingElement(
this.lt, this.tagName, this.attributes, this.slash, this.gt);
@override
FileSpan get span {
var start = attributes.fold<FileSpan>(
lt.span.expand(tagName.span), (out, a) => out.expand(a.span));
return slash != null
? start.expand(slash.span).expand(gt.span)
: start.expand(gt.span);
}
}
class RegularElement extends Element {
final Token lt, gt, lt2, slash, gt2;
final Identifier tagName, tagName2;
final Iterable<Attribute> attributes;
final Iterable<ElementChild> children;
RegularElement(this.lt, this.tagName, this.attributes, this.gt, this.children,
this.lt2, this.slash, this.tagName2, this.gt2);
@override
FileSpan get span {
var openingTag = attributes
.fold<FileSpan>(
lt.span.expand(tagName.span), (out, a) => out.expand(a.span))
.expand(gt.span);
if (gt2 == null) return openingTag;
return children
.fold<FileSpan>(openingTag, (out, c) => out.expand(c.span))
.expand(lt2.span)
.expand(slash.span)
.expand(tagName2.span)
.expand(gt2.span);
}
}

View file

@ -0,0 +1,21 @@
import 'package:source_span/source_span.dart';
class JaelError extends Error {
final JaelErrorSeverity severity;
final String message;
final FileSpan span;
JaelError(this.severity, this.message, this.span);
@override
String toString() {
var label = severity == JaelErrorSeverity.warning ? 'warning' : 'error';
return '$label: ${span.start.toolString}: $message\n' +
span.highlight(color: true);
}
}
enum JaelErrorSeverity {
warning,
error,
}

View file

@ -0,0 +1,33 @@
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import 'ast_node.dart';
import 'token.dart';
abstract class Expression extends AstNode {
compute(SymbolTable scope);
}
abstract class Literal extends Expression {}
class Negation extends Expression {
final Token exclamation;
final Expression expression;
Negation(this.exclamation, this.expression);
@override
FileSpan get span {
return exclamation.span.expand(expression.span);
}
@override
compute(SymbolTable scope) {
var v = expression.compute(scope) as bool;
if (scope.resolve('!strict!')?.value == false) {
v = v == true;
}
return !v;
}
}

View file

@ -0,0 +1,47 @@
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import 'expression.dart';
import 'token.dart';
class Identifier extends Expression {
final Token id;
Identifier(this.id);
@override
compute(SymbolTable scope) {
switch (name) {
case 'null':
return null;
case 'true':
return true;
case 'false':
return false;
default:
var symbol = scope.resolve(name);
if (symbol == null) {
if (scope.resolve('!strict!')?.value == false) return null;
throw ArgumentError('The name "$name" does not exist in this scope.');
}
return scope.resolve(name).value;
}
}
String get name => id.span.text;
@override
FileSpan get span => id.span;
}
class SyntheticIdentifier extends Identifier {
@override
final String name;
SyntheticIdentifier(this.name, [Token token]) : super(token);
@override
FileSpan get span {
if (id != null) return id.span;
throw UnsupportedError('Cannot get the span of a SyntheticIdentifier.');
}
}

View file

@ -0,0 +1,18 @@
import 'package:source_span/source_span.dart';
import 'element.dart';
import 'expression.dart';
import 'token.dart';
class Interpolation extends ElementChild {
final Token doubleCurlyL, doubleCurlyR;
final Expression expression;
Interpolation(this.doubleCurlyL, this.expression, this.doubleCurlyR);
bool get isRaw => doubleCurlyL.span.text.endsWith('-');
@override
FileSpan get span {
return doubleCurlyL.span.expand(expression.span).expand(doubleCurlyR.span);
}
}

View file

@ -0,0 +1,53 @@
import 'package:source_span/source_span.dart';
import 'ast_node.dart';
import 'expression.dart';
import 'identifier.dart';
import 'token.dart';
class MapLiteral extends Literal {
final Token lCurly, rCurly;
final List<KeyValuePair> pairs;
MapLiteral(this.lCurly, this.pairs, this.rCurly);
@override
compute(scope) {
return pairs.fold<Map>({}, (out, p) {
var key, value;
if (p.colon == null) {
if (p.key is! Identifier) {
key = value = p.key.compute(scope);
} else {
key = (p.key as Identifier).name;
value = p.key.compute(scope);
}
} else {
key = p.key.compute(scope);
value = p.value.compute(scope);
}
return out..[key] = value;
});
}
@override
FileSpan get span {
return pairs
.fold<FileSpan>(lCurly.span, (out, p) => out.expand(p.span))
.expand(rCurly.span);
}
}
class KeyValuePair extends AstNode {
final Expression key, value;
final Token colon;
KeyValuePair(this.key, this.colon, this.value);
@override
FileSpan get span {
if (colon == null) return key.span;
return colon.span.expand(colon.span).expand(value.span);
}
}

View file

@ -0,0 +1,24 @@
import 'dart:mirrors';
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import 'expression.dart';
import 'identifier.dart';
import 'token.dart';
class MemberExpression extends Expression {
final Expression expression;
final Token op;
final Identifier name;
MemberExpression(this.expression, this.op, this.name);
@override
compute(SymbolTable scope) {
var target = expression.compute(scope);
if (op.span.text == '?.' && target == null) return null;
return reflect(target).getField(Symbol(name.name)).reflectee;
}
@override
FileSpan get span => expression.span.expand(op.span).expand(name.span);
}

View file

@ -0,0 +1,32 @@
import 'dart:mirrors';
import 'package:source_span/source_span.dart';
import 'call.dart';
import 'expression.dart';
import 'member.dart';
import 'token.dart';
class NewExpression extends Expression {
final Token $new;
final Call call;
NewExpression(this.$new, this.call);
@override
FileSpan get span => $new.span.expand(call.span);
@override
compute(scope) {
var targetType = call.target.compute(scope);
var positional = call.computePositional(scope);
var named = call.computeNamed(scope);
var name = '';
if (call.target is MemberExpression) {
name = (call.target as MemberExpression).name.name;
}
return reflectClass(targetType as Type)
.newInstance(Symbol(name), positional, named)
.reflectee;
}
}

View file

@ -0,0 +1,47 @@
import 'dart:math' as math;
import 'package:source_span/source_span.dart';
import 'expression.dart';
import 'token.dart';
class NumberLiteral extends Literal {
final Token number;
num _value;
NumberLiteral(this.number);
@override
FileSpan get span => number.span;
static num parse(String value) {
var e = value.indexOf('E');
e != -1 ? e : e = value.indexOf('e');
if (e == -1) return num.parse(value);
var plainNumber = num.parse(value.substring(0, e));
var exp = value.substring(e + 1);
return plainNumber * math.pow(10, num.parse(exp));
}
@override
compute(scope) {
return _value ??= parse(number.span.text);
}
}
class HexLiteral extends Literal {
final Token hex;
num _value;
HexLiteral(this.hex);
@override
FileSpan get span => hex.span;
static num parse(String value) => int.parse(value.substring(2), radix: 16);
@override
compute(scope) {
return _value ??= parse(hex.span.text);
}
}

View file

@ -0,0 +1,75 @@
import 'package:charcode/charcode.dart';
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import '../ast/ast.dart';
import 'expression.dart';
import 'token.dart';
class StringLiteral extends Literal {
final Token string;
final String value;
StringLiteral(this.string, this.value);
static String parseValue(Token string) {
var text = string.span.text.substring(1, string.span.text.length - 1);
var codeUnits = text.codeUnits;
var buf = StringBuffer();
for (int i = 0; i < codeUnits.length; i++) {
var ch = codeUnits[i];
if (ch == $backslash) {
if (i < codeUnits.length - 5 && codeUnits[i + 1] == $u) {
var c1 = codeUnits[i += 2],
c2 = codeUnits[++i],
c3 = codeUnits[++i],
c4 = codeUnits[++i];
var hexString = String.fromCharCodes([c1, c2, c3, c4]);
var hexNumber = int.parse(hexString, radix: 16);
buf.write(String.fromCharCode(hexNumber));
continue;
}
if (i < codeUnits.length - 1) {
var next = codeUnits[++i];
switch (next) {
case $b:
buf.write('\b');
break;
case $f:
buf.write('\f');
break;
case $n:
buf.writeCharCode($lf);
break;
case $r:
buf.writeCharCode($cr);
break;
case $t:
buf.writeCharCode($tab);
break;
default:
buf.writeCharCode(next);
}
} else {
throw JaelError(JaelErrorSeverity.error,
'Unexpected "\\" in string literal.', string.span);
}
} else {
buf.writeCharCode(ch);
}
}
return buf.toString();
}
@override
compute(SymbolTable scope) {
return value;
}
@override
FileSpan get span => string.span;
}

View file

@ -0,0 +1,61 @@
import 'package:source_span/source_span.dart';
class Token {
final TokenType type;
final FileSpan span;
final Match match;
Token(this.type, this.span, this.match);
@override
String toString() {
return '${span.start.toolString}: "${span.text}" => $type';
}
}
enum TokenType {
/*
* HTML
*/
doctype,
htmlComment,
lt,
gt,
slash,
equals,
id,
text,
// Keywords
$new,
/*
* Expression
*/
lBracket,
rBracket,
lDoubleCurly,
rDoubleCurly,
lCurly,
rCurly,
lParen,
rParen,
asterisk,
colon,
comma,
dot,
exclamation,
percent,
plus,
minus,
elvis,
elvis_dot,
lte,
gte,
equ,
nequ,
number,
hex,
string,
question,
}

View file

@ -0,0 +1,194 @@
import 'ast/ast.dart';
/// Jael formatter
class JaelFormatter {
final num tabSize;
final bool insertSpaces;
final int maxLineLength;
var _buffer = StringBuffer();
int _level = 0;
String _spaces;
static String _spaceString(int tabSize) {
var b = StringBuffer();
for (int i = 0; i < tabSize; i++) {
b.write(' ');
}
return b.toString();
}
JaelFormatter(this.tabSize, this.insertSpaces, this.maxLineLength) {
_spaces = insertSpaces ? _spaceString(tabSize.toInt()) : '\t';
}
void _indent() {
_level++;
}
void _outdent() {
if (_level > 0) _level--;
}
void _applySpacing() {
for (int i = 0; i < _level; i++) {
_buffer.write(_spaces);
}
}
int get _spaceLength {
var out = 0;
for (int i = 0; i < _level; i++) {
out += _spaces.length;
}
return out;
}
String apply(Document document) {
if (document?.doctype != null) {
_buffer.write('<!doctype');
if (document.doctype.html != null) _buffer.write(' html');
if (document.doctype.public != null) _buffer.write(' public');
if (document.doctype.url != null) {
_buffer.write('${document.doctype.url}');
}
_buffer.writeln('>');
}
_formatChild(document?.root, 0);
return _buffer.toString().trim();
}
int _formatChild(ElementChild child, int lineLength,
{bool isFirst = false, bool isLast = false}) {
if (child == null) {
return lineLength;
} else if (child is Element) return _formatElement(child, lineLength);
String s;
if (child is Interpolation) {
var b = StringBuffer('{{');
if (child.isRaw) b.write('-');
b.write(' ');
b.write(child.expression.span.text.trim());
b.write(' }}');
s = b.toString();
} else {
s = child.span.text;
}
if (isFirst) {
s = s.trimLeft();
}
if (isLast) {
s = s.trimRight();
}
var ll = lineLength + s.length;
if (ll <= maxLineLength) {
_buffer.write(s);
return ll;
} else {
_buffer.writeln(s);
return _spaceLength;
}
}
int _formatElement(Element element, int lineLength) {
// print([
// element.tagName.name,
// element.children.map((c) => c.runtimeType),
// ]);
var header = '<${element.tagName.name}';
var attrParts = element.attributes.isEmpty
? <String>[]
: element.attributes.map(_formatAttribute);
var attrLen = attrParts.isEmpty
? 0
: attrParts.map((s) => s.length).reduce((a, b) => a + b);
_applySpacing();
_buffer.write(header);
// If the line will be less than maxLineLength characters, write all attrs.
var ll = lineLength +
(element is SelfClosingElement ? 2 : 1) +
header.length +
attrLen;
if (ll <= maxLineLength) {
attrParts.forEach(_buffer.write);
} else {
// Otherwise, them out with tabs.
_buffer.writeln();
_indent();
var i = 0;
for (var p in attrParts) {
if (i++ > 0) {
_buffer.writeln();
}
_applySpacing();
_buffer.write(p);
}
_outdent();
}
if (element is SelfClosingElement) {
_buffer.writeln('/>');
return _spaceLength;
} else {
_buffer.write('>');
if (element.children.isNotEmpty) {
_buffer.writeln();
}
}
_indent();
var lll = _spaceLength;
var i = 1;
ElementChild last;
for (var c in element.children) {
if (lll == _spaceLength && c is! Element) {
_applySpacing();
}
lll = _formatChild(c, lineLength + lll,
isFirst: i == 1 || last is Element,
isLast: i == element.children.length);
if (i++ == element.children.length && c is! Element) {
_buffer.writeln();
}
last = c;
}
_outdent();
if (element.children.isNotEmpty) {
// _buffer.writeln();
_applySpacing();
}
_buffer.writeln('</${element.tagName.name}>');
return lineLength;
}
String _formatAttribute(Attribute attr) {
var b = StringBuffer();
b.write(' ${attr.name}');
if (attr.value != null) {
if (attr.value is Identifier) {
var id = attr.value as Identifier;
if (id.name == 'true') {
b.write(id.name);
} else if (id.name != 'false') {
if (attr.nequ != null) b.write('!=');
if (attr.equals != null) b.write('=');
b.write(id.name);
}
} else {
if (attr.nequ != null) b.write('!=');
if (attr.equals != null) b.write('=');
b.write(attr.value.span.text);
}
}
return b.toString();
}
}

View file

@ -0,0 +1,395 @@
import 'dart:convert';
import 'package:code_buffer/code_buffer.dart';
import 'package:symbol_table/symbol_table.dart';
import 'ast/ast.dart';
import 'text/parser.dart';
import 'text/scanner.dart';
/// Parses a Jael document.
Document parseDocument(String text,
{sourceUrl, bool asDSX = false, void onError(JaelError error)}) {
var scanner = scan(text, sourceUrl: sourceUrl, asDSX: asDSX);
//scanner.tokens.forEach(print);
if (scanner.errors.isNotEmpty && onError != null) {
scanner.errors.forEach(onError);
} else if (scanner.errors.isNotEmpty) throw scanner.errors.first;
var parser = Parser(scanner, asDSX: asDSX);
var doc = parser.parseDocument();
if (parser.errors.isNotEmpty && onError != null) {
parser.errors.forEach(onError);
} else if (parser.errors.isNotEmpty) throw parser.errors.first;
return doc;
}
class Renderer {
const Renderer();
/// Render an error page.
static void errorDocument(Iterable<JaelError> errors, CodeBuffer buf) {
buf
..writeln('<!DOCTYPE html>')
..writeln('<html lang="en">')
..indent()
..writeln('<head>')
..indent()
..writeln(
'<meta name="viewport" content="width=device-width, initial-scale=1">',
)
..writeln('<title>${errors.length} Error(s)</title>')
..outdent()
..writeln('</head>')
..writeln('<body>')
..writeln('<h1>${errors.length} Error(s)</h1>')
..writeln('<ul>')
..indent();
for (var error in errors) {
var type =
error.severity == JaelErrorSeverity.warning ? 'warning' : 'error';
buf
..writeln('<li>')
..indent()
..writeln(
'<b>$type:</b> ${error.span.start.toolString}: ${error.message}')
..writeln('<br>')
..writeln(
'<span style="color: red;">' +
htmlEscape
.convert(error.span.highlight(color: false))
.replaceAll('\n', '<br>') +
'</span>',
)
..outdent()
..writeln('</li>');
}
buf
..outdent()
..writeln('</ul>')
..writeln('</body>')
..writeln('</html>');
}
/// Renders a [document] into the [buffer] as HTML.
///
/// If [strictResolution] is `false` (default: `true`), then undefined identifiers will return `null`
/// instead of throwing.
void render(Document document, CodeBuffer buffer, SymbolTable scope,
{bool strictResolution = true}) {
scope.create('!strict!', value: strictResolution != false);
if (document.doctype != null) buffer.writeln(document.doctype.span.text);
renderElement(
document.root, buffer, scope, document.doctype?.public == null);
}
void renderElement(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
var childScope = scope.createChild();
if (element.attributes.any((a) => a.name == 'for-each')) {
renderForeach(element, buffer, childScope, html5);
return;
} else if (element.attributes.any((a) => a.name == 'if')) {
renderIf(element, buffer, childScope, html5);
return;
} else if (element.tagName.name == 'declare') {
renderDeclare(element, buffer, childScope, html5);
return;
} else if (element.tagName.name == 'switch') {
renderSwitch(element, buffer, childScope, html5);
return;
} else if (element.tagName.name == 'element') {
registerCustomElement(element, buffer, childScope, html5);
return;
} else {
var customElementValue =
scope.resolve(customElementName(element.tagName.name))?.value;
if (customElementValue is Element) {
renderCustomElement(element, buffer, childScope, html5);
return;
}
}
buffer..write('<')..write(element.tagName.name);
for (var attribute in element.attributes) {
var value = attribute.value?.compute(childScope);
if (value == false || value == null) continue;
buffer.write(' ${attribute.name}');
if (value == true) {
continue;
} else {
buffer.write('="');
}
String msg;
if (value is Iterable) {
msg = value.join(' ');
} else if (value is Map) {
msg = value.keys.fold<StringBuffer>(StringBuffer(), (buf, k) {
var v = value[k];
if (v == null) return buf;
return buf..write('$k: $v;');
}).toString();
} else {
msg = value.toString();
}
buffer.write(attribute.isRaw ? msg : htmlEscape.convert(msg));
buffer.write('"');
}
if (element is SelfClosingElement) {
if (html5) {
buffer.writeln('>');
} else {
buffer.writeln('/>');
}
} else {
buffer.writeln('>');
buffer.indent();
for (int i = 0; i < element.children.length; i++) {
var child = element.children.elementAt(i);
renderElementChild(element, child, buffer, childScope, html5, i,
element.children.length);
}
buffer.writeln();
buffer.outdent();
buffer.writeln('</${element.tagName.name}>');
}
}
void renderForeach(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
var attribute = element.attributes.singleWhere((a) => a.name == 'for-each');
if (attribute.value == null) return;
var asAttribute = element.attributes
.firstWhere((a) => a.name == 'as', orElse: () => null);
var indexAsAttribute = element.attributes
.firstWhere((a) => a.name == 'index-as', orElse: () => null);
var alias = asAttribute?.value?.compute(scope)?.toString() ?? 'item';
var indexAs = indexAsAttribute?.value?.compute(scope)?.toString() ?? 'i';
var otherAttributes = element.attributes.where(
(a) => a.name != 'for-each' && a.name != 'as' && a.name != 'index-as');
Element strippedElement;
if (element is SelfClosingElement) {
strippedElement = SelfClosingElement(element.lt, element.tagName,
otherAttributes, element.slash, element.gt);
} else if (element is RegularElement) {
strippedElement = RegularElement(
element.lt,
element.tagName,
otherAttributes,
element.gt,
element.children,
element.lt2,
element.slash,
element.tagName2,
element.gt2);
}
int i = 0;
for (var item in attribute.value.compute(scope)) {
var childScope = scope.createChild(values: {alias: item, indexAs: i++});
renderElement(strippedElement, buffer, childScope, html5);
}
}
void renderIf(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
var attribute = element.attributes.singleWhere((a) => a.name == 'if');
var vv = attribute.value.compute(scope);
if (scope.resolve('!strict!')?.value == false) {
vv = vv == true;
}
var v = vv as bool;
if (!v) return;
var otherAttributes = element.attributes.where((a) => a.name != 'if');
Element strippedElement;
if (element is SelfClosingElement) {
strippedElement = SelfClosingElement(element.lt, element.tagName,
otherAttributes, element.slash, element.gt);
} else if (element is RegularElement) {
strippedElement = RegularElement(
element.lt,
element.tagName,
otherAttributes,
element.gt,
element.children,
element.lt2,
element.slash,
element.tagName2,
element.gt2);
}
renderElement(strippedElement, buffer, scope, html5);
}
void renderDeclare(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
for (var attribute in element.attributes) {
scope.create(attribute.name,
value: attribute.value?.compute(scope), constant: true);
}
for (int i = 0; i < element.children.length; i++) {
var child = element.children.elementAt(i);
renderElementChild(
element, child, buffer, scope, html5, i, element.children.length);
}
}
void renderSwitch(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
var value = element.attributes
.firstWhere((a) => a.name == 'value', orElse: () => null)
?.value
?.compute(scope);
var cases = element.children
.whereType<Element>()
.where((c) => c.tagName.name == 'case');
for (var child in cases) {
var comparison = child.attributes
.firstWhere((a) => a.name == 'value', orElse: () => null)
?.value
?.compute(scope);
if (comparison == value) {
for (int i = 0; i < child.children.length; i++) {
var c = child.children.elementAt(i);
renderElementChild(
element, c, buffer, scope, html5, i, child.children.length);
}
return;
}
}
var defaultCase = element.children.firstWhere(
(c) => c is Element && c.tagName.name == 'default',
orElse: () => null) as Element;
if (defaultCase != null) {
for (int i = 0; i < defaultCase.children.length; i++) {
var child = defaultCase.children.elementAt(i);
renderElementChild(element, child, buffer, scope, html5, i,
defaultCase.children.length);
}
}
}
void renderElementChild(Element parent, ElementChild child, CodeBuffer buffer,
SymbolTable scope, bool html5, int index, int total) {
if (child is Text && parent?.tagName?.name != 'textarea') {
if (index == 0) {
buffer.write(child.span.text.trimLeft());
} else if (index == total - 1) {
buffer.write(child.span.text.trimRight());
} else {
buffer.write(child.span.text);
}
} else if (child is Interpolation) {
var value = child.expression.compute(scope);
if (value != null) {
if (child.isRaw) {
buffer.write(value);
} else {
buffer.write(htmlEscape.convert(value.toString()));
}
}
} else if (child is Element) {
if (buffer?.lastLine?.text?.isNotEmpty == true) buffer.writeln();
renderElement(child, buffer, scope, html5);
}
}
static String customElementName(String name) => 'elements@$name';
void registerCustomElement(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
if (element is! RegularElement) {
throw JaelError(JaelErrorSeverity.error,
"Custom elements cannot be self-closing.", element.span);
}
var name = element.getAttribute('name')?.value?.compute(scope)?.toString();
if (name == null) {
throw JaelError(
JaelErrorSeverity.error,
"Attribute 'name' is required when registering a custom element.",
element.tagName.span);
}
try {
var p = scope.isRoot ? scope : scope.parent;
p.create(customElementName(name), value: element, constant: true);
} on StateError {
throw JaelError(
JaelErrorSeverity.error,
"Cannot re-define element '$name' in this scope.",
element.getAttribute('name').span);
}
}
void renderCustomElement(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
var template = scope.resolve(customElementName(element.tagName.name)).value
as RegularElement;
var renderAs = element.getAttribute('as')?.value?.compute(scope);
var attrs = element.attributes.where((a) => a.name != 'as');
for (var attribute in attrs) {
if (attribute.name.startsWith('@')) {
scope.create(attribute.name.substring(1),
value: attribute.value?.compute(scope), constant: true);
}
}
if (renderAs == false) {
for (int i = 0; i < template.children.length; i++) {
var child = template.children.elementAt(i);
renderElementChild(
element, child, buffer, scope, html5, i, element.children.length);
}
} else {
var tagName = renderAs?.toString() ?? 'div';
var syntheticElement = RegularElement(
template.lt,
SyntheticIdentifier(tagName),
element.attributes
.where((a) => a.name != 'as' && !a.name.startsWith('@')),
template.gt,
template.children,
template.lt2,
template.slash,
SyntheticIdentifier(tagName),
template.gt2);
renderElement(syntheticElement, buffer, scope, html5);
}
}
}

View file

@ -0,0 +1,166 @@
part of jael.src.text.parselet;
const Map<TokenType, InfixParselet> infixParselets = {
TokenType.lParen: CallParselet(),
TokenType.elvis_dot: MemberParselet(),
TokenType.dot: MemberParselet(),
TokenType.lBracket: IndexerParselet(),
TokenType.asterisk: BinaryParselet(14),
TokenType.slash: BinaryParselet(14),
TokenType.percent: BinaryParselet(14),
TokenType.plus: BinaryParselet(13),
TokenType.minus: BinaryParselet(13),
TokenType.lt: BinaryParselet(11),
TokenType.lte: BinaryParselet(11),
TokenType.gt: BinaryParselet(11),
TokenType.gte: BinaryParselet(11),
TokenType.equ: BinaryParselet(10),
TokenType.nequ: BinaryParselet(10),
TokenType.question: ConditionalParselet(),
TokenType.equals: BinaryParselet(3),
TokenType.elvis: BinaryParselet(3),
};
class ConditionalParselet implements InfixParselet {
@override
int get precedence => 4;
const ConditionalParselet();
@override
Expression parse(Parser parser, Expression left, Token token) {
var ifTrue = parser.parseExpression(0);
if (ifTrue == null) {
parser.errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression in conditional expression.', token.span));
return null;
}
if (!parser.next(TokenType.colon)) {
parser.errors.add(JaelError(JaelErrorSeverity.error,
'Missing ":" in conditional expression.', ifTrue.span));
return null;
}
var colon = parser.current;
var ifFalse = parser.parseExpression(0);
if (ifFalse == null) {
parser.errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression in conditional expression.', colon.span));
return null;
}
return Conditional(left, token, ifTrue, colon, ifFalse);
}
}
class BinaryParselet implements InfixParselet {
final int precedence;
const BinaryParselet(this.precedence);
@override
Expression parse(Parser parser, Expression left, Token token) {
var right = parser.parseExpression(precedence);
if (right == null) {
if (token.type != TokenType.gt) {
parser.errors.add(JaelError(
JaelErrorSeverity.error,
'Missing expression after operator "${token.span.text}", following expression ${left.span.text}.',
token.span));
}
return null;
}
return BinaryExpression(left, token, right);
}
}
class CallParselet implements InfixParselet {
const CallParselet();
@override
int get precedence => 19;
@override
Expression parse(Parser parser, Expression left, Token token) {
List<Expression> arguments = [];
List<NamedArgument> namedArguments = [];
Expression argument = parser.parseExpression(0);
while (argument != null) {
arguments.add(argument);
if (!parser.next(TokenType.comma)) break;
parser.skipExtraneous(TokenType.comma);
argument = parser.parseExpression(0);
}
NamedArgument namedArgument = parser.parseNamedArgument();
while (namedArgument != null) {
namedArguments.add(namedArgument);
if (!parser.next(TokenType.comma)) break;
parser.skipExtraneous(TokenType.comma);
namedArgument = parser.parseNamedArgument();
}
if (!parser.next(TokenType.rParen)) {
var lastSpan = arguments.isEmpty ? null : arguments.last.span;
lastSpan ??= token.span;
parser.errors.add(JaelError(JaelErrorSeverity.error,
'Missing ")" after argument list.', lastSpan));
return null;
}
return Call(left, token, parser.current, arguments, namedArguments);
}
}
class IndexerParselet implements InfixParselet {
const IndexerParselet();
@override
int get precedence => 19;
@override
Expression parse(Parser parser, Expression left, Token token) {
var indexer = parser.parseExpression(0);
if (indexer == null) {
parser.errors.add(JaelError(
JaelErrorSeverity.error, 'Missing expression after "[".', left.span));
return null;
}
if (!parser.next(TokenType.rBracket)) {
parser.errors.add(
JaelError(JaelErrorSeverity.error, 'Missing "]".', indexer.span));
return null;
}
return IndexerExpression(left, token, indexer, parser.current);
}
}
class MemberParselet implements InfixParselet {
const MemberParselet();
@override
int get precedence => 19;
@override
Expression parse(Parser parser, Expression left, Token token) {
var name = parser.parseIdentifier();
if (name == null) {
parser.errors.add(JaelError(JaelErrorSeverity.error,
'Expected the name of a property following "."', token.span));
return null;
}
return MemberExpression(left, token, name);
}
}

View file

@ -0,0 +1,15 @@
library jael.src.text.parselet;
import '../../ast/ast.dart';
import '../parser.dart';
part 'infix.dart';
part 'prefix.dart';
abstract class PrefixParselet {
Expression parse(Parser parser, Token token);
}
abstract class InfixParselet {
int get precedence;
Expression parse(Parser parser, Expression left, Token token);
}

View file

@ -0,0 +1,159 @@
part of jael.src.text.parselet;
const Map<TokenType, PrefixParselet> prefixParselets = {
TokenType.exclamation: NotParselet(),
TokenType.$new: NewParselet(),
TokenType.number: NumberParselet(),
TokenType.hex: HexParselet(),
TokenType.string: StringParselet(),
TokenType.lCurly: MapParselet(),
TokenType.lBracket: ArrayParselet(),
TokenType.id: IdentifierParselet(),
TokenType.lParen: ParenthesisParselet(),
};
class NotParselet implements PrefixParselet {
const NotParselet();
@override
Expression parse(Parser parser, Token token) {
var expression = parser.parseExpression(0);
if (expression == null) {
parser.errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression after "!" in negation expression.', token.span));
}
return Negation(token, expression);
}
}
class NewParselet implements PrefixParselet {
const NewParselet();
@override
Expression parse(Parser parser, Token token) {
var call = parser.parseExpression(0);
if (call == null) {
parser.errors.add(JaelError(
JaelErrorSeverity.error,
'"new" must precede a call expression. Nothing was found.',
call.span));
return null;
} else if (call is Call) {
return NewExpression(token, call);
} else {
parser.errors.add(JaelError(
JaelErrorSeverity.error,
'"new" must precede a call expression, not a(n) ${call.runtimeType}.',
call.span));
return null;
}
}
}
class NumberParselet implements PrefixParselet {
const NumberParselet();
@override
Expression parse(Parser parser, Token token) => NumberLiteral(token);
}
class HexParselet implements PrefixParselet {
const HexParselet();
@override
Expression parse(Parser parser, Token token) => HexLiteral(token);
}
class StringParselet implements PrefixParselet {
const StringParselet();
@override
Expression parse(Parser parser, Token token) =>
StringLiteral(token, StringLiteral.parseValue(token));
}
class ArrayParselet implements PrefixParselet {
const ArrayParselet();
@override
Expression parse(Parser parser, Token token) {
List<Expression> items = [];
Expression item = parser.parseExpression(0);
while (item != null) {
items.add(item);
if (!parser.next(TokenType.comma)) break;
parser.skipExtraneous(TokenType.comma);
item = parser.parseExpression(0);
}
if (!parser.next(TokenType.rBracket)) {
var lastSpan = items.isEmpty ? null : items.last.span;
lastSpan ??= token.span;
parser.errors.add(JaelError(JaelErrorSeverity.error,
'Missing "]" to terminate array literal.', lastSpan));
return null;
}
return Array(token, parser.current, items);
}
}
class MapParselet implements PrefixParselet {
const MapParselet();
@override
Expression parse(Parser parser, Token token) {
var pairs = <KeyValuePair>[];
var pair = parser.parseKeyValuePair();
while (pair != null) {
pairs.add(pair);
if (!parser.next(TokenType.comma)) break;
parser.skipExtraneous(TokenType.comma);
pair = parser.parseKeyValuePair();
}
if (!parser.next(TokenType.rCurly)) {
var lastSpan = pairs.isEmpty ? token.span : pairs.last.span;
parser.errors.add(JaelError(
JaelErrorSeverity.error, 'Missing "}" in map literal.', lastSpan));
return null;
}
return MapLiteral(token, pairs, parser.current);
}
}
class IdentifierParselet implements PrefixParselet {
const IdentifierParselet();
@override
Expression parse(Parser parser, Token token) => Identifier(token);
}
class ParenthesisParselet implements PrefixParselet {
const ParenthesisParselet();
@override
Expression parse(Parser parser, Token token) {
var expression = parser.parseExpression(0);
if (expression == null) {
parser.errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression after "(".', token.span));
return null;
}
if (!parser.next(TokenType.rParen)) {
parser.errors.add(
JaelError(JaelErrorSeverity.error, 'Missing ")".', expression.span));
return null;
}
return expression;
}
}

View file

@ -0,0 +1,415 @@
import '../ast/ast.dart';
import 'parselet/parselet.dart';
import 'scanner.dart';
class Parser {
final List<JaelError> errors = [];
final Scanner scanner;
final bool asDSX;
Token _current;
int _index = -1;
Parser(this.scanner, {this.asDSX = false});
Token get current => _current;
int _nextPrecedence() {
var tok = peek();
if (tok == null) return 0;
var parser = infixParselets[tok.type];
return parser?.precedence ?? 0;
}
bool next(TokenType type) {
if (_index >= scanner.tokens.length - 1) return false;
var peek = scanner.tokens[_index + 1];
if (peek.type != type) return false;
_current = peek;
_index++;
return true;
}
Token peek() {
if (_index >= scanner.tokens.length - 1) return null;
return scanner.tokens[_index + 1];
}
Token maybe(TokenType type) => next(type) ? _current : null;
void skipExtraneous(TokenType type) {
while (next(type)) {
// Skip...
}
}
Document parseDocument() {
var doctype = parseDoctype();
if (doctype == null) {
var root = parseElement();
if (root == null) return null;
return Document(null, root);
}
var root = parseElement();
if (root == null) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing root element after !DOCTYPE declaration.', doctype.span));
return null;
}
return Document(doctype, root);
}
StringLiteral implicitString() {
if (next(TokenType.string)) {
return prefixParselets[TokenType.string].parse(this, _current)
as StringLiteral;
}
/*else if (next(TokenType.text)) {
}*/
return null;
}
Doctype parseDoctype() {
if (!next(TokenType.lt)) return null;
var lt = _current;
if (!next(TokenType.doctype)) {
_index--;
return null;
}
var doctype = _current, html = parseIdentifier();
if (html?.span?.text?.toLowerCase() != 'html') {
errors.add(JaelError(
JaelErrorSeverity.error,
'Expected "html" in doctype declaration.',
html?.span ?? doctype.span));
return null;
}
var public = parseIdentifier();
if (public == null) {
if (!next(TokenType.gt)) {
errors.add(JaelError(JaelErrorSeverity.error,
'Expected ">" in doctype declaration.', html.span));
return null;
}
return Doctype(lt, doctype, html, null, null, null, _current);
}
if (public?.span?.text?.toLowerCase() != 'public') {
errors.add(JaelError(
JaelErrorSeverity.error,
'Expected "public" in doctype declaration.',
public?.span ?? html.span));
return null;
}
var stringParser = prefixParselets[TokenType.string];
if (!next(TokenType.string)) {
errors.add(JaelError(JaelErrorSeverity.error,
'Expected string in doctype declaration.', public.span));
return null;
}
var name = stringParser.parse(this, _current) as StringLiteral;
if (!next(TokenType.string)) {
errors.add(JaelError(JaelErrorSeverity.error,
'Expected string in doctype declaration.', name.span));
return null;
}
var url = stringParser.parse(this, _current) as StringLiteral;
if (!next(TokenType.gt)) {
errors.add(JaelError(JaelErrorSeverity.error,
'Expected ">" in doctype declaration.', url.span));
return null;
}
return Doctype(lt, doctype, html, public, name, url, _current);
}
ElementChild parseElementChild() =>
parseHtmlComment() ??
parseInterpolation() ??
parseText() ??
parseElement();
HtmlComment parseHtmlComment() =>
next(TokenType.htmlComment) ? HtmlComment(_current) : null;
Text parseText() => next(TokenType.text) ? Text(_current) : null;
Interpolation parseInterpolation() {
if (!next(asDSX ? TokenType.lCurly : TokenType.lDoubleCurly)) return null;
var doubleCurlyL = _current;
var expression = parseExpression(0);
if (expression == null) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression in interpolation.', doubleCurlyL.span));
return null;
}
if (!next(asDSX ? TokenType.rCurly : TokenType.rDoubleCurly)) {
var expected = asDSX ? '}' : '}}';
errors.add(JaelError(JaelErrorSeverity.error,
'Missing closing "$expected" in interpolation.', expression.span));
return null;
}
return Interpolation(doubleCurlyL, expression, _current);
}
Element parseElement() {
if (!next(TokenType.lt)) return null;
var lt = _current;
if (next(TokenType.slash)) {
// We entered a closing tag, don't keep reading...
_index -= 2;
return null;
}
var tagName = parseIdentifier();
if (tagName == null) {
errors.add(
JaelError(JaelErrorSeverity.error, 'Missing tag name.', lt.span));
return null;
}
List<Attribute> attributes = [];
var attribute = parseAttribute();
while (attribute != null) {
attributes.add(attribute);
attribute = parseAttribute();
}
if (next(TokenType.slash)) {
// Try for self-closing...
var slash = _current;
if (!next(TokenType.gt)) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing ">" in self-closing "${tagName.name}" tag.', slash.span));
return null;
}
return SelfClosingElement(lt, tagName, attributes, slash, _current);
}
if (!next(TokenType.gt)) {
errors.add(JaelError(
JaelErrorSeverity.error,
'Missing ">" in "${tagName.name}" tag.',
attributes.isEmpty ? tagName.span : attributes.last.span));
return null;
}
var gt = _current;
// Implicit self-closing
if (Element.selfClosing.contains(tagName.name)) {
return SelfClosingElement(lt, tagName, attributes, null, gt);
}
List<ElementChild> children = [];
var child = parseElementChild();
while (child != null) {
// if (child is! HtmlComment) children.add(child);
children.add(child);
child = parseElementChild();
}
// Parse closing tag
if (!next(TokenType.lt)) {
errors.add(JaelError(
JaelErrorSeverity.error,
'Missing closing tag for "${tagName.name}" tag.',
children.isEmpty ? tagName.span : children.last.span));
return null;
}
var lt2 = _current;
if (!next(TokenType.slash)) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing "/" in "${tagName.name}" closing tag.', lt2.span));
return null;
}
var slash = _current, tagName2 = parseIdentifier();
if (tagName2 == null) {
errors.add(JaelError(
JaelErrorSeverity.error,
'Missing "${tagName.name}" in "${tagName.name}" closing tag.',
slash.span));
return null;
}
if (tagName2.name != tagName.name) {
errors.add(JaelError(
JaelErrorSeverity.error,
'Mismatched closing tags. Expected "${tagName.span.text}"; got "${tagName2.name}" instead.',
lt2.span));
return null;
}
if (!next(TokenType.gt)) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing ">" in "${tagName.name}" closing tag.', tagName2.span));
return null;
}
return RegularElement(
lt, tagName, attributes, gt, children, lt2, slash, tagName2, _current);
}
Attribute parseAttribute() {
Identifier id;
StringLiteral string;
if ((id = parseIdentifier()) != null) {
// Nothing
} else if (next(TokenType.string)) {
string = StringLiteral(_current, StringLiteral.parseValue(_current));
} else {
return null;
}
Token equals, nequ;
if (next(TokenType.equals)) {
equals = _current;
} else if (!asDSX && next(TokenType.nequ)) {
nequ = _current;
} else {
return Attribute(id, string, null, null, null);
}
if (!asDSX) {
var value = parseExpression(0);
if (value == null) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression in attribute.', equals?.span ?? nequ.span));
return null;
}
return Attribute(id, string, equals, nequ, value);
} else {
// Find either a string, or an interpolation.
var value = implicitString();
if (value != null) {
return Attribute(id, string, equals, nequ, value);
}
var interpolation = parseInterpolation();
if (interpolation != null) {
return Attribute(id, string, equals, nequ, interpolation.expression);
}
errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression in attribute.', equals?.span ?? nequ.span));
return null;
}
}
Expression parseExpression(int precedence) {
// Only consume a token if it could potentially be a prefix parselet
for (var type in prefixParselets.keys) {
if (next(type)) {
var left = prefixParselets[type].parse(this, _current);
while (precedence < _nextPrecedence()) {
_current = scanner.tokens[++_index];
if (_current.type == TokenType.slash &&
peek()?.type == TokenType.gt) {
// Handle `/>`
//
// Don't register this as an infix expression.
// Instead, backtrack, and return the current expression.
_index--;
return left;
}
var infix = infixParselets[_current.type];
var newLeft = infix.parse(this, left, _current);
if (newLeft == null) {
if (_current.type == TokenType.gt) _index--;
return left;
}
left = newLeft;
}
return left;
}
}
// Nothing was parsed; return null.
return null;
}
Identifier parseIdentifier() =>
next(TokenType.id) ? Identifier(_current) : null;
KeyValuePair parseKeyValuePair() {
var key = parseExpression(0);
if (key == null) return null;
if (!next(TokenType.colon)) return KeyValuePair(key, null, null);
var colon = _current, value = parseExpression(0);
if (value == null) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression in key-value pair.', colon.span));
return null;
}
return KeyValuePair(key, colon, value);
}
NamedArgument parseNamedArgument() {
var name = parseIdentifier();
if (name == null) return null;
if (!next(TokenType.colon)) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing ":" in named argument.', name.span));
return null;
}
var colon = _current, value = parseExpression(0);
if (value == null) {
errors.add(JaelError(JaelErrorSeverity.error,
'Missing expression in named argument.', colon.span));
return null;
}
return NamedArgument(name, colon, value);
}
}

View file

@ -0,0 +1,267 @@
import 'dart:collection';
import 'package:charcode/ascii.dart';
import 'package:string_scanner/string_scanner.dart';
import '../ast/ast.dart';
final RegExp _whitespace = RegExp(r'[ \n\r\t]+');
final RegExp _id =
RegExp(r'@?(([A-Za-z][A-Za-z0-9_]*-)*([A-Za-z][A-Za-z0-9_]*))');
final RegExp _string1 = RegExp(
r"'((\\(['\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^'\\]))*'");
final RegExp _string2 = RegExp(
r'"((\\(["\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^"\\]))*"');
Scanner scan(String text, {sourceUrl, bool asDSX = false}) =>
_Scanner(text, sourceUrl)..scan(asDSX: asDSX);
abstract class Scanner {
List<JaelError> get errors;
List<Token> get tokens;
}
final RegExp _htmlComment = RegExp(r'<!--[^$]*-->');
final Map<Pattern, TokenType> _expressionPatterns = {
//final Map<Pattern, TokenType> _htmlPatterns = {
'{{': TokenType.lDoubleCurly,
'{{-': TokenType.lDoubleCurly,
//
_htmlComment: TokenType.htmlComment,
'!DOCTYPE': TokenType.doctype,
'!doctype': TokenType.doctype,
'<': TokenType.lt,
'>': TokenType.gt,
'/': TokenType.slash,
'=': TokenType.equals,
'!=': TokenType.nequ,
_string1: TokenType.string,
_string2: TokenType.string,
_id: TokenType.id,
//};
//final Map<Pattern, TokenType> _expressionPatterns = {
'}}': TokenType.rDoubleCurly,
// Keywords
'new': TokenType.$new,
// Misc.
'*': TokenType.asterisk,
':': TokenType.colon,
',': TokenType.comma,
'.': TokenType.dot,
'??': TokenType.elvis,
'?.': TokenType.elvis_dot,
'=': TokenType.equals,
'!': TokenType.exclamation,
'-': TokenType.minus,
'%': TokenType.percent,
'+': TokenType.plus,
'[': TokenType.lBracket,
']': TokenType.rBracket,
'{': TokenType.lCurly,
'}': TokenType.rCurly,
'(': TokenType.lParen,
')': TokenType.rParen,
'/': TokenType.slash,
'<': TokenType.lt,
'<=': TokenType.lte,
'>': TokenType.gt,
'>=': TokenType.gte,
'==': TokenType.equ,
'!=': TokenType.nequ,
'=': TokenType.equals,
RegExp(r'-?[0-9]+(\.[0-9]+)?([Ee][0-9]+)?'): TokenType.number,
RegExp(r'0x[A-Fa-f0-9]+'): TokenType.hex,
_string1: TokenType.string,
_string2: TokenType.string,
_id: TokenType.id,
};
class _Scanner implements Scanner {
final List<JaelError> errors = [];
final List<Token> tokens = [];
_ScannerState state = _ScannerState.html;
final Queue<String> openTags = Queue();
SpanScanner _scanner;
_Scanner(String text, sourceUrl) {
_scanner = SpanScanner(text, sourceUrl: sourceUrl);
}
void scan({bool asDSX = false}) {
while (!_scanner.isDone) {
if (state == _ScannerState.html) {
scanHtml(asDSX);
} else if (state == _ScannerState.freeText) {
// Just keep parsing until we hit "</"
var start = _scanner.state, end = start;
while (!_scanner.isDone) {
// Skip through comments
if (_scanner.scan(_htmlComment)) continue;
// Break on {{ or {
if (_scanner.matches(asDSX ? '{' : '{{')) {
state = _ScannerState.html;
//_scanner.position--;
break;
}
var ch = _scanner.readChar();
if (ch == $lt) {
// && !_scanner.isDone) {
if (_scanner.matches('/')) {
// If we reached "</", backtrack and break into HTML
openTags.removeFirst();
_scanner.position--;
state = _ScannerState.html;
break;
} else if (_scanner.matches(_id)) {
// Also break when we reach <foo.
//
// HOWEVER, that is also JavaScript. So we must
// only break in this case when the current tag is NOT "script".
var shouldBreak =
(openTags.isEmpty || openTags.first != 'script');
if (!shouldBreak) {
// Try to see if we are closing a script tag
var replay = _scanner.state;
_scanner
..readChar()
..scan(_whitespace);
//print(_scanner.emptySpan.highlight());
if (_scanner.matches(_id)) {
//print(_scanner.lastMatch[0]);
shouldBreak = _scanner.lastMatch[0] == 'script';
_scanner.position--;
}
if (!shouldBreak) {
_scanner.state = replay;
}
}
if (shouldBreak) {
openTags.removeFirst();
_scanner.position--;
state = _ScannerState.html;
break;
}
}
}
// Otherwise, just add to the "buffer"
end = _scanner.state;
}
var span = _scanner.spanFrom(start, end);
if (span.text.isNotEmpty) {
tokens.add(Token(TokenType.text, span, null));
}
}
}
}
void scanHtml(bool asDSX) {
var brackets = Queue<Token>();
do {
// Only continue if we find a left bracket
if (true) {
// || _scanner.matches('<') || _scanner.matches('{{')) {
var potential = <Token>[];
while (true) {
// Scan whitespace
_scanner.scan(_whitespace);
_expressionPatterns.forEach((pattern, type) {
if (_scanner.matches(pattern)) {
potential.add(Token(type, _scanner.lastSpan, _scanner.lastMatch));
}
});
potential.sort((a, b) => b.span.length.compareTo(a.span.length));
if (potential.isEmpty) break;
var token = potential.first;
tokens.add(token);
_scanner.scan(token.span.text);
if (token.type == TokenType.lt) {
brackets.addFirst(token);
// Try to see if we are at a tag.
var replay = _scanner.state;
_scanner.scan(_whitespace);
if (_scanner.matches(_id)) {
openTags.addFirst(_scanner.lastMatch[0]);
} else {
_scanner.state = replay;
}
} else if (token.type == TokenType.slash) {
// Only push if we're at </foo
if (brackets.isNotEmpty && brackets.first.type == TokenType.lt) {
brackets
..removeFirst()
..addFirst(token);
}
} else if (token.type == TokenType.gt) {
// Only pop the bracket if we're at foo>, </foo> or foo/>
if (brackets.isNotEmpty && brackets.first.type == TokenType.slash) {
// </foo>
brackets.removeFirst();
// Now, ONLY continue parsing HTML if the next character is '<'.
var replay = _scanner.state;
_scanner.scan(_whitespace);
if (!_scanner.matches('<')) {
_scanner.state = replay;
state = _ScannerState.freeText;
break;
}
}
//else if (_scanner.matches('>')) brackets.removeFirst();
else if (brackets.isNotEmpty &&
brackets.first.type == TokenType.lt) {
// We're at foo>, try to parse text?
brackets.removeFirst();
var replay = _scanner.state;
_scanner.scan(_whitespace);
if (!_scanner.matches('<')) {
_scanner.state = replay;
state = _ScannerState.freeText;
break;
}
}
} else if (token.type ==
(asDSX ? TokenType.rCurly : TokenType.rDoubleCurly)) {
state = _ScannerState.freeText;
break;
}
potential.clear();
}
}
} while (brackets.isNotEmpty && !_scanner.isDone);
state = _ScannerState.freeText;
}
}
enum _ScannerState { html, freeText }

View file

View file

@ -0,0 +1,19 @@
name: jael
version: 2.0.2
description: A simple server-side HTML templating engine for Dart. Comparable to Blade or Liquid.
author: Tobe O <thosakwe@gmail.com>
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

View file

@ -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 = '''
<div>
<element name="square-root">
The square root of {{ n }} is {{ sqrt(n).toInt() }}.
</element>
<square-root @n=16 />
</div>
''';
var html = render(template, {'sqrt': sqrt});
print(html);
expect(
html,
'''
<div>
<div>
The square root of 16 is 4.
</div>
</div>
'''
.trim());
});
test('render into explicit tag name', () {
var template = '''
<div>
<element name="square-root">
The square root of {{ n }} is {{ sqrt(n).toInt() }}.
</element>
<square-root as="span" @n=16 />
</div>
''';
var html = render(template, {'sqrt': sqrt});
print(html);
expect(
html,
'''
<div>
<span>
The square root of 16 is 4.
</span>
</div>
'''
.trim());
});
test('pass attributes', () {
var template = '''
<div>
<element name="square-root">
The square root of {{ n }} is {{ sqrt(n).toInt() }}.
</element>
<square-root foo="bar" baz="quux" @n=16 />
</div>
''';
var html = render(template, {'sqrt': sqrt});
print(html);
expect(
html,
'''
<div>
<div foo="bar" baz="quux">
The square root of 16 is 4.
</div>
</div>
'''
.trim());
});
test('render without tag name', () {
var template = '''
<div>
<element name="square-root">
The square root of {{ n }} is {{ sqrt(n).toInt() }}.
</element>
<square-root as=false @n=16 />
</div>
''';
var html = render(template, {'sqrt': sqrt});
print(html);
expect(
html,
'''
<div>
The square root of 16 is 4.
</div>
'''
.trim());
});
}
String render(String template, [Map<String, dynamic> 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();
}

View file

@ -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('''
<foo bar="baz" yes={no} />
''');
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('''
<foo bar="baz" yes={no}>
<bar>{24 * 3}</bar>
</foo>
''');
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);
}

View file

@ -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 = '''
<html>
<body>
<h1>Hello</h1>
<img ready="always" data-img-src=profile['avatar'] />
<input name="csrf_token" type="hidden" value=csrf_token>
</body>
</html>
''';
var buf = CodeBuffer();
jael.Document document;
SymbolTable scope;
try {
document = jael.parseDocument(template, sourceUrl: 'test.jael');
scope = SymbolTable<dynamic>(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(),
'''
<html>
<body>
<h1>
Hello
</h1>
<img ready="always" data-img-src="thosakwe.png">
<input name="csrf_token" type="hidden" value="foo">
</body>
</html>
'''
.trim());
});
test('interpolation', () {
const template = '''
<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h1>Pokémon</h1>
{{ pokemon.name }} - {{ pokemon.type }}
<img>
</body>
</html>
''';
var buf = CodeBuffer();
//jael.scan(template, sourceUrl: 'test.jael').tokens.forEach(print);
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
var scope = SymbolTable<dynamic>(values: {
'pokemon': const _Pokemon('Darkrai', 'Dark'),
});
const jael.Renderer().render(document, buf, scope);
print(buf);
expect(
buf.toString().replaceAll('\n', '').replaceAll(' ', '').trim(),
'''
<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h1>
Pokémon
</h1>
Darkrai - Dark
<img/>
</body>
</html>
'''
.replaceAll('\n', '')
.replaceAll(' ', '')
.trim());
});
test('for loop', () {
const template = '''
<html>
<body>
<h1>Pokémon</h1>
<ul>
<li for-each=starters as="starter" index-as="idx">#{{ idx }} {{ starter.name }} - {{ starter.type }}</li>
</ul>
</body>
</html>
''';
var buf = CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
var scope = SymbolTable<dynamic>(values: {
'starters': starters,
});
const jael.Renderer().render(document, buf, scope);
print(buf);
expect(
buf.toString(),
'''
<html>
<body>
<h1>
Pokémon
</h1>
<ul>
<li>
#0 Bulbasaur - Grass
</li>
<li>
#1 Charmander - Fire
</li>
<li>
#2 Squirtle - Water
</li>
</ul>
</body>
</html>
'''
.trim());
});
test('conditional', () {
const template = '''
<html>
<body>
<h1>Conditional</h1>
<b if=starters.isEmpty>Empty</b>
<b if=starters.isNotEmpty>Not empty</b>
</body>
</html>
''';
var buf = CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
var scope = SymbolTable<dynamic>(values: {
'starters': starters,
});
const jael.Renderer().render(document, buf, scope);
print(buf);
expect(
buf.toString(),
'''
<html>
<body>
<h1>
Conditional
</h1>
<b>
Not empty
</b>
</body>
</html>
'''
.trim());
});
test('declare', () {
const template = '''
<div>
<declare one=1 two=2 three=3>
<ul>
<li>{{one}}</li>
<li>{{two}}</li>
<li>{{three}}</li>
</ul>
<ul>
<declare three=4>
<li>{{one}}</li>
<li>{{two}}</li>
<li>{{three}}</li>
</declare>
</ul>
</declare>
</div>
''';
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(),
'''
<div>
<ul>
<li>
1
</li>
<li>
2
</li>
<li>
3
</li>
</ul>
<ul>
<li>
1
</li>
<li>
2
</li>
<li>
4
</li>
</ul>
</div>
'''
.trim());
});
test('unescaped attr/interp', () {
const template = '''
<div>
<img src!="<SCARY XSS>" />
{{- "<MORE SCARY XSS>" }}
</div>
''';
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(),
'''
<div>
<img src="<SCARY XSS>">
<MORE SCARY XSS>
</div>
'''
.replaceAll('\n', '')
.replaceAll(' ', '')
.trim());
});
test('quoted attribute name', () {
const template = '''
<button '(click)'="myEventHandler(\$event)"></button>
''';
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(),
'''
<button (click)="myEventHandler(\$event)">
</button>
'''
.trim());
});
test('switch', () {
const template = '''
<switch value=account.isDisabled>
<case value=true>
BAN HAMMER LOLOL
</case>
<case value=false>
You are in good standing.
</case>
<default>
Weird...
</default>
</switch>
''';
var buf = CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
var scope = SymbolTable<dynamic>(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 = '''
<switch value=account.isDisabled>
<case value=true>
BAN HAMMER LOLOL
</case>
<case value=false>
You are in good standing.
</case>
<default>
Weird...
</default>
</switch>
''';
var buf = CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
var scope = SymbolTable<dynamic>(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});
}

View file

@ -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);
}
}

View file

@ -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('<img src="foo.png" />', 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('<p>It\'s lit</p>', 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('<p>Hello\nworld</p>', 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('<ul number=1 + 2>three{{four > five.six}}</ul>',
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(
'''
<script aria-label="script">
window.alert('a string');
</script>
'''
.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));
});
}

View file

@ -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

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -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<String> 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;
}
}

View file

@ -0,0 +1 @@
export 'src/server.dart';

View file

@ -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 = <JaelError>[];
var _scope = new SymbolTable<JaelObject>();
var allDefinitions = <Variable<JaelObject>>[];
SymbolTable<JaelObject> get parentScope =>
_scope.isRoot ? _scope : _scope.parent;
SymbolTable<JaelObject> 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 <block> at ${element.span.start.toolString}');
} else if (element.tagName.name == 'case') {
ensureAttributeIsPresent(element, 'value');
//logger.info('Found <case> 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 <extend> at ${element.span.start.toolString}');
}
} else if (element is SelfClosingElement) {
if (element.tagName.name == 'include') {
//logger.info('Found <include> 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;
}
}

View file

@ -0,0 +1,32 @@
import 'dart:collection';
import 'package:source_span/source_span.dart';
abstract class JaelObject {
final FileSpan span;
final usages = <SymbolUsage>[];
String get name;
JaelObject(this.span);
}
class JaelCustomElement extends JaelObject {
final String name;
final attributes = new SplayTreeSet<String>();
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 }

View file

@ -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<Diagnostics>();
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<ApplyWorkspaceEditParams>();
@override
Stream<Diagnostics> get diagnostics => _diagnostics.stream;
@override
Future<void> get onDone => _done.future;
@override
Stream<ApplyWorkspaceEditParams> get workspaceEdits => _workspaceEdits.stream;
@override
Future<void> 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<ServerCapabilities> 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<List<int>>()
.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<File> 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<List<int>>()
.pipe(file.openWrite());
logger.info('Opened Jael file ${file.path}');
}
return file;
}
Future<Scanner> scannerForId(TextDocumentIdentifier documentId) async {
var file = await fileForId(documentId);
return scan(await file.readAsString(), sourceUrl: file.uri);
}
Future<Analyzer> 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<JaelObject> 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</name>';
});
});
} else if (value is JaelVariable) {
return new CompletionItem((b) {
b
..kind = CompletionItemKind.variable
..label = symbol.name;
});
}
return null;
}
void emitDiagnostics(String uri, Iterable<Diagnostic> 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<TextDocumentContentChangeEvent> 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<List> textDocumentCodeAction(TextDocumentIdentifier documentId,
Range range, CodeActionContext context) async {
// TODO: implement textDocumentCodeAction
return [];
}
@override
Future<CompletionList> 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<String> 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<JaelObject> 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<Location> 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<List<DocumentHighlight>> 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<Hover> 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<List<Location>> textDocumentImplementation(
TextDocumentIdentifier documentId, Position position) async {
var defn = await textDocumentDefinition(documentId, position);
return defn == null ? [] : [defn];
}
@override
Future<List<Location>> 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<WorkspaceEdit> 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<List<SymbolInformation>> 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<void> workspaceExecuteCommand(String command, List arguments) async {
// TODO: implement workspaceExecuteCommand
}
@override
Future<List<SymbolInformation>> workspaceSymbol(String query) async {
var values = <JaelObject>[];
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<List<TextEdit>> textDocumentFormatting(
TextDocumentIdentifier documentId,
FormattingOptions formattingOptions) async {
try {
var errors = <JaelError>[];
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<ShowMessageParams> 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);
}
}

View file

@ -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 <thosakwe@gmail.com>
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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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`.**

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -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<jael.Document> 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;
},
]);
}

View file

@ -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<Document> 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<Document> resolve(Document document, Directory currentDirectory,
{void onError(JaelError error), Iterable<Patcher> 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<Document> applyInheritance(Document document, Directory currentDirectory,
void onError(JaelError error), Iterable<Patcher> 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 <extend src="..."> templates.
// To produce an accurate representation, we need to:
// 1. Find the root template, and store a copy in a variable.
// 2: For each <extend> 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 <extend>, 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<String, RegularElement> definedOverrides,
bool anyTemplatesRemain) {
var children = <ElementChild>[];
// 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<String, RegularElement> findBlockOverrides(
Element tmpl, void onError(JaelError e)) {
var out = <String, RegularElement>{};
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<DocumentHierarchy> resolveHierarchy(Document document,
Directory currentDirectory, void onError(JaelError e)) async {
var extendsTemplates = new Queue<Element>();
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<Element> extendsTemplates; // FIFO
DocumentHierarchy(this.rootDocument, this.extendsTemplates);
Element get root => rootDocument.root;
}
Iterable<ElementChild> replaceBlocks(
Element element,
Map<String, RegularElement> 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 <block> 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<String, RegularElement> 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<String, RegularElement> 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<ElementChild> allChildrenOfRegularElement(
RegularElement el,
Map<String, RegularElement> definedOverrides,
void onError(JaelError e),
bool replaceWithDefault,
bool anyTemplatesRemain) {
var children = <ElementChild>[];
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<Document> resolveIncludes(Document document, Directory currentDirectory,
void onError(JaelError error)) async {
return new Document(document.doctype,
await _expandIncludes(document.root, currentDirectory, onError));
}
Future<Element> _expandIncludes(Element element, Directory currentDirectory,
void onError(JaelError error)) async {
if (element.tagName.name != 'include') {
if (element is SelfClosingElement)
return element;
else if (element is RegularElement) {
List<ElementChild> expanded = [];
for (var child in element.children) {
if (child is Element) {
expanded.add(await _expandIncludes(child, currentDirectory, onError));
} else {
expanded.add(child);
}
}
return new RegularElement(
element.lt,
element.tagName,
element.attributes,
element.gt,
expanded,
element.lt2,
element.slash,
element.tagName2,
element.gt2);
} else {
throw new UnsupportedError(
'Unsupported element type: ${element.runtimeType}');
}
}
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;
}
}

View file

@ -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 <thosakwe@gmail.com>
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

View file

@ -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('<b>a.jl</b>');
// b.jl
fileSystem.file('b.jl').writeAsStringSync(
'<i><include src="a.jl" /><block name="greeting"><p>Hello</p></block></i>');
// c.jl
// NOTE: This SHOULD NOT produce "yes", because the only children expanded within an <extend>
// are <block>s.
fileSystem.file('c.jl').writeAsStringSync(
'<extend src="b.jl"><block name="greeting">Goodbye</block>Yes</extend>');
// 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(
'<extend src="c.jl"><block name="greeting">Saluton!</block>Yes</extend>');
// e.jl
fileSystem.file('e.jl').writeAsStringSync(
'<extend src="c.jl"><block name="greeting">Angel <b><block name="name">default</block></b></block></extend>');
// fox.jl
fileSystem.file('fox.jl').writeAsStringSync(
'<div><block name="dance">The name is <block name="name">default-name</block></block></div>');
// trot.jl
fileSystem.file('trot.jl').writeAsStringSync(
'<extend src="fox.jl"><block name="name">CONGA <i><block name="exclaim">YEAH</block></i></block></extend>');
// foxtrot.jl
fileSystem.file('foxtrot.jl').writeAsStringSync(
'<extend src="trot.jl"><block name="exclaim">framework</block></extend>');
});
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(),
'''
<i>
<b>
a.jl
</b>
Goodbye
</i>
'''
.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(),
'''
<i>
<b>
a.jl
</b>
<p>
Hello
</p>
</i>
'''
.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(),
'''
<i>
<b>
a.jl
</b>
Goodbye
</i>
'''
.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(),
'''
<div>
The name is CONGA
<i>
framework
</i>
</div>
'''
.trim());
});
}

View file

@ -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('<b>a.jl</b>');
// b.jl
fileSystem.file('b.jl').writeAsStringSync('<i><include src="a.jl"></i>');
// c.jl
fileSystem.file('c.jl').writeAsStringSync('<u><include src="b.jl"></u>');
});
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(),
'''
<u>
<i>
<b>
a.jl
</b>
</i>
</u>
'''
.trim());
});
}

16
packages/jael/jael_web/.gitignore vendored Normal file
View file

@ -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

View file

@ -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.

View file

@ -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.**

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -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"]

View file

@ -0,0 +1,30 @@
import 'package:jael_web/jael_web.dart';
import 'package:jael_web/elements.dart';
part 'main.g.dart';
@Jael(template: '''
<div>
<h1>Hello, Jael!</h1>
<i>Current time: {{now}}</i>
</div>
''')
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'),
]),
]);
}
}

View file

@ -0,0 +1,18 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// JaelComponentGenerator
// **************************************************************************
abstract class _HelloJaelTemplate implements Component<dynamic> {
DateTime get now;
@override
DomNode render() {
return h('div', {}, [
h('h1', {}, [text('Hello, Jael!')]),
h('i', {}, [text('Current time: '), text(now.toString())])
]);
}
}

View file

@ -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: '<div>Tick count: {{state.ticks}}</div>')
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();
}
}

View file

@ -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())]);
}
}

View file

@ -0,0 +1,25 @@
import 'package:jael_web/jael_web.dart';
part 'using_components.g.dart';
@Jael(template: '''
<div>
<h1>Welcome to my app</h1>
<LabeledInput name="username" />
</div>
''')
class MyApp extends Component with _MyAppJaelTemplate {}
@Jael(template: '''
<div>
<label>
<b>{{name}}:</b>
</label>
<br>
<input name=name placeholder="Enter " + name + "..." type="text">
</div>
''')
class LabeledInput extends Component with _LabeledInputJaelTemplate {
final String name;
LabeledInput({this.name});
}

View file

@ -0,0 +1,35 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'using_components.dart';
// **************************************************************************
// JaelComponentGenerator
// **************************************************************************
abstract class _MyAppJaelTemplate implements Component<dynamic> {
@override
DomNode render() {
return h('div', {}, [
h('h1', {}, [text('Welcome to my app')]),
LabeledInput(name: "username")
]);
}
}
abstract class _LabeledInputJaelTemplate implements Component<dynamic> {
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"
}, [])
]);
}
}

View file

@ -0,0 +1 @@
export 'src/builder/builder.dart';

View file

@ -0,0 +1 @@
export 'src/elements.dart';

Some files were not shown because too many files have changed in this diff Show more