__init__.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. from __future__ import print_function
  2. import sys
  3. import re
  4. import copy
  5. from cssbeautifier.__version__ import __version__
  6. #
  7. # The MIT License (MIT)
  8. # Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
  9. # Permission is hereby granted, free of charge, to any person
  10. # obtaining a copy of this software and associated documentation files
  11. # (the "Software"), to deal in the Software without restriction,
  12. # including without limitation the rights to use, copy, modify, merge,
  13. # publish, distribute, sublicense, and/or sell copies of the Software,
  14. # and to permit persons to whom the Software is furnished to do so,
  15. # subject to the following conditions:
  16. # The above copyright notice and this permission notice shall be
  17. # included in all copies or substantial portions of the Software.
  18. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  19. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  20. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  21. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
  22. # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
  23. # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  24. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  25. # SOFTWARE.
  26. class BeautifierOptions:
  27. def __init__(self):
  28. self.indent_size = 4
  29. self.indent_char = ' '
  30. self.indent_with_tabs = False
  31. self.preserve_newlines = False
  32. self.selector_separator_newline = True
  33. self.end_with_newline = False
  34. self.newline_between_rules = True
  35. self.space_around_combinator = False
  36. self.eol = 'auto'
  37. self.css = None
  38. self.js = None
  39. self.html = None
  40. # deprecated
  41. self.space_around_selector_separator = False
  42. def mergeOpts(self, targetType):
  43. finalOpts = copy.copy(self)
  44. local = getattr(finalOpts, targetType)
  45. if (local):
  46. delattr(finalOpts, targetType)
  47. for key in local:
  48. setattr(finalOpts, key, local[key])
  49. return finalOpts
  50. def __repr__(self):
  51. return \
  52. """indent_size = %d
  53. indent_char = [%s]
  54. indent_with_tabs = [%s]
  55. preserve_newlines = [%s]
  56. separate_selectors_newline = [%s]
  57. end_with_newline = [%s]
  58. newline_between_rules = [%s]
  59. space_around_combinator = [%s]
  60. """ % (self.indent_size, self.indent_char, self.indent_with_tabs, self.preserve_newlines,
  61. self.selector_separator_newline, self.end_with_newline, self.newline_between_rules,
  62. self.space_around_combinator)
  63. def default_options():
  64. return BeautifierOptions()
  65. def beautify(string, opts=default_options()):
  66. b = Beautifier(string, opts)
  67. return b.beautify()
  68. def beautify_file(file_name, opts=default_options()):
  69. if file_name == '-': # stdin
  70. stream = sys.stdin
  71. else:
  72. stream = open(file_name)
  73. content = ''.join(stream.readlines())
  74. b = Beautifier(content, opts)
  75. return b.beautify()
  76. def usage(stream=sys.stdout):
  77. print("cssbeautifier.py@" + __version__ + """
  78. CSS beautifier (http://jsbeautifier.org/)
  79. """, file=stream)
  80. if stream == sys.stderr:
  81. return 1
  82. else:
  83. return 0
  84. WHITE_RE = re.compile("^\s+$")
  85. WORD_RE = re.compile("[\w$\-_]")
  86. class Printer:
  87. def __init__(self, beautifier, indent_char, indent_size, default_indent=""):
  88. self.beautifier = beautifier
  89. self.newlines_from_last_ws_eat = 0
  90. self.indentSize = indent_size
  91. self.singleIndent = (indent_size) * indent_char
  92. self.indentLevel = 0
  93. self.nestedLevel = 0
  94. self.baseIndentString = default_indent
  95. self.output = []
  96. def __lastCharWhitespace(self):
  97. return len(self.output) > 0 and WHITE_RE.search(self.output[-1]) is not None
  98. def indent(self):
  99. self.indentLevel += 1
  100. self.baseIndentString += self.singleIndent
  101. def outdent(self):
  102. if self.indentLevel:
  103. self.indentLevel -= 1
  104. self.baseIndentString = self.baseIndentString[:-(len(self.singleIndent))]
  105. def push(self, string):
  106. self.output.append(string)
  107. def openBracket(self):
  108. self.singleSpace()
  109. self.output.append("{")
  110. if self.beautifier.eatWhitespace(True) == 0:
  111. self.newLine()
  112. def closeBracket(self,newLine):
  113. if newLine:
  114. self.newLine()
  115. self.output.append("}")
  116. self.beautifier.eatWhitespace(True)
  117. if self.beautifier.newlines_from_last_ws_eat == 0:
  118. self.newLine()
  119. def semicolon(self):
  120. self.output.append(";")
  121. def comment(self, comment):
  122. self.output.append(comment)
  123. def newLine(self, keepWhitespace=False):
  124. if len(self.output) > 0 :
  125. if not keepWhitespace and self.output[-1] != '\n':
  126. self.trim()
  127. elif self.output[-1] == self.baseIndentString:
  128. self.output.pop()
  129. self.output.append("\n")
  130. if len(self.baseIndentString) > 0:
  131. self.output.append(self.baseIndentString)
  132. def trim(self):
  133. while self.__lastCharWhitespace():
  134. self.output.pop()
  135. def singleSpace(self):
  136. if len(self.output) > 0 and not self.__lastCharWhitespace():
  137. self.output.append(" ")
  138. def preserveSingleSpace(self,isAfterSpace):
  139. if isAfterSpace:
  140. self.singleSpace()
  141. def result(self):
  142. if self.baseIndentString:
  143. return self.baseIndentString + "".join(self.output);
  144. else:
  145. return "".join(self.output)
  146. class Beautifier:
  147. def __init__(self, source_text, opts=default_options()):
  148. # This is not pretty, but given how we did the version import
  149. # it is the only way to do this without having setup.py fail on a missing six dependency.
  150. self.six = __import__("six")
  151. # in javascript, these two differ
  152. # in python they are the same, different methods are called on them
  153. self.lineBreak = re.compile(self.six.u("\r\n|[\n\r\u2028\u2029]"))
  154. self.allLineBreaks = self.lineBreak
  155. if not source_text:
  156. source_text = ''
  157. opts = opts.mergeOpts('css')
  158. # Continue to accept deprecated option
  159. opts.space_around_combinator = opts.space_around_combinator or opts.space_around_selector_separator
  160. self.opts = opts
  161. self.indentSize = opts.indent_size
  162. self.indentChar = opts.indent_char
  163. self.pos = -1
  164. self.ch = None
  165. if self.opts.indent_with_tabs:
  166. self.indentChar = "\t"
  167. self.indentSize = 1
  168. if self.opts.eol == 'auto':
  169. self.opts.eol = '\n'
  170. if self.lineBreak.search(source_text or ''):
  171. self.opts.eol = self.lineBreak.search(source_text).group()
  172. self.opts.eol = self.opts.eol.replace('\\r', '\r').replace('\\n', '\n')
  173. # HACK: newline parsing inconsistent. This brute force normalizes the input newlines.
  174. self.source_text = re.sub(self.allLineBreaks, '\n', source_text)
  175. # https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
  176. # also in CONDITIONAL_GROUP_RULE below
  177. self.NESTED_AT_RULE = [ \
  178. "@page", \
  179. "@font-face", \
  180. "@keyframes", \
  181. "@media", \
  182. "@supports", \
  183. "@document"]
  184. self.CONDITIONAL_GROUP_RULE = [ \
  185. "@media", \
  186. "@supports", \
  187. "@document"]
  188. m = re.search("^[\t ]*", self.source_text)
  189. baseIndentString = m.group(0)
  190. self.printer = Printer(self, self.indentChar, self.indentSize, baseIndentString)
  191. def next(self):
  192. self.pos = self.pos + 1
  193. if self.pos < len(self.source_text):
  194. self.ch = self.source_text[self.pos]
  195. else:
  196. self.ch = ''
  197. return self.ch
  198. def peek(self,skipWhitespace=False):
  199. start = self.pos
  200. if skipWhitespace:
  201. self.eatWhitespace()
  202. result = ""
  203. if self.pos + 1 < len(self.source_text):
  204. result = self.source_text[self.pos + 1]
  205. if skipWhitespace:
  206. self.pos = start - 1
  207. self.next()
  208. return result
  209. def eatString(self, endChars):
  210. start = self.pos
  211. while self.next():
  212. if self.ch == "\\":
  213. self.next()
  214. elif self.ch in endChars:
  215. break
  216. elif self.ch == "\n":
  217. break
  218. return self.source_text[start:self.pos] + self.ch
  219. def peekString(self, endChar):
  220. start = self.pos
  221. st = self.eatString(endChar)
  222. self.pos = start - 1
  223. self.next()
  224. return st
  225. def eatWhitespace(self, pn=False):
  226. result = 0
  227. while WHITE_RE.search(self.peek()) is not None:
  228. self.next()
  229. if self.ch == "\n" and pn and self.opts.preserve_newlines:
  230. self.printer.newLine(True)
  231. result += 1
  232. self.newlines_from_last_ws_eat = result
  233. return result
  234. def skipWhitespace(self):
  235. result = ''
  236. if self.ch and WHITE_RE.search(self.ch):
  237. result = self.ch
  238. while WHITE_RE.search(self.next()) is not None:
  239. result += self.ch
  240. return result
  241. def eatComment(self):
  242. start = self.pos
  243. singleLine = self.peek() == "/"
  244. self.next()
  245. while self.next():
  246. if not singleLine and self.ch == "*" and self.peek() == "/":
  247. self.next()
  248. break
  249. elif singleLine and self.ch == "\n":
  250. return self.source_text[start:self.pos]
  251. return self.source_text[start:self.pos] + self.ch
  252. def lookBack(self, string):
  253. past = self.source_text[self.pos - len(string):self.pos]
  254. return past.lower() == string
  255. # Nested pseudo-class if we are insideRule
  256. # and the next special character found opens
  257. # a new block
  258. def foundNestedPseudoClass(self):
  259. i = self.pos + 1
  260. openParen = 0
  261. while i < len(self.source_text):
  262. ch = self.source_text[i]
  263. if ch == "{":
  264. return True
  265. elif ch == "(":
  266. # pseudoclasses can contain ()
  267. openParen += 1
  268. elif ch == ")":
  269. if openParen == 0:
  270. return False
  271. openParen -= 1
  272. elif ch == ";" or ch == "}":
  273. return False
  274. i += 1;
  275. return False
  276. def beautify(self):
  277. printer = self.printer
  278. insideRule = False
  279. insidePropertyValue = False
  280. enteringConditionalGroup = False
  281. top_ch = ''
  282. last_top_ch = ''
  283. parenLevel = 0
  284. while True:
  285. whitespace = self.skipWhitespace()
  286. isAfterSpace = whitespace != ''
  287. isAfterNewline = '\n' in whitespace
  288. last_top_ch = top_ch
  289. top_ch = self.ch
  290. if not self.ch:
  291. break
  292. elif self.ch == '/' and self.peek() == '*':
  293. header = printer.indentLevel == 0
  294. if not isAfterNewline or header:
  295. printer.newLine()
  296. comment = self.eatComment()
  297. printer.comment(comment)
  298. printer.newLine()
  299. if header:
  300. printer.newLine(True)
  301. elif self.ch == '/' and self.peek() == '/':
  302. if not isAfterNewline and last_top_ch != '{':
  303. printer.trim()
  304. printer.singleSpace()
  305. printer.comment(self.eatComment())
  306. printer.newLine()
  307. elif self.ch == '@':
  308. printer.preserveSingleSpace(isAfterSpace)
  309. # deal with less propery mixins @{...}
  310. if self.peek(True) == '{':
  311. printer.push(self.eatString('}'));
  312. else:
  313. printer.push(self.ch)
  314. # strip trailing space, if present, for hash property check
  315. variableOrRule = self.peekString(": ,;{}()[]/='\"")
  316. if variableOrRule[-1] in ": ":
  317. # wwe have a variable or pseudo-class, add it and insert one space before continuing
  318. self.next()
  319. variableOrRule = self.eatString(": ")
  320. if variableOrRule[-1].isspace():
  321. variableOrRule = variableOrRule[:-1]
  322. printer.push(variableOrRule)
  323. printer.singleSpace();
  324. if variableOrRule[-1].isspace():
  325. variableOrRule = variableOrRule[:-1]
  326. # might be a nesting at-rule
  327. if variableOrRule in self.NESTED_AT_RULE:
  328. printer.nestedLevel += 1
  329. if variableOrRule in self.CONDITIONAL_GROUP_RULE:
  330. enteringConditionalGroup = True
  331. elif self.ch == '#' and self.peek() == '{':
  332. printer.preserveSingleSpace(isAfterSpace)
  333. printer.push(self.eatString('}'));
  334. elif self.ch == '{':
  335. if self.peek(True) == '}':
  336. self.eatWhitespace()
  337. self.next()
  338. printer.singleSpace()
  339. printer.push("{")
  340. printer.closeBracket(False)
  341. if self.newlines_from_last_ws_eat < 2 and self.opts.newline_between_rules and printer.indentLevel == 0:
  342. printer.newLine(True)
  343. else:
  344. printer.indent()
  345. printer.openBracket()
  346. # when entering conditional groups, only rulesets are allowed
  347. if enteringConditionalGroup:
  348. enteringConditionalGroup = False
  349. insideRule = printer.indentLevel > printer.nestedLevel
  350. else:
  351. # otherwise, declarations are also allowed
  352. insideRule = printer.indentLevel >= printer.nestedLevel
  353. elif self.ch == '}':
  354. printer.outdent()
  355. printer.closeBracket(True)
  356. insideRule = False
  357. insidePropertyValue = False
  358. if printer.nestedLevel:
  359. printer.nestedLevel -= 1
  360. if self.newlines_from_last_ws_eat < 2 and self.opts.newline_between_rules and printer.indentLevel == 0:
  361. printer.newLine(True)
  362. elif self.ch == ":":
  363. self.eatWhitespace()
  364. if (insideRule or enteringConditionalGroup) and \
  365. not (self.lookBack('&') or self.foundNestedPseudoClass()) and \
  366. not self.lookBack('('):
  367. # 'property: value' delimiter
  368. # which could be in a conditional group query
  369. printer.push(":")
  370. if not insidePropertyValue:
  371. insidePropertyValue = True
  372. printer.singleSpace()
  373. else:
  374. # sass/less parent reference don't use a space
  375. # sass nested pseudo-class don't use a space
  376. # preserve space before pseudoclasses/pseudoelements, as it means "in any child"
  377. if (self.lookBack(' ')) and (printer.output[-1] != ' '):
  378. printer.push(" ")
  379. if self.peek() == ":":
  380. # pseudo-element
  381. self.next()
  382. printer.push("::")
  383. else:
  384. # pseudo-element
  385. printer.push(":")
  386. elif self.ch == '"' or self.ch == '\'':
  387. printer.preserveSingleSpace(isAfterSpace)
  388. printer.push(self.eatString(self.ch))
  389. elif self.ch == ';':
  390. insidePropertyValue = False
  391. printer.semicolon()
  392. if self.eatWhitespace(True) == 0:
  393. printer.newLine()
  394. elif self.ch == '(':
  395. # may be a url
  396. if self.lookBack("url"):
  397. printer.push(self.ch)
  398. self.eatWhitespace()
  399. if self.next():
  400. if self.ch is not ')' and self.ch is not '"' \
  401. and self.ch is not '\'':
  402. printer.push(self.eatString(')'))
  403. else:
  404. self.pos = self.pos - 1
  405. else:
  406. parenLevel += 1
  407. printer.preserveSingleSpace(isAfterSpace)
  408. printer.push(self.ch)
  409. self.eatWhitespace()
  410. elif self.ch == ')':
  411. printer.push(self.ch)
  412. parenLevel -= 1
  413. elif self.ch == ',':
  414. printer.push(self.ch)
  415. if self.eatWhitespace(True) == 0 and not insidePropertyValue and self.opts.selector_separator_newline and parenLevel < 1:
  416. printer.newLine()
  417. else:
  418. printer.singleSpace()
  419. elif (self.ch == '>' or self.ch == '+' or self.ch == '~') and \
  420. not insidePropertyValue and parenLevel < 1:
  421. # handle combinator spacing
  422. if self.opts.space_around_combinator:
  423. printer.singleSpace()
  424. printer.push(self.ch)
  425. printer.singleSpace()
  426. else:
  427. printer.push(self.ch)
  428. self.eatWhitespace()
  429. # squash extra whitespace
  430. if self.ch and WHITE_RE.search(self.ch):
  431. self.ch = ''
  432. elif self.ch == ']':
  433. printer.push(self.ch)
  434. elif self.ch == '[':
  435. printer.preserveSingleSpace(isAfterSpace)
  436. printer.push(self.ch)
  437. elif self.ch == '=':
  438. # no whitespace before or after
  439. self.eatWhitespace()
  440. printer.push('=')
  441. if WHITE_RE.search(self.ch):
  442. self.ch = ''
  443. else:
  444. printer.preserveSingleSpace(isAfterSpace)
  445. printer.push(self.ch)
  446. sweet_code = re.sub('[\r\n\t ]+$', '', printer.result())
  447. # establish end_with_newline
  448. if self.opts.end_with_newline:
  449. sweet_code += '\n'
  450. if not self.opts.eol == '\n':
  451. sweet_code = sweet_code.replace('\n', self.opts.eol)
  452. return sweet_code