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 }]/),