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