PageRenderTime 57ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/compressor/__init__.py

https://github.com/oesmith/django-css
Python | 316 lines | 304 code | 12 blank | 0 comment | 10 complexity | bc49d531db08890f791e54d9764cfc28 MD5 | raw file
Possible License(s): JSON
  1. import os
  2. import re
  3. import subprocess
  4. from BeautifulSoup import BeautifulSoup
  5. from tempfile import NamedTemporaryFile
  6. from textwrap import dedent
  7. try:
  8. from hashlib import sha1
  9. except ImportError:
  10. from sha import new as sha1
  11. from django import template
  12. from django.conf import settings as django_settings
  13. from django.template.loader import render_to_string
  14. from django.core.files.base import ContentFile
  15. from django.core.files.storage import default_storage
  16. from django.utils.encoding import smart_str
  17. from compressor.conf import settings
  18. from compressor import filters
  19. register = template.Library()
  20. class UncompressableFileError(Exception):
  21. pass
  22. def get_hexdigest(plaintext):
  23. p = smart_str(plaintext)
  24. return sha1(p).hexdigest()
  25. def exe_exists(program):
  26. def is_exe(fpath):
  27. return os.path.exists(fpath) and os.access(fpath, os.X_OK)
  28. fpath, fname = os.path.split(program)
  29. if fpath:
  30. if is_exe(program):
  31. return True
  32. else:
  33. for path in os.environ["PATH"].split(os.pathsep):
  34. exe_file = os.path.join(path, program)
  35. if is_exe(exe_file):
  36. return True
  37. return False
  38. class Compressor(object):
  39. def __init__(self, content, ouput_prefix="compressed", xhtml=False):
  40. self.content = content
  41. self.ouput_prefix = ouput_prefix
  42. self.split_content = []
  43. self.soup = BeautifulSoup(self.content)
  44. self.xhtml = xhtml
  45. try:
  46. from django.contrib.sites.models import Site
  47. self.domain = Site.objects.get_current().domain
  48. except:
  49. self.domain = ''
  50. def content_hash(self):
  51. """docstring for content_hash"""
  52. pass
  53. def split_contents(self):
  54. raise NotImplementedError('split_contents must be defined in a subclass')
  55. def get_filename(self, url):
  56. if not url.startswith(settings.MEDIA_URL):
  57. raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, settings.MEDIA_URL))
  58. basename = url[len(settings.MEDIA_URL):]
  59. filename = os.path.join(settings.MEDIA_ROOT, basename)
  60. return filename
  61. @property
  62. def mtimes(self):
  63. return (os.path.getmtime(h[1]) for h in self.split_contents() if h[0] == 'file')
  64. @property
  65. def cachekey(self):
  66. """
  67. cachekey for this block of css or js.
  68. """
  69. cachebits = [self.content]
  70. cachebits.extend([str(m) for m in self.mtimes])
  71. cachestr = "".join(cachebits)
  72. return "%s.django_compressor.%s.%s" % (self.domain, get_hexdigest(cachestr)[:12], settings.COMPRESS)
  73. @property
  74. def hunks(self):
  75. """
  76. Returns a list of processed data
  77. """
  78. if getattr(self, '_hunks', ''):
  79. return self._hunks
  80. self._hunks = []
  81. for kind, v, elem in self.split_contents():
  82. if kind == 'hunk':
  83. input = v
  84. if self.filters:
  85. input = self.filter(input, 'input', elem=elem)
  86. self._hunks.append(input)
  87. if kind == 'file':
  88. fd = open(v, 'rb')
  89. input = fd.read()
  90. if self.filters:
  91. input = self.filter(input, 'input', filename=v, elem=elem)
  92. self._hunks.append(input)
  93. fd.close()
  94. return self._hunks
  95. def concat(self):
  96. return "\n".join(self.hunks)
  97. def filter(self, content, method, **kwargs):
  98. content = content
  99. for f in self.filters:
  100. filter = getattr(filters.get_class(f)(content, filter_type=self.type), method)
  101. try:
  102. if callable(filter):
  103. content = filter(**kwargs)
  104. except NotImplementedError:
  105. pass
  106. return str(content)
  107. @property
  108. def combined(self):
  109. if getattr(self, '_output', ''):
  110. return self._output
  111. output = self.concat()
  112. filter_method = getattr(self, 'filter_method', None)
  113. if self.filters:
  114. output = self.filter(output, 'output')
  115. self._output = output
  116. return self._output
  117. @property
  118. def hash(self):
  119. return get_hexdigest(self.combined)[:12]
  120. @property
  121. def new_filepath(self):
  122. filename = "".join((self.hash, self.extension))
  123. filepath = "/".join((settings.OUTPUT_DIR.strip('/'), self.ouput_prefix, filename))
  124. return filepath
  125. def save_file(self):
  126. if default_storage.exists(self.new_filepath):
  127. return False
  128. default_storage.save(self.new_filepath, ContentFile(self.combined))
  129. return True
  130. def return_compiled_content(self, content):
  131. """
  132. Return compiled css
  133. """
  134. if self.type != 'css':
  135. return content
  136. if not self.split_content:
  137. self.split_contents()
  138. if self.xhtml:
  139. return os.linesep.join((unicode(i[2]) for i in self.split_content))
  140. else:
  141. return os.linesep.join((re.sub("\s?/>",">",unicode(i[2])) for i in self.split_content))
  142. def output(self):
  143. """
  144. Return the versioned file path if COMPRESS = True
  145. """
  146. if not settings.COMPRESS:
  147. return self.return_compiled_content(self.content)
  148. url = "/".join((settings.MEDIA_URL.rstrip('/'), self.new_filepath))
  149. self.save_file()
  150. context = getattr(self, 'extra_context', {})
  151. context['url'] = url
  152. context['xhtml'] = self.xhtml
  153. return render_to_string(self.template_name, context)
  154. class CssCompressor(Compressor):
  155. def __init__(self, content, ouput_prefix="css", xhtml=False):
  156. self.extension = ".css"
  157. self.template_name = "compressor/css.html"
  158. self.filters = ['compressor.filters.css_default.CssAbsoluteFilter', 'compressor.filters.css_default.CssMediaFilter']
  159. self.filters.extend(settings.COMPRESS_CSS_FILTERS)
  160. self.type = 'css'
  161. super(CssCompressor, self).__init__(content, ouput_prefix, xhtml)
  162. @staticmethod
  163. def compile(filename,compiler):
  164. """
  165. Runs compiler on given file.
  166. Results are expected to appear nearby, same name, .css extension
  167. """
  168. try:
  169. bin = compiler['binary_path']
  170. except:
  171. raise Exception("Path to CSS compiler must be included in COMPILER_FORMATS")
  172. arguments = compiler.get('arguments','').replace("*",filename)
  173. command = '%s %s' % (bin, arguments)
  174. p = subprocess.Popen(command,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
  175. if p.wait() != 0:
  176. err = p.stderr.read()
  177. p.stderr.close()
  178. if not err:
  179. err = 'Invalid command to CSS compiler: %s' % command
  180. raise Exception(err)
  181. def compile_inline(self,data,ext):
  182. """
  183. Compile inline css. Have to compile to a file, because some css compilers
  184. may not output to stdout, but we know they all output to a file. It's a
  185. little hackish, but you shouldn't be compiling in production anyway,
  186. right?
  187. """
  188. compiler = settings.COMPILER_FORMATS[ext]
  189. try:
  190. bin = compiler['binary_path']
  191. except:
  192. raise Exception("Path to CSS compiler must be included in COMPILER_FORMATS")
  193. tmp_file = NamedTemporaryFile(mode='w',suffix=ext)
  194. tmp_file.write(dedent(data))
  195. tmp_file.flush()
  196. path, ext = os.path.splitext(tmp_file.name)
  197. tmp_css = ''.join((path,'.css'))
  198. self.compile(path,compiler)
  199. data = open(tmp_css,'r').read()
  200. # cleanup
  201. tmp_file.close()
  202. os.remove(tmp_css)
  203. return data
  204. @staticmethod
  205. def recompile(filename):
  206. """
  207. Needed for CCS Compilers, returns True when file needs recompiling
  208. """
  209. path, ext = os.path.splitext(filename)
  210. compiled_filename = path + '.css'
  211. if not os.path.exists(compiled_filename):
  212. return True
  213. else:
  214. if os.path.getmtime(filename) > os.path.getmtime(compiled_filename):
  215. return True
  216. else:
  217. return False
  218. def split_contents(self):
  219. """ Iterates over the elements in the block """
  220. if self.split_content:
  221. return self.split_content
  222. split = self.soup.findAll({'link' : True, 'style' : True})
  223. for elem in split:
  224. if elem.name == 'link' and elem['rel'] == 'stylesheet':
  225. filename = self.get_filename(elem['href'])
  226. path, ext = os.path.splitext(filename)
  227. if ext in settings.COMPILER_FORMATS.keys():
  228. if self.recompile(filename):
  229. self.compile(path,settings.COMPILER_FORMATS[ext])
  230. basename = os.path.splitext(os.path.basename(filename))[0]
  231. elem = BeautifulSoup(re.sub(basename+ext,basename+'.css',unicode(elem)))
  232. filename = path + '.css'
  233. try:
  234. self.split_content.append(('file', filename, elem))
  235. except UncompressableFileError:
  236. if django_settings.DEBUG:
  237. raise
  238. if elem.name == 'style':
  239. data = elem.string
  240. elem_type = elem.get('type', '').lower()
  241. if elem_type and elem_type != "text/css":
  242. # it has to be preprocessed
  243. if '/' in elem_type:
  244. # we accept 'text/ccss' and plain 'ccss' too
  245. elem_type = elem_type.split('/')[1]
  246. # TODO: that dot-adding compatibility stuff looks strange.
  247. # do we really need a dot in COMPILER_FORMATS keys?
  248. ext = '.'+elem_type
  249. data = self.compile_inline(data,ext)
  250. elem = ''.join(("<style type='text/css'>\n",data,"\n</style>"))
  251. self.split_content.append(('hunk', data, elem))
  252. return self.split_content
  253. class JsCompressor(Compressor):
  254. def __init__(self, content, ouput_prefix="js", xhtml=False):
  255. self.extension = ".js"
  256. self.template_name = "compressor/js.html"
  257. self.filters = settings.COMPRESS_JS_FILTERS
  258. self.type = 'js'
  259. super(JsCompressor, self).__init__(content, ouput_prefix, xhtml)
  260. def split_contents(self):
  261. """ Iterates over the elements in the block """
  262. if self.split_content:
  263. return self.split_content
  264. split = self.soup.findAll('script')
  265. for elem in split:
  266. if elem.has_key('src'):
  267. try:
  268. self.split_content.append(('file', self.get_filename(elem['src']), elem))
  269. except UncompressableFileError:
  270. if django_settings.DEBUG:
  271. raise
  272. else:
  273. self.split_content.append(('hunk', elem.string, elem))
  274. return self.split_content