diff --git a/jael/CHANGELOG.md b/jael/CHANGELOG.md index fcb450c2..c40d909f 100644 --- a/jael/CHANGELOG.md +++ b/jael/CHANGELOG.md @@ -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`. diff --git a/jael/README.md b/jael/README.md index 0cd6cf9d..207cfc67 100644 --- a/jael/README.md +++ b/jael/README.md @@ -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', diff --git a/jael/bin/jaelfmt.dart b/jael/bin/jaelfmt.dart new file mode 100644 index 00000000..b2727caf --- /dev/null +++ b/jael/bin/jaelfmt.dart @@ -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: '') + ..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 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 formatPath(String path, ArgResults argResults) async { + var stat = await FileStat.stat(path); + await formatStat(stat, path, argResults); +} + +Future 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 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); +} diff --git a/jael/lib/jael.dart b/jael/lib/jael.dart index 0fc65dbe..e6528dc9 100644 --- a/jael/lib/jael.dart +++ b/jael/lib/jael.dart @@ -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'; diff --git a/jael/lib/src/ast/call.dart b/jael/lib/src/ast/call.dart index 196d7877..75822bf2 100644 --- a/jael/lib/src/ast/call.dart +++ b/jael/lib/src/ast/call.dart @@ -17,7 +17,7 @@ class Call extends Expression { @override FileSpan get span { return arguments - .fold(lParen.span, (out, a) => out.expand(a.span)) + .fold(target.span, (out, a) => out.expand(a.span)) .expand(namedArguments.fold( lParen.span, (out, a) => out.expand(a.span))) .expand(rParen.span); diff --git a/jael/lib/src/formatter.dart b/jael/lib/src/formatter.dart new file mode 100644 index 00000000..6fd319ac --- /dev/null +++ b/jael/lib/src/formatter.dart @@ -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(''); + } + + _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 + ? [] + : 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(''); + + 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(); + } +} diff --git a/jael/lib/src/renderer.dart b/jael/lib/src/renderer.dart index 06b013a6..098ffd76 100644 --- a/jael/lib/src/renderer.dart +++ b/jael/lib/src/renderer.dart @@ -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() + .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); diff --git a/jael/lib/src/text/parser.dart b/jael/lib/src/text/parser.dart index 8e457058..1b127985 100644 --- a/jael/lib/src/text/parser.dart +++ b/jael/lib/src/text/parser.dart @@ -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(); } diff --git a/jael/pubspec.yaml b/jael/pubspec.yaml index d4875504..fc54a994 100644 --- a/jael/pubspec.yaml +++ b/jael/pubspec.yaml @@ -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 -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 \ No newline at end of file diff --git a/jael/test/render/render_test.dart b/jael/test/render/render_test.dart index d10e4bc6..7df8fea2 100644 --- a/jael/test/render/render_test.dart +++ b/jael/test/render/render_test.dart @@ -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(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(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(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(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(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(values: { 'account': new _Account(isDisabled: null), }); diff --git a/jael/test/text/scan_test.dart b/jael/test/text/scan_test.dart index 7632d8d2..a5fad7e6 100644 --- a/jael/test/text/scan_test.dart +++ b/jael/test/text/scan_test.dart @@ -5,7 +5,7 @@ import 'common.dart'; main() { test('plain html', () { - var tokens = scan('', sourceUrl: 'test.jl').tokens; + var tokens = scan('', sourceUrl: 'test.jael').tokens; tokens.forEach(print); expect(tokens, hasLength(7)); @@ -19,7 +19,7 @@ main() { }); test('single quotes', () { - var tokens = scan('

It\'s lit

', sourceUrl: 'test.jl').tokens; + var tokens = scan('

It\'s lit

', sourceUrl: 'test.jael').tokens; tokens.forEach(print); expect(tokens, hasLength(8)); @@ -34,7 +34,7 @@ main() { }); test('text node', () { - var tokens = scan('

Hello\nworld

', sourceUrl: 'test.jl').tokens; + var tokens = scan('

Hello\nworld

', sourceUrl: 'test.jael').tokens; tokens.forEach(print); expect(tokens, hasLength(8)); @@ -50,7 +50,7 @@ main() { test('mixed', () { var tokens = scan('
    three{{four > five.six}}
', - sourceUrl: 'test.jl') + sourceUrl: 'test.jael') .tokens; tokens.forEach(print); @@ -85,7 +85,7 @@ main() { ''' .trim(), - sourceUrl: 'test.jl', + sourceUrl: 'test.jael', ).tokens; tokens.forEach(print);