Skip to content

Commit

Permalink
Fixed #2219 - formatting of new Angular control flow syntax (#2221)
Browse files Browse the repository at this point in the history
* Fixed #2219 - formatting of new Angular control flow syntax

* Add 'angular' templating option; use it for html beautifier control flow syntax

* Add more precise selection for angular control flow close tag

* Print angular control flow tokens with basic formatting

* Add tests for fixing issue #2219

* Change angular control flow selection to do via pattern

* Fix selecting control flow closing brace if it is not preceded by whitespace

* Fix regex for control flow start pattern; only select control flow open if indent_handlebars is true

* Changing angular at-string detection regex

Limiting this to a smaller set.

---------

Co-authored-by: Liam Newman <[email protected]>
  • Loading branch information
gergely-gyorgy-both and bitwiseman authored Feb 15, 2024
1 parent 29b51e0 commit 5aff965
Show file tree
Hide file tree
Showing 11 changed files with 601 additions and 20 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion js/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
];

Expand Down
4 changes: 2 additions & 2 deletions js/src/core/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion js/src/core/templatablepattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions js/src/html/beautifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -319,6 +330,38 @@ 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.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;
};

Beautifier.prototype._handle_control_flow_close = function(printer, raw_token) {
var parser_token = {
text: raw_token.text,
type: raw_token.type
};

printer.deindent();
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;
};

Beautifier.prototype._handle_tag_close = function(printer, raw_token, last_tag_token) {
var parser_token = {
text: raw_token.text,
Expand Down
2 changes: 1 addition & 1 deletion js/src/html/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
63 changes: 54 additions & 9 deletions js/src/html/tokenizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -61,11 +63,13 @@ 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 =>]|\/>/),
element_name: templatable_reader.until(/[\n\r\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 }]/),
Expand All @@ -79,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;
Expand All @@ -97,14 +102,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() {
Expand All @@ -123,8 +130,9 @@ 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, 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);
Expand Down Expand Up @@ -189,7 +197,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();
Expand All @@ -206,7 +214,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();
Expand All @@ -221,11 +229,48 @@ Tokenizer.prototype._read_open_handlebars = function(c, open_token) {
return 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 AND indenting is set
if (!this._options.templating.includes('angular') || !this._options.indent_handlebars) {
return token;
}

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) {
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 === '}' && open_token && open_token.type === TOKEN.CONTROL_FLOW_OPEN) {
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 "/>"
Expand Down Expand Up @@ -312,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]) {
Expand All @@ -321,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);
Expand Down
2 changes: 1 addition & 1 deletion python/jsbeautifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions python/jsbeautifier/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)

Expand Down
3 changes: 2 additions & 1 deletion python/jsbeautifier/core/templatablepattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(self):
self.handlebars = False
self.php = False
self.smarty = False
self.angular = False


class TemplatePatterns:
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 5aff965

Please sign in to comment.