A fork of hyde, the static site generation. Some patches will be pushed upstream.
 
 
 

269 lines
9.4 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. The generator class and related utility functions.
  4. """
  5. from hyde.exceptions import HydeException
  6. from hyde.fs import File, Folder
  7. from hyde.model import Context
  8. from hyde.plugin import Plugin
  9. from hyde.template import Template
  10. from contextlib import contextmanager
  11. from hyde.util import getLoggerWithNullHandler
  12. logger = getLoggerWithNullHandler('hyde.engine')
  13. class Generator(object):
  14. """
  15. Generates output from a node or resource.
  16. """
  17. def __init__(self, site):
  18. super(Generator, self).__init__()
  19. self.site = site
  20. self.generated_once = False
  21. self.__context__ = dict(site=site)
  22. if hasattr(site.config, 'context'):
  23. self.__context__.update(
  24. Context.load(site.sitepath, site.config.context))
  25. self.template = None
  26. Plugin.load_all(site)
  27. self.events = Plugin.get_proxy(self.site)
  28. @contextmanager
  29. def context_for_resource(self, resource):
  30. """
  31. Context manager that intializes the context for a given
  32. resource and rolls it back after the resource is processed.
  33. """
  34. # TODO: update metadata and other resource
  35. # specific properties here.
  36. self.__context__.update(resource=resource)
  37. yield self.__context__
  38. self.__context__.update(resource=None)
  39. def context_for_path(self, path):
  40. resource = self.site.resource_from_path(path)
  41. if not resource:
  42. return {}
  43. ctx = self.__context__.copy
  44. ctx.resource = resource
  45. return ctx
  46. def load_template_if_needed(self):
  47. """
  48. Loads and configures the template environement from the site
  49. configuration if its not done already.
  50. """
  51. class GeneratorProxy(object):
  52. """
  53. An interface to templates and plugins for
  54. providing restricted access to the methods.
  55. """
  56. def __init__(self, preprocessor=None, postprocessor=None, context_for_path=None):
  57. self.preprocessor = preprocessor
  58. self.postprocessor = postprocessor
  59. self.context_for_path = context_for_path
  60. if not self.template:
  61. logger.info("Generating site at [%s]" % self.site.sitepath)
  62. self.template = Template.find_template(self.site)
  63. logger.debug("Using [%s] as the template",
  64. self.template.__class__.__name__)
  65. logger.info("Configuring the template environment")
  66. self.template.configure(self.site,
  67. engine=GeneratorProxy(
  68. context_for_path=self.context_for_path,
  69. preprocessor=self.events.begin_text_resource,
  70. postprocessor=self.events.text_resource_complete))
  71. self.events.template_loaded(self.template)
  72. def initialize(self):
  73. """
  74. Start Generation. Perform setup tasks and inform plugins.
  75. """
  76. logger.info("Begin Generation")
  77. self.events.begin_generation()
  78. def load_site_if_needed(self):
  79. """
  80. Checks if the site requries a reload and loads if
  81. necessary.
  82. """
  83. #TODO: Perhaps this is better suited in Site
  84. if not len(self.site.content.child_nodes):
  85. logger.info("Reading site contents")
  86. self.site.load()
  87. def finalize(self):
  88. """
  89. Generation complete. Inform plugins and cleanup.
  90. """
  91. logger.info("Generation Complete")
  92. self.events.generation_complete()
  93. def has_resource_changed(self, resource):
  94. """
  95. Checks if the given resource has changed since the
  96. last generation.
  97. """
  98. if not self.generated_once:
  99. return True
  100. logger.debug("Checking for changes in %s" % resource)
  101. self.load_site_if_needed()
  102. self.load_template_if_needed()
  103. target = File(self.site.config.deploy_root_path.child(
  104. resource.relative_deploy_path))
  105. if not target.exists or target.older_than(resource.source_file):
  106. logger.debug("Found changes in %s" % resource)
  107. return True
  108. if resource.source_file.is_binary or not resource.uses_template:
  109. logger.debug("No Changes found in %s" % resource)
  110. return False
  111. deps = self.template.get_dependencies(resource.relative_path)
  112. if not deps or None in deps:
  113. logger.debug("No changes found in %s" % resource)
  114. return False
  115. content = self.site.content.source_folder
  116. layout = Folder(self.site.sitepath).child_folder('layout')
  117. logger.debug("Checking for changes in dependents:%s" % deps)
  118. for dep in deps:
  119. source = File(content.child(dep))
  120. if not source.exists:
  121. source = File(layout.child(dep))
  122. if not source.exists:
  123. return True
  124. if target.older_than(source):
  125. return True
  126. logger.debug("No changes found in %s" % resource)
  127. return False
  128. def generate_all(self):
  129. """
  130. Generates the entire website
  131. """
  132. logger.info("Reading site contents")
  133. self.load_template_if_needed()
  134. self.initialize()
  135. self.load_site_if_needed()
  136. self.events.begin_site()
  137. logger.info("Generating site to [%s]" %
  138. self.site.config.deploy_root_path)
  139. self.__generate_node__(self.site.content)
  140. self.events.site_complete()
  141. self.finalize()
  142. self.generated_once = True
  143. def generate_node_at_path(self, node_path=None):
  144. """
  145. Generates a single node. If node_path is non-existent or empty,
  146. generates the entire site.
  147. """
  148. if not self.generated_once:
  149. return self.generate_all()
  150. self.load_template_if_needed()
  151. self.load_site_if_needed()
  152. node = None
  153. if node_path:
  154. node = self.site.content.node_from_path(node_path)
  155. self.generate_node(node)
  156. def generate_node(self, node=None):
  157. """
  158. Generates the given node. If node is invalid, empty or
  159. non-existent, generates the entire website.
  160. """
  161. if not node or not self.generated_once:
  162. return self.generate_all()
  163. self.load_template_if_needed()
  164. self.initialize()
  165. self.load_site_if_needed()
  166. try:
  167. self.__generate_node__(node)
  168. self.finalize()
  169. except HydeException:
  170. self.generate_all()
  171. def generate_resource_at_path(self, resource_path=None):
  172. """
  173. Generates a single resource. If resource_path is non-existent or empty,
  174. generats the entire website.
  175. """
  176. if not self.generated_once:
  177. return self.generate_all()
  178. self.load_template_if_needed()
  179. self.load_site_if_needed()
  180. resource = None
  181. if resource_path:
  182. resource = self.site.content.resource_from_path(resource_path)
  183. self.generate_resource(resource)
  184. def generate_resource(self, resource=None):
  185. """
  186. Generates the given resource. If resource is invalid, empty or
  187. non-existent, generates the entire website.
  188. """
  189. if not resource or not self.generated_once:
  190. return self.generate_all()
  191. self.load_template_if_needed()
  192. self.initialize()
  193. self.load_site_if_needed()
  194. try:
  195. self.__generate_resource__(resource)
  196. self.finalize()
  197. except HydeException:
  198. self.generate_all()
  199. def __generate_node__(self, node):
  200. for node in node.walk():
  201. logger.debug("Generating Node [%s]", node)
  202. self.events.begin_node(node)
  203. for resource in node.resources:
  204. self.__generate_resource__(resource)
  205. self.events.node_complete(node)
  206. def __generate_resource__(self, resource):
  207. if not resource.is_processable:
  208. logger.debug("Skipping [%s]", resource)
  209. return
  210. logger.debug("Processing [%s]", resource)
  211. with self.context_for_resource(resource) as context:
  212. if resource.source_file.is_text:
  213. text = resource.source_file.read_all()
  214. text = self.events.begin_text_resource(resource, text) or text
  215. if resource.uses_template:
  216. logger.debug("Rendering [%s]", resource)
  217. try:
  218. text = self.template.render(text, context)
  219. except Exception:
  220. logger.error("Error occurred when"
  221. " processing template:[%s]" % resource)
  222. raise
  223. text = self.events.text_resource_complete(
  224. resource, text) or text
  225. target = File(self.site.config.deploy_root_path.child(
  226. resource.relative_deploy_path))
  227. target.parent.make()
  228. target.write(text)
  229. else:
  230. logger.debug("Copying binary file [%s]", resource)
  231. self.events.begin_binary_resource(resource)
  232. target = File(self.site.config.deploy_root_path.child(
  233. resource.relative_deploy_path))
  234. target.parent.make()
  235. resource.source_file.copy_to(target)
  236. self.events.binary_resource_complete(resource)