diff --git a/js/lib/beautify-css.js b/js/lib/beautify-css.js index 60caf84e0..fa092b904 100644 --- a/js/lib/beautify-css.js +++ b/js/lib/beautify-css.js @@ -154,6 +154,7 @@ var indentString = source_text.match(/^[\r\n]*[\t ]*/)[0]; var singleIndent = new Array(indentSize + 1).join(indentCharacter); var indentLevel = 0; + var nestedLevel = 0; function indent() { indentLevel++; @@ -207,6 +208,8 @@ /*_____________________--------------------_____________________*/ var insideRule = false; + var enteringConditionalGroup = false; + while (true) { var isAfterSpace = skipWhitespace(); @@ -221,6 +224,20 @@ } } else if (ch === '/' && peek() === '/') { // single line comment output.push(eatComment(true), indentString); + } else if (ch === '@') { + // strip trailing space, if present, for hash property checks + var atRule = eatString(" ").replace(/ $/, ''); + + // pass along the space we found as a separate item + output.push(atRule, ch); + + // might be a nesting at-rule + if (atRule in css_beautify.NESTED_AT_RULE) { + nestedLevel += 1; + if (atRule in css_beautify.CONDITIONAL_GROUP_RULE) { + enteringConditionalGroup = true; + } + } } else if (ch === '{') { eatWhitespace(); if (peek() === '}') { @@ -229,15 +246,38 @@ } else { indent(); print["{"](ch); + // when entering conditional groups, only rulesets are allowed + if (enteringConditionalGroup) { + enteringConditionalGroup = false; + insideRule = (indentLevel > nestedLevel); + } else { + // otherwise, declarations are also allowed + insideRule = (indentLevel >= nestedLevel); + } } } else if (ch === '}') { outdent(); print["}"](ch); insideRule = false; + if (nestedLevel) { + nestedLevel--; + } } else if (ch === ":") { eatWhitespace(); - output.push(ch, " "); - insideRule = true; + if (insideRule || enteringConditionalGroup) { + // 'property: value' delimiter + // which could be in a conditional group query + output.push(ch, " "); + } else { + if (peek() === ":") { + // pseudo-element + next(); + output.push("::"); + } else { + // pseudo-class + output.push(ch); + } + } } else if (ch === '"' || ch === '\'') { output.push(eatString(ch)); } else if (ch === ';') { @@ -305,6 +345,22 @@ return sweetCode; } + // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule + css_beautify.NESTED_AT_RULE = { + "@page": true, + "@font-face": true, + "@keyframes": true, + // also in CONDITIONAL_GROUP_RULE below + "@media": true, + "@supports": true, + "@document": true + }; + css_beautify.CONDITIONAL_GROUP_RULE = { + "@media": true, + "@supports": true, + "@document": true + }; + /*global define */ if (typeof define === "function") { // Add support for require.js diff --git a/js/test/beautify-tests.js b/js/test/beautify-tests.js index 115a018a7..f2fef641f 100755 --- a/js/test/beautify-tests.js +++ b/js/test/beautify-tests.js @@ -1734,6 +1734,7 @@ function run_beautifier_tests(test_obj, Urlencoded, js_beautify, html_beautify, btc(".tabs{background:url('back.jpg')}", ".tabs {\n\tbackground: url('back.jpg')\n}\n"); btc("#bla, #foo{color:red}", "#bla,\n#foo {\n\tcolor: red\n}\n"); btc("@media print {.tab{}}", "@media print {\n\t.tab {}\n}\n"); + btc("@media print {.tab{background-image:url(foo@2x.png)}}", "@media print {\n\t.tab {\n\t\tbackground-image: url(foo@2x.png)\n\t}\n}\n"); // comments btc("/* test */", "/* test */\n"); @@ -1753,6 +1754,30 @@ function run_beautifier_tests(test_obj, Urlencoded, js_beautify, html_beautify, btc("#bla, #foo{color:red}", "#bla,\n#foo {\n\tcolor: red\n}\n"); btc("a, img {padding: 0.2px}", "a,\nimg {\n\tpadding: 0.2px\n}\n"); + // block nesting + btc("#foo {\n\tbackground-image: url(foo@2x.png);\n\t@font-face {\n\t\tfont-family: 'Bitstream Vera Serif Bold';\n\t\tsrc: url('http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf');\n\t}\n}\n"); + btc("@media screen {\n\t#foo:hover {\n\t\tbackground-image: url(foo@2x.png);\n\t}\n\t@font-face {\n\t\tfont-family: 'Bitstream Vera Serif Bold';\n\t\tsrc: url('http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf');\n\t}\n}\n"); +/* +@font-face { + font-family: 'Bitstream Vera Serif Bold'; + src: url('http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf'); +} +@media screen { + #foo:hover { + background-image: url(foo.png); + } + @media screen and (min-device-pixel-ratio: 2) { + @font-face { + font-family: 'Helvetica Neue' + } + #foo:hover { + background-image: url(foo@2x.png); + } + } +} +*/ + btc("@font-face {\n\tfont-family: 'Bitstream Vera Serif Bold';\n\tsrc: url('http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf');\n}\n@media screen {\n\t#foo:hover {\n\t\tbackground-image: url(foo.png);\n\t}\n\t@media screen and (min-device-pixel-ratio: 2) {\n\t\t@font-face {\n\t\t\tfont-family: 'Helvetica Neue'\n\t\t}\n\t\t#foo:hover {\n\t\t\tbackground-image: url(foo@2x.png);\n\t\t}\n\t}\n}\n"); + // test options opts.indent_size = 2; opts.indent_char = ' '; @@ -1760,8 +1785,21 @@ function run_beautifier_tests(test_obj, Urlencoded, js_beautify, html_beautify, btc("#bla, #foo{color:green}", "#bla, #foo {\n color: green\n}\n"); btc("@media print {.tab{}}", "@media print {\n .tab {}\n}\n"); + btc("@media print {.tab,.bat{}}", "@media print {\n .tab, .bat {}\n}\n"); btc("#bla, #foo{color:black}", "#bla, #foo {\n color: black\n}\n"); + // pseudo-classes and pseudo-elements + btc("#foo:hover {\n background-image: url(foo@2x.png)\n}\n"); + btc("#foo *:hover {\n color: purple\n}\n"); + btc("::selection {\n color: #ff0000;\n}\n"); + + // TODO: don't break nested pseduo-classes + btc("@media screen {.tab,.bat:hover {color:red}}", "@media screen {\n .tab, .bat:hover {\n color: red\n }\n}\n"); + + // particular edge case with braces and semicolons inside tags that allows custom text + btc("a:not(\"foobar\\\";{}omg\"){\ncontent: 'example\\';{} text';\ncontent: \"example\\\";{} text\";}", + "a:not(\"foobar\\\";{}omg\") {\n content: 'example\\';{} text';\n content: \"example\\\";{} text\";\n}\n"); + return sanitytest; }