Normalize quotes in CSS like in JS

master
Simon Lydell 2017-08-14 08:57:16 +02:00
parent d8b5fe2de9
commit 4979f58c15
12 changed files with 460 additions and 105 deletions

View File

@ -163,7 +163,7 @@ function genericPrint(path, options, print) {
return concat([n.value, " "]);
}
case "media-value": {
return n.value;
return adjustStrings(n.value, options);
}
case "media-keyword": {
return n.value;
@ -198,7 +198,7 @@ function genericPrint(path, options, print) {
"[",
n.attribute,
n.operator ? n.operator : "",
n.value ? n.value : "",
n.value ? adjustStrings(n.value, options) : "",
n.insensitive ? " i" : "",
"]"
]);
@ -337,11 +337,7 @@ function genericPrint(path, options, print) {
return concat([n.value, " "]);
}
case "value-string": {
return concat([
n.quoted ? n.raws.quote : "",
n.value,
n.quoted ? n.raws.quote : ""
]);
return util.printString(n.raws.quote + n.value + n.raws.quote, options);
}
case "value-atword": {
return concat(["@", n.value]);
@ -404,4 +400,10 @@ function printValue(value) {
return value;
}
const STRING_REGEX = /(['"])(?:(?!\1)[^\\]|\\[\s\S])*\1/g;
function adjustStrings(value, options) {
return value.replace(STRING_REGEX, match => util.printString(match, options));
}
module.exports = genericPrint;

View File

@ -4320,97 +4320,9 @@ function adjustClause(node, clause, forceSpace) {
function nodeStr(node, options, isFlowOrTypeScriptDirectiveLiteral) {
const raw = rawText(node);
// `rawContent` is the string exactly like it appeared in the input source
// code, with its enclosing quote.
const rawContent = raw.slice(1, -1);
const double = { quote: '"', regex: /"/g };
const single = { quote: "'", regex: /'/g };
const preferred = options.singleQuote ? single : double;
const alternate = preferred === single ? double : single;
let shouldUseAlternateQuote = false;
const isDirectiveLiteral =
isFlowOrTypeScriptDirectiveLiteral || node.type === "DirectiveLiteral";
let canChangeDirectiveQuotes = false;
// If `rawContent` contains at least one of the quote preferred for enclosing
// the string, we might want to enclose with the alternate quote instead, to
// minimize the number of escaped quotes.
// Also check for the alternate quote, to determine if we're allowed to swap
// the quotes on a DirectiveLiteral.
if (
rawContent.includes(preferred.quote) ||
rawContent.includes(alternate.quote)
) {
const numPreferredQuotes = (rawContent.match(preferred.regex) || []).length;
const numAlternateQuotes = (rawContent.match(alternate.regex) || []).length;
shouldUseAlternateQuote = numPreferredQuotes > numAlternateQuotes;
} else {
canChangeDirectiveQuotes = true;
}
const enclosingQuote =
options.parser === "json"
? double.quote
: shouldUseAlternateQuote ? alternate.quote : preferred.quote;
// Directives are exact code unit sequences, which means that you can't
// change the escape sequences they use.
// See https://github.com/prettier/prettier/issues/1555
// and https://tc39.github.io/ecma262/#directive-prologue
if (isDirectiveLiteral) {
if (canChangeDirectiveQuotes) {
return enclosingQuote + rawContent + enclosingQuote;
}
return raw;
}
// It might sound unnecessary to use `makeString` even if `node.raw` already
// is enclosed with `enclosingQuote`, but it isn't. `node.raw` could contain
// unnecessary escapes (such as in `"\'"`). Always using `makeString` makes
// sure that we consistently output the minimum amount of escaped quotes.
return makeString(rawContent, enclosingQuote);
}
function makeString(rawContent, enclosingQuote) {
const otherQuote = enclosingQuote === '"' ? "'" : '"';
// Matches _any_ escape and unescaped quotes (both single and double).
const regex = /\\([\s\S])|(['"])/g;
// Escape and unescape single and double quotes as needed to be able to
// enclose `rawContent` with `enclosingQuote`.
const newContent = rawContent.replace(regex, (match, escaped, quote) => {
// If we matched an escape, and the escaped character is a quote of the
// other type than we intend to enclose the string with, there's no need for
// it to be escaped, so return it _without_ the backslash.
if (escaped === otherQuote) {
return escaped;
}
// If we matched an unescaped quote and it is of the _same_ type as we
// intend to enclose the string with, it must be escaped, so return it with
// a backslash.
if (quote === enclosingQuote) {
return "\\" + quote;
}
if (quote) {
return quote;
}
// Unescape any unnecessarily escaped character.
// Adapted from https://github.com/eslint/eslint/blob/de0b4ad7bd820ade41b1f606008bea68683dc11a/lib/rules/no-useless-escape.js#L27
return /^[^\\nrvtbfux\r\n\u2028\u2029"'0-7]$/.test(escaped)
? escaped
: "\\" + escaped;
});
return enclosingQuote + newContent + enclosingQuote;
return util.printString(raw, options, isDirectiveLiteral);
}
function printRegex(node) {

View File

@ -475,6 +475,98 @@ function getAlignmentSize(value, tabWidth, startIndex) {
return size;
}
function printString(raw, options, isDirectiveLiteral) {
// `rawContent` is the string exactly like it appeared in the input source
// code, without its enclosing quotes.
const rawContent = raw.slice(1, -1);
const double = { quote: '"', regex: /"/g };
const single = { quote: "'", regex: /'/g };
const preferred = options.singleQuote ? single : double;
const alternate = preferred === single ? double : single;
let shouldUseAlternateQuote = false;
let canChangeDirectiveQuotes = false;
// If `rawContent` contains at least one of the quote preferred for enclosing
// the string, we might want to enclose with the alternate quote instead, to
// minimize the number of escaped quotes.
// Also check for the alternate quote, to determine if we're allowed to swap
// the quotes on a DirectiveLiteral.
if (
rawContent.includes(preferred.quote) ||
rawContent.includes(alternate.quote)
) {
const numPreferredQuotes = (rawContent.match(preferred.regex) || []).length;
const numAlternateQuotes = (rawContent.match(alternate.regex) || []).length;
shouldUseAlternateQuote = numPreferredQuotes > numAlternateQuotes;
} else {
canChangeDirectiveQuotes = true;
}
const enclosingQuote =
options.parser === "json"
? double.quote
: shouldUseAlternateQuote ? alternate.quote : preferred.quote;
// Directives are exact code unit sequences, which means that you can't
// change the escape sequences they use.
// See https://github.com/prettier/prettier/issues/1555
// and https://tc39.github.io/ecma262/#directive-prologue
if (isDirectiveLiteral) {
if (canChangeDirectiveQuotes) {
return enclosingQuote + rawContent + enclosingQuote;
}
return raw;
}
// It might sound unnecessary to use `makeString` even if the string already
// is enclosed with `enclosingQuote`, but it isn't. The string could contain
// unnecessary escapes (such as in `"\'"`). Always using `makeString` makes
// sure that we consistently output the minimum amount of escaped quotes.
return makeString(rawContent, enclosingQuote, options.parser !== "postcss");
}
function makeString(rawContent, enclosingQuote, unescapeUnnecessaryEscapes) {
const otherQuote = enclosingQuote === '"' ? "'" : '"';
// Matches _any_ escape and unescaped quotes (both single and double).
const regex = /\\([\s\S])|(['"])/g;
// Escape and unescape single and double quotes as needed to be able to
// enclose `rawContent` with `enclosingQuote`.
const newContent = rawContent.replace(regex, (match, escaped, quote) => {
// If we matched an escape, and the escaped character is a quote of the
// other type than we intend to enclose the string with, there's no need for
// it to be escaped, so return it _without_ the backslash.
if (escaped === otherQuote) {
return escaped;
}
// If we matched an unescaped quote and it is of the _same_ type as we
// intend to enclose the string with, it must be escaped, so return it with
// a backslash.
if (quote === enclosingQuote) {
return "\\" + quote;
}
if (quote) {
return quote;
}
// Unescape any unnecessarily escaped character.
// Adapted from https://github.com/eslint/eslint/blob/de0b4ad7bd820ade41b1f606008bea68683dc11a/lib/rules/no-useless-escape.js#L27
return unescapeUnnecessaryEscapes &&
/^[^\\nrvtbfux\r\n\u2028\u2029"'0-7]$/.test(escaped)
? escaped
: "\\" + escaped;
});
return enclosingQuote + newContent + enclosingQuote;
}
module.exports = {
getPrecedence,
shouldFlatten,
@ -500,5 +592,6 @@ module.exports = {
hasBlockComments,
isBlockComment,
hasClosureCompilerTypeCastComment,
getAlignmentSize
getAlignmentSize,
printString
};

View File

@ -34,8 +34,8 @@ exports[`bug.css 1`] = `
@font-face {
src: url(if(
$bootstrap-sass-asset-helper,
twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot'),
'#{$icon-font-path}#{$icon-font-name}.eot'
twbs-font-path("#{$icon-font-path}#{$icon-font-name}.eot"),
"#{$icon-font-path}#{$icon-font-name}.eot"
));
}
// Catchall baseclass

View File

@ -12,6 +12,6 @@ exports[`url.css 1`] = `
$dir: 'fonts';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@import url('foo');
$dir: 'fonts';
$dir: "fonts";
`;

View File

@ -29,8 +29,8 @@ a {
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
a {
~ .Pagination-itemWrapper:not(.is-separator):not([data-priority^='#{$priority}'])
~ .Pagination-itemWrapper.is-separator[data-priority^='#{$priority}'] {
~ .Pagination-itemWrapper:not(.is-separator):not([data-priority^="#{$priority}"])
~ .Pagination-itemWrapper.is-separator[data-priority^="#{$priority}"] {
display: flex;
}
}

View File

@ -21,7 +21,7 @@ exports[`inline_url.css 1`] = `
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.breadItem {
background-image: url('/images/product/simple_product_manager/breadcrumb/chevron_right.png');
background-image: url("/images/product/simple_product_manager/breadcrumb/chevron_right.png");
background-image: url(/images/product/simple_product_manager/breadcrumb/chevron_right.png);
-fb-sprite: url(fbglyph:cross-outline, fig-white);
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mO4/B8AAqgB0yr7dJgAAAAASUVORK5CYII=);

View File

@ -0,0 +1,279 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`quotes.css 1`] = `
@supports (content: one "two" three 'four') {
a[href="foo" y],
abbr[title^='It\\'s a trap!'],
img[src=""] {
/* Simple strings. */
content: "abc";
content: 'abc';
/* Escape. */
content: '\\A';
/* Emoji. */
content: '🐶';
/* Empty string. */
content: "";
content: '';
/* Single double quote. */
content: "\\"";
content: '"';
/* Single single quote. */
content: "'";
content: '\\'';
/* One of each. */
content: "\\"'";
content: '"\\'';
/* One of each with unnecessary escapes. */
content: "\\"\\'";
content: '\\"\\'';
/* More double quotes than single quotes. */
content: "\\"'\\"";
content: '"\\'"';
/* More single quotes than double quotes. */
content: "\\"''";
content: '"\\'\\'';
/* Two of each. */
content: "\\"\\"''";
content: '""\\'\\'';
/* Single backslash. */
content: '\\\\';
content: "\\\\";
/* Backslases. */
content: "\\"\\\\\\"\\\\\\\\\\" '\\'\\\\'\\\\\\'\\\\\\\\'";
content: '\\'\\\\\\'\\\\\\\\\\' "\\"\\\\"\\\\\\"\\\\\\\\"';
/* Somewhat more real-word example. */
content: "He's sayin': \\"How's it goin'?\\" Don't ask me why.";
content: 'He\\'s sayin\\': "How\\'s it goin\\'?" Don\\'t ask me why.';
/* Somewhat more real-word example 2. */
content: "var backslash = \\"\\\\\\", doubleQuote = '\\"';";
content: 'var backslash = "\\\\", doubleQuote = \\'"\\';';
/* Leave all "escapes" alone. */
content: "\\Abc4 foo \\n" /* "comment" */ "\\end";
content: '\\Abc4 foo \\n' /* 'comment' */ '\\end';
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@supports (content: one "two" three "four") {
a[href="foo" y],
abbr[title^="It's a trap!"],
img[src=""] {
/* Simple strings. */
content: "abc";
content: "abc";
/* Escape. */
content: "\\A";
/* Emoji. */
content: "🐶";
/* Empty string. */
content: "";
content: "";
/* Single double quote. */
content: '"';
content: '"';
/* Single single quote. */
content: "'";
content: "'";
/* One of each. */
content: "\\"'";
content: "\\"'";
/* One of each with unnecessary escapes. */
content: "\\"'";
content: "\\"'";
/* More double quotes than single quotes. */
content: '"\\'"';
content: '"\\'"';
/* More single quotes than double quotes. */
content: "\\"''";
content: "\\"''";
/* Two of each. */
content: "\\"\\"''";
content: "\\"\\"''";
/* Single backslash. */
content: "\\\\";
content: "\\\\";
/* Backslases. */
content: "\\"\\\\\\"\\\\\\\\\\" ''\\\\'\\\\'\\\\\\\\'";
content: '\\'\\\\\\'\\\\\\\\\\' ""\\\\"\\\\"\\\\\\\\"';
/* Somewhat more real-word example. */
content: "He's sayin': \\"How's it goin'?\\" Don't ask me why.";
content: "He's sayin': \\"How's it goin'?\\" Don't ask me why.";
/* Somewhat more real-word example 2. */
content: 'var backslash = "\\\\", doubleQuote = \\'"\\';';
content: 'var backslash = "\\\\", doubleQuote = \\'"\\';';
/* Leave all "escapes" alone. */
content: "\\Abc4 foo \\n" "\\end";
content: "\\Abc4 foo \\n" "\\end";
}
}
`;
exports[`quotes.css 2`] = `
@supports (content: one "two" three 'four') {
a[href="foo" y],
abbr[title^='It\\'s a trap!'],
img[src=""] {
/* Simple strings. */
content: "abc";
content: 'abc';
/* Escape. */
content: '\\A';
/* Emoji. */
content: '🐶';
/* Empty string. */
content: "";
content: '';
/* Single double quote. */
content: "\\"";
content: '"';
/* Single single quote. */
content: "'";
content: '\\'';
/* One of each. */
content: "\\"'";
content: '"\\'';
/* One of each with unnecessary escapes. */
content: "\\"\\'";
content: '\\"\\'';
/* More double quotes than single quotes. */
content: "\\"'\\"";
content: '"\\'"';
/* More single quotes than double quotes. */
content: "\\"''";
content: '"\\'\\'';
/* Two of each. */
content: "\\"\\"''";
content: '""\\'\\'';
/* Single backslash. */
content: '\\\\';
content: "\\\\";
/* Backslases. */
content: "\\"\\\\\\"\\\\\\\\\\" '\\'\\\\'\\\\\\'\\\\\\\\'";
content: '\\'\\\\\\'\\\\\\\\\\' "\\"\\\\"\\\\\\"\\\\\\\\"';
/* Somewhat more real-word example. */
content: "He's sayin': \\"How's it goin'?\\" Don't ask me why.";
content: 'He\\'s sayin\\': "How\\'s it goin\\'?" Don\\'t ask me why.';
/* Somewhat more real-word example 2. */
content: "var backslash = \\"\\\\\\", doubleQuote = '\\"';";
content: 'var backslash = "\\\\", doubleQuote = \\'"\\';';
/* Leave all "escapes" alone. */
content: "\\Abc4 foo \\n" /* "comment" */ "\\end";
content: '\\Abc4 foo \\n' /* 'comment' */ '\\end';
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@supports (content: one 'two' three 'four') {
a[href='foo' y],
abbr[title^="It's a trap!"],
img[src=''] {
/* Simple strings. */
content: 'abc';
content: 'abc';
/* Escape. */
content: '\\A';
/* Emoji. */
content: '🐶';
/* Empty string. */
content: '';
content: '';
/* Single double quote. */
content: '"';
content: '"';
/* Single single quote. */
content: "'";
content: "'";
/* One of each. */
content: '"\\'';
content: '"\\'';
/* One of each with unnecessary escapes. */
content: '"\\'';
content: '"\\'';
/* More double quotes than single quotes. */
content: '"\\'"';
content: '"\\'"';
/* More single quotes than double quotes. */
content: "\\"''";
content: "\\"''";
/* Two of each. */
content: '""\\'\\'';
content: '""\\'\\'';
/* Single backslash. */
content: '\\\\';
content: '\\\\';
/* Backslases. */
content: "\\"\\\\\\"\\\\\\\\\\" ''\\\\'\\\\'\\\\\\\\'";
content: '\\'\\\\\\'\\\\\\\\\\' ""\\\\"\\\\"\\\\\\\\"';
/* Somewhat more real-word example. */
content: "He's sayin': \\"How's it goin'?\\" Don't ask me why.";
content: "He's sayin': \\"How's it goin'?\\" Don't ask me why.";
/* Somewhat more real-word example 2. */
content: 'var backslash = "\\\\", doubleQuote = \\'"\\';';
content: 'var backslash = "\\\\", doubleQuote = \\'"\\';';
/* Leave all "escapes" alone. */
content: '\\Abc4 foo \\n' '\\end';
content: '\\Abc4 foo \\n' '\\end';
}
}
`;

View File

@ -0,0 +1,2 @@
run_spec(__dirname, { parser: "postcss" });
run_spec(__dirname, { parser: "postcss", singleQuote: true });

View File

@ -0,0 +1,67 @@
@supports (content: one "two" three 'four') {
a[href="foo" y],
abbr[title^='It\'s a trap!'],
img[src=""] {
/* Simple strings. */
content: "abc";
content: 'abc';
/* Escape. */
content: '\A';
/* Emoji. */
content: '🐶';
/* Empty string. */
content: "";
content: '';
/* Single double quote. */
content: "\"";
content: '"';
/* Single single quote. */
content: "'";
content: '\'';
/* One of each. */
content: "\"'";
content: '"\'';
/* One of each with unnecessary escapes. */
content: "\"\'";
content: '\"\'';
/* More double quotes than single quotes. */
content: "\"'\"";
content: '"\'"';
/* More single quotes than double quotes. */
content: "\"''";
content: '"\'\'';
/* Two of each. */
content: "\"\"''";
content: '""\'\'';
/* Single backslash. */
content: '\\';
content: "\\";
/* Backslases. */
content: "\"\\\"\\\\\" '\'\\'\\\'\\\\'";
content: '\'\\\'\\\\\' "\"\\"\\\"\\\\"';
/* Somewhat more real-word example. */
content: "He's sayin': \"How's it goin'?\" Don't ask me why.";
content: 'He\'s sayin\': "How\'s it goin\'?" Don\'t ask me why.';
/* Somewhat more real-word example 2. */
content: "var backslash = \"\\\", doubleQuote = '\"';";
content: 'var backslash = "\\", doubleQuote = \'"\';';
/* Leave all "escapes" alone. */
content: "\Abc4 foo \n" /* "comment" */ "\end";
content: '\Abc4 foo \n' /* 'comment' */ '\end';
}
}

View File

@ -6,7 +6,7 @@ exports[`content.css 1`] = `
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
div {
content: " test 1/2 ";
content: ' test 1+2';
content: " test 1+2";
}
`;

View File

@ -4,7 +4,7 @@ exports[`font-face.css 1`] = `
@font-face {font-family:'HelveticaNeueW02-45Ligh';src:url("/fonts/pictos-web.eot");src:local("☺"),url("/fonts/pictos-web.woff") format("woff"),url("/fonts/pictos-web.ttf") format("truetype"),url("/fonts/pictos-web.svg#webfontIyfZbseF") format("svg");font-weight:normal;font-style:normal;}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@font-face {
font-family: 'HelveticaNeueW02-45Ligh';
font-family: "HelveticaNeueW02-45Ligh";
src: url("/fonts/pictos-web.eot");
src: local("☺"), url("/fonts/pictos-web.woff") format("woff"),
url("/fonts/pictos-web.ttf") format("truetype"),