123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- from __future__ import print_function
- import sys
- import re
- import copy
- from cssbeautifier.__version__ import __version__
- #
- # The MIT License (MIT)
- # Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
- # Permission is hereby granted, free of charge, to any person
- # obtaining a copy of this software and associated documentation files
- # (the "Software"), to deal in the Software without restriction,
- # including without limitation the rights to use, copy, modify, merge,
- # publish, distribute, sublicense, and/or sell copies of the Software,
- # and to permit persons to whom the Software is furnished to do so,
- # subject to the following conditions:
- # The above copyright notice and this permission notice shall be
- # included in all copies or substantial portions of the Software.
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
- # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
- # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- # SOFTWARE.
- class BeautifierOptions:
- 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
- self.space_around_combinator = False
- self.eol = 'auto'
- self.css = None
- self.js = None
- self.html = None
- # deprecated
- self.space_around_selector_separator = False
- def mergeOpts(self, targetType):
- finalOpts = copy.copy(self)
- local = getattr(finalOpts, targetType)
- if (local):
- delattr(finalOpts, targetType)
- for key in local:
- setattr(finalOpts, key, local[key])
- return finalOpts
- def __repr__(self):
- return \
- """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.preserve_newlines,
- self.selector_separator_newline, self.end_with_newline, self.newline_between_rules,
- self.space_around_combinator)
- def default_options():
- return BeautifierOptions()
- def beautify(string, opts=default_options()):
- b = Beautifier(string, opts)
- return b.beautify()
- def beautify_file(file_name, opts=default_options()):
- if file_name == '-': # stdin
- stream = sys.stdin
- else:
- stream = open(file_name)
- content = ''.join(stream.readlines())
- b = Beautifier(content, opts)
- return b.beautify()
- def usage(stream=sys.stdout):
- print("cssbeautifier.py@" + __version__ + """
- CSS beautifier (http://jsbeautifier.org/)
- """, file=stream)
- if stream == sys.stderr:
- return 1
- else:
- return 0
- WHITE_RE = re.compile("^\s+$")
- WORD_RE = re.compile("[\w$\-_]")
- class Printer:
- def __init__(self, beautifier, indent_char, indent_size, default_indent=""):
- self.beautifier = beautifier
- self.newlines_from_last_ws_eat = 0
- self.indentSize = indent_size
- self.singleIndent = (indent_size) * indent_char
- self.indentLevel = 0
- self.nestedLevel = 0
- self.baseIndentString = default_indent
- self.output = []
- def __lastCharWhitespace(self):
- return len(self.output) > 0 and WHITE_RE.search(self.output[-1]) is not None
- def indent(self):
- self.indentLevel += 1
- self.baseIndentString += self.singleIndent
- def outdent(self):
- if self.indentLevel:
- self.indentLevel -= 1
- self.baseIndentString = self.baseIndentString[:-(len(self.singleIndent))]
- def push(self, string):
- self.output.append(string)
- def openBracket(self):
- self.singleSpace()
- self.output.append("{")
- if self.beautifier.eatWhitespace(True) == 0:
- self.newLine()
- def closeBracket(self,newLine):
- if newLine:
- self.newLine()
- self.output.append("}")
- self.beautifier.eatWhitespace(True)
- if self.beautifier.newlines_from_last_ws_eat == 0:
- self.newLine()
- def semicolon(self):
- self.output.append(";")
- def comment(self, comment):
- self.output.append(comment)
- def newLine(self, keepWhitespace=False):
- if len(self.output) > 0 :
- if not keepWhitespace and self.output[-1] != '\n':
- self.trim()
- elif self.output[-1] == self.baseIndentString:
- self.output.pop()
- self.output.append("\n")
- if len(self.baseIndentString) > 0:
- self.output.append(self.baseIndentString)
- def trim(self):
- while self.__lastCharWhitespace():
- self.output.pop()
- def singleSpace(self):
- if len(self.output) > 0 and not self.__lastCharWhitespace():
- self.output.append(" ")
- def preserveSingleSpace(self,isAfterSpace):
- if isAfterSpace:
- self.singleSpace()
- def result(self):
- if self.baseIndentString:
- return self.baseIndentString + "".join(self.output);
- else:
- return "".join(self.output)
- class Beautifier:
- def __init__(self, source_text, opts=default_options()):
- # This is not pretty, but given how we did the version import
- # it is the only way to do this without having setup.py fail on a missing six dependency.
- self.six = __import__("six")
- # in javascript, these two differ
- # in python they are the same, different methods are called on them
- self.lineBreak = re.compile(self.six.u("\r\n|[\n\r\u2028\u2029]"))
- self.allLineBreaks = self.lineBreak
- if not source_text:
- source_text = ''
- opts = opts.mergeOpts('css')
- # Continue to accept deprecated option
- opts.space_around_combinator = opts.space_around_combinator or opts.space_around_selector_separator
- self.opts = opts
- self.indentSize = opts.indent_size
- self.indentChar = opts.indent_char
- self.pos = -1
- self.ch = None
- if self.opts.indent_with_tabs:
- self.indentChar = "\t"
- self.indentSize = 1
- if self.opts.eol == 'auto':
- self.opts.eol = '\n'
- if self.lineBreak.search(source_text or ''):
- self.opts.eol = self.lineBreak.search(source_text).group()
- self.opts.eol = self.opts.eol.replace('\\r', '\r').replace('\\n', '\n')
- # HACK: newline parsing inconsistent. This brute force normalizes the input newlines.
- self.source_text = re.sub(self.allLineBreaks, '\n', source_text)
- # 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"]
- m = re.search("^[\t ]*", self.source_text)
- baseIndentString = m.group(0)
- self.printer = Printer(self, self.indentChar, self.indentSize, baseIndentString)
- def next(self):
- self.pos = self.pos + 1
- if self.pos < len(self.source_text):
- self.ch = self.source_text[self.pos]
- else:
- self.ch = ''
- return self.ch
- def peek(self,skipWhitespace=False):
- start = self.pos
- if skipWhitespace:
- self.eatWhitespace()
- result = ""
- if self.pos + 1 < len(self.source_text):
- result = self.source_text[self.pos + 1]
- if skipWhitespace:
- self.pos = start - 1
- self.next()
- return result
- def eatString(self, endChars):
- start = self.pos
- while self.next():
- if self.ch == "\\":
- self.next()
- elif self.ch in endChars:
- break
- elif self.ch == "\n":
- break
- return self.source_text[start:self.pos] + self.ch
- def peekString(self, endChar):
- start = self.pos
- st = self.eatString(endChar)
- self.pos = start - 1
- self.next()
- return st
- def eatWhitespace(self, pn=False):
- result = 0
- while WHITE_RE.search(self.peek()) is not None:
- self.next()
- if self.ch == "\n" and pn and self.opts.preserve_newlines:
- self.printer.newLine(True)
- result += 1
- self.newlines_from_last_ws_eat = result
- return result
- def skipWhitespace(self):
- result = ''
- if self.ch and WHITE_RE.search(self.ch):
- result = self.ch
- while WHITE_RE.search(self.next()) is not None:
- result += self.ch
- return result
- def eatComment(self):
- start = self.pos
- singleLine = self.peek() == "/"
- self.next()
- while self.next():
- if not singleLine and self.ch == "*" and self.peek() == "/":
- self.next()
- break
- elif singleLine and self.ch == "\n":
- return self.source_text[start:self.pos]
- return self.source_text[start:self.pos] + self.ch
- def lookBack(self, string):
- past = self.source_text[self.pos - len(string):self.pos]
- return past.lower() == string
- # Nested pseudo-class if we are insideRule
- # and the next special character found opens
- # a new block
- def foundNestedPseudoClass(self):
- i = self.pos + 1
- openParen = 0
- while i < len(self.source_text):
- ch = self.source_text[i]
- if ch == "{":
- return True
- elif ch == "(":
- # pseudoclasses can contain ()
- openParen += 1
- elif ch == ")":
- if openParen == 0:
- return False
- openParen -= 1
- elif ch == ";" or ch == "}":
- return False
- i += 1;
- return False
- def beautify(self):
- printer = self.printer
- insideRule = False
- insidePropertyValue = False
- enteringConditionalGroup = False
- top_ch = ''
- last_top_ch = ''
- parenLevel = 0
- while True:
- whitespace = self.skipWhitespace()
- isAfterSpace = whitespace != ''
- isAfterNewline = '\n' in whitespace
- last_top_ch = top_ch
- top_ch = self.ch
- if not self.ch:
- break
- elif self.ch == '/' and self.peek() == '*':
- header = printer.indentLevel == 0
- if not isAfterNewline or header:
- printer.newLine()
- comment = self.eatComment()
- printer.comment(comment)
- printer.newLine()
- if header:
- printer.newLine(True)
- elif self.ch == '/' and self.peek() == '/':
- if not isAfterNewline and last_top_ch != '{':
- printer.trim()
- printer.singleSpace()
- printer.comment(self.eatComment())
- printer.newLine()
- elif self.ch == '@':
- printer.preserveSingleSpace(isAfterSpace)
- # deal with less propery mixins @{...}
- if self.peek(True) == '{':
- printer.push(self.eatString('}'));
- else:
- printer.push(self.ch)
- # strip trailing space, if present, for hash property check
- variableOrRule = self.peekString(": ,;{}()[]/='\"")
- if variableOrRule[-1] in ": ":
- # wwe have a variable or pseudo-class, add it and insert one space before continuing
- self.next()
- variableOrRule = self.eatString(": ")
- if variableOrRule[-1].isspace():
- variableOrRule = variableOrRule[:-1]
- printer.push(variableOrRule)
- printer.singleSpace();
- if variableOrRule[-1].isspace():
- variableOrRule = variableOrRule[:-1]
- # might be a nesting at-rule
- if variableOrRule in self.NESTED_AT_RULE:
- printer.nestedLevel += 1
- if variableOrRule in self.CONDITIONAL_GROUP_RULE:
- enteringConditionalGroup = True
- elif self.ch == '#' and self.peek() == '{':
- printer.preserveSingleSpace(isAfterSpace)
- printer.push(self.eatString('}'));
- elif self.ch == '{':
- if self.peek(True) == '}':
- self.eatWhitespace()
- self.next()
- printer.singleSpace()
- printer.push("{")
- printer.closeBracket(False)
- if self.newlines_from_last_ws_eat < 2 and self.opts.newline_between_rules and printer.indentLevel == 0:
- printer.newLine(True)
- 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(True)
- insideRule = False
- insidePropertyValue = False
- if printer.nestedLevel:
- printer.nestedLevel -= 1
- if self.newlines_from_last_ws_eat < 2 and self.opts.newline_between_rules and printer.indentLevel == 0:
- printer.newLine(True)
- elif self.ch == ":":
- self.eatWhitespace()
- if (insideRule or enteringConditionalGroup) and \
- not (self.lookBack('&') or self.foundNestedPseudoClass()) and \
- not self.lookBack('('):
- # 'property: value' delimiter
- # which could be in a conditional group query
- printer.push(":")
- if not insidePropertyValue:
- insidePropertyValue = True
- printer.singleSpace()
- else:
- # sass/less parent reference don't use a space
- # sass nested pseudo-class don't use a space
- # preserve space before pseudoclasses/pseudoelements, as it means "in any child"
- if (self.lookBack(' ')) and (printer.output[-1] != ' '):
- printer.push(" ")
- if self.peek() == ":":
- # pseudo-element
- self.next()
- printer.push("::")
- else:
- # pseudo-element
- printer.push(":")
- elif self.ch == '"' or self.ch == '\'':
- printer.preserveSingleSpace(isAfterSpace)
- printer.push(self.eatString(self.ch))
- elif self.ch == ';':
- insidePropertyValue = False
- printer.semicolon()
- if self.eatWhitespace(True) == 0:
- printer.newLine()
- elif self.ch == '(':
- # may be a url
- if self.lookBack("url"):
- printer.push(self.ch)
- self.eatWhitespace()
- if self.next():
- if self.ch is not ')' and self.ch is not '"' \
- and self.ch is not '\'':
- printer.push(self.eatString(')'))
- else:
- self.pos = self.pos - 1
- else:
- parenLevel += 1
- printer.preserveSingleSpace(isAfterSpace)
- printer.push(self.ch)
- self.eatWhitespace()
- elif self.ch == ')':
- printer.push(self.ch)
- parenLevel -= 1
- elif self.ch == ',':
- printer.push(self.ch)
- if self.eatWhitespace(True) == 0 and not insidePropertyValue and self.opts.selector_separator_newline and parenLevel < 1:
- printer.newLine()
- else:
- printer.singleSpace()
- elif (self.ch == '>' or self.ch == '+' or self.ch == '~') and \
- not insidePropertyValue and parenLevel < 1:
- # handle combinator spacing
- if self.opts.space_around_combinator:
- printer.singleSpace()
- printer.push(self.ch)
- printer.singleSpace()
- else:
- printer.push(self.ch)
- self.eatWhitespace()
- # squash extra whitespace
- if self.ch and WHITE_RE.search(self.ch):
- self.ch = ''
- elif self.ch == ']':
- printer.push(self.ch)
- elif self.ch == '[':
- printer.preserveSingleSpace(isAfterSpace)
- printer.push(self.ch)
- elif self.ch == '=':
- # no whitespace before or after
- self.eatWhitespace()
- printer.push('=')
- if WHITE_RE.search(self.ch):
- self.ch = ''
- else:
- printer.preserveSingleSpace(isAfterSpace)
- printer.push(self.ch)
- sweet_code = re.sub('[\r\n\t ]+$', '', printer.result())
- # establish end_with_newline
- if self.opts.end_with_newline:
- sweet_code += '\n'
- if not self.opts.eol == '\n':
- sweet_code = sweet_code.replace('\n', self.opts.eol)
- return sweet_code
|