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.
 
 
 

352 lines
11 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. Parses & holds information about the site to be generated.
  4. """
  5. from hyde.exceptions import HydeException
  6. from hyde.fs import FS, File, Folder
  7. from hyde.model import Config
  8. import logging
  9. from logging import NullHandler
  10. logger = logging.getLogger('hyde.engine')
  11. logger.addHandler(NullHandler())
  12. class Processable(object):
  13. """
  14. A node or resource.
  15. """
  16. def __init__(self, source):
  17. super(Processable, self).__init__()
  18. self.source = FS.file_or_folder(source)
  19. self.is_processable = True
  20. self.uses_template = True
  21. @property
  22. def name(self):
  23. """
  24. The resource name
  25. """
  26. return self.source.name
  27. def __repr__(self):
  28. return self.path
  29. @property
  30. def path(self):
  31. """
  32. Gets the source path of this node.
  33. """
  34. return self.source.path
  35. class Resource(Processable):
  36. """
  37. Represents any file that is processed by hyde
  38. """
  39. def __init__(self, source_file, node):
  40. super(Resource, self).__init__(source_file)
  41. self.source_file = source_file
  42. if not node:
  43. raise HydeException("Resource cannot exist without a node")
  44. if not source_file:
  45. raise HydeException("Source file is required"
  46. " to instantiate a resource")
  47. self.node = node
  48. self.site = node.site
  49. self._relative_deploy_path = None
  50. @property
  51. def relative_path(self):
  52. """
  53. Gets the path relative to the root folder (Content, Media, Layout)
  54. """
  55. return self.source_file.get_relative_path(self.node.root.source_folder)
  56. def get_relative_deploy_path(self):
  57. """
  58. Gets the path where the file will be created
  59. after its been processed.
  60. """
  61. return self._relative_deploy_path \
  62. if self._relative_deploy_path \
  63. else self.relative_path
  64. def set_relative_deploy_path(self, path):
  65. """
  66. Sets the path where the file ought to be created
  67. after its been processed.
  68. """
  69. self._relative_deploy_path = path
  70. self.site.content.resource_deploy_path_changed(self)
  71. relative_deploy_path = property(get_relative_deploy_path, set_relative_deploy_path)
  72. class Node(Processable):
  73. """
  74. Represents any folder that is processed by hyde
  75. """
  76. def __init__(self, source_folder, parent=None):
  77. super(Node, self).__init__(source_folder)
  78. if not source_folder:
  79. raise HydeException("Source folder is required"
  80. " to instantiate a node.")
  81. self.root = self
  82. self.module = None
  83. self.site = None
  84. self.source_folder = Folder(str(source_folder))
  85. self.parent = parent
  86. if parent:
  87. self.root = self.parent.root
  88. self.module = self.parent.module if self.parent.module else self
  89. self.site = parent.site
  90. self.child_nodes = []
  91. self.resources = []
  92. def contains_resource(self, resource_name):
  93. """
  94. Returns True if the given resource name exists as a file
  95. in this node's source folder.
  96. """
  97. return File(self.source_folder.child(resource_name)).exists
  98. def get_resource(self, resource_name):
  99. """
  100. Gets the resource if the given resource name exists as a file
  101. in this node's source folder.
  102. """
  103. if self.contains_resource(resource_name):
  104. return self.root.resource_from_path(
  105. self.source_folder.child(resource_name))
  106. return None
  107. def add_child_node(self, folder):
  108. """
  109. Creates a new child node and adds it to the list of child nodes.
  110. """
  111. if folder.parent != self.source_folder:
  112. raise HydeException("The given folder [%s] is not a"
  113. " direct descendant of [%s]" %
  114. (folder, self.source_folder))
  115. node = Node(folder, self)
  116. self.child_nodes.append(node)
  117. return node
  118. def add_child_resource(self, afile):
  119. """
  120. Creates a new resource and adds it to the list of child resources.
  121. """
  122. if afile.parent != self.source_folder:
  123. raise HydeException("The given file [%s] is not"
  124. " a direct descendant of [%s]" %
  125. (afile, self.source_folder))
  126. resource = Resource(afile, self)
  127. self.resources.append(resource)
  128. return resource
  129. def walk(self):
  130. """
  131. Walks the node, first yielding itself then
  132. yielding the child nodes depth-first.
  133. """
  134. yield self
  135. for child in self.child_nodes:
  136. for node in child.walk():
  137. yield node
  138. def walk_resources(self):
  139. """
  140. Walks the resources in this hierarchy.
  141. """
  142. for node in self.walk():
  143. for resource in node.resources:
  144. yield resource
  145. @property
  146. def relative_path(self):
  147. """
  148. Gets the path relative to the root folder (Content, Media, Layout)
  149. """
  150. return self.source_folder.get_relative_path(self.root.source_folder)
  151. @property
  152. def url(self):
  153. return '/' + self.relative_path
  154. @property
  155. def full_url(self):
  156. return self.site.config.base_url.rstrip('/') + self.url
  157. class RootNode(Node):
  158. """
  159. Represents one of the roots of site: Content, Media or Layout
  160. """
  161. def __init__(self, source_folder, site):
  162. super(RootNode, self).__init__(source_folder)
  163. self.site = site
  164. self.node_map = {}
  165. self.node_deploy_map = {}
  166. self.resource_map = {}
  167. self.resource_deploy_map = {}
  168. def node_from_path(self, path):
  169. """
  170. Gets the node that maps to the given path.
  171. If no match is found it returns None.
  172. """
  173. if Folder(path) == self.source_folder:
  174. return self
  175. return self.node_map.get(str(Folder(path)), None)
  176. def node_from_relative_path(self, relative_path):
  177. """
  178. Gets the content node that maps to the given relative path.
  179. If no match is found it returns None.
  180. """
  181. return self.node_from_path(
  182. self.source_folder.child(str(relative_path)))
  183. def resource_from_path(self, path):
  184. """
  185. Gets the resource that maps to the given path.
  186. If no match is found it returns None.
  187. """
  188. return self.resource_map.get(str(File(path)), None)
  189. def resource_from_relative_path(self, relative_path):
  190. """
  191. Gets the content resource that maps to the given relative path.
  192. If no match is found it returns None.
  193. """
  194. return self.resource_from_path(
  195. self.source_folder.child(str(relative_path)))
  196. def resource_deploy_path_changed(self, resource):
  197. """
  198. Handles the case where the relative deploy path of a
  199. resource has changed.
  200. """
  201. self.resource_deploy_map[str(resource.relative_deploy_path)] = resource
  202. def resource_from_relative_deploy_path(self, relative_deploy_path):
  203. """
  204. Gets the content resource whose deploy path maps to
  205. the given relative path. If no match is found it returns None.
  206. """
  207. if relative_deploy_path in self.resource_deploy_map:
  208. return self.resource_deploy_map[relative_deploy_path]
  209. return self.resource_from_relative_path(relative_deploy_path)
  210. def add_node(self, a_folder):
  211. """
  212. Adds a new node to this folder's hierarchy.
  213. Also adds to to the hashtable of path to node associations
  214. for quick lookup.
  215. """
  216. folder = Folder(a_folder)
  217. node = self.node_from_path(folder)
  218. if node:
  219. logger.info("Node exists at [%s]" % node.relative_path)
  220. return node
  221. if not folder.is_descendant_of(self.source_folder):
  222. raise HydeException("The given folder [%s] does not"
  223. " belong to this hierarchy [%s]" %
  224. (folder, self.source_folder))
  225. p_folder = folder
  226. parent = None
  227. hierarchy = []
  228. while not parent:
  229. hierarchy.append(p_folder)
  230. p_folder = p_folder.parent
  231. parent = self.node_from_path(p_folder)
  232. hierarchy.reverse()
  233. node = parent if parent else self
  234. for h_folder in hierarchy:
  235. node = node.add_child_node(h_folder)
  236. self.node_map[str(h_folder)] = node
  237. logger.info("Added node [%s] to [%s]" % (
  238. node.relative_path, self.source_folder))
  239. return node
  240. def add_resource(self, a_file):
  241. """
  242. Adds a file to the parent node. Also adds to to the
  243. hashtable of path to resource associations for quick lookup.
  244. """
  245. afile = File(a_file)
  246. resource = self.resource_from_path(afile)
  247. if resource:
  248. logger.info("Resource exists at [%s]" % resource.relative_path)
  249. if not afile.is_descendant_of(self.source_folder):
  250. raise HydeException("The given file [%s] does not reside"
  251. " in this hierarchy [%s]" %
  252. (afile, self.source_folder))
  253. node = self.node_from_path(afile.parent)
  254. if not node:
  255. node = self.add_node(afile.parent)
  256. resource = node.add_child_resource(afile)
  257. self.resource_map[str(afile)] = resource
  258. logger.info("Added resource [%s] to [%s]" %
  259. (resource.relative_path, self.source_folder))
  260. return resource
  261. def load(self):
  262. """
  263. Walks the `source_folder` and loads the sitemap.
  264. Creates nodes and resources, reads metadata and injects attributes.
  265. This is the model for hyde.
  266. """
  267. if not self.source_folder.exists:
  268. raise HydeException("The given source folder[%s]"
  269. " does not exist" % self.source_folder)
  270. with self.source_folder.walker as walker:
  271. @walker.folder_visitor
  272. def visit_folder(folder):
  273. self.add_node(folder)
  274. @walker.file_visitor
  275. def visit_file(afile):
  276. self.add_resource(afile)
  277. class Site(object):
  278. """
  279. Represents the site to be generated.
  280. """
  281. def __init__(self, sitepath=None, config=None):
  282. super(Site, self).__init__()
  283. self.sitepath = Folder(Folder(sitepath).fully_expanded_path)
  284. self.config = config if config else Config(self.sitepath)
  285. self.content = RootNode(self.config.content_root_path, self)
  286. self.plugins = []
  287. def load(self):
  288. """
  289. Walks the content and media folders to load up the sitemap.
  290. """
  291. self.content.load()