Jael formatter + 2.0.2

This commit is contained in:
Tobe O 2019-07-29 18:01:24 -04:00
parent 8f0f5db53d
commit bf5c4084b4
11 changed files with 337 additions and 29 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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