diff --git a/README.md b/README.md
index 80d31d361..bf9365ee6 100644
--- a/README.md
+++ b/README.md
@@ -368,6 +368,7 @@ HTML Beautifier Options:
-S, --indent-scripts [keep|separate|normal] ["normal"]
-w, --wrap-line-length Maximum characters per line (0 disables) [250]
-A, --wrap-attributes Wrap attributes to new lines [auto|force|force-aligned|force-expand-multiline|aligned-multiple|preserve|preserve-aligned] ["auto"]
+ -M, --wrap-attributes-min-attrs Minimum number of html tag attributes for force wrap attribute options [2]
-i, --wrap-attributes-indent-size Indent wrapped attributes to after N characters [indent-size] (ignored if wrap-attributes is "aligned")
-d, --inline List of tags to be considered inline tags
-U, --unformatted List of tags (defaults to inline) that should not be reformatted
diff --git a/js/src/cli.js b/js/src/cli.js
index 40b456893..71d478eca 100755
--- a/js/src/cli.js
+++ b/js/src/cli.js
@@ -99,6 +99,7 @@ var path = require('path'),
"unescape_strings": Boolean,
"wrap_line_length": Number,
"wrap_attributes": ["auto", "force", "force-aligned", "force-expand-multiline", "aligned-multiple", "preserve", "preserve-aligned"],
+ "wrap_attributes_min_attrs": Number,
"wrap_attributes_indent_size": Number,
"e4x": Boolean,
"end_with_newline": Boolean,
@@ -163,6 +164,7 @@ var path = require('path'),
"N": ["--newline_between_rules"],
// HTML-only
"A": ["--wrap_attributes"],
+ "M": ["--wrap_attributes_min_attrs"],
"i": ["--wrap_attributes_indent_size"],
"W": ["--max_char"], // obsolete since 1.3.5
"d": ["--inline"],
@@ -395,6 +397,7 @@ function usage(err) {
msg.push(' -S, --indent-scripts [keep|separate|normal] ["normal"]');
msg.push(' -w, --wrap-line-length Wrap lines that exceed N characters [0]');
msg.push(' -A, --wrap-attributes Wrap html tag attributes to new lines [auto|force|force-aligned|force-expand-multiline|aligned-multiple|preserve|preserve-aligned] ["auto"]');
+ msg.push(' -M, --wrap-attributes-min-attrs Minimum number of html tag attributes for force wrap attribute options [2]');
msg.push(' -i, --wrap-attributes-indent-size Indent wrapped tags to after N characters [indent-level]');
msg.push(' -p, --preserve-newlines Preserve line-breaks (--no-preserve-newlines disables)');
msg.push(' -m, --max-preserve-newlines Number of line-breaks to be preserved in one chunk [10]');
diff --git a/js/src/html/beautifier.js b/js/src/html/beautifier.js
index f6decd381..f678b5062 100644
--- a/js/src/html/beautifier.js
+++ b/js/src/html/beautifier.js
@@ -296,11 +296,11 @@ Beautifier.prototype.beautify = function() {
while (raw_token.type !== TOKEN.EOF) {
if (raw_token.type === TOKEN.TAG_OPEN || raw_token.type === TOKEN.COMMENT) {
- parser_token = this._handle_tag_open(printer, raw_token, last_tag_token, last_token);
+ parser_token = this._handle_tag_open(printer, raw_token, last_tag_token, last_token, tokens);
last_tag_token = parser_token;
} else if ((raw_token.type === TOKEN.ATTRIBUTE || raw_token.type === TOKEN.EQUALS || raw_token.type === TOKEN.VALUE) ||
(raw_token.type === TOKEN.TEXT && !last_tag_token.tag_complete)) {
- parser_token = this._handle_inside_tag(printer, raw_token, last_tag_token, tokens);
+ parser_token = this._handle_inside_tag(printer, raw_token, last_tag_token, last_token);
} else if (raw_token.type === TOKEN.TAG_CLOSE) {
parser_token = this._handle_tag_close(printer, raw_token, last_tag_token);
} else if (raw_token.type === TOKEN.TEXT) {
@@ -357,7 +357,7 @@ Beautifier.prototype._handle_tag_close = function(printer, raw_token, last_tag_t
return parser_token;
};
-Beautifier.prototype._handle_inside_tag = function(printer, raw_token, last_tag_token, tokens) {
+Beautifier.prototype._handle_inside_tag = function(printer, raw_token, last_tag_token, last_token) {
var wrapped = last_tag_token.has_wrapped_attrs;
var parser_token = {
text: raw_token.text,
@@ -378,7 +378,6 @@ Beautifier.prototype._handle_inside_tag = function(printer, raw_token, last_tag_
} else {
if (raw_token.type === TOKEN.ATTRIBUTE) {
printer.set_space_before_token(true);
- last_tag_token.attr_count += 1;
} else if (raw_token.type === TOKEN.EQUALS) { //no space before =
printer.set_space_before_token(false);
} else if (raw_token.type === TOKEN.VALUE && raw_token.previous.type === TOKEN.EQUALS) { //no space before value
@@ -391,29 +390,15 @@ Beautifier.prototype._handle_inside_tag = function(printer, raw_token, last_tag_
wrapped = wrapped || raw_token.newlines !== 0;
}
-
- if (this._is_wrap_attributes_force) {
- var force_attr_wrap = last_tag_token.attr_count > 1;
- if (this._is_wrap_attributes_force_expand_multiline && last_tag_token.attr_count === 1) {
- var is_only_attribute = true;
- var peek_index = 0;
- var peek_token;
- do {
- peek_token = tokens.peek(peek_index);
- if (peek_token.type === TOKEN.ATTRIBUTE) {
- is_only_attribute = false;
- break;
- }
- peek_index += 1;
- } while (peek_index < 4 && peek_token.type !== TOKEN.EOF && peek_token.type !== TOKEN.TAG_CLOSE);
-
- force_attr_wrap = !is_only_attribute;
- }
-
- if (force_attr_wrap) {
- printer.print_newline(false);
- wrapped = true;
- }
+ // Wrap for 'force' options, and if the number of attributes is at least that specified in 'wrap_attributes_min_attrs':
+ // 1. always wrap the second and beyond attributes
+ // 2. wrap the first attribute only if 'force-expand-multiline' is specified
+ if (this._is_wrap_attributes_force &&
+ last_tag_token.attr_count >= this._options.wrap_attributes_min_attrs &&
+ (last_token.type !== TOKEN.TAG_OPEN || // ie. second attribute and beyond
+ this._is_wrap_attributes_force_expand_multiline)) {
+ printer.print_newline(false);
+ wrapped = true;
}
}
printer.print_token(raw_token);
@@ -542,12 +527,12 @@ Beautifier.prototype._print_custom_beatifier_text = function(printer, raw_token,
}
};
-Beautifier.prototype._handle_tag_open = function(printer, raw_token, last_tag_token, last_token) {
+Beautifier.prototype._handle_tag_open = function(printer, raw_token, last_tag_token, last_token, tokens) {
var parser_token = this._get_tag_open_token(raw_token);
if ((last_tag_token.is_unformatted || last_tag_token.is_content_unformatted) &&
!last_tag_token.is_empty_element &&
- raw_token.type === TOKEN.TAG_OPEN && raw_token.text.indexOf('') === 0) {
+ raw_token.type === TOKEN.TAG_OPEN && !parser_token.is_start_tag) {
// End element tags for unformatted or content_unformatted elements
// are printed raw to keep any newlines inside them exactly the same.
printer.add_raw_token(raw_token);
@@ -561,6 +546,19 @@ Beautifier.prototype._handle_tag_open = function(printer, raw_token, last_tag_to
printer.print_token(raw_token);
}
+ // count the number of attributes
+ if (parser_token.is_start_tag && this._is_wrap_attributes_force) {
+ var peek_index = 0;
+ var peek_token;
+ do {
+ peek_token = tokens.peek(peek_index);
+ if (peek_token.type === TOKEN.ATTRIBUTE) {
+ parser_token.attr_count += 1;
+ }
+ peek_index += 1;
+ } while (peek_token.type !== TOKEN.EOF && peek_token.type !== TOKEN.TAG_CLOSE);
+ }
+
//indent attributes an auto, forced, aligned or forced-align line-wrap
if (this._is_wrap_attributes_force_aligned || this._is_wrap_attributes_aligned_multiple || this._is_wrap_attributes_preserve_aligned) {
parser_token.alignment_size = raw_token.text.length + 1;
diff --git a/js/src/html/options.js b/js/src/html/options.js
index 7df089ac4..3965e5cf5 100644
--- a/js/src/html/options.js
+++ b/js/src/html/options.js
@@ -43,6 +43,7 @@ function Options(options) {
this.indent_handlebars = this._get_boolean('indent_handlebars', true);
this.wrap_attributes = this._get_selection('wrap_attributes',
['auto', 'force', 'force-aligned', 'force-expand-multiline', 'aligned-multiple', 'preserve', 'preserve-aligned']);
+ this.wrap_attributes_min_attrs = this._get_number('wrap_attributes_min_attrs', 2);
this.wrap_attributes_indent_size = this._get_number('wrap_attributes_indent_size', this.indent_size);
this.extra_liners = this._get_array('extra_liners', ['head', 'body', '/html']);
diff --git a/test/data/html/tests.js b/test/data/html/tests.js
index 1bfdc8aff..062f5398b 100644
--- a/test/data/html/tests.js
+++ b/test/data/html/tests.js
@@ -1193,6 +1193,105 @@ exports.test_data = {
'^^^indent_attr$$$(typeaheadOnSelect)="onSuggestionSelected($event)" />'
]
}]
+ }, {
+ name: "Test wrap_attributes_min_attrs with force/force-xx options",
+ description: "",
+ matrix: [{
+ options: [
+ { name: "wrap_attributes", value: "'force'" },
+ { name: "wrap_attributes_min_attrs", value: "4" }
+ ],
+ indent_attr: ' ',
+ indent_attr_first: ' ',
+ indent_end: ' ',
+ newline_end: ''
+ }, {
+ options: [
+ { name: "wrap_attributes", value: "'force-aligned'" },
+ { name: "wrap_attributes_min_attrs", value: "4" }
+ ],
+ indent_attr: ' ',
+ indent_attr_first: ' ',
+ indent_end: ' ',
+ newline_end: ''
+ }, {
+ options: [
+ { name: "wrap_attributes", value: "'force-expand-multiline'" },
+ { name: "wrap_attributes_min_attrs", value: "4" }
+ ],
+ indent_attr: ' ',
+ indent_attr_first: '\n ',
+ indent_end: '\n',
+ newline_end: '\n'
+ }],
+ tests: [{
+ input: [
+ ''
+ ],
+ output: [
+ ''
+ ]
+ }, {
+ input: [
+ ''
+ ],
+ output: ''
+ }, {
+ input: [
+ '',
+ 'lorem ipsum
',
+ ''
+ ],
+ output: [
+ '',
+ ' lorem ipsum
',
+ ''
+ ]
+ }]
+ }, {
+ name: "Test wrap_attributes_min_attrs = 1 with force/force-xx options",
+ description: "",
+ matrix: [{
+ // Should not wrap, by design
+ options: [
+ { name: "wrap_attributes", value: "'force'" },
+ { name: "wrap_attributes_min_attrs", value: "1" }
+ ],
+ indent_attr: ' ',
+ newline_end: ' '
+ }, {
+ // Should not wrap, by design
+ options: [
+ { name: "wrap_attributes", value: "'force-aligned'" },
+ { name: "wrap_attributes_min_attrs", value: "1" }
+ ],
+ indent_attr: ' ',
+ newline_end: ' '
+ }, {
+ // Should wrap
+ options: [
+ { name: "wrap_attributes", value: "'force-expand-multiline'" },
+ { name: "wrap_attributes_min_attrs", value: "1" }
+ ],
+ indent_attr: '\n ',
+ newline_end: '\n'
+ }],
+ tests: [{
+ input: [
+ ''
+ ],
+ output: ''
+ }]
}, {
name: "Handlebars Indenting Off",
description: "Test handlebar behavior when indenting is off",