From 57bc8065bd643913c1fbabbe2429752e6723de39 Mon Sep 17 00:00:00 2001 From: Liam Newman Date: Mon, 29 Sep 2014 15:30:18 -0700 Subject: [PATCH] css colon character fix port to python Closes #446 Closes #418 --- js/test/beautify-tests.js | 2 +- python/cssbeautifier/__init__.py | 71 ++++++++++++++++++++++++++---- python/cssbeautifier/tests/test.py | 43 ++++++++++++++++++ 3 files changed, 106 insertions(+), 10 deletions(-) diff --git a/js/test/beautify-tests.js b/js/test/beautify-tests.js index bb4fe6243..eebf829f0 100755 --- a/js/test/beautify-tests.js +++ b/js/test/beautify-tests.js @@ -2215,7 +2215,7 @@ function run_beautifier_tests(test_obj, Urlencoded, js_beautify, html_beautify, btc("#bla, #foo{color:red}", "#bla,\n#foo {\n\tcolor: red\n}"); btc("@media print {.tab{}}", "@media print {\n\t.tab {}\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}"); - + //lead-in whitespace determines base-indent. // lead-in newlines are stripped. btc("\n\na, img {padding: 0.2px}", "a,\nimg {\n\tpadding: 0.2px\n}"); diff --git a/python/cssbeautifier/__init__.py b/python/cssbeautifier/__init__.py index f5466f0af..abff39edf 100644 --- a/python/cssbeautifier/__init__.py +++ b/python/cssbeautifier/__init__.py @@ -86,6 +86,9 @@ class Printer: def __init__(self, indent_char, indent_size, default_indent=""): self.indentSize = indent_size self.singleIndent = (indent_size) * indent_char + self.indentLevel = 0 + self.nestedLevel = 0 + self.baseIndentString = default_indent self.output = [] if self.baseIndentString: @@ -95,10 +98,13 @@ def __lastCharWhitespace(self): return WHITE_RE.search(self.output[-1]) is not None def indent(self): + self.indentLevel += 1 self.baseIndentString += self.singleIndent def outdent(self): - self.baseIndentString = self.baseIndentString[:-(len(self.singleIndent))] + if self.indentLevel: + self.indentLevel -= 1 + self.baseIndentString = self.baseIndentString[:-(len(self.singleIndent))] def push(self, string): self.output.append(string) @@ -113,10 +119,6 @@ def closeBracket(self): self.output.append("}") self.newLine() - def colon(self): - self.output.append(":") - self.singleSpace() - def semicolon(self): self.output.append(";") self.newLine() @@ -152,6 +154,20 @@ def __init__(self, source_text, opts=default_options()): self.indentChar = opts.indent_char self.pos = -1 self.ch = None + + # https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule + # also in CONDITIONAL_GROUP_RULE below + self.NESTED_AT_RULE = [ \ + "@page", \ + "@font-face", \ + "@keyframes", \ + "@media", \ + "@supports", \ + "@document"] + self.CONDITIONAL_GROUP_RULE = [ \ + "@media", \ + "@supports", \ + "@document"] def next(self): self.pos = self.pos + 1 @@ -219,6 +235,8 @@ def beautify(self): printer = Printer(self.indentChar, self.indentSize, baseIndentString) insideRule = False + enteringConditionalGroup = False + while True: isAfterSpace = self.skipWhitespace() @@ -233,6 +251,22 @@ def beautify(self): elif self.ch == '/' and self.peek() == '/': printer.comment(self.eatComment(True)[0:-1]) printer.newLine() + elif self.ch == '@': + # strip trailing space, if present, for hash property check + atRule = self.eatString(" ") + if(atRule[-1] == " "): + atRule = atRule[:-1] + + # pass along the space we found as a separate item + printer.push(atRule) + printer.push(self.ch) + + # might be a nesting at-rule + if atRule in self.NESTED_AT_RULE: + printer.nestedLevel += 1 + if atRule in self.CONDITIONAL_GROUP_RULE: + enteringConditionalGroup = True + elif self.ch == '{': self.eatWhitespace() if self.peek() == '}': @@ -241,14 +275,34 @@ def beautify(self): else: printer.indent() printer.openBracket() + # when entering conditional groups, only rulesets are allowed + if enteringConditionalGroup: + enteringConditionalGroup = False + insideRule = printer.indentLevel > printer.nestedLevel + else: + # otherwise, declarations are also allowed + insideRule = printer.indentLevel >= printer.nestedLevel elif self.ch == '}': printer.outdent() printer.closeBracket() insideRule = False + if printer.nestedLevel: + printer.nestedLevel -= 1 elif self.ch == ":": self.eatWhitespace() - printer.colon() - insideRule = True + if insideRule or enteringConditionalGroup: + # 'property: value' delimiter + # which could be in a conditional group query + printer.push(self.ch) + printer.singleSpace() + else: + if self.peek() == ":": + # pseudo-element + self.next() + printer.push("::") + else: + # pseudo-element + printer.push(self.ch) elif self.ch == '"' or self.ch == '\'': printer.push(self.eatString(self.ch)) elif self.ch == ';': @@ -307,5 +361,4 @@ def beautify(self): if self.opts.end_with_newline: sweet_code += "\n" - return sweet_code - + return sweet_code \ No newline at end of file diff --git a/python/cssbeautifier/tests/test.py b/python/cssbeautifier/tests/test.py index c7714eb4d..7e419251c 100644 --- a/python/cssbeautifier/tests/test.py +++ b/python/cssbeautifier/tests/test.py @@ -34,6 +34,7 @@ def testBasics(self): t(".tabs{background:url('back.jpg')}", ".tabs {\n\tbackground: url('back.jpg')\n}") t("#bla, #foo{color:red}", "#bla,\n#foo {\n\tcolor: red\n}") t("@media print {.tab{}}", "@media print {\n\t.tab {}\n}") + t("@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}") # may not eat the space before "[" t('html.js [data-custom="123"] {\n\topacity: 1.00;\n}') @@ -46,6 +47,7 @@ def testBasics(self): t(" \t \na, img {padding: 0.2px}", " \t a,\n \t img {\n \t \tpadding: 0.2px\n \t }") t("\n\n a, img {padding: 0.2px}", "a,\nimg {\n\tpadding: 0.2px\n}") + def testComments(self): self.resetOptions() t = self.decodesto @@ -71,6 +73,34 @@ def testSeperateSelectors(self): t("#bla, #foo{color:red}", "#bla,\n#foo {\n\tcolor: red\n}") t("a, img {padding: 0.2px}", "a,\nimg {\n\tpadding: 0.2px\n}") + + def testBlockNesting(self): + self.resetOptions() + t = self.decodesto + + t("#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}") + t("@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}") + +# @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); +# } +# } +# } + t("@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}") + + def testOptions(self): self.resetOptions() self.options.indent_size = 2 @@ -80,8 +110,21 @@ def testOptions(self): t("#bla, #foo{color:green}", "#bla, #foo {\n color: green\n}") t("@media print {.tab{}}", "@media print {\n .tab {}\n}") + t("@media print {.tab,.bat{}}", "@media print {\n .tab, .bat {}\n}") t("#bla, #foo{color:black}", "#bla, #foo {\n color: black\n}") + # pseudo-classes and pseudo-elements + t("#foo:hover {\n background-image: url(foo@2x.png)\n}") + t("#foo *:hover {\n color: purple\n}") + t("::selection {\n color: #ff0000;\n}") + + # TODO: don't break nested pseduo-classes + t("@media screen {.tab,.bat:hover {color:red}}", "@media screen {\n .tab, .bat:hover {\n color: red\n }\n}") + + # particular edge case with braces and semicolons inside tags that allows custom text + t( "a:not(\"foobar\\\";{}omg\"){\ncontent: 'example\\';{} text';\ncontent: \"example\\\";{} text\";}", + "a:not(\"foobar\\\";{}omg\") {\n content: 'example\\';{} text';\n content: \"example\\\";{} text\";\n}") + def decodesto(self, input, expectation=None): if expectation == None: expectation = input