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.
 
 
 

332 lines
10 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. Plugins related to structure
  4. """
  5. from hyde.ext.plugins.meta import Metadata
  6. from hyde.plugin import Plugin
  7. from hyde.site import Resource
  8. from hyde.util import pairwalk
  9. from fswrap import File, Folder
  10. import os
  11. from fnmatch import fnmatch
  12. import operator
  13. #
  14. # Folder Flattening
  15. #
  16. class FlattenerPlugin(Plugin):
  17. """
  18. The plugin class for flattening nested folders.
  19. """
  20. def __init__(self, site):
  21. super(FlattenerPlugin, self).__init__(site)
  22. def begin_site(self):
  23. """
  24. Finds all the folders that need flattening and changes the
  25. relative deploy path of all resources in those folders.
  26. """
  27. items = []
  28. try:
  29. items = self.site.config.flattener.items
  30. except AttributeError:
  31. pass
  32. for item in items:
  33. node = None
  34. target = ''
  35. try:
  36. node = self.site.content.node_from_relative_path(item.source)
  37. target = Folder(item.target)
  38. except AttributeError:
  39. continue
  40. if node:
  41. for resource in node.walk_resources():
  42. target_path = target.child(resource.name)
  43. self.logger.debug(
  44. 'Flattening resource path [%s] to [%s]' %
  45. (resource, target_path))
  46. resource.relative_deploy_path = target_path
  47. for child in node.walk():
  48. child.relative_deploy_path = target.path
  49. #
  50. # Combine
  51. #
  52. class CombinePlugin(Plugin):
  53. """
  54. To use this combine, the following configuration should be added
  55. to meta data::
  56. combine:
  57. sort: false #Optional. Defaults to true.
  58. root: content/media #Optional. Path must be relative to content folder - default current folder
  59. recurse: true #Optional. Default false.
  60. files:
  61. - ns1.*.js
  62. - ns2.*.js
  63. where: top
  64. remove: yes
  65. `files` is a list of resources (or just a resource) that should be
  66. combined. Globbing is performed. `where` indicate where the
  67. combination should be done. This could be `top` or `bottom` of the
  68. file. `remove` tell if we should remove resources that have been
  69. combined into the resource.
  70. """
  71. def __init__(self, site):
  72. super(CombinePlugin, self).__init__(site)
  73. def _combined(self, resource):
  74. """
  75. Return the list of resources to combine to build this one.
  76. """
  77. try:
  78. config = resource.meta.combine
  79. except AttributeError:
  80. return [] # Not a combined resource
  81. try:
  82. files = config.files
  83. except AttributeError:
  84. raise AttributeError("No resources to combine for [%s]" % resource)
  85. if type(files) is str:
  86. files = [ files ]
  87. # Grab resources to combine
  88. # select site root
  89. try:
  90. root = self.site.content.node_from_relative_path(resource.meta.combine.root)
  91. except AttributeError:
  92. root = resource.node
  93. # select walker
  94. try:
  95. recurse = resource.meta.combine.recurse
  96. except AttributeError:
  97. recurse = False
  98. walker = root.walk_resources() if recurse else root.resources
  99. # Must we sort?
  100. try:
  101. sort = resource.meta.combine.sort
  102. except AttributeError:
  103. sort = True
  104. if sort:
  105. resources = sorted([r for r in walker if any(fnmatch(r.name, f) for f in files)],
  106. key=operator.attrgetter('name'))
  107. else:
  108. resources = [(f, r) for r in walker for f in files if fnmatch(r.name, f)]
  109. resources = [r[1] for f in files for r in resources if f in r]
  110. if not resources:
  111. self.logger.debug("No resources to combine for [%s]" % resource)
  112. return []
  113. return resources
  114. def begin_site(self):
  115. """
  116. Initialize the plugin and search for the combined resources
  117. """
  118. for node in self.site.content.walk():
  119. for resource in node.resources:
  120. resources = self._combined(resource)
  121. if not resources:
  122. continue
  123. # Build depends
  124. if not hasattr(resource, 'depends'):
  125. resource.depends = []
  126. resource.depends.extend(
  127. [r.relative_path for r in resources
  128. if r.relative_path not in resource.depends])
  129. # Remove combined resources if needed
  130. if hasattr(resource.meta.combine, "remove") and \
  131. resource.meta.combine.remove:
  132. for r in resources:
  133. self.logger.debug(
  134. "Resource [%s] removed because combined" % r)
  135. r.is_processable = False
  136. def begin_text_resource(self, resource, text):
  137. """
  138. When generating a resource, add combined file if needed.
  139. """
  140. resources = self._combined(resource)
  141. if not resources:
  142. return
  143. where = "bottom"
  144. try:
  145. where = resource.meta.combine.where
  146. except AttributeError:
  147. pass
  148. if where not in [ "top", "bottom" ]:
  149. raise ValueError("%r should be either `top` or `bottom`" % where)
  150. self.logger.debug(
  151. "Combining %d resources for [%s]" % (len(resources),
  152. resource))
  153. if where == "top":
  154. return "".join([r.source.read_all() for r in resources] + [text])
  155. else:
  156. return "".join([text] + [r.source.read_all() for r in resources])
  157. #
  158. # Pagination
  159. #
  160. class Page:
  161. def __init__(self, posts, number):
  162. self.posts = posts
  163. self.number = number
  164. class Paginator:
  165. """
  166. Iterates resources which have pages associated with them.
  167. """
  168. file_pattern = 'page$PAGE/$FILE$EXT'
  169. def __init__(self, settings):
  170. self.sorter = getattr(settings, 'sorter', None)
  171. self.size = getattr(settings, 'size', 10)
  172. self.file_pattern = getattr(settings, 'file_pattern', self.file_pattern)
  173. def _relative_url(self, source_path, number, basename, ext):
  174. """
  175. Create a new URL for a new page. The first page keeps the same name;
  176. the subsequent pages are named according to file_pattern.
  177. """
  178. path = File(source_path)
  179. if number != 1:
  180. filename = self.file_pattern.replace('$PAGE', str(number)) \
  181. .replace('$FILE', basename) \
  182. .replace('$EXT', ext)
  183. path = path.parent.child(os.path.normpath(filename))
  184. return path
  185. def _new_resource(self, base_resource, node, page_number):
  186. """
  187. Create a new resource as a copy of a base_resource, with a page of
  188. resources associated with it.
  189. """
  190. res = Resource(base_resource.source_file, node)
  191. res.node.meta = Metadata(node.meta)
  192. res.meta = Metadata(base_resource.meta, res.node.meta)
  193. path = self._relative_url(base_resource.relative_path,
  194. page_number,
  195. base_resource.source_file.name_without_extension,
  196. base_resource.source_file.extension)
  197. res.set_relative_deploy_path(path)
  198. return res
  199. @staticmethod
  200. def _attach_page_to_resource(page, resource):
  201. """
  202. Hook up a page and a resource.
  203. """
  204. resource.page = page
  205. page.resource = resource
  206. @staticmethod
  207. def _add_dependencies_to_resource(dependencies, resource):
  208. """
  209. Add a bunch of resources as dependencies to another resource.
  210. """
  211. if not hasattr(resource, 'depends'):
  212. resource.depends = []
  213. resource.depends.extend([dep.relative_path for dep in dependencies
  214. if dep.relative_path not in resource.depends])
  215. def _walk_pages_in_node(self, node):
  216. """
  217. Segregate each resource into a page.
  218. """
  219. walker = 'walk_resources'
  220. if self.sorter:
  221. walker = 'walk_resources_sorted_by_%s' % self.sorter
  222. walker = getattr(node, walker, getattr(node, 'walk_resources'))
  223. posts = list(walker())
  224. number = 1
  225. while posts:
  226. yield Page(posts[:self.size], number)
  227. posts = posts[self.size:]
  228. number += 1
  229. def walk_paged_resources(self, node, resource):
  230. """
  231. Group the resources and return the new page resources.
  232. """
  233. added_resources = []
  234. pages = list(self._walk_pages_in_node(node))
  235. if pages:
  236. deps = reduce(list.__add__, [page.posts for page in pages], [])
  237. Paginator._attach_page_to_resource(pages[0], resource)
  238. Paginator._add_dependencies_to_resource(deps, resource)
  239. for page in pages[1:]:
  240. # make new resource
  241. new_resource = self._new_resource(resource, node, page.number)
  242. Paginator._attach_page_to_resource(page, new_resource)
  243. new_resource.depends = resource.depends
  244. added_resources.append(new_resource)
  245. for prev, next in pairwalk(pages):
  246. next.previous = prev
  247. prev.next = next
  248. return added_resources
  249. class PaginatorPlugin(Plugin):
  250. """
  251. Paginator plugin.
  252. Configuration: in a resource's metadata:
  253. paginator:
  254. sorter: time
  255. size: 5
  256. file_pattern: page$PAGE/$FILE$EXT # optional
  257. then in the resource's content:
  258. {% for res in resource.page.posts %}
  259. {% refer to res.relative_path as post %}
  260. {{ post }}
  261. {% endfor %}
  262. {{ resource.page.previous }}
  263. {{ resource.page.next }}
  264. """
  265. def __init__(self, site):
  266. super(PaginatorPlugin, self).__init__(site)
  267. def begin_site(self):
  268. for node in self.site.content.walk():
  269. added_resources = []
  270. paged_resources = (res for res in node.resources
  271. if hasattr(res.meta, 'paginator'))
  272. for resource in paged_resources:
  273. paginator = Paginator(resource.meta.paginator)
  274. added_resources += paginator.walk_paged_resources(node, resource)
  275. node.resources += added_resources