From 59bf9908e40a96a608cc1ec5350cd082ae1ce1bd Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Thu, 2 Aug 2018 14:31:54 +0200 Subject: [PATCH] Fixes #964 - allows for configurable line breaks. Use `{ format: { breakWith: 'lf' } }` option to configure what line break looks like, allows `'crlf'` or `'lf'`, defaults to current system one so former on Windows and latter on Unix. --- History.md | 1 + README.md | 2 ++ lib/options/format.js | 28 ++++++++++++++++++++++++++++ lib/writer/helpers.js | 13 ++++++------- lib/writer/simple.js | 6 ++---- lib/writer/source-maps.js | 5 ++--- test/integration-test.js | 16 ++++++++++++++++ test/options/format-test.js | 12 +++++++++++- 8 files changed, 68 insertions(+), 15 deletions(-) diff --git a/History.md b/History.md index e2bc54121..3d82a7e81 100644 --- a/History.md +++ b/History.md @@ -5,6 +5,7 @@ * Fixed issue [#861](https://github.com/jakubpawlowicz/clean-css/issues/861) - new `transition` property optimizer. * Fixed issue [#895](https://github.com/jakubpawlowicz/clean-css/issues/895) - ignoring specific styles. * Fixed issue [#947](https://github.com/jakubpawlowicz/clean-css/issues/947) - selector based filtering. +* Fixed issue [#964](https://github.com/jakubpawlowicz/clean-css/issues/964) - adds configurable line breaks. * Fixed issue [#986](https://github.com/jakubpawlowicz/clean-css/issues/986) - level 2 optimizations and CSS 4 colors. * Fixed issue [#1000](https://github.com/jakubpawlowicz/clean-css/issues/1000) - carriage return handling in tokenizer. * Fixed issue [#1038](https://github.com/jakubpawlowicz/clean-css/issues/1038) - `font-variation-settings` quoting. diff --git a/README.md b/README.md index 9d44ca670..45b2e59f3 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ clean-css 4.2 will introduce the following changes / features: * new `transition` property optimizer; * preserves any CSS content between `/* clean-css ignore:start */` and `/* clean-css ignore:end */` comments; * allows filtering based on selector in `transform` callback, see [example](#how-to-apply-arbitrary-transformations-to-css-properties); +* adds configurable line breaks via `format: { breakWith: 'lf' }` option; ## Constructor options @@ -264,6 +265,7 @@ new CleanCSS({ beforeBlockEnds: false, // controls if a line break comes before a block ends; defaults to `false` betweenSelectors: false // controls if a line break comes between selectors; defaults to `false` }, + breakWith: '\n', // controls the new line character, can be `'\r\n'` or `'\n'` (aliased as `'windows'` and `'unix'` or `'crlf'` and `'lf'`); defaults to system one, so former on Windows and latter on Unix indentBy: 0, // controls number of characters to indent with; defaults to `0` indentWith: 'space', // controls a character to indent with, can be `'space'` or `'tab'`; defaults to `'space'` spaces: { // controls where to insert spaces diff --git a/lib/options/format.js b/lib/options/format.js index e6416899f..3474862ff 100644 --- a/lib/options/format.js +++ b/lib/options/format.js @@ -1,3 +1,5 @@ +var systemLineBreak = require('os').EOL; + var override = require('../utils/override'); var Breaks = { @@ -12,6 +14,12 @@ var Breaks = { BetweenSelectors: 'betweenSelectors' }; +var BreakWith = { + CarriageReturnLineFeed: '\r\n', + LineFeed: '\n', + System: systemLineBreak +}; + var IndentWith = { Space: ' ', Tab: '\t' @@ -25,6 +33,7 @@ var Spaces = { var DEFAULTS = { breaks: breaks(false), + breakWith: BreakWith.System, indentBy: 0, indentWith: IndentWith.Space, spaces: spaces(false), @@ -76,6 +85,10 @@ function formatFrom(source) { return false; } + if (typeof source == 'object' && 'breakWith' in source) { + source = override(source, { breakWith: mapBreakWith(source.breakWith) }); + } + if (typeof source == 'object' && 'indentBy' in source) { source = override(source, { indentBy: parseInt(source.indentBy) }); } @@ -168,6 +181,21 @@ function normalizeValue(value) { } } +function mapBreakWith(value) { + switch (value) { + case 'windows': + case 'crlf': + case BreakWith.CarriageReturnLineFeed: + return BreakWith.CarriageReturnLineFeed; + case 'unix': + case 'lf': + case BreakWith.LineFeed: + return BreakWith.LineFeed; + default: + return systemLineBreak; + } +} + function mapIndentWith(value) { switch (value) { case 'space': diff --git a/lib/writer/helpers.js b/lib/writer/helpers.js index 3ee2642a8..11727402c 100644 --- a/lib/writer/helpers.js +++ b/lib/writer/helpers.js @@ -1,4 +1,3 @@ -var lineBreak = require('os').EOL; var emptyCharacter = ''; var Breaks = require('../options/format').Breaks; @@ -153,7 +152,7 @@ function openBrace(context, where, needsPrefixSpace) { context.indentWith = context.format.indentWith.repeat(context.indentBy); return (needsPrefixSpace && allowsSpace(context, Spaces.BeforeBlockBegins) ? Marker.SPACE : emptyCharacter) + Marker.OPEN_CURLY_BRACKET + - (allowsBreak(context, where) ? lineBreak : emptyCharacter) + + (allowsBreak(context, where) ? context.format.breakWith : emptyCharacter) + context.indentWith; } else { return Marker.OPEN_CURLY_BRACKET; @@ -164,10 +163,10 @@ function closeBrace(context, where, beforeBlockEnd, isLast) { if (context.format) { context.indentBy -= context.format.indentBy; context.indentWith = context.format.indentWith.repeat(context.indentBy); - return (allowsBreak(context, Breaks.AfterProperty) || beforeBlockEnd && allowsBreak(context, Breaks.BeforeBlockEnds) ? lineBreak : emptyCharacter) + + return (allowsBreak(context, Breaks.AfterProperty) || beforeBlockEnd && allowsBreak(context, Breaks.BeforeBlockEnds) ? context.format.breakWith : emptyCharacter) + context.indentWith + Marker.CLOSE_CURLY_BRACKET + - (isLast ? emptyCharacter : (allowsBreak(context, where) ? lineBreak : emptyCharacter) + context.indentWith); + (isLast ? emptyCharacter : (allowsBreak(context, where) ? context.format.breakWith : emptyCharacter) + context.indentWith); } else { return Marker.CLOSE_CURLY_BRACKET; } @@ -181,13 +180,13 @@ function colon(context) { function semicolon(context, where, isLast) { return context.format ? - Marker.SEMICOLON + (isLast || !allowsBreak(context, where) ? emptyCharacter : lineBreak + context.indentWith) : + Marker.SEMICOLON + (isLast || !allowsBreak(context, where) ? emptyCharacter : context.format.breakWith + context.indentWith) : Marker.SEMICOLON; } function comma(context) { return context.format ? - Marker.COMMA + (allowsBreak(context, Breaks.BetweenSelectors) ? lineBreak : emptyCharacter) + context.indentWith : + Marker.COMMA + (allowsBreak(context, Breaks.BetweenSelectors) ? context.format.breakWith : emptyCharacter) + context.indentWith : Marker.COMMA; } @@ -220,7 +219,7 @@ function all(context, tokens) { break; case Token.COMMENT: store(context, token); - store(context, allowsBreak(context, Breaks.AfterComment) ? lineBreak : emptyCharacter); + store(context, allowsBreak(context, Breaks.AfterComment) ? context.format.breakWith : emptyCharacter); break; case Token.RAW: store(context, token); diff --git a/lib/writer/simple.js b/lib/writer/simple.js index 21e7f88a7..20fde2a29 100644 --- a/lib/writer/simple.js +++ b/lib/writer/simple.js @@ -1,7 +1,5 @@ var all = require('./helpers').all; -var lineBreak = require('os').EOL; - function store(serializeContext, token) { var value = typeof token == 'string' ? token : @@ -15,8 +13,8 @@ function store(serializeContext, token) { function wrap(serializeContext, value) { if (serializeContext.column + value.length > serializeContext.format.wrapAt) { - track(serializeContext, lineBreak); - serializeContext.output.push(lineBreak); + track(serializeContext, serializeContext.format.breakWith); + serializeContext.output.push(serializeContext.format.breakWith); } } diff --git a/lib/writer/source-maps.js b/lib/writer/source-maps.js index 4729eb055..6856579f0 100644 --- a/lib/writer/source-maps.js +++ b/lib/writer/source-maps.js @@ -1,7 +1,6 @@ var SourceMapGenerator = require('source-map').SourceMapGenerator; var all = require('./helpers').all; -var lineBreak = require('os').EOL; var isRemoteResource = require('../utils/is-remote-resource'); var isWindows = process.platform == 'win32'; @@ -23,8 +22,8 @@ function store(serializeContext, element) { function wrap(serializeContext, value) { if (serializeContext.column + value.length > serializeContext.format.wrapAt) { - track(serializeContext, lineBreak, false); - serializeContext.output.push(lineBreak); + track(serializeContext, serializeContext.format.breakWith, false); + serializeContext.output.push(serializeContext.format.breakWith); } } diff --git a/test/integration-test.js b/test/integration-test.js index bd7b12fce..7a97c723b 100644 --- a/test/integration-test.js +++ b/test/integration-test.js @@ -308,6 +308,22 @@ vows.describe('integration tests') ] }, { format: 'keep-breaks', level: { 1: { specialComments: 0 } } }) ) + .addBatch( + optimizerContext('CRLF line breaks', { + 'uses specified one': [ + '.block{color:red;display:block}', + '.block{color:red;\r\ndisplay:block\r\n}' + ] + }, { format: { breaks: { afterProperty: true }, breakWith: 'crlf' } }) + ) + .addBatch( + optimizerContext('LF line breaks', { + 'uses specified one': [ + '.block{color:red;display:block}', + '.block{color:red;\ndisplay:block\n}' + ] + }, { format: { breaks: { afterProperty: true }, breakWith: 'lf' } }) + ) .addBatch( optimizerContext('selectors', { 'not expand + in selectors mixed with calc methods': [ diff --git a/test/options/format-test.js b/test/options/format-test.js index 87a789d5f..15149e20f 100644 --- a/test/options/format-test.js +++ b/test/options/format-test.js @@ -1,4 +1,5 @@ var assert = require('assert'); +var systemLineBreak = require('os').EOL; var vows = require('vows'); @@ -39,6 +40,7 @@ vows.describe(formatFrom) beforeBlockEnds: false, betweenSelectors: false }, + breakWith: systemLineBreak, indentBy: 0, indentWith: ' ', spaces: { @@ -53,7 +55,7 @@ vows.describe(formatFrom) }, 'hash': { 'topic': function () { - return formatFrom({ breaks: { afterProperty: true }, indentBy: 1 }); + return formatFrom({ breaks: { afterProperty: true }, breakWith: '\r\n', indentBy: 1 }); }, 'is merged with default': function (formatOptions) { assert.deepEqual(formatOptions, { @@ -68,6 +70,7 @@ vows.describe(formatFrom) beforeBlockEnds: false, betweenSelectors: false }, + breakWith: '\r\n', indentBy: 1, indentWith: ' ', spaces: { @@ -97,6 +100,7 @@ vows.describe(formatFrom) beforeBlockEnds: false, betweenSelectors: false }, + breakWith: systemLineBreak, indentBy: 2, indentWith: ' ', spaces: { @@ -126,6 +130,7 @@ vows.describe(formatFrom) beforeBlockEnds: false, betweenSelectors: false }, + breakWith: systemLineBreak, indentBy: 0, indentWith: '\t', spaces: { @@ -155,6 +160,7 @@ vows.describe(formatFrom) beforeBlockEnds: false, betweenSelectors: false }, + breakWith: systemLineBreak, indentBy: 0, indentWith: '\t', spaces: { @@ -184,6 +190,7 @@ vows.describe(formatFrom) beforeBlockEnds: false, betweenSelectors: false }, + breakWith: systemLineBreak, indentBy: 3, indentWith: ' ', spaces: { @@ -213,6 +220,7 @@ vows.describe(formatFrom) beforeBlockEnds: false, betweenSelectors: false }, + breakWith: systemLineBreak, indentBy: 0, indentWith: '\t', spaces: { @@ -242,6 +250,7 @@ vows.describe(formatFrom) beforeBlockEnds: true, betweenSelectors: true }, + breakWith: systemLineBreak, indentBy: 2, indentWith: ' ', spaces: { @@ -271,6 +280,7 @@ vows.describe(formatFrom) beforeBlockEnds: true, betweenSelectors: false }, + breakWith: systemLineBreak, indentBy: 0, indentWith: ' ', spaces: {