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.
 
 
 

417 lines
13 KiB

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