js_minifer.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. # -*- coding: utf-8 -*-
  2. """JavaScript Minifier functions for CSS-JS-Minify."""
  3. import re
  4. from io import StringIO # pure-Python StringIO supports unicode.
  5. from .css_minifer import condense_semicolons
  6. __all__ = ('js_minify', )
  7. def remove_commented_lines(js):
  8. """Force remove commented out lines from Javascript."""
  9. result = ""
  10. for line in js.splitlines():
  11. line = re.sub(r"/\*.*\*/", "", line) # (/*COMMENT */)
  12. line = re.sub(r"//.*", "", line) # (//COMMENT)
  13. result += '\n'+line
  14. return result
  15. def simple_replacer_js(js):
  16. """Force strip simple replacements from Javascript."""
  17. return condense_semicolons(js.replace("debugger;", ";").replace(
  18. ";}", "}").replace("; ", ";").replace(" ;", ";").rstrip("\n;"))
  19. def js_minify_keep_comments(js):
  20. """Return a minified version of the Javascript string."""
  21. ins, outs = StringIO(js), StringIO()
  22. JavascriptMinify(ins, outs).minify()
  23. return force_single_line_js(outs.getvalue())
  24. def force_single_line_js(js):
  25. """Force Javascript to a single line, even if need to add semicolon."""
  26. return ";".join(js.splitlines()) if len(js.splitlines()) > 1 else js
  27. class JavascriptMinify(object):
  28. """Minify an input stream of Javascript, writing to an output stream."""
  29. def __init__(self, instream=None, outstream=None):
  30. """Init class."""
  31. self.ins, self.outs = instream, outstream
  32. def minify(self, instream=None, outstream=None):
  33. """Minify Javascript using StringIO."""
  34. if instream and outstream:
  35. self.ins, self.outs = instream, outstream
  36. write, read = self.outs.write, self.ins.read
  37. space_strings = ("abcdefghijklmnopqrstuvwxyz"
  38. "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\")
  39. starters, enders = '{[(+-', '}])+-"\''
  40. newlinestart_strings = starters + space_strings
  41. newlineend_strings = enders + space_strings
  42. do_newline, do_space = False, False
  43. doing_single_comment, doing_multi_comment = False, False
  44. previous_before_comment, in_quote = '', ''
  45. in_re, quote_buf = False, []
  46. previous = read(1)
  47. next1 = read(1)
  48. if previous == '/':
  49. if next1 == '/':
  50. doing_single_comment = True
  51. elif next1 == '*':
  52. doing_multi_comment = True
  53. else:
  54. write(previous)
  55. elif not previous:
  56. return
  57. elif previous >= '!':
  58. if previous in "'\"":
  59. in_quote = previous
  60. write(previous)
  61. previous_non_space = previous
  62. else:
  63. previous_non_space = ' '
  64. if not next1:
  65. return
  66. while True:
  67. next2 = read(1)
  68. if not next2:
  69. last = next1.strip()
  70. conditional_1 = (doing_single_comment or doing_multi_comment)
  71. if not conditional_1 and last not in ('', '/'):
  72. write(last)
  73. break
  74. if doing_multi_comment:
  75. if next1 == '*' and next2 == '/':
  76. doing_multi_comment = False
  77. next2 = read(1)
  78. elif doing_single_comment:
  79. if next1 in '\r\n':
  80. doing_single_comment = False
  81. while next2 in '\r\n':
  82. next2 = read(1)
  83. if not next2:
  84. break
  85. if previous_before_comment in ')}]':
  86. do_newline = True
  87. elif previous_before_comment in space_strings:
  88. write('\n')
  89. elif in_quote:
  90. quote_buf.append(next1)
  91. if next1 == in_quote:
  92. numslashes = 0
  93. for c in reversed(quote_buf[:-1]):
  94. if c != '\\':
  95. break
  96. else:
  97. numslashes += 1
  98. if numslashes % 2 == 0:
  99. in_quote = ''
  100. write(''.join(quote_buf))
  101. elif next1 in '\r\n':
  102. conditional_2 = previous_non_space in newlineend_strings
  103. if conditional_2 or previous_non_space > '~':
  104. while 1:
  105. if next2 < '!':
  106. next2 = read(1)
  107. if not next2:
  108. break
  109. else:
  110. conditional_3 = next2 in newlinestart_strings
  111. if conditional_3 or next2 > '~' or next2 == '/':
  112. do_newline = True
  113. break
  114. elif next1 < '!' and not in_re:
  115. conditional_4 = next2 in space_strings or next2 > '~'
  116. conditional_5 = previous_non_space in space_strings
  117. conditional_6 = previous_non_space > '~'
  118. if (conditional_5 or conditional_6) and (conditional_4):
  119. do_space = True
  120. elif next1 == '/':
  121. if in_re:
  122. if previous != '\\':
  123. in_re = False
  124. write('/')
  125. elif next2 == '/':
  126. doing_single_comment = True
  127. previous_before_comment = previous_non_space
  128. elif next2 == '*':
  129. doing_multi_comment = True
  130. else:
  131. in_re = previous_non_space in '(,=:[?!&|'
  132. write('/')
  133. else:
  134. if do_space:
  135. do_space = False
  136. write(' ')
  137. if do_newline:
  138. write('\n')
  139. do_newline = False
  140. write(next1)
  141. if not in_re and next1 in "'\"":
  142. in_quote = next1
  143. quote_buf = []
  144. previous = next1
  145. next1 = next2
  146. if previous >= '!':
  147. previous_non_space = previous
  148. def js_minify(js):
  149. """Minify a JavaScript string."""
  150. js = remove_commented_lines(js)
  151. js = js_minify_keep_comments(js)
  152. return js.strip()