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.
 
 
 

302 lines
12 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. Sphinx plugin.
  4. This plugin lets you easily include sphinx-generated documentation as part
  5. of your Hyde site. It is simultaneously a Hyde plugin and a Sphinx plugin.
  6. To make this work, you need to:
  7. * install sphinx, obviously
  8. * include your sphinx source files in the Hyde source tree
  9. * put the sphinx conf.py file in the Hyde site directory
  10. * point conf.py:master_doc at an appropriate file in the source tree
  11. For example you might have your site set up like this::
  12. site.yaml <-- hyde config file
  13. conf.py <-- sphinx config file
  14. contents/
  15. index.html <-- non-sphinx files, handled by hyde
  16. other.html
  17. api/
  18. index.rst <-- files to processed by sphinx
  19. mymodule.rst
  20. When the site is built, the .rst files will first be processed by sphinx
  21. to generate a HTML docuent, which will then be passed through the normal
  22. hyde templating workflow. You would end up with::
  23. deploy/
  24. index.html <-- files generated by hyde
  25. other.html
  26. api/
  27. index.html <-- files generated by sphinx, then hyde
  28. mymodule.html
  29. """
  30. # We need absolute import so that we can import the main "sphinx"
  31. # module even though this module is also called "sphinx". Ugh.
  32. from __future__ import absolute_import
  33. import os
  34. import json
  35. import tempfile
  36. from hyde.plugin import Plugin
  37. from hyde.model import Expando
  38. from hyde.ext.plugins.meta import MetaPlugin as _MetaPlugin
  39. from commando.util import getLoggerWithNullHandler
  40. from fswrap import File, Folder
  41. logger = getLoggerWithNullHandler('hyde.ext.plugins.sphinx')
  42. try:
  43. import sphinx
  44. from sphinx.builders.html import JSONHTMLBuilder
  45. except ImportError:
  46. logger.error("The sphinx plugin requires sphinx.")
  47. logger.error("`pip install -U sphinx` to get it.")
  48. raise
  49. class SphinxPlugin(Plugin):
  50. """The plugin class for rendering sphinx-generated documentation."""
  51. def __init__(self, site):
  52. self.sphinx_build_dir = None
  53. self._sphinx_config = None
  54. super(SphinxPlugin, self).__init__(site)
  55. @property
  56. def plugin_name(self):
  57. """The name of the plugin, obivously."""
  58. return "sphinx"
  59. @property
  60. def settings(self):
  61. """Settings for this plugin.
  62. This property combines default settings with those specified in the
  63. site config to produce the final settings for this plugin.
  64. """
  65. settings = Expando({})
  66. settings.sanity_check = True
  67. settings.conf_path = "."
  68. settings.block_map = {}
  69. try:
  70. user_settings = getattr(self.site.config, self.plugin_name)
  71. except AttributeError:
  72. pass
  73. else:
  74. for name in dir(user_settings):
  75. if not name.startswith("_"):
  76. setattr(settings, name, getattr(user_settings, name))
  77. return settings
  78. @property
  79. def sphinx_config(self):
  80. """Configuration options for sphinx.
  81. This is a lazily-generated property giving the options from the
  82. sphinx configuration file. It's generated by actualy executing
  83. the config file, so don't do anything silly in there.
  84. """
  85. if self._sphinx_config is None:
  86. conf_path = self.settings.conf_path
  87. conf_path = self.site.sitepath.child_folder(conf_path)
  88. # Sphinx always execs the config file in its parent dir.
  89. conf_file = conf_path.child("conf.py")
  90. self._sphinx_config = {"__file__": conf_file}
  91. curdir = os.getcwd()
  92. os.chdir(conf_path.path)
  93. try:
  94. execfile(conf_file, self._sphinx_config)
  95. finally:
  96. os.chdir(curdir)
  97. return self._sphinx_config
  98. def begin_site(self):
  99. """Event hook for when site processing begins.
  100. This hook checks that the site is correctly configured for building
  101. with sphinx, and adjusts any sphinx-controlled resources so that
  102. hyde will process them correctly.
  103. """
  104. settings = self.settings
  105. if settings.sanity_check:
  106. self._sanity_check()
  107. # Find and adjust all the resource that will be handled by sphinx.
  108. # We need to:
  109. # * change the deploy name from .rst to .html
  110. # * if a block_map is given, switch off default_block
  111. suffix = self.sphinx_config.get("source_suffix", ".rst")
  112. for resource in self.site.content.walk_resources():
  113. if resource.source_file.path.endswith(suffix):
  114. new_name = resource.source_file.name_without_extension + \
  115. ".html"
  116. target_folder = File(resource.relative_deploy_path).parent
  117. resource.relative_deploy_path = target_folder.child(new_name)
  118. if settings.block_map:
  119. resource.meta.default_block = None
  120. def begin_text_resource(self, resource, text):
  121. """Event hook for processing an individual resource.
  122. If the input resource is a sphinx input file, this method will replace
  123. replace the text of the file with the sphinx-generated documentation.
  124. Sphinx itself is run lazily the first time this method is called.
  125. This means that if no sphinx-related resources need updating, then
  126. we entirely avoid running sphinx.
  127. """
  128. suffix = self.sphinx_config.get("source_suffix", ".rst")
  129. if not resource.source_file.path.endswith(suffix):
  130. return text
  131. if self.sphinx_build_dir is None:
  132. self._run_sphinx()
  133. output = []
  134. settings = self.settings
  135. sphinx_output = self._get_sphinx_output(resource)
  136. # If they're set up a block_map, use the specific blocks.
  137. # Otherwise, output just the body for use by default_block.
  138. if not settings.block_map:
  139. output.append(sphinx_output["body"])
  140. else:
  141. for (nm, content) in sphinx_output.iteritems():
  142. try:
  143. block = getattr(settings.block_map, nm)
  144. except AttributeError:
  145. pass
  146. else:
  147. output.append("{%% block %s %%}" % (block,))
  148. output.append(content)
  149. output.append("{% endblock %}")
  150. return "\n".join(output)
  151. def site_complete(self):
  152. """Event hook for when site processing ends.
  153. This simply cleans up any temorary build file.
  154. """
  155. if self.sphinx_build_dir is not None:
  156. self.sphinx_build_dir.delete()
  157. def _sanity_check(self):
  158. """Check the current site for sanity.
  159. This method checks that the site is propertly set up for building
  160. things with sphinx, e.g. it has a config file, a master document,
  161. the hyde sphinx extension is enabled, and so-on.
  162. """
  163. # Check that the sphinx config file actually exists.
  164. try:
  165. sphinx_config = self.sphinx_config
  166. except EnvironmentError:
  167. logger.error("Could not read the sphinx config file.")
  168. conf_path = self.settings.conf_path
  169. conf_path = self.site.sitepath.child_folder(conf_path)
  170. conf_file = conf_path.child("conf.py")
  171. logger.error(
  172. "Please ensure %s is a valid sphinx config", conf_file)
  173. logger.error("or set sphinx.conf_path to the directory")
  174. logger.error("containing your sphinx conf.py")
  175. raise
  176. # Check that the hyde_json extension is loaded
  177. extensions = sphinx_config.get("extensions", [])
  178. if "hyde.ext.plugins.sphinx" not in extensions:
  179. logger.error("The hyde_json sphinx extension is not configured.")
  180. logger.error("Please add 'hyde.ext.plugins.sphinx' to the list")
  181. logger.error("of extensions in your sphinx conf.py file.")
  182. logger.info(
  183. "(set sphinx.sanity_check=false to disable this check)")
  184. raise RuntimeError("sphinx is not configured correctly")
  185. # Check that the master doc exists in the source tree.
  186. master_doc = sphinx_config.get("master_doc", "index")
  187. master_doc += sphinx_config.get("source_suffix", ".rst")
  188. master_doc = os.path.join(self.site.content.path, master_doc)
  189. if not os.path.exists(master_doc):
  190. logger.error("The sphinx master document doesn't exist.")
  191. logger.error("Please create the file %s", master_doc)
  192. logger.error("or change the 'master_doc' setting in your")
  193. logger.error("sphinx conf.py file.")
  194. logger.info(
  195. "(set sphinx.sanity_check=false to disable this check)")
  196. raise RuntimeError("sphinx is not configured correctly")
  197. # Check that I am *before* the other plugins,
  198. # with the possible exception of MetaPlugin
  199. for plugin in self.site.plugins:
  200. if plugin is self:
  201. break
  202. if not isinstance(plugin, _MetaPlugin):
  203. logger.error("The sphinx plugin is installed after the")
  204. logger.error("plugin %r.", plugin.__class__.__name__)
  205. logger.error("It's quite likely that this will break things.")
  206. logger.error("Please move the sphinx plugin to the top")
  207. logger.error("of the plugins list.")
  208. logger.info(
  209. "(sphinx.sanity_check=false to disable this check)")
  210. raise RuntimeError("sphinx is not configured correctly")
  211. def _run_sphinx(self):
  212. """Run sphinx to generate the necessary output files.
  213. This method creates a temporary directory for sphinx's output, then
  214. run sphinx against the Hyde input directory.
  215. """
  216. logger.info("running sphinx")
  217. self.sphinx_build_dir = Folder(tempfile.mkdtemp())
  218. conf_path = self.site.sitepath.child_folder(self.settings.conf_path)
  219. sphinx_args = ["sphinx-build"]
  220. sphinx_args.extend([
  221. "-b", "hyde_json",
  222. "-c", conf_path.path,
  223. self.site.content.path,
  224. self.sphinx_build_dir.path
  225. ])
  226. if sphinx.main(sphinx_args) != 0:
  227. raise RuntimeError("sphinx build failed")
  228. def _get_sphinx_output(self, resource):
  229. """Get the sphinx output for a given resource.
  230. This returns a dict mapping block names to HTML text fragments.
  231. The most important fragment is "body" which contains the main text
  232. of the document. The other fragments are for things like navigation,
  233. related pages and so-on.
  234. """
  235. relpath = File(resource.relative_path)
  236. relpath = relpath.parent.child(
  237. relpath.name_without_extension + ".fjson")
  238. with open(self.sphinx_build_dir.child(relpath), "rb") as f:
  239. return json.load(f)
  240. class HydeJSONHTMLBuilder(JSONHTMLBuilder):
  241. """A slightly-customised JSONHTMLBuilder, for use by Hyde.
  242. This is a Sphinx builder that serilises the generated HTML fragments into
  243. a JSON docuent, so they can be later retrieved and dealt with at will.
  244. The only customistion we do over the standard JSONHTMLBuilder is to
  245. reference documents with a .html suffix, so that internal link will
  246. work correctly once things have been processed by Hyde.
  247. """
  248. name = "hyde_json"
  249. def get_target_uri(self, docname, typ=None):
  250. return docname + ".html"
  251. def setup(app):
  252. """Sphinx plugin setup function.
  253. This function allows the module to act as a Sphinx plugin as well as a
  254. Hyde plugin. It simply registers the HydeJSONHTMLBuilder class.
  255. """
  256. app.add_builder(HydeJSONHTMLBuilder)