Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add minimum attributes option for attribute wrap #1818

Merged
merged 7 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions js/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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]');
Expand Down
56 changes: 27 additions & 29 deletions js/src/html/beautifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions js/src/html/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);

Expand Down
99 changes: 99 additions & 0 deletions test/data/html/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
'<input type="four attributes should wrap" class="form-control" autocomplete="off"',
'[(ngModel)]="myValue" />'
],
output: [
'<input{{indent_attr_first}}type="four attributes should wrap"',
'{{indent_attr}}class="form-control"',
'{{indent_attr}}autocomplete="off"',
'{{indent_attr}}[(ngModel)]="myValue"{{indent_end}}/>'
]
}, {
input: [
'<input type="three attributes should not wrap" autocomplete="off"',
'[(ngModel)]="myValue" />'
],
output: '<input type="three attributes should not wrap" autocomplete="off" [(ngModel)]="myValue" />'
}, {
input: [
'<cmpnt v-bind:xx="four attributes with valueless attribute should wrap" ' +
'@someevent="dosomething" someprop',
'class="xx-button">',
'<div class="alert alert-info" style="margin-left: 1px;" role="alert">lorem ipsum</div>',
'</cmpnt>'
],
output: [
'<cmpnt{{indent_attr_first}}v-bind:xx="four attributes with valueless attribute should wrap"',
'{{indent_attr}}@someevent="dosomething"',
'{{indent_attr}}someprop',
'{{indent_attr}}class="xx-button"{{newline_end}}>',
' <div class="alert alert-info" style="margin-left: 1px;" role="alert">lorem ipsum</div>',
'</cmpnt>'
]
}]
}, {
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: [
'<input type="one attribute"/>'
],
output: '<input{{indent_attr}}type="one attribute"{{newline_end}}/>'
}]
}, {
name: "Handlebars Indenting Off",
description: "Test handlebar behavior when indenting is off",
Expand Down