From 616304cc84a2617ae4c046a9e01aeec069cb4412 Mon Sep 17 00:00:00 2001
From: Gergely Gyorgy Both
Date: Sun, 12 Nov 2023 00:37:11 +0100
Subject: [PATCH 1/9] Fixed #2219 - formatting of new Angular control flow
syntax
---
js/src/html/beautifier.js | 35 ++++++++++++++++++++++++++++++++
js/src/html/tokenizer.js | 42 +++++++++++++++++++++++++++++++++------
2 files changed, 71 insertions(+), 6 deletions(-)
diff --git a/js/src/html/beautifier.js b/js/src/html/beautifier.js
index f862beaa5..f959c9c6b 100644
--- a/js/src/html/beautifier.js
+++ b/js/src/html/beautifier.js
@@ -111,6 +111,13 @@ Printer.prototype.indent = function() {
this.indent_level++;
};
+Printer.prototype.deindent = function() {
+ if (this.indent_level > 0 ) {
+ this.indent_level--;
+ this._output.set_indent(this.indent_level, this.alignment_size);
+ }
+};
+
Printer.prototype.get_full_indent = function(level) {
level = this.indent_level + (level || 0);
if (level < 1) {
@@ -305,6 +312,10 @@ Beautifier.prototype.beautify = function() {
parser_token = this._handle_tag_close(printer, raw_token, last_tag_token);
} else if (raw_token.type === TOKEN.TEXT) {
parser_token = this._handle_text(printer, raw_token, last_tag_token);
+ } else if(raw_token.type === TOKEN.CONTROL_FLOW_OPEN) {
+ parser_token = this._handle_control_flow_open(printer, raw_token);
+ } else if(raw_token.type === TOKEN.CONTROL_FLOW_CLOSE) {
+ parser_token = this._handle_control_flow_close(printer, raw_token);
} else {
// This should never happen, but if it does. Print the raw token
printer.add_raw_token(raw_token);
@@ -319,6 +330,30 @@ Beautifier.prototype.beautify = function() {
return sweet_code;
};
+Beautifier.prototype._handle_control_flow_open = function(printer, raw_token) {
+ var parser_token = {
+ text: raw_token.text,
+ type: raw_token.type
+ };
+
+ printer.print_newline(true); // TODO: handle indentation based on brace_style (and preserve-inline)
+ printer.print_token(raw_token);
+ printer.indent();
+ return parser_token;
+};
+
+Beautifier.prototype._handle_control_flow_close = function(printer, raw_token) {
+ var parser_token = {
+ text: raw_token.text,
+ type: raw_token.type
+ };
+
+ printer.deindent();
+ printer.print_newline(true);
+ printer.print_token(raw_token);
+ return parser_token;
+};
+
Beautifier.prototype._handle_tag_close = function(printer, raw_token, last_tag_token) {
var parser_token = {
text: raw_token.text,
diff --git a/js/src/html/tokenizer.js b/js/src/html/tokenizer.js
index 6c473af40..cb8c75b27 100644
--- a/js/src/html/tokenizer.js
+++ b/js/src/html/tokenizer.js
@@ -37,6 +37,8 @@ var Pattern = require('../core/pattern').Pattern;
var TOKEN = {
TAG_OPEN: 'TK_TAG_OPEN',
TAG_CLOSE: 'TK_TAG_CLOSE',
+ CONTROL_FLOW_OPEN: 'TK_CONTROL_FLOW_OPEN',
+ CONTROL_FLOW_CLOSE: 'TK_CONTROL_FLOW_CLOSE',
ATTRIBUTE: 'TK_ATTRIBUTE',
EQUALS: 'TK_EQUALS',
VALUE: 'TK_VALUE',
@@ -97,14 +99,16 @@ Tokenizer.prototype._is_comment = function(current_token) { // jshint unused:fal
};
Tokenizer.prototype._is_opening = function(current_token) {
- return current_token.type === TOKEN.TAG_OPEN;
+ return current_token.type === TOKEN.TAG_OPEN || current_token.type === TOKEN.CONTROL_FLOW_OPEN;
};
Tokenizer.prototype._is_closing = function(current_token, open_token) {
- return current_token.type === TOKEN.TAG_CLOSE &&
+ return (current_token.type === TOKEN.TAG_CLOSE &&
(open_token && (
((current_token.text === '>' || current_token.text === '/>') && open_token.text[0] === '<') ||
- (current_token.text === '}}' && open_token.text[0] === '{' && open_token.text[1] === '{')));
+ (current_token.text === '}}' && open_token.text[0] === '{' && open_token.text[1] === '{')))
+ ) || (current_token.type === TOKEN.CONTROL_FLOW_CLOSE &&
+ (current_token.text === '}' && open_token.text.endsWith('{')));
};
Tokenizer.prototype._reset = function() {
@@ -123,6 +127,7 @@ Tokenizer.prototype._get_next_token = function(previous_token, open_token) { //
token = token || this._read_open_handlebars(c, open_token);
token = token || this._read_attribute(c, previous_token, open_token);
token = token || this._read_close(c, open_token);
+ token = token || this._read_control_flows(c);
token = token || this._read_raw_content(c, previous_token, open_token);
token = token || this._read_content_word(c);
token = token || this._read_comment_or_cdata(c);
@@ -189,7 +194,7 @@ Tokenizer.prototype._read_processing = function(c) { // jshint unused:false
Tokenizer.prototype._read_open = function(c, open_token) {
var resulting_string = null;
var token = null;
- if (!open_token) {
+ if (!open_token || open_token.type === TOKEN.CONTROL_FLOW_OPEN) {
if (c === '<') {
resulting_string = this._input.next();
@@ -206,7 +211,7 @@ Tokenizer.prototype._read_open = function(c, open_token) {
Tokenizer.prototype._read_open_handlebars = function(c, open_token) {
var resulting_string = null;
var token = null;
- if (!open_token) {
+ if (!open_token || open_token.type === TOKEN.CONTROL_FLOW_OPEN) {
if (this._options.indent_handlebars && c === '{' && this._input.peek(1) === '{') {
if (this._input.peek(2) === '!') {
resulting_string = this.__patterns.handlebars_comment.read();
@@ -221,11 +226,36 @@ Tokenizer.prototype._read_open_handlebars = function(c, open_token) {
return token;
};
+Tokenizer.prototype._read_control_flows = function (c) {
+ var resulting_string = '';
+ var token = null;
+ if (c === '@' && /[a-zA-Z0-9]/.test(this._input.peek(1))) {
+ var opening_parentheses_count = 0;
+ var closing_parentheses_count = 0;
+ while(!(resulting_string.endsWith('{') && opening_parentheses_count === closing_parentheses_count)) {
+ var next_char = this._input.next();
+ if(next_char === null) {
+ break;
+ } else if(next_char === '(') {
+ opening_parentheses_count++;
+ } else if(next_char === ')') {
+ closing_parentheses_count++;
+ }
+ resulting_string += next_char;
+ }
+ token = this._create_token(TOKEN.CONTROL_FLOW_OPEN, resulting_string);
+ } else if (c === '}' && this._input.peek(1) !== '}' && this._input.peek(-1) !== '}') {
+ resulting_string = this._input.next();
+ token = this._create_token(TOKEN.CONTROL_FLOW_CLOSE, resulting_string);
+ }
+ return token;
+};
+
Tokenizer.prototype._read_close = function(c, open_token) {
var resulting_string = null;
var token = null;
- if (open_token) {
+ if (open_token && open_token.type === TOKEN.TAG_OPEN) {
if (open_token.text[0] === '<' && (c === '>' || (c === '/' && this._input.peek(1) === '>'))) {
resulting_string = this._input.next();
if (c === '/') { // for close tag "/>"
From cf6a64ea9d536981e58ae9b6c4d892d49c6974b2 Mon Sep 17 00:00:00 2001
From: Gergely Gyorgy Both
Date: Mon, 13 Nov 2023 09:36:27 +0100
Subject: [PATCH 2/9] Add 'angular' templating option; use it for html
beautifier control flow syntax
---
README.md | 4 ++--
js/src/cli.js | 2 +-
js/src/core/options.js | 4 ++--
js/src/core/templatablepattern.js | 3 ++-
js/src/html/options.js | 2 +-
js/src/html/tokenizer.js | 7 +++++++
python/jsbeautifier/__init__.py | 2 +-
python/jsbeautifier/core/options.py | 4 ++--
python/jsbeautifier/core/templatablepattern.py | 3 ++-
9 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 83153f3e2..08e79157c 100644
--- a/README.md
+++ b/README.md
@@ -223,7 +223,7 @@ Beautifier Options:
-C, --comma-first Put commas at the beginning of new line instead of end
-O, --operator-position Set operator position (before-newline|after-newline|preserve-newline) [before-newline]
--indent-empty-lines Keep indentation on empty lines
- --templating List of templating languages (auto,django,erb,handlebars,php,smarty) ["auto"] auto = none in JavaScript, all in HTML
+ --templating List of templating languages (auto,django,erb,handlebars,php,smarty,angular) ["auto"] auto = none in JavaScript, all in HTML
```
Which correspond to the underscored option keys for both library interfaces
@@ -379,7 +379,7 @@ HTML Beautifier Options:
--indent_scripts Sets indent level inside script tags ("normal", "keep", "separate")
--unformatted_content_delimiter Keep text content together between this string [""]
--indent-empty-lines Keep indentation on empty lines
- --templating List of templating languages (auto,none,django,erb,handlebars,php,smarty) ["auto"] auto = none in JavaScript, all in html
+ --templating List of templating languages (auto,none,django,erb,handlebars,php,smarty,angular) ["auto"] auto = none in JavaScript, all in html
```
## Directives
diff --git a/js/src/cli.js b/js/src/cli.js
index 452d56bd5..b9d6fc647 100755
--- a/js/src/cli.js
+++ b/js/src/cli.js
@@ -370,7 +370,7 @@ function usage(err) {
' [first newline in file, otherwise "\\n]',
' -n, --end-with-newline End output with newline',
' --indent-empty-lines Keep indentation on empty lines',
- ' --templating List of templating languages (auto,none,django,erb,handlebars,php,smarty) ["auto"] auto = none in JavaScript, all in html',
+ ' --templating List of templating languages (auto,none,django,erb,handlebars,php,smarty,angular) ["auto"] auto = none in JavaScript, all in html',
' --editorconfig Use EditorConfig to set up the options'
];
diff --git a/js/src/core/options.js b/js/src/core/options.js
index f784bcee1..5976351df 100644
--- a/js/src/core/options.js
+++ b/js/src/core/options.js
@@ -67,10 +67,10 @@ function Options(options, merge_child_field) {
this.indent_empty_lines = this._get_boolean('indent_empty_lines');
- // valid templating languages ['django', 'erb', 'handlebars', 'php', 'smarty']
+ // valid templating languages ['django', 'erb', 'handlebars', 'php', 'smarty', 'angular']
// For now, 'auto' = all off for javascript, all on for html (and inline javascript).
// other values ignored
- this.templating = this._get_selection_list('templating', ['auto', 'none', 'django', 'erb', 'handlebars', 'php', 'smarty'], ['auto']);
+ this.templating = this._get_selection_list('templating', ['auto', 'none', 'django', 'erb', 'handlebars', 'php', 'smarty', 'angular'], ['auto']);
}
Options.prototype._get_array = function(name, default_value) {
diff --git a/js/src/core/templatablepattern.js b/js/src/core/templatablepattern.js
index 58a0a35ba..4ada56948 100644
--- a/js/src/core/templatablepattern.js
+++ b/js/src/core/templatablepattern.js
@@ -36,7 +36,8 @@ var template_names = {
erb: false,
handlebars: false,
php: false,
- smarty: false
+ smarty: false,
+ angular: false
};
// This lets templates appear anywhere we would do a readUntil
diff --git a/js/src/html/options.js b/js/src/html/options.js
index f71d2b00c..3b3c761ad 100644
--- a/js/src/html/options.js
+++ b/js/src/html/options.js
@@ -33,7 +33,7 @@ var BaseOptions = require('../core/options').Options;
function Options(options) {
BaseOptions.call(this, options, 'html');
if (this.templating.length === 1 && this.templating[0] === 'auto') {
- this.templating = ['django', 'erb', 'handlebars', 'php'];
+ this.templating = ['django', 'erb', 'handlebars', 'php', 'angular'];
}
this.indent_inner_html = this._get_boolean('indent_inner_html');
diff --git a/js/src/html/tokenizer.js b/js/src/html/tokenizer.js
index cb8c75b27..97e7badb9 100644
--- a/js/src/html/tokenizer.js
+++ b/js/src/html/tokenizer.js
@@ -229,9 +229,16 @@ Tokenizer.prototype._read_open_handlebars = function(c, open_token) {
Tokenizer.prototype._read_control_flows = function (c) {
var resulting_string = '';
var token = null;
+ // Only check for control flows if angular templating is set
+ if(!this._options.templating.includes('angular')) {
+ return token;
+ }
+
if (c === '@' && /[a-zA-Z0-9]/.test(this._input.peek(1))) {
var opening_parentheses_count = 0;
var closing_parentheses_count = 0;
+ // The opening brace of the control flow is where the number of opening and closing parentheses equal
+ // e.g. @if({value: true} !== null) {
while(!(resulting_string.endsWith('{') && opening_parentheses_count === closing_parentheses_count)) {
var next_char = this._input.next();
if(next_char === null) {
diff --git a/python/jsbeautifier/__init__.py b/python/jsbeautifier/__init__.py
index 36920ff65..a835edea4 100644
--- a/python/jsbeautifier/__init__.py
+++ b/python/jsbeautifier/__init__.py
@@ -131,7 +131,7 @@ def usage(stream=sys.stdout):
NOTE: Line continues until next wrap point is found.
-n, --end-with-newline End output with newline
--indent-empty-lines Keep indentation on empty lines
- --templating List of templating languages (auto,none,django,erb,handlebars,php,smarty) ["auto"] auto = none in JavaScript, all in html
+ --templating List of templating languages (auto,none,django,erb,handlebars,php,smarty,angular) ["auto"] auto = none in JavaScript, all in html
--editorconfig Enable setting configuration from EditorConfig
Rarely needed options:
diff --git a/python/jsbeautifier/core/options.py b/python/jsbeautifier/core/options.py
index d46cb6bf1..126da5e45 100644
--- a/python/jsbeautifier/core/options.py
+++ b/python/jsbeautifier/core/options.py
@@ -76,12 +76,12 @@ def __init__(self, options=None, merge_child_field=None):
self.indent_empty_lines = self._get_boolean("indent_empty_lines")
- # valid templating languages ['django', 'erb', 'handlebars', 'php', 'smarty']
+ # valid templating languages ['django', 'erb', 'handlebars', 'php', 'smarty', 'angular']
# For now, 'auto' = all off for javascript, all on for html (and inline javascript).
# other values ignored
self.templating = self._get_selection_list(
"templating",
- ["auto", "none", "django", "erb", "handlebars", "php", "smarty"],
+ ["auto", "none", "django", "erb", "handlebars", "php", "smarty", "angular"],
["auto"],
)
diff --git a/python/jsbeautifier/core/templatablepattern.py b/python/jsbeautifier/core/templatablepattern.py
index 995462824..a2f81139d 100644
--- a/python/jsbeautifier/core/templatablepattern.py
+++ b/python/jsbeautifier/core/templatablepattern.py
@@ -35,6 +35,7 @@ def __init__(self):
self.handlebars = False
self.php = False
self.smarty = False
+ self.angular = False
class TemplatePatterns:
@@ -78,7 +79,7 @@ def _update(self):
def read_options(self, options):
result = self._create()
- for language in ["django", "erb", "handlebars", "php", "smarty"]:
+ for language in ["django", "erb", "handlebars", "php", "smarty", "angular"]:
setattr(result._disabled, language, not (language in options.templating))
result._update()
return result
From 1867e2451b3cdbc07ba5fdbbd0cd54d89d13f3bc Mon Sep 17 00:00:00 2001
From: Gergely Gyorgy Both
Date: Mon, 13 Nov 2023 15:53:42 +0100
Subject: [PATCH 3/9] Add more precise selection for angular control flow close
tag
---
js/src/html/tokenizer.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/js/src/html/tokenizer.js b/js/src/html/tokenizer.js
index 97e7badb9..2f3e223bd 100644
--- a/js/src/html/tokenizer.js
+++ b/js/src/html/tokenizer.js
@@ -127,7 +127,7 @@ Tokenizer.prototype._get_next_token = function(previous_token, open_token) { //
token = token || this._read_open_handlebars(c, open_token);
token = token || this._read_attribute(c, previous_token, open_token);
token = token || this._read_close(c, open_token);
- token = token || this._read_control_flows(c);
+ token = token || this._read_control_flows(c, open_token);
token = token || this._read_raw_content(c, previous_token, open_token);
token = token || this._read_content_word(c);
token = token || this._read_comment_or_cdata(c);
@@ -226,7 +226,7 @@ Tokenizer.prototype._read_open_handlebars = function(c, open_token) {
return token;
};
-Tokenizer.prototype._read_control_flows = function (c) {
+Tokenizer.prototype._read_control_flows = function (c, open_token) {
var resulting_string = '';
var token = null;
// Only check for control flows if angular templating is set
@@ -251,7 +251,7 @@ Tokenizer.prototype._read_control_flows = function (c) {
resulting_string += next_char;
}
token = this._create_token(TOKEN.CONTROL_FLOW_OPEN, resulting_string);
- } else if (c === '}' && this._input.peek(1) !== '}' && this._input.peek(-1) !== '}') {
+ } else if (c === '}' && open_token && open_token.type === TOKEN.CONTROL_FLOW_OPEN) {
resulting_string = this._input.next();
token = this._create_token(TOKEN.CONTROL_FLOW_CLOSE, resulting_string);
}
From 08ecdd3f4bb208f1066c73abc8411f95ffe82353 Mon Sep 17 00:00:00 2001
From: Gergely Gyorgy Both
Date: Mon, 13 Nov 2023 15:54:32 +0100
Subject: [PATCH 4/9] Print angular control flow tokens with basic formatting
---
js/src/html/beautifier.js | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/js/src/html/beautifier.js b/js/src/html/beautifier.js
index f959c9c6b..f70451cca 100644
--- a/js/src/html/beautifier.js
+++ b/js/src/html/beautifier.js
@@ -335,8 +335,12 @@ Beautifier.prototype._handle_control_flow_open = function(printer, raw_token) {
text: raw_token.text,
type: raw_token.type
};
-
- printer.print_newline(true); // TODO: handle indentation based on brace_style (and preserve-inline)
+ printer.set_space_before_token(raw_token.newlines || raw_token.whitespace_before !== '', true);
+ if(raw_token.newlines) {
+ printer.print_preserved_newlines(raw_token);
+ } else {
+ printer.set_space_before_token(raw_token.newlines || raw_token.whitespace_before !== '', true);
+ }
printer.print_token(raw_token);
printer.indent();
return parser_token;
@@ -349,7 +353,11 @@ Beautifier.prototype._handle_control_flow_close = function(printer, raw_token) {
};
printer.deindent();
- printer.print_newline(true);
+ if(raw_token.newlines) {
+ printer.print_preserved_newlines(raw_token);
+ } else {
+ printer.set_space_before_token(raw_token.newlines || raw_token.whitespace_before !== '', true);
+ }
printer.print_token(raw_token);
return parser_token;
};
From 1ffd900e0d0ba9631c11824ec66750a0e11d07b9 Mon Sep 17 00:00:00 2001
From: Gergely Gyorgy Both
Date: Wed, 15 Nov 2023 13:45:22 +0100
Subject: [PATCH 5/9] Add tests for fixing issue #2219
---
index.html | 3 +-
js/src/html/beautifier.js | 10 +-
js/src/html/tokenizer.js | 14 +-
test/data/html/tests.js | 298 ++++++++++++++++++++++++++++++++++++++
4 files changed, 312 insertions(+), 13 deletions(-)
diff --git a/index.html b/index.html
index 2d7704101..ecfeb872c 100644
--- a/index.html
+++ b/index.html
@@ -39,7 +39,8 @@
-
+
+
diff --git a/js/src/html/beautifier.js b/js/src/html/beautifier.js
index f70451cca..32c882a57 100644
--- a/js/src/html/beautifier.js
+++ b/js/src/html/beautifier.js
@@ -112,7 +112,7 @@ Printer.prototype.indent = function() {
};
Printer.prototype.deindent = function() {
- if (this.indent_level > 0 ) {
+ if (this.indent_level > 0) {
this.indent_level--;
this._output.set_indent(this.indent_level, this.alignment_size);
}
@@ -312,9 +312,9 @@ Beautifier.prototype.beautify = function() {
parser_token = this._handle_tag_close(printer, raw_token, last_tag_token);
} else if (raw_token.type === TOKEN.TEXT) {
parser_token = this._handle_text(printer, raw_token, last_tag_token);
- } else if(raw_token.type === TOKEN.CONTROL_FLOW_OPEN) {
+ } else if (raw_token.type === TOKEN.CONTROL_FLOW_OPEN) {
parser_token = this._handle_control_flow_open(printer, raw_token);
- } else if(raw_token.type === TOKEN.CONTROL_FLOW_CLOSE) {
+ } else if (raw_token.type === TOKEN.CONTROL_FLOW_CLOSE) {
parser_token = this._handle_control_flow_close(printer, raw_token);
} else {
// This should never happen, but if it does. Print the raw token
@@ -336,7 +336,7 @@ Beautifier.prototype._handle_control_flow_open = function(printer, raw_token) {
type: raw_token.type
};
printer.set_space_before_token(raw_token.newlines || raw_token.whitespace_before !== '', true);
- if(raw_token.newlines) {
+ if (raw_token.newlines) {
printer.print_preserved_newlines(raw_token);
} else {
printer.set_space_before_token(raw_token.newlines || raw_token.whitespace_before !== '', true);
@@ -353,7 +353,7 @@ Beautifier.prototype._handle_control_flow_close = function(printer, raw_token) {
};
printer.deindent();
- if(raw_token.newlines) {
+ if (raw_token.newlines) {
printer.print_preserved_newlines(raw_token);
} else {
printer.set_space_before_token(raw_token.newlines || raw_token.whitespace_before !== '', true);
diff --git a/js/src/html/tokenizer.js b/js/src/html/tokenizer.js
index 2f3e223bd..7d5e01d3f 100644
--- a/js/src/html/tokenizer.js
+++ b/js/src/html/tokenizer.js
@@ -107,7 +107,7 @@ Tokenizer.prototype._is_closing = function(current_token, open_token) {
(open_token && (
((current_token.text === '>' || current_token.text === '/>') && open_token.text[0] === '<') ||
(current_token.text === '}}' && open_token.text[0] === '{' && open_token.text[1] === '{')))
- ) || (current_token.type === TOKEN.CONTROL_FLOW_CLOSE &&
+ ) || (current_token.type === TOKEN.CONTROL_FLOW_CLOSE &&
(current_token.text === '}' && open_token.text.endsWith('{')));
};
@@ -226,11 +226,11 @@ Tokenizer.prototype._read_open_handlebars = function(c, open_token) {
return token;
};
-Tokenizer.prototype._read_control_flows = function (c, open_token) {
+Tokenizer.prototype._read_control_flows = function(c, open_token) {
var resulting_string = '';
var token = null;
// Only check for control flows if angular templating is set
- if(!this._options.templating.includes('angular')) {
+ if (!this._options.templating.includes('angular')) {
return token;
}
@@ -239,13 +239,13 @@ Tokenizer.prototype._read_control_flows = function (c, open_token) {
var closing_parentheses_count = 0;
// The opening brace of the control flow is where the number of opening and closing parentheses equal
// e.g. @if({value: true} !== null) {
- while(!(resulting_string.endsWith('{') && opening_parentheses_count === closing_parentheses_count)) {
+ while (!(resulting_string.endsWith('{') && opening_parentheses_count === closing_parentheses_count)) {
var next_char = this._input.next();
- if(next_char === null) {
+ if (next_char === null) {
break;
- } else if(next_char === '(') {
+ } else if (next_char === '(') {
opening_parentheses_count++;
- } else if(next_char === ')') {
+ } else if (next_char === ')') {
closing_parentheses_count++;
}
resulting_string += next_char;
diff --git a/test/data/html/tests.js b/test/data/html/tests.js
index 6d37e432e..e79b2a4bc 100644
--- a/test/data/html/tests.js
+++ b/test/data/html/tests.js
@@ -3839,6 +3839,304 @@ exports.test_data = {
''
]
}]
+ }, {
+ name: "Indenting angular control flow with indent size 2",
+ description: "https://github.com/beautify-web/js-beautify/issues/2219",
+ template: "^^^ $$$",
+ options: [
+ { name: "templating", value: "'angular'" },
+ { name: "indent_size", value: "2" }
+ ],
+ tests: [{
+ input: [
+ '@if (a > b) {',
+ '{{a}} is greater than {{b}}',
+ '}',
+ '',
+ '@if (a > b) {',
+ '{{a}} is greater than {{b}}',
+ '} @else if (b > a) {',
+ '{{a}} is less than {{b}}',
+ '} @else {',
+ '{{a}} is equal to {{b}}',
+ '}',
+ '',
+ '@for (item of items; track item.name) {',
+ ' {{ item.name }} ',
+ '} @empty {',
+ ' There are no items. ',
+ '}',
+ '',
+ '@switch (condition) {',
+ '@case (caseA) { ',
+ 'Case A.',
+ '}',
+ '@case (caseB) {',
+ 'Case B.',
+ '}',
+ '@default {',
+ 'Default case.',
+ '}',
+ '}'
+ ],
+ output: [
+ '@if (a > b) {',
+ ' {{a}} is greater than {{b}}',
+ '}',
+ '',
+ '@if (a > b) {',
+ ' {{a}} is greater than {{b}}',
+ '} @else if (b > a) {',
+ ' {{a}} is less than {{b}}',
+ '} @else {',
+ ' {{a}} is equal to {{b}}',
+ '}',
+ '',
+ '@for (item of items; track item.name) {',
+ ' {{ item.name }} ',
+ '} @empty {',
+ ' There are no items. ',
+ '}',
+ '',
+ '@switch (condition) {',
+ ' @case (caseA) {',
+ ' Case A.',
+ ' }',
+ ' @case (caseB) {',
+ ' Case B.',
+ ' }',
+ ' @default {',
+ ' Default case.',
+ ' }',
+ '}'
+ ]
+ }]
+ }, {
+ name: "Indenting angular control flow with default indent size",
+ description: "https://github.com/beautify-web/js-beautify/issues/2219",
+ template: "^^^ $$$",
+ options: [
+ { name: "templating", value: "'angular, handlebars'" }
+ ],
+ tests: [{
+ input: [
+ '@if (a > b) {',
+ '{{a}} is greater than {{b}}',
+ '}',
+ '',
+ '@if (a > b) {',
+ '{{a}} is greater than {{b}}',
+ '} @else if (b > a) {',
+ '{{a}} is less than {{b}}',
+ '} @else {',
+ '{{a}} is equal to {{b}}',
+ '}',
+ '',
+ '@for (item of items; track item.name) {',
+ ' {{ item.name }} ',
+ '} @empty {',
+ ' There are no items. ',
+ '}',
+ '',
+ '@switch (condition) {',
+ '@case (caseA) { ',
+ 'Case A.',
+ '}',
+ '@case (caseB) {',
+ 'Case B.',
+ '}',
+ '@default {',
+ 'Default case.',
+ '}',
+ '}'
+ ],
+ output: [
+ '@if (a > b) {',
+ ' {{a}} is greater than {{b}}',
+ '}',
+ '',
+ '@if (a > b) {',
+ ' {{a}} is greater than {{b}}',
+ '} @else if (b > a) {',
+ ' {{a}} is less than {{b}}',
+ '} @else {',
+ ' {{a}} is equal to {{b}}',
+ '}',
+ '',
+ '@for (item of items; track item.name) {',
+ ' {{ item.name }} ',
+ '} @empty {',
+ ' There are no items. ',
+ '}',
+ '',
+ '@switch (condition) {',
+ ' @case (caseA) {',
+ ' Case A.',
+ ' }',
+ ' @case (caseB) {',
+ ' Case B.',
+ ' }',
+ ' @default {',
+ ' Default case.',
+ ' }',
+ '}'
+ ]
+ }, {
+ input: [
+ '@if (a > b) {',
+ ' {{a}} is greater than {{b}}',
+ ' }',
+ '',
+ ' @if (a > b) {',
+ ' {{a}} is greater than {{b}}',
+ ' } @else if (b > a) {',
+ ' {{a}} is less than {{b}}',
+ ' } @else {',
+ ' {{a}} is equal to {{b}}',
+ '}',
+ '',
+ ' @for (item of items; track item.name) {',
+ ' {{ item.name }} ',
+ ' } @empty {',
+ ' There are no items. ',
+ ' }',
+ '',
+ ' @switch (condition) {',
+ '@case (caseA) { ',
+ 'Case A.',
+ ' }',
+ ' @case (caseB) {',
+ 'Case B.',
+ ' }',
+ ' @default {',
+ 'Default case.',
+ '}',
+ ' }'
+ ],
+ output: [
+ '@if (a > b) {',
+ ' {{a}} is greater than {{b}}',
+ '}',
+ '',
+ '@if (a > b) {',
+ ' {{a}} is greater than {{b}}',
+ '} @else if (b > a) {',
+ ' {{a}} is less than {{b}}',
+ '} @else {',
+ ' {{a}} is equal to {{b}}',
+ '}',
+ '',
+ '@for (item of items; track item.name) {',
+ ' {{ item.name }} ',
+ '} @empty {',
+ ' There are no items. ',
+ '}',
+ '',
+ '@switch (condition) {',
+ ' @case (caseA) {',
+ ' Case A.',
+ ' }',
+ ' @case (caseB) {',
+ ' Case B.',
+ ' }',
+ ' @default {',
+ ' Default case.',
+ ' }',
+ '}'
+ ]
+ }, {
+ input: [
+ '@if( {value: true}; as val) {',
+ '{{val.value}}
',
+ '}'
+ ],
+ output: [
+ '@if( {value: true}; as val) {',
+ ' {{val.value}}
',
+ '}'
+ ]
+ }, {
+ input: [
+ '@if( {value: true}; as val) {',
+ '',
+ '@defer {',
+ '{{val.value}}',
+ '}',
+ '
',
+ '}'
+ ],
+ output: [
+ '@if( {value: true}; as val) {',
+ ' ',
+ ' @defer {',
+ ' {{val.value}}',
+ ' }',
+ '
',
+ '}'
+ ]
+ }, {
+ unchanged: [
+ ' @if(true) { {{"{}" + " }"}} }
'
+ ]
+ }, {
+ input: [
+ '',
+ '@for (item of items; track item.id; let idx = $index, e = $even) {',
+ 'Item #{{ idx }}: {{ item.name }}',
+ '
',
+ 'Item #{{ idx }}: {{ item.name }}',
+ '
',
+ '}',
+ '
'
+ ],
+ output: [
+ '',
+ ' @for (item of items; track item.id; let idx = $index, e = $even) {',
+ ' Item #{{ idx }}: {{ item.name }}',
+ '
',
+ ' Item #{{ idx }}: {{ item.name }}',
+ '
',
+ ' }',
+ '
'
+ ]
+ }, {
+ input: [
+ '',
+ '@for (item of items; track item.id; let idx = $index, e = $even) {',
+ '{{{value: true} | json}}',
+ '
',
+ 'Item #{{ idx }}: {{ item.name }}',
+ '
',
+ '{{ {value: true} }}',
+ '
',
+ '@if(true) {',
+ '{{ {value: true} }}',
+ ' }',
+ '',
+ 'Placeholder',
+ '
',
+ '}',
+ '
'
+ ],
+ output: [
+ '',
+ ' @for (item of items; track item.id; let idx = $index, e = $even) {',
+ ' {{{value: true} | json}}',
+ '
',
+ ' Item #{{ idx }}: {{ item.name }}',
+ '
',
+ ' {{ {value: true} }}',
+ '
',
+ ' @if(true) {',
+ ' {{ {value: true} }}',
+ ' }',
+ '',
+ ' Placeholder',
+ '
',
+ ' }',
+ '
'
+ ]
+ }]
}, {
name: "New Test Suite"
}]
From 3d53f6d7f99d6f3c989e2cf663e2e14e0f370fc8 Mon Sep 17 00:00:00 2001
From: Gergely Gyorgy Both
Date: Sun, 3 Dec 2023 15:46:15 +0100
Subject: [PATCH 6/9] Change angular control flow selection to do via pattern
---
js/src/html/tokenizer.js | 10 ++++++++--
test/data/html/tests.js | 18 ++++++++++++++++++
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/js/src/html/tokenizer.js b/js/src/html/tokenizer.js
index 7d5e01d3f..f4ef7e7c8 100644
--- a/js/src/html/tokenizer.js
+++ b/js/src/html/tokenizer.js
@@ -68,6 +68,7 @@ var Tokenizer = function(input_string, options) {
attribute: templatable_reader.until(/[\n\r\t =>]|\/>/),
element_name: templatable_reader.until(/[\n\r\t >\/]/),
+ angular_control_flow_start: pattern_reader.matching(/\@([a-zA-z]+[\n\t ]*)+[({]/),
handlebars_comment: pattern_reader.starting_with(/{{!--/).until_after(/--}}/),
handlebars: pattern_reader.starting_with(/{{/).until_after(/}}/),
handlebars_open: pattern_reader.until(/[\n\r\t }]/),
@@ -234,8 +235,13 @@ Tokenizer.prototype._read_control_flows = function(c, open_token) {
return token;
}
- if (c === '@' && /[a-zA-Z0-9]/.test(this._input.peek(1))) {
- var opening_parentheses_count = 0;
+ if (c === '@') {
+ resulting_string = this.__patterns.angular_control_flow_start.read();
+ if (resulting_string === '') {
+ return token;
+ }
+
+ var opening_parentheses_count = resulting_string.endsWith('(') ? 1 : 0;
var closing_parentheses_count = 0;
// The opening brace of the control flow is where the number of opening and closing parentheses equal
// e.g. @if({value: true} !== null) {
diff --git a/test/data/html/tests.js b/test/data/html/tests.js
index e79b2a4bc..70bdf2136 100644
--- a/test/data/html/tests.js
+++ b/test/data/html/tests.js
@@ -4136,6 +4136,24 @@ exports.test_data = {
' }',
''
]
+ }, {
+ comment: 'If no whitespace before @, then don\'t indent',
+ input: [
+ 'My email is loremipsum@if.com (only for work).',
+ 'loremipsum@if {',
+ '',
+ 'Text',
+ '
',
+ '}'
+ ],
+ output: [
+ 'My email is loremipsum@if.com (only for work).',
+ 'loremipsum@if {',
+ '',
+ ' Text',
+ '
',
+ '}'
+ ]
}]
}, {
name: "New Test Suite"
From cc5429fc58a328466b73892b3133d52a291d958e Mon Sep 17 00:00:00 2001
From: Gergely Gyorgy Both
Date: Mon, 4 Dec 2023 08:12:37 +0100
Subject: [PATCH 7/9] Fix selecting control flow closing brace if it is not
preceded by whitespace
---
js/src/html/tokenizer.js | 8 +++++---
test/data/html/tests.js | 24 ++++++++++++++++++++++++
2 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/js/src/html/tokenizer.js b/js/src/html/tokenizer.js
index f4ef7e7c8..9030708e8 100644
--- a/js/src/html/tokenizer.js
+++ b/js/src/html/tokenizer.js
@@ -63,6 +63,7 @@ var Tokenizer = function(input_string, options) {
this.__patterns = {
word: templatable_reader.until(/[\n\r\t <]/),
+ word_control_flow_close_excluded: templatable_reader.until(/[\n\r\t <}]/),
single_quote: templatable_reader.until_after(/'/),
double_quote: templatable_reader.until_after(/"/),
attribute: templatable_reader.until(/[\n\r\t =>]|\/>/),
@@ -82,6 +83,7 @@ var Tokenizer = function(input_string, options) {
if (this._options.indent_handlebars) {
this.__patterns.word = this.__patterns.word.exclude('handlebars');
+ this.__patterns.word_control_flow_close_excluded = this.__patterns.word_control_flow_close_excluded.exclude('handlebars');
}
this._unformatted_content_delimiter = null;
@@ -130,7 +132,7 @@ Tokenizer.prototype._get_next_token = function(previous_token, open_token) { //
token = token || this._read_close(c, open_token);
token = token || this._read_control_flows(c, open_token);
token = token || this._read_raw_content(c, previous_token, open_token);
- token = token || this._read_content_word(c);
+ token = token || this._read_content_word(c, open_token);
token = token || this._read_comment_or_cdata(c);
token = token || this._read_processing(c);
token = token || this._read_open(c, open_token);
@@ -355,7 +357,7 @@ Tokenizer.prototype._read_raw_content = function(c, previous_token, open_token)
return null;
};
-Tokenizer.prototype._read_content_word = function(c) {
+Tokenizer.prototype._read_content_word = function(c, open_token) {
var resulting_string = '';
if (this._options.unformatted_content_delimiter) {
if (c === this._options.unformatted_content_delimiter[0]) {
@@ -364,7 +366,7 @@ Tokenizer.prototype._read_content_word = function(c) {
}
if (!resulting_string) {
- resulting_string = this.__patterns.word.read();
+ resulting_string = (open_token && open_token.type === TOKEN.CONTROL_FLOW_OPEN) ? this.__patterns.word_control_flow_close_excluded.read() : this.__patterns.word.read();
}
if (resulting_string) {
return this._create_token(TOKEN.TEXT, resulting_string);
diff --git a/test/data/html/tests.js b/test/data/html/tests.js
index 70bdf2136..5220cbc2b 100644
--- a/test/data/html/tests.js
+++ b/test/data/html/tests.js
@@ -4154,6 +4154,30 @@ exports.test_data = {
'
',
'}'
]
+ }, {
+ comment: 'Check if control flow is indented well if we have oneliners with no space before the closing token',
+ input: [
+ '{{b}} @if (a > b) {is less than}@else{is greater than or equal to} {{a}}',
+ '',
+ 'Hello there',
+ '
',
+ '',
+ '{{b}} @if (a > b) {is less than}@else{',
+ 'is greater than or equal to} {{a}}',
+ 'Hello there',
+ '
'
+ ],
+ output: [
+ '{{b}} @if (a > b) {is less than}@else{is greater than or equal to} {{a}}',
+ '',
+ ' Hello there',
+ '
',
+ '',
+ ' {{b}} @if (a > b) {is less than}@else{',
+ ' is greater than or equal to} {{a}}',
+ ' Hello there',
+ '
'
+ ]
}]
}, {
name: "New Test Suite"
From 8900b866e6899a7ea6a67d297298019239140a2a Mon Sep 17 00:00:00 2001
From: Gergely Gyorgy Both
Date: Wed, 6 Dec 2023 12:43:31 +0100
Subject: [PATCH 8/9] Fix regex for control flow start pattern; only select
control flow open if indent_handlebars is true
---
js/src/html/tokenizer.js | 6 +-
test/data/html/tests.js | 151 +++++++++++++++++++++++++++++++++++++++
2 files changed, 154 insertions(+), 3 deletions(-)
diff --git a/js/src/html/tokenizer.js b/js/src/html/tokenizer.js
index 9030708e8..231f674af 100644
--- a/js/src/html/tokenizer.js
+++ b/js/src/html/tokenizer.js
@@ -69,7 +69,7 @@ var Tokenizer = function(input_string, options) {
attribute: templatable_reader.until(/[\n\r\t =>]|\/>/),
element_name: templatable_reader.until(/[\n\r\t >\/]/),
- angular_control_flow_start: pattern_reader.matching(/\@([a-zA-z]+[\n\t ]*)+[({]/),
+ angular_control_flow_start: pattern_reader.matching(/\@[^\n\t ][^({]*[({]/),
handlebars_comment: pattern_reader.starting_with(/{{!--/).until_after(/--}}/),
handlebars: pattern_reader.starting_with(/{{/).until_after(/}}/),
handlebars_open: pattern_reader.until(/[\n\r\t }]/),
@@ -232,8 +232,8 @@ Tokenizer.prototype._read_open_handlebars = function(c, open_token) {
Tokenizer.prototype._read_control_flows = function(c, open_token) {
var resulting_string = '';
var token = null;
- // Only check for control flows if angular templating is set
- if (!this._options.templating.includes('angular')) {
+ // Only check for control flows if angular templating is set AND indenting is set
+ if (!this._options.templating.includes('angular') || !this._options.indent_handlebars) {
return token;
}
diff --git a/test/data/html/tests.js b/test/data/html/tests.js
index 5220cbc2b..36dfa363c 100644
--- a/test/data/html/tests.js
+++ b/test/data/html/tests.js
@@ -4178,6 +4178,157 @@ exports.test_data = {
' Hello there',
''
]
+ }, {
+ comment: 'Multiline conditions should also be recognized and indented correctly',
+ input: [
+ '@if(',
+ 'condition1',
+ '&& condition2',
+ ') {',
+ 'Text inside if',
+ '}'
+ ],
+ output: [
+ '@if(',
+ 'condition1',
+ '&& condition2',
+ ') {',
+ ' Text inside if',
+ '}'
+ ]
+ }, {
+ comment: 'Indentation should work if opening brace is in new line',
+ input: [
+ '@if( condition )',
+ '{',
+ 'Text inside if',
+ '}'
+ ],
+ output: [
+ '@if( condition )',
+ '{',
+ ' Text inside if',
+ '}'
+ ]
+ }, {
+ comment: 'Indentation should work if condition is in new line',
+ input: [
+ '@if',
+ '( condition )',
+ '{',
+ 'Text inside if',
+ '} @else if',
+ '(condition2)',
+ '{',
+ '',
+ 'Text',
+ '
',
+ '}'
+ ],
+ output: [
+ '@if',
+ '( condition )',
+ '{',
+ ' Text inside if',
+ '} @else if',
+ '(condition2)',
+ '{',
+ ' ',
+ ' Text',
+ '
',
+ '}'
+ ]
+ }]
+ }, {
+ name: "No indenting for angular control flow should be done if indent_handlebars is false",
+ description: "https://github.com/beautify-web/js-beautify/issues/2219",
+ template: "^^^ $$$",
+ options: [
+ { name: "templating", value: "'angular, handlebars'" },
+ { name: "indent_handlebars", value: "false" }
+ ],
+ tests: [{
+ unchanged: [
+ '@if (a > b) {',
+ '{{a}} is greater than {{b}}',
+ '}',
+ '',
+ '@if (a > b) {',
+ '{{a}} is greater than {{b}}',
+ '} @else if (b > a) {',
+ '{{a}} is less than {{b}}',
+ '} @else {',
+ '{{a}} is equal to {{b}}',
+ '}',
+ '',
+ '@for (item of items; track item.name) {',
+ ' {{ item.name }} ',
+ '} @empty {',
+ ' There are no items. ',
+ '}',
+ '',
+ '@switch (condition) {',
+ '@case (caseA) {',
+ 'Case A.',
+ '}',
+ '@case (caseB) {',
+ 'Case B.',
+ '}',
+ '@default {',
+ 'Default case.',
+ '}',
+ '}'
+ ]
+ }, {
+ unchanged: [
+ '@if( {value: true}; as val) {',
+ '{{val.value}}
',
+ '}'
+ ]
+ }, {
+ input: [
+ '@if( {value: true}; as val) {',
+ '',
+ '@defer {',
+ '{{val.value}}',
+ '}',
+ '
',
+ '}'
+ ],
+ output: [
+ '@if( {value: true}; as val) {',
+ '',
+ ' @defer {',
+ ' {{val.value}}',
+ ' }',
+ '
',
+ '}'
+ ]
+ }, {
+ unchanged: [
+ ' @if(true) { {{"{}" + " }"}} }
'
+ ]
+ }, {
+ input: [
+ '',
+ '@for (item of items; track item.id; let idx = $index, e = $even) {',
+ 'Item #{{ idx }}: {{ item.name }}',
+ '
',
+ 'Item #{{ idx }}: {{ item.name }}',
+ '
',
+ '}',
+ '
'
+ ],
+ output: [
+ '',
+ ' @for (item of items; track item.id; let idx = $index, e = $even) {',
+ ' Item #{{ idx }}: {{ item.name }}',
+ '
',
+ ' Item #{{ idx }}: {{ item.name }}',
+ '
',
+ ' }',
+ '
'
+ ]
}]
}, {
name: "New Test Suite"
From 0deb3005fd69559f15a4a47757cc6be41a1798aa Mon Sep 17 00:00:00 2001
From: Liam Newman
Date: Wed, 31 Jan 2024 11:29:34 -0800
Subject: [PATCH 9/9] Changing angular at-string detection regex
Limiting this to a smaller set.
---
js/src/html/tokenizer.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/js/src/html/tokenizer.js b/js/src/html/tokenizer.js
index 231f674af..764c60ae5 100644
--- a/js/src/html/tokenizer.js
+++ b/js/src/html/tokenizer.js
@@ -69,7 +69,7 @@ var Tokenizer = function(input_string, options) {
attribute: templatable_reader.until(/[\n\r\t =>]|\/>/),
element_name: templatable_reader.until(/[\n\r\t >\/]/),
- angular_control_flow_start: pattern_reader.matching(/\@[^\n\t ][^({]*[({]/),
+ angular_control_flow_start: pattern_reader.matching(/\@[a-zA-Z]+[^({]*[({]/),
handlebars_comment: pattern_reader.starting_with(/{{!--/).until_after(/--}}/),
handlebars: pattern_reader.starting_with(/{{/).until_after(/}}/),
handlebars_open: pattern_reader.until(/[\n\r\t }]/),