A fork of hyde, the static site generation. Some patches will be pushed upstream.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

405 lines
12 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. CSS plugins
  4. """
  5. from hyde.plugin import CLTransformer, Plugin
  6. from hyde.exceptions import HydeException
  7. import os
  8. import re
  9. import subprocess
  10. import sys
  11. from fswrap import File
  12. #
  13. # Less CSS
  14. #
  15. class LessCSSPlugin(CLTransformer):
  16. """
  17. The plugin class for less css
  18. """
  19. def __init__(self, site):
  20. super(LessCSSPlugin, self).__init__(site)
  21. self.import_finder = \
  22. re.compile('^\\s*@import\s+(?:\'|\")([^\'\"]*)(?:\'|\")\s*\;\s*$',
  23. re.MULTILINE)
  24. @property
  25. def executable_name(self):
  26. return "lessc"
  27. def _should_parse_resource(self, resource):
  28. """
  29. Check user defined
  30. """
  31. return resource.source_file.kind == 'less' and \
  32. getattr(resource, 'meta', {}).get('parse', True)
  33. def _should_replace_imports(self, resource):
  34. return getattr(resource, 'meta', {}).get('uses_template', True)
  35. def begin_site(self):
  36. """
  37. Find all the less css files and set their relative deploy path.
  38. """
  39. for resource in self.site.content.walk_resources():
  40. if self._should_parse_resource(resource):
  41. new_name = resource.source_file.name_without_extension + ".css"
  42. target_folder = File(resource.relative_deploy_path).parent
  43. resource.relative_deploy_path = target_folder.child(new_name)
  44. def begin_text_resource(self, resource, text):
  45. """
  46. Replace @import statements with {% include %} statements.
  47. """
  48. if not self._should_parse_resource(resource) or \
  49. not self._should_replace_imports(resource):
  50. return text
  51. def import_to_include(match):
  52. if not match.lastindex:
  53. return ''
  54. path = match.groups(1)[0]
  55. afile = File(resource.source_file.parent.child(path))
  56. if len(afile.kind.strip()) == 0:
  57. afile = File(afile.path + '.less')
  58. ref = self.site.content.resource_from_path(afile.path)
  59. if not ref:
  60. raise HydeException(
  61. "Cannot import from path [%s]" % afile.path)
  62. ref.is_processable = False
  63. return self.template.get_include_statement(ref.relative_path)
  64. text = self.import_finder.sub(import_to_include, text)
  65. return text
  66. @property
  67. def plugin_name(self):
  68. """
  69. The name of the plugin.
  70. """
  71. return "less"
  72. def text_resource_complete(self, resource, text):
  73. """
  74. Save the file to a temporary place and run less compiler.
  75. Read the generated file and return the text as output.
  76. Set the target path to have a css extension.
  77. """
  78. if not self._should_parse_resource(resource):
  79. return
  80. supported = [
  81. "verbose",
  82. ("silent", "s"),
  83. ("compress", "x"),
  84. "O0",
  85. "O1",
  86. "O2",
  87. "include-path="
  88. ]
  89. less = self.app
  90. source = File.make_temp(text)
  91. target = File.make_temp('')
  92. args = [unicode(less)]
  93. args.extend(self.process_args(supported))
  94. args.extend([unicode(source), unicode(target)])
  95. try:
  96. self.call_app(args)
  97. except subprocess.CalledProcessError:
  98. HydeException.reraise(
  99. "Cannot process %s. Error occurred when "
  100. "processing [%s]" % (self.app.name, resource.source_file),
  101. sys.exc_info())
  102. return target.read_all()
  103. #
  104. # Stylus CSS
  105. #
  106. class StylusPlugin(CLTransformer):
  107. """
  108. The plugin class for stylus css
  109. """
  110. def __init__(self, site):
  111. super(StylusPlugin, self).__init__(site)
  112. self.import_finder = \
  113. re.compile('^\\s*@import\s+(?:\'|\")([^\'\"]*)(?:\'|\")\s*\;?\s*$',
  114. re.MULTILINE)
  115. def begin_site(self):
  116. """
  117. Find all the styl files and set their relative deploy path.
  118. """
  119. for resource in self.site.content.walk_resources():
  120. if resource.source_file.kind == 'styl':
  121. new_name = resource.source_file.name_without_extension + ".css"
  122. target_folder = File(resource.relative_deploy_path).parent
  123. resource.relative_deploy_path = target_folder.child(new_name)
  124. def begin_text_resource(self, resource, text):
  125. """
  126. Replace @import statements with {% include %} statements.
  127. """
  128. if not resource.source_file.kind == 'styl':
  129. return
  130. def import_to_include(match):
  131. """
  132. Converts a css import statement to include statement.
  133. """
  134. if not match.lastindex:
  135. return ''
  136. path = match.groups(1)[0]
  137. first_child = resource.source_file.parent.child(path)
  138. afile = File(File(first_child).fully_expanded_path)
  139. if len(afile.kind.strip()) == 0:
  140. afile = File(afile.path + '.styl')
  141. ref = self.site.content.resource_from_path(afile.path)
  142. if not ref:
  143. try:
  144. include = self.settings.args.include
  145. except AttributeError:
  146. include = False
  147. if not include:
  148. raise HydeException(
  149. "Cannot import from path [%s]" % afile.path)
  150. else:
  151. ref.is_processable = False
  152. return "\n" + \
  153. self.template.get_include_statement(ref.relative_path) + \
  154. "\n"
  155. return '@import "' + path + '"\n'
  156. text = self.import_finder.sub(import_to_include, text)
  157. return text
  158. @property
  159. def defaults(self):
  160. """
  161. Returns `compress` if not in development mode.
  162. """
  163. try:
  164. mode = self.site.config.mode
  165. except AttributeError:
  166. mode = "production"
  167. defaults = {"compress": ""}
  168. if mode.startswith('dev'):
  169. defaults = {}
  170. return defaults
  171. @property
  172. def plugin_name(self):
  173. """
  174. The name of the plugin.
  175. """
  176. return "stylus"
  177. def text_resource_complete(self, resource, text):
  178. """
  179. Save the file to a temporary place and run stylus compiler.
  180. Read the generated file and return the text as output.
  181. Set the target path to have a css extension.
  182. """
  183. if not resource.source_file.kind == 'styl':
  184. return
  185. stylus = self.app
  186. source = File.make_temp(text.strip())
  187. supported = [("compress", "c"), ("include", "I")]
  188. args = [unicode(stylus)]
  189. args.extend(self.process_args(supported))
  190. args.append(unicode(source))
  191. try:
  192. self.call_app(args)
  193. except subprocess.CalledProcessError:
  194. HydeException.reraise(
  195. "Cannot process %s. Error occurred when "
  196. "processing [%s]" % (stylus.name, resource.source_file),
  197. sys.exc_info())
  198. target = File(source.path + '.css')
  199. return target.read_all()
  200. #
  201. # Clever CSS
  202. #
  203. class CleverCSSPlugin(Plugin):
  204. """
  205. The plugin class for CleverCSS
  206. """
  207. def __init__(self, site):
  208. super(CleverCSSPlugin, self).__init__(site)
  209. try:
  210. import clevercss
  211. except ImportError, e:
  212. raise HydeException('Unable to import CleverCSS: ' + e.message)
  213. else:
  214. self.clevercss = clevercss
  215. def _should_parse_resource(self, resource):
  216. """
  217. Check user defined
  218. """
  219. return resource.source_file.kind == 'ccss' and \
  220. getattr(resource, 'meta', {}).get('parse', True)
  221. def _should_replace_imports(self, resource):
  222. return getattr(resource, 'meta', {}).get('uses_template', True)
  223. def begin_site(self):
  224. """
  225. Find all the clevercss files and set their relative deploy path.
  226. """
  227. for resource in self.site.content.walk_resources():
  228. if self._should_parse_resource(resource):
  229. new_name = resource.source_file.name_without_extension + ".css"
  230. target_folder = File(resource.relative_deploy_path).parent
  231. resource.relative_deploy_path = target_folder.child(new_name)
  232. def begin_text_resource(self, resource, text):
  233. """
  234. Replace @import statements with {% include %} statements.
  235. """
  236. if not self._should_parse_resource(resource) or \
  237. not self._should_replace_imports(resource):
  238. return text
  239. import_finder = re.compile(
  240. '^\\s*@import\s+(?:\'|\")([^\'\"]*)(?:\'|\")\s*\;\s*$',
  241. re.MULTILINE)
  242. def import_to_include(match):
  243. if not match.lastindex:
  244. return ''
  245. path = match.groups(1)[0]
  246. afile = File(resource.source_file.parent.child(path))
  247. if len(afile.kind.strip()) == 0:
  248. afile = File(afile.path + '.ccss')
  249. ref = self.site.content.resource_from_path(afile.path)
  250. if not ref:
  251. raise HydeException(
  252. "Cannot import from path [%s]" % afile.path)
  253. ref.is_processable = False
  254. return self.template.get_include_statement(ref.relative_path)
  255. text = import_finder.sub(import_to_include, text)
  256. return text
  257. def text_resource_complete(self, resource, text):
  258. """
  259. Run clevercss compiler on text.
  260. """
  261. if not self._should_parse_resource(resource):
  262. return
  263. return self.clevercss.convert(text, self.settings)
  264. #
  265. # Sassy CSS
  266. #
  267. class SassyCSSPlugin(Plugin):
  268. """
  269. The plugin class for SassyCSS
  270. """
  271. def __init__(self, site):
  272. super(SassyCSSPlugin, self).__init__(site)
  273. try:
  274. import scss
  275. except ImportError, e:
  276. raise HydeException('Unable to import pyScss: ' + e.message)
  277. else:
  278. self.scss = scss
  279. def _should_parse_resource(self, resource):
  280. """
  281. Check user defined
  282. """
  283. return resource.source_file.kind == 'scss' and \
  284. getattr(resource, 'meta', {}).get('parse', True)
  285. @property
  286. def options(self):
  287. """
  288. Returns options depending on development mode
  289. """
  290. try:
  291. mode = self.site.config.mode
  292. except AttributeError:
  293. mode = "production"
  294. debug = mode.startswith('dev')
  295. opts = {'compress': not debug, 'debug_info': debug}
  296. site_opts = self.settings.get('options', {})
  297. opts.update(site_opts)
  298. return opts
  299. @property
  300. def vars(self):
  301. """
  302. Returns scss variables.
  303. """
  304. return self.settings.get('vars', {})
  305. @property
  306. def includes(self):
  307. """
  308. Returns scss load paths.
  309. """
  310. return self.settings.get('includes', [])
  311. def begin_site(self):
  312. """
  313. Find all the sassycss files and set their relative deploy path.
  314. """
  315. self.scss.STATIC_URL = self.site.content_url('/')
  316. self.scss.STATIC_ROOT = self.site.config.content_root_path.path
  317. self.scss.ASSETS_URL = self.site.media_url('/')
  318. self.scss.ASSETS_ROOT = self.site.config.deploy_root_path.child(
  319. self.site.config.media_root)
  320. for resource in self.site.content.walk_resources():
  321. if self._should_parse_resource(resource):
  322. new_name = resource.source_file.name_without_extension + ".css"
  323. target_folder = File(resource.relative_deploy_path).parent
  324. resource.relative_deploy_path = target_folder.child(new_name)
  325. def text_resource_complete(self, resource, text):
  326. """
  327. Run sassycss compiler on text.
  328. """
  329. if not self._should_parse_resource(resource):
  330. return
  331. includes = [resource.node.path] + self.includes
  332. includes = [path.rstrip(os.sep) + os.sep for path in includes]
  333. options = self.options
  334. if 'load_paths' not in options:
  335. options['load_paths'] = []
  336. options['load_paths'].extend(includes)
  337. scss = self.scss.Scss(scss_opts=options, scss_vars=self.vars)
  338. return scss.compile(text)