diff --git a/README.md b/README.md index f62a24faa..ede1d7e97 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/beautifier.js b/js/src/html/beautifier.js index f862beaa5..32c882a57 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,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, 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 6c473af40..764c60ae5 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', @@ -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 }]/), @@ -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; @@ -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() { @@ -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); @@ -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(); @@ -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(); @@ -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 "/>" @@ -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]) { @@ -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); 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 diff --git a/test/data/html/tests.js b/test/data/html/tests.js index 291818ed2..ed4071807 100644 --- a/test/data/html/tests.js +++ b/test/data/html/tests.js @@ -3839,6 +3839,497 @@ 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', + '
    ', + ' }', + '
    ' + ] + }, { + 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', + '

    ', + '}' + ] + }, { + 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', + '
    ' + ] + }, { + 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" }]