Jael formatter + 2.0.2
This commit is contained in:
parent
8f0f5db53d
commit
bf5c4084b4
11 changed files with 337 additions and 29 deletions
|
@ -1,6 +1,6 @@
|
|||
# 2.1.0
|
||||
# 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`.
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
A simple server-side HTML templating engine for Dart.
|
||||
|
||||
[See documentation.](https://angel-dart.gitbook.io/angel/front-end/jael)
|
||||
[See documentation.](https://docs.angel-dart.dev/packages/front-end/jael)
|
||||
|
||||
# Installation
|
||||
In your `pubspec.yaml`:
|
||||
|
@ -34,7 +34,7 @@ void myFunction() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl', asDSX: false);
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael', asDSX: false);
|
||||
var scope = new SymbolTable(values: {
|
||||
'profile': {
|
||||
'avatar': 'thosakwe.png',
|
||||
|
|
110
jael/bin/jaelfmt.dart
Normal file
110
jael/bin/jaelfmt.dart
Normal file
|
@ -0,0 +1,110 @@
|
|||
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('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) {
|
||||
await file.writeAsStringSync(formatted);
|
||||
} 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);
|
||||
}
|
|
@ -1,4 +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';
|
||||
|
|
|
@ -17,7 +17,7 @@ class Call extends Expression {
|
|||
@override
|
||||
FileSpan get span {
|
||||
return arguments
|
||||
.fold<FileSpan>(lParen.span, (out, a) => out.expand(a.span))
|
||||
.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);
|
||||
|
|
192
jael/lib/src/formatter.dart
Normal file
192
jael/lib/src/formatter.dart
Normal file
|
@ -0,0 +1,192 @@
|
|||
import 'ast/ast.dart';
|
||||
|
||||
/// Jael formatter
|
||||
class JaelFormatter {
|
||||
final num tabSize;
|
||||
final bool insertSpaces;
|
||||
final int maxLineLength;
|
||||
var _buffer = new StringBuffer();
|
||||
int _level = 0;
|
||||
String _spaces;
|
||||
|
||||
static String _spaceString(int tabSize) {
|
||||
var b = new 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();
|
||||
}
|
||||
}
|
|
@ -263,10 +263,11 @@ class Renderer {
|
|||
?.value
|
||||
?.compute(scope);
|
||||
|
||||
var cases =
|
||||
element.children.where((c) => c is Element && c.tagName.name == 'case');
|
||||
var cases = element.children
|
||||
.whereType<Element>()
|
||||
.where((c) => c.tagName.name == 'case');
|
||||
|
||||
for (Element child in cases) {
|
||||
for (var child in cases) {
|
||||
var comparison = child.attributes
|
||||
.firstWhere((a) => a.name == 'value', orElse: () => null)
|
||||
?.value
|
||||
|
@ -282,9 +283,9 @@ class Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
Element defaultCase = element.children.firstWhere(
|
||||
var defaultCase = element.children.firstWhere(
|
||||
(c) => c is Element && c.tagName.name == 'default',
|
||||
orElse: () => null);
|
||||
orElse: () => null) as Element;
|
||||
if (defaultCase != null) {
|
||||
for (int i = 0; i < defaultCase.children.length; i++) {
|
||||
var child = defaultCase.children.elementAt(i);
|
||||
|
|
|
@ -232,7 +232,8 @@ class Parser {
|
|||
var child = parseElementChild();
|
||||
|
||||
while (child != null) {
|
||||
if (child is! HtmlComment) children.add(child);
|
||||
// if (child is! HtmlComment) children.add(child);
|
||||
children.add(child);
|
||||
child = parseElementChild();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
name: jael
|
||||
version: 2.0.1+2
|
||||
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://angel-dart.gitbook.io/angel/front-end/jael
|
||||
homepage: https://docs.angel-dart.dev/packages/front-end/jael
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev <=3.0.0"
|
||||
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
|
||||
|
@ -13,3 +14,5 @@ dependencies:
|
|||
symbol_table: ^2.0.0
|
||||
dev_dependencies:
|
||||
test: ^1.0.0
|
||||
executables:
|
||||
jaelfmt: jaelfmt
|
|
@ -20,7 +20,7 @@ main() {
|
|||
SymbolTable scope;
|
||||
|
||||
try {
|
||||
document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
scope = new SymbolTable<dynamic>(values: {
|
||||
'csrf_token': 'foo',
|
||||
'profile': {
|
||||
|
@ -65,8 +65,8 @@ main() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
//jael.scan(template, sourceUrl: 'test.jl').tokens.forEach(print);
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
//jael.scan(template, sourceUrl: 'test.jael').tokens.forEach(print);
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
var scope = new SymbolTable<dynamic>(values: {
|
||||
'pokemon': const _Pokemon('Darkrai', 'Dark'),
|
||||
});
|
||||
|
@ -106,7 +106,7 @@ main() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
var scope = new SymbolTable<dynamic>(values: {
|
||||
'starters': starters,
|
||||
});
|
||||
|
@ -151,7 +151,7 @@ main() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
var scope = new SymbolTable<dynamic>(values: {
|
||||
'starters': starters,
|
||||
});
|
||||
|
@ -197,7 +197,7 @@ main() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
var scope = new SymbolTable();
|
||||
|
||||
const jael.Renderer().render(document, buf, scope);
|
||||
|
@ -243,7 +243,7 @@ main() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
var scope = new SymbolTable();
|
||||
|
||||
const jael.Renderer().render(document, buf, scope);
|
||||
|
@ -268,7 +268,7 @@ main() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
var scope = new SymbolTable();
|
||||
|
||||
const jael.Renderer().render(document, buf, scope);
|
||||
|
@ -299,7 +299,7 @@ main() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
var scope = new SymbolTable<dynamic>(values: {
|
||||
'account': new _Account(isDisabled: true),
|
||||
});
|
||||
|
@ -326,7 +326,7 @@ main() {
|
|||
''';
|
||||
|
||||
var buf = new CodeBuffer();
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
|
||||
var document = jael.parseDocument(template, sourceUrl: 'test.jael');
|
||||
var scope = new SymbolTable<dynamic>(values: {
|
||||
'account': new _Account(isDisabled: null),
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'common.dart';
|
|||
|
||||
main() {
|
||||
test('plain html', () {
|
||||
var tokens = scan('<img src="foo.png" />', sourceUrl: 'test.jl').tokens;
|
||||
var tokens = scan('<img src="foo.png" />', sourceUrl: 'test.jael').tokens;
|
||||
tokens.forEach(print);
|
||||
|
||||
expect(tokens, hasLength(7));
|
||||
|
@ -19,7 +19,7 @@ main() {
|
|||
});
|
||||
|
||||
test('single quotes', () {
|
||||
var tokens = scan('<p>It\'s lit</p>', sourceUrl: 'test.jl').tokens;
|
||||
var tokens = scan('<p>It\'s lit</p>', sourceUrl: 'test.jael').tokens;
|
||||
tokens.forEach(print);
|
||||
|
||||
expect(tokens, hasLength(8));
|
||||
|
@ -34,7 +34,7 @@ main() {
|
|||
});
|
||||
|
||||
test('text node', () {
|
||||
var tokens = scan('<p>Hello\nworld</p>', sourceUrl: 'test.jl').tokens;
|
||||
var tokens = scan('<p>Hello\nworld</p>', sourceUrl: 'test.jael').tokens;
|
||||
tokens.forEach(print);
|
||||
|
||||
expect(tokens, hasLength(8));
|
||||
|
@ -50,7 +50,7 @@ main() {
|
|||
|
||||
test('mixed', () {
|
||||
var tokens = scan('<ul number=1 + 2>three{{four > five.six}}</ul>',
|
||||
sourceUrl: 'test.jl')
|
||||
sourceUrl: 'test.jael')
|
||||
.tokens;
|
||||
tokens.forEach(print);
|
||||
|
||||
|
@ -85,7 +85,7 @@ main() {
|
|||
</script>
|
||||
'''
|
||||
.trim(),
|
||||
sourceUrl: 'test.jl',
|
||||
sourceUrl: 'test.jael',
|
||||
).tokens;
|
||||
tokens.forEach(print);
|
||||
|
||||
|
|
Loading…
Reference in a new issue