diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad57ebb57..fbfea2b75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ Also, check your change needs any tooling updates. For example, the CDN urls re ### 4. Submit a Pull Request * Run `./build full` locally after commit but before creation of Pull Request. You may start a Pull Request if this does not succeed, but the PR will not be accepted without additional changes. -* Include description of changea. Include examples of input and expected output if possible. +* Include description of changes. Include examples of input and expected output if possible. * Pull requests must pass build checks on all platforms before being accepted. We use travis-ci and appveyor to run tests on Linux and Windows, across multiple versions of Node.js and Python. # Folders diff --git a/js/lib/beautify-css.js b/js/lib/beautify-css.js index b18d61df9..2c5e081a1 100644 --- a/js/lib/beautify-css.js +++ b/js/lib/beautify-css.js @@ -41,6 +41,7 @@ The options are (default in brackets): indent_size (4) — indentation size, indent_char (space) — character to indent with, + preserve_newlines (default false) - whether existing line breaks should be preserved, selector_separator_newline (true) - separate selectors with newline or not (e.g. "a,\nbr" or "a, br") end_with_newline (false) - end with a newline @@ -98,6 +99,7 @@ var indentSize = options.indent_size ? parseInt(options.indent_size, 10) : 4; var indentCharacter = options.indent_char || ' '; + var preserve_newlines = (options.preserve_newlines === undefined) ? false : options.preserve_newlines; var selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline; var end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline; var newline_between_rules = (options.newline_between_rules === undefined) ? true : options.newline_between_rules; @@ -234,6 +236,16 @@ return false; } + function removeWhiteSpaceOnEmptyLines(input) { + var output = input.split('\n'); + for (var i = 0; i < output.length; i++) { + if (output[i].trim().length === 0) { + output[i] = ''; + } + } + return output.join('\n'); + } + // printer var basebaseIndentString = source_text.match(/^[\t ]*/)[0]; var singleIndent = new Array(indentSize + 1).join(indentCharacter); @@ -311,6 +323,8 @@ var whitespace = skipWhitespace(); var isAfterSpace = whitespace !== ''; var isAfterNewline = whitespace.indexOf('\n') !== -1; + var newLines = whitespace.replace(/ /g, '').replace(/\t/g, ''); + var isAfterEmptyline = newLines.indexOf('\n\n') !== -1; last_top_ch = top_ch; top_ch = ch; @@ -490,7 +504,15 @@ ch = '='; output.push(ch); } else { - print.preserveSingleSpace(); + if (isAfterEmptyline && preserve_newlines) { + var newLineCount = newLines.split('\n').length - 2; + for (var i = 0; i < newLineCount; i++) { + print.newLine(true); + } + eatWhitespace(); + } else { + print.preserveSingleSpace(); + } output.push(ch); } } @@ -503,6 +525,10 @@ sweetCode += output.join('').replace(/[\r\n\t ]+$/, ''); + if (preserve_newlines) { + sweetCode = removeWhiteSpaceOnEmptyLines(sweetCode); + } + // establish end_with_newline if (end_with_newline) { sweetCode += '\n'; diff --git a/js/lib/cli.js b/js/lib/cli.js index f959c16d2..48fc55df5 100755 --- a/js/lib/cli.js +++ b/js/lib/cli.js @@ -360,8 +360,9 @@ function usage(err) { msg.push(' -E, --extra_liners List of tags (defaults to [head,body,/html] that should have an extra newline'); break; case "css": - msg.push(' -L, --selector-separator-newline Add a newline between multiple selectors.'); - msg.push(' -N, --newline-between-rules Add a newline between CSS rules.'); + msg.push(' -L, --selector-separator-newline Add a newline between multiple selectors.'); + msg.push(' -N, --newline-between-rules Add a newline between CSS rules.'); + msg.push(' -p, --preserve-newlines Preserve line-breaks'); } if (err) { diff --git a/js/test/generated/beautify-css-tests.js b/js/test/generated/beautify-css-tests.js index 608ce9f26..7b6d95611 100644 --- a/js/test/generated/beautify-css-tests.js +++ b/js/test/generated/beautify-css-tests.js @@ -53,6 +53,7 @@ function run_css_tests(test_obj, Urlencoded, js_beautify, html_beautify, css_bea default_opts.end_with_newline = false; default_opts.newline_between_rules = false; default_opts.space_around_combinator = false; + default_opts.preserve_newlines = false; default_opts.space_around_selector_separator = false; function reset_options() @@ -324,6 +325,27 @@ function run_css_tests(test_obj, Urlencoded, js_beautify, html_beautify, css_bea t('a:first-child,a:first-child{color:red;div:first-child,div:hover{color:black;}}', 'a:first-child,\na:first-child {\n\tcolor: red;\n\tdiv:first-child,\n\tdiv:hover {\n\t\tcolor: black;\n\t}\n}'); + //============================================================ + // Preserve Newlines - (separator_input = "\n\n", separator_output = "\n\n") + reset_options(); + opts.preserve_newlines = true; + t('.div {}\n\n.span {}'); + t('#bla, #foo{\n\tcolor:black;\n\n\tfont-size: 12px;\n}', '#bla,\n#foo {\n\tcolor: black;\n\n\tfont-size: 12px;\n}'); + + // Preserve Newlines - (separator_input = "\n\n", separator_output = "\n") + reset_options(); + opts.preserve_newlines = false; + t('.div {}\n\n.span {}', '.div {}\n.span {}'); + t('#bla, #foo{\n\tcolor:black;\n\n\tfont-size: 12px;\n}', '#bla,\n#foo {\n\tcolor: black;\n\tfont-size: 12px;\n}'); + + + //============================================================ + // Preserve Newlines and add tabs + reset_options(); + opts.preserve_newlines = true; + t('.tool-tip {\n\tposition: relative;\n\n\t\t\n\t.tool-tip-content {\n\t\t&>* {\n\t\t\tmargin-top: 0;\n\t\t}\n\t\t\n\n\t\t.mixin-box-shadow(.2rem .2rem .5rem rgba(0, 0, 0, .15));\n\t\tpadding: 1rem;\n\t\tposition: absolute;\n\t\tz-index: 10;\n\t}\n}', '.tool-tip {\n\tposition: relative;\n\n\n\t.tool-tip-content {\n\t\t&>* {\n\t\t\tmargin-top: 0;\n\t\t}\n\n\n\t\t.mixin-box-shadow(.2rem .2rem .5rem rgba(0, 0, 0, .15));\n\t\tpadding: 1rem;\n\t\tposition: absolute;\n\t\tz-index: 10;\n\t}\n}'); + + //============================================================ // Newline Between Rules - (separator = "\n") reset_options(); diff --git a/python/cssbeautifier/__init__.py b/python/cssbeautifier/__init__.py index 56c05ff4f..0482b958c 100644 --- a/python/cssbeautifier/__init__.py +++ b/python/cssbeautifier/__init__.py @@ -35,6 +35,7 @@ def __init__(self): self.indent_size = 4 self.indent_char = ' ' self.indent_with_tabs = False + self.preserve_newlines = False self.selector_separator_newline = True self.end_with_newline = False self.newline_between_rules = True @@ -65,11 +66,12 @@ def __repr__(self): """indent_size = %d indent_char = [%s] indent_with_tabs = [%s] +preserve_newlines = [%s] separate_selectors_newline = [%s] end_with_newline = [%s] newline_between_rules = [%s] space_around_combinator = [%s] -""" % (self.indent_size, self.indent_char, self.indent_with_tabs, +""" % (self.indent_size, self.indent_char, self.indent_with_tabs, self.preserve_newlines, self.selector_separator_newline, self.end_with_newline, self.newline_between_rules, self.space_around_combinator) @@ -329,6 +331,13 @@ def foundNestedPseudoClass(self): return False + def removeWhiteSpaceOnEmptyLines(self, input): + output = input.split('\n') + for i in range(len(output)): + if len(output[i].strip()) == 0: + output[i] = '' + + return '\n'.join(output) def beautify(self): m = re.search("^[\t ]*", self.source_text) @@ -346,6 +355,8 @@ def beautify(self): whitespace = self.skipWhitespace() isAfterSpace = whitespace != '' isAfterNewline = '\n' in whitespace + newLines = whitespace.replace(" ", "").replace("\t", "") + isAfterEmptyline = '\n\n' in newLines last_top_ch = top_ch top_ch = self.ch @@ -510,11 +521,21 @@ def beautify(self): self.ch = '=' printer.push(self.ch) else: - printer.preserveSingleSpace(isAfterSpace) + if isAfterEmptyline and self.opts.preserve_newlines: + newLineCount = range(len(newLines.split('\n')) - 2) + for i in newLineCount: + printer.newLine(True) + + self.eatWhitespace() + else: + printer.preserveSingleSpace(isAfterSpace) printer.push(self.ch) sweet_code = re.sub('[\r\n\t ]+$', '', printer.result()) + if self.opts.preserve_newlines: + sweet_code = self.removeWhiteSpaceOnEmptyLines(sweet_code) + # establish end_with_newline if self.opts.end_with_newline: sweet_code += '\n' diff --git a/python/cssbeautifier/tests/generated/tests.py b/python/cssbeautifier/tests/generated/tests.py index bc7286048..2d7224ebf 100644 --- a/python/cssbeautifier/tests/generated/tests.py +++ b/python/cssbeautifier/tests/generated/tests.py @@ -58,6 +58,7 @@ def setUpClass(cls): default_options.end_with_newline = false default_options.newline_between_rules = false default_options.space_around_combinator = false + default_options.preserve_newlines = false default_options.space_around_selector_separator = false cls.default_options = default_options @@ -282,6 +283,27 @@ def testGenerated(self): t('a:first-child,a:first-child{color:red;div:first-child,div:hover{color:black;}}', 'a:first-child,\na:first-child {\n\tcolor: red;\n\tdiv:first-child,\n\tdiv:hover {\n\t\tcolor: black;\n\t}\n}') + #============================================================ + # Preserve Newlines - (separator_input = "\n\n", separator_output = "\n\n") + self.reset_options(); + self.options.preserve_newlines = true + t('.div {}\n\n.span {}') + t('#bla, #foo{\n\tcolor:black;\n\n\tfont-size: 12px;\n}', '#bla,\n#foo {\n\tcolor: black;\n\n\tfont-size: 12px;\n}') + + # Preserve Newlines - (separator_input = "\n\n", separator_output = "\n") + self.reset_options(); + self.options.preserve_newlines = false + t('.div {}\n\n.span {}', '.div {}\n.span {}') + t('#bla, #foo{\n\tcolor:black;\n\n\tfont-size: 12px;\n}', '#bla,\n#foo {\n\tcolor: black;\n\tfont-size: 12px;\n}') + + + #============================================================ + # Preserve Newlines and add tabs + self.reset_options(); + self.options.preserve_newlines = true + t('.tool-tip {\n\tposition: relative;\n\n\t\t\n\t.tool-tip-content {\n\t\t&>* {\n\t\t\tmargin-top: 0;\n\t\t}\n\t\t\n\n\t\t.mixin-box-shadow(.2rem .2rem .5rem rgba(0, 0, 0, .15));\n\t\tpadding: 1rem;\n\t\tposition: absolute;\n\t\tz-index: 10;\n\t}\n}', '.tool-tip {\n\tposition: relative;\n\n\n\t.tool-tip-content {\n\t\t&>* {\n\t\t\tmargin-top: 0;\n\t\t}\n\n\n\t\t.mixin-box-shadow(.2rem .2rem .5rem rgba(0, 0, 0, .15));\n\t\tpadding: 1rem;\n\t\tposition: absolute;\n\t\tz-index: 10;\n\t}\n}') + + #============================================================ # Newline Between Rules - (separator = "\n") self.reset_options(); diff --git a/test/data/css/tests.js b/test/data/css/tests.js index 9aa4a2f71..5759d3f65 100644 --- a/test/data/css/tests.js +++ b/test/data/css/tests.js @@ -32,6 +32,7 @@ exports.test_data = { { name: "end_with_newline", value: "false" }, { name: "newline_between_rules", value: "false" }, { name: "space_around_combinator", value: "false" }, + { name: "preserve_newlines", value: "false" }, // deprecated { name: "space_around_selector_separator", value: "false" } ], @@ -208,6 +209,34 @@ exports.test_data = { output: 'a:first-child,{{separator}}a:first-child {\n\tcolor: red;\n\tdiv:first-child,{{separator1}}div:hover {\n\t\tcolor: black;\n\t}\n}' } ] + }, { + name: "Preserve Newlines", + description: "", + matrix: [{ + options: [ + { name: "preserve_newlines", value: "true" } + ], + separator_input: '\\n\\n', + separator_output: '\\n\\n', + }, { + options: [ + { name: "preserve_newlines", value: "false" } + ], + separator_input: '\\n\\n', + separator_output: '\\n', + }], + tests: [ + { input: '.div {}{{separator_input}}.span {}', output: '.div {}{{separator_output}}.span {}' }, + { input: '#bla, #foo{\n\tcolor:black;{{separator_input}}\tfont-size: 12px;\n}', output: '#bla,\n#foo {\n\tcolor: black;{{separator_output}}\tfont-size: 12px;\n}' } + ], + }, { + name: "Preserve Newlines and add tabs", + options: [{ name: "preserve_newlines", value: "true" }], + description: "", + tests: [{ + input: '.tool-tip {\n\tposition: relative;\n\n\t\t\n\t.tool-tip-content {\n\t\t&>* {\n\t\t\tmargin-top: 0;\n\t\t}\n\t\t\n\n\t\t.mixin-box-shadow(.2rem .2rem .5rem rgba(0, 0, 0, .15));\n\t\tpadding: 1rem;\n\t\tposition: absolute;\n\t\tz-index: 10;\n\t}\n}', + output: '.tool-tip {\n\tposition: relative;\n\n\n\t.tool-tip-content {\n\t\t&>* {\n\t\t\tmargin-top: 0;\n\t\t}\n\\n\\n\t\t.mixin-box-shadow(.2rem .2rem .5rem rgba(0, 0, 0, .15));\n\t\tpadding: 1rem;\n\t\tposition: absolute;\n\t\tz-index: 10;\n\t}\n}' + }], }, { name: "Newline Between Rules", description: "",