Add 'packages/jael/' from commit 'af168281d94cda98a8fd333618696e92f4e035c5'
git-subtree-dir: packages/jael git-subtree-mainline:834de0300f
git-subtree-split:af168281d9
This commit is contained in:
commit
edfd785dfe
113 changed files with 8435 additions and 0 deletions
51
packages/jael/.gitignore
vendored
Normal file
51
packages/jael/.gitignore
vendored
Normal 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
|
6
packages/jael/.idea/misc.xml
Normal file
6
packages/jael/.idea/misc.xml
Normal 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>
|
8
packages/jael/.idea/modules.xml
Normal file
8
packages/jael/.idea/modules.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
7
packages/jael/.idea/runConfigurations/jael__example.xml
Normal file
7
packages/jael/.idea/runConfigurations/jael__example.xml
Normal 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>
|
7
packages/jael/.idea/runConfigurations/main_dart.xml
Normal file
7
packages/jael/.idea/runConfigurations/main_dart.xml
Normal 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>
|
|
@ -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>
|
8
packages/jael/.idea/runConfigurations/tests_in_jael.xml
Normal file
8
packages/jael/.idea/runConfigurations/tests_in_jael.xml
Normal 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>
|
6
packages/jael/.idea/vcs.xml
Normal file
6
packages/jael/.idea/vcs.xml
Normal 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>
|
2
packages/jael/.travis.yml
Normal file
2
packages/jael/.travis.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
language: dart
|
||||||
|
script: bash ./travis.sh
|
21
packages/jael/LICENSE
Normal file
21
packages/jael/LICENSE
Normal 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
34
packages/jael/README.md
Normal 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
16
packages/jael/angel_jael/.gitignore
vendored
Normal 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
|
13
packages/jael/angel_jael/CHANGELOG.md
Normal file
13
packages/jael/angel_jael/CHANGELOG.md
Normal 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`.
|
21
packages/jael/angel_jael/LICENSE
Normal file
21
packages/jael/angel_jael/LICENSE
Normal 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.
|
83
packages/jael/angel_jael/README.md
Normal file
83
packages/jael/angel_jael/README.md
Normal 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`.
|
3
packages/jael/angel_jael/analysis_options.yaml
Normal file
3
packages/jael/angel_jael/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
44
packages/jael/angel_jael/example/main.dart
Normal file
44
packages/jael/angel_jael/example/main.dart
Normal 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}');
|
||||||
|
}
|
15
packages/jael/angel_jael/example/views/index.jael
Normal file
15
packages/jael/angel_jael/example/views/index.jael
Normal 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>
|
19
packages/jael/angel_jael/example/views/layout.jael
Normal file
19
packages/jael/angel_jael/example/views/layout.jael
Normal 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>
|
71
packages/jael/angel_jael/lib/angel_jael.dart
Normal file
71
packages/jael/angel_jael/lib/angel_jael.dart
Normal 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();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
0
packages/jael/angel_jael/mono_pkg.yaml
Normal file
0
packages/jael/angel_jael/mono_pkg.yaml
Normal file
19
packages/jael/angel_jael/pubspec.yaml
Normal file
19
packages/jael/angel_jael/pubspec.yaml
Normal 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
|
84
packages/jael/angel_jael/test/all_test.dart
Normal file
84
packages/jael/angel_jael/test/all_test.dart
Normal 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
39
packages/jael/jael.iml
Normal 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
16
packages/jael/jael/.gitignore
vendored
Normal 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
|
42
packages/jael/jael/CHANGELOG.md
Normal file
42
packages/jael/jael/CHANGELOG.md
Normal 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.
|
21
packages/jael/jael/LICENSE
Normal file
21
packages/jael/jael/LICENSE
Normal 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.
|
50
packages/jael/jael/README.md
Normal file
50
packages/jael/jael/README.md
Normal 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.`.
|
4
packages/jael/jael/analysis_options.yaml
Normal file
4
packages/jael/jael/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
include: package:pedantic/analysis_options.yaml
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
124
packages/jael/jael/bin/jaelfmt.dart
Normal file
124
packages/jael/jael/bin/jaelfmt.dart
Normal 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);
|
||||||
|
}
|
37
packages/jael/jael/example/main.dart
Normal file
37
packages/jael/jael/example/main.dart
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
packages/jael/jael/lib/jael.dart
Normal file
5
packages/jael/jael/lib/jael.dart
Normal 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';
|
41
packages/jael/jael/lib/src/ast/array.dart
Normal file
41
packages/jael/jael/lib/src/ast/array.dart
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
18
packages/jael/jael/lib/src/ast/ast.dart
Normal file
18
packages/jael/jael/lib/src/ast/ast.dart
Normal 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';
|
5
packages/jael/jael/lib/src/ast/ast_node.dart
Normal file
5
packages/jael/jael/lib/src/ast/ast_node.dart
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
|
||||||
|
abstract class AstNode {
|
||||||
|
FileSpan get span;
|
||||||
|
}
|
27
packages/jael/jael/lib/src/ast/attribute.dart
Normal file
27
packages/jael/jael/lib/src/ast/attribute.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
47
packages/jael/jael/lib/src/ast/binary.dart
Normal file
47
packages/jael/jael/lib/src/ast/binary.dart
Normal 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);
|
||||||
|
}
|
56
packages/jael/jael/lib/src/ast/call.dart
Normal file
56
packages/jael/jael/lib/src/ast/call.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
31
packages/jael/jael/lib/src/ast/conditional.dart
Normal file
31
packages/jael/jael/lib/src/ast/conditional.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
60
packages/jael/jael/lib/src/ast/document.dart
Normal file
60
packages/jael/jael/lib/src/ast/document.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
96
packages/jael/jael/lib/src/ast/element.dart
Normal file
96
packages/jael/jael/lib/src/ast/element.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
21
packages/jael/jael/lib/src/ast/error.dart
Normal file
21
packages/jael/jael/lib/src/ast/error.dart
Normal 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,
|
||||||
|
}
|
33
packages/jael/jael/lib/src/ast/expression.dart
Normal file
33
packages/jael/jael/lib/src/ast/expression.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
47
packages/jael/jael/lib/src/ast/identifier.dart
Normal file
47
packages/jael/jael/lib/src/ast/identifier.dart
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
18
packages/jael/jael/lib/src/ast/interpolation.dart
Normal file
18
packages/jael/jael/lib/src/ast/interpolation.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
53
packages/jael/jael/lib/src/ast/map.dart
Normal file
53
packages/jael/jael/lib/src/ast/map.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
24
packages/jael/jael/lib/src/ast/member.dart
Normal file
24
packages/jael/jael/lib/src/ast/member.dart
Normal 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);
|
||||||
|
}
|
32
packages/jael/jael/lib/src/ast/new.dart
Normal file
32
packages/jael/jael/lib/src/ast/new.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
47
packages/jael/jael/lib/src/ast/number.dart
Normal file
47
packages/jael/jael/lib/src/ast/number.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
75
packages/jael/jael/lib/src/ast/string.dart
Normal file
75
packages/jael/jael/lib/src/ast/string.dart
Normal 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;
|
||||||
|
}
|
61
packages/jael/jael/lib/src/ast/token.dart
Normal file
61
packages/jael/jael/lib/src/ast/token.dart
Normal 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,
|
||||||
|
}
|
194
packages/jael/jael/lib/src/formatter.dart
Normal file
194
packages/jael/jael/lib/src/formatter.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
395
packages/jael/jael/lib/src/renderer.dart
Normal file
395
packages/jael/jael/lib/src/renderer.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
166
packages/jael/jael/lib/src/text/parselet/infix.dart
Normal file
166
packages/jael/jael/lib/src/text/parselet/infix.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
15
packages/jael/jael/lib/src/text/parselet/parselet.dart
Normal file
15
packages/jael/jael/lib/src/text/parselet/parselet.dart
Normal 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);
|
||||||
|
}
|
159
packages/jael/jael/lib/src/text/parselet/prefix.dart
Normal file
159
packages/jael/jael/lib/src/text/parselet/prefix.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
415
packages/jael/jael/lib/src/text/parser.dart
Normal file
415
packages/jael/jael/lib/src/text/parser.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
267
packages/jael/jael/lib/src/text/scanner.dart
Normal file
267
packages/jael/jael/lib/src/text/scanner.dart
Normal 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 }
|
0
packages/jael/jael/mono_pkg.yaml
Normal file
0
packages/jael/jael/mono_pkg.yaml
Normal file
19
packages/jael/jael/pubspec.yaml
Normal file
19
packages/jael/jael/pubspec.yaml
Normal 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
|
113
packages/jael/jael/test/render/custom_element_test.dart
Normal file
113
packages/jael/jael/test/render/custom_element_test.dart
Normal 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();
|
||||||
|
}
|
43
packages/jael/jael/test/render/dsx_test.dart
Normal file
43
packages/jael/jael/test/render/dsx_test.dart
Normal 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);
|
||||||
|
}
|
357
packages/jael/jael/test/render/render_test.dart
Normal file
357
packages/jael/jael/test/render/render_test.dart
Normal 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});
|
||||||
|
}
|
24
packages/jael/jael/test/text/common.dart
Normal file
24
packages/jael/jael/test/text/common.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
106
packages/jael/jael/test/text/scan_test.dart
Normal file
106
packages/jael/jael/test/text/scan_test.dart
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
21
packages/jael/jael_language_server/.gitignore
vendored
Normal file
21
packages/jael/jael_language_server/.gitignore
vendored
Normal 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
|
3
packages/jael/jael_language_server/analysis_options.yaml
Normal file
3
packages/jael/jael_language_server/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export 'src/server.dart';
|
153
packages/jael/jael_language_server/lib/src/analyzer.dart
Normal file
153
packages/jael/jael_language_server/lib/src/analyzer.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
32
packages/jael/jael_language_server/lib/src/object.dart
Normal file
32
packages/jael/jael_language_server/lib/src/object.dart
Normal 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 }
|
554
packages/jael/jael_language_server/lib/src/server.dart
Normal file
554
packages/jael/jael_language_server/lib/src/server.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
0
packages/jael/jael_language_server/mono_pkg.yaml
Normal file
0
packages/jael/jael_language_server/mono_pkg.yaml
Normal file
22
packages/jael/jael_language_server/pubspec.yaml
Normal file
22
packages/jael/jael_language_server/pubspec.yaml
Normal 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
|
15
packages/jael/jael_preprocessor/.gitignore
vendored
Normal file
15
packages/jael/jael_preprocessor/.gitignore
vendored
Normal 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
|
12
packages/jael/jael_preprocessor/CHANGELOG.md
Normal file
12
packages/jael/jael_preprocessor/CHANGELOG.md
Normal 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.
|
21
packages/jael/jael_preprocessor/LICENSE
Normal file
21
packages/jael/jael_preprocessor/LICENSE
Normal 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.
|
49
packages/jael/jael_preprocessor/README.md
Normal file
49
packages/jael/jael_preprocessor/README.md
Normal 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`.**
|
3
packages/jael/jael_preprocessor/analysis_options.yaml
Normal file
3
packages/jael/jael_preprocessor/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
15
packages/jael/jael_preprocessor/example/main.dart
Normal file
15
packages/jael/jael_preprocessor/example/main.dart
Normal 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;
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
355
packages/jael/jael_preprocessor/lib/jael_preprocessor.dart
Normal file
355
packages/jael/jael_preprocessor/lib/jael_preprocessor.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
0
packages/jael/jael_preprocessor/mono_pkg.yaml
Normal file
0
packages/jael/jael_preprocessor/mono_pkg.yaml
Normal file
14
packages/jael/jael_preprocessor/pubspec.yaml
Normal file
14
packages/jael/jael_preprocessor/pubspec.yaml
Normal 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
|
153
packages/jael/jael_preprocessor/test/block_test.dart
Normal file
153
packages/jael/jael_preprocessor/test/block_test.dart
Normal 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());
|
||||||
|
});
|
||||||
|
}
|
49
packages/jael/jael_preprocessor/test/include_test.dart
Normal file
49
packages/jael/jael_preprocessor/test/include_test.dart
Normal 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
16
packages/jael/jael_web/.gitignore
vendored
Normal 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
|
21
packages/jael/jael_web/LICENSE
Normal file
21
packages/jael/jael_web/LICENSE
Normal 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.
|
6
packages/jael/jael_web/README.md
Normal file
6
packages/jael/jael_web/README.md
Normal 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.**
|
3
packages/jael/jael_web/analysis_options.yaml
Normal file
3
packages/jael/jael_web/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
10
packages/jael/jael_web/build.yaml
Normal file
10
packages/jael/jael_web/build.yaml
Normal 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"]
|
30
packages/jael/jael_web/example/main.dart
Normal file
30
packages/jael/jael_web/example/main.dart
Normal 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'),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
18
packages/jael/jael_web/example/main.g.dart
Normal file
18
packages/jael/jael_web/example/main.g.dart
Normal 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())])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
32
packages/jael/jael_web/example/stateful.dart
Normal file
32
packages/jael/jael_web/example/stateful.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
16
packages/jael/jael_web/example/stateful.g.dart
Normal file
16
packages/jael/jael_web/example/stateful.g.dart
Normal 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())]);
|
||||||
|
}
|
||||||
|
}
|
25
packages/jael/jael_web/example/using_components.dart
Normal file
25
packages/jael/jael_web/example/using_components.dart
Normal 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});
|
||||||
|
}
|
35
packages/jael/jael_web/example/using_components.g.dart
Normal file
35
packages/jael/jael_web/example/using_components.g.dart
Normal 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"
|
||||||
|
}, [])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
1
packages/jael/jael_web/lib/builder.dart
Normal file
1
packages/jael/jael_web/lib/builder.dart
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export 'src/builder/builder.dart';
|
1
packages/jael/jael_web/lib/elements.dart
Normal file
1
packages/jael/jael_web/lib/elements.dart
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export 'src/elements.dart';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue