diff --git a/readthedocs_ext/readthedocs.py b/readthedocs_ext/readthedocs.py index 9b2caba..1e55555 100644 --- a/readthedocs_ext/readthedocs.py +++ b/readthedocs_ext/readthedocs.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import codecs +import json import os import re import types @@ -10,12 +11,21 @@ import sphinx from sphinx import package_dir -from sphinx.builders.html import StandaloneHTMLBuilder, DirectoryHTMLBuilder, SingleFileHTMLBuilder +from sphinx.builders.html import (DirectoryHTMLBuilder, SingleFileHTMLBuilder, + StandaloneHTMLBuilder) from sphinx.util.console import bold from .embed import EmbedDirective from .mixins import BuilderMixin +try: + # Avaliable from Sphinx 1.6 + from sphinx.util.logging import getLogger +except ImportError: + from logging import getLogger + +log = getLogger(__name__) + MEDIA_MAPPING = { "_static/jquery.js": "%sjavascript/jquery/jquery-2.0.3.min.js", "_static/underscore.js": "%sjavascript/underscore.js", @@ -23,6 +33,19 @@ } +# Whitelist keys that we want to output +# to the json artifacts. +KEYS = [ + 'body', + 'title', + 'sourcename', + 'current_page_name', + 'rellinks', + 'toc', + 'page_source_suffix', +] + + def finalize_media(app): """ Point media files at our media server. """ @@ -126,6 +149,44 @@ def rtd_render(self, template, render_context): app.builder.templates) +def generate_json_artifacts(app, pagename, templatename, context, doctree): + """ + Generate JSON artifacts for each page. + + This way we can skip generating this in other build step. + """ + try: + if not app.config.rtd_generate_json_artifacts: + return + # We need to get the output directory where the docs are built + # _build/json. + build_json = os.path.abspath( + os.path.join(app.outdir, '..', 'json') + ) + outjson = os.path.join(build_json, pagename + '.fjson') + outdir = os.path.dirname(outjson) + if not os.path.exists(outdir): + os.makedirs(outdir) + with open(outjson, 'w+') as json_file: + to_context = { + key: context.get(key, '') + for key in KEYS + } + json.dump(to_context, json_file, indent=4) + except TypeError: + log.exception( + 'Fail to encode JSON for page {page}'.format(page=outjson) + ) + except IOError: + log.exception( + 'Fail to save JSON output for page {page}'.format(page=outjson) + ) + except Exception as e: + log.exception( + 'Failure in JSON search dump for page {page}'.format(page=outjson) + ) + + class HtmlBuilderMixin(BuilderMixin): static_readthedocs_files = [ @@ -213,11 +274,13 @@ def setup(app): app.add_builder(ReadtheDocsSingleFileHTMLBuilderLocalMedia) app.connect('builder-inited', finalize_media) app.connect('html-page-context', update_body) + app.connect('html-page-context', generate_json_artifacts) # Embed app.add_directive('readthedocs-embed', EmbedDirective) app.add_config_value('readthedocs_embed_project', '', 'html') app.add_config_value('readthedocs_embed_version', '', 'html') app.add_config_value('readthedocs_embed_doc', '', 'html') + app.add_config_value('rtd_generate_json_artifacts', False, 'html') return {} diff --git a/tests/pyexample-json/conf.py b/tests/pyexample-json/conf.py new file mode 100644 index 0000000..3d207bb --- /dev/null +++ b/tests/pyexample-json/conf.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +extensions = ['readthedocs_ext.readthedocs'] + +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' +project = u'pyexample' +copyright = u'2015, rtfd' +author = u'rtfd' +version = '0.1' +release = '0.1' +language = None +exclude_patterns = ['_build'] +pygments_style = 'sphinx' +todo_include_todos = False +html_theme = 'alabaster' +html_static_path = ['_static'] +htmlhelp_basename = 'pyexampledoc' +rtd_generate_json_artifacts = True diff --git a/tests/pyexample-json/index.rst b/tests/pyexample-json/index.rst new file mode 100644 index 0000000..1ea26b8 --- /dev/null +++ b/tests/pyexample-json/index.rst @@ -0,0 +1,9 @@ +.. pyexample documentation master file, created by + sphinx-quickstart on Fri May 29 13:34:37 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to pyexample's documentation! +===================================== + +Hey there friend! diff --git a/tests/test_integration.py b/tests/test_integration.py index 56386f8..a031680 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -9,7 +9,12 @@ class LanguageIntegrationTests(unittest.TestCase): def _run_test(self, test_dir, test_file, test_string, builder='html'): with build_output(test_dir, test_file, builder) as data: - self.assertIn(test_string, data) + if not isinstance(test_string, list): + test_strings = [test_string] + else: + test_strings = test_string + for string in test_strings: + self.assertIn(string, data) class IntegrationTests(LanguageIntegrationTests): @@ -56,3 +61,25 @@ def test_searchtools_is_patched(self): HtmlBuilderMixin.REPLACEMENT_PATTERN ) self.assertIn(HtmlBuilderMixin.REPLACEMENT_TEXT, data) + + def test_generate_json_artifacts(self): + self._run_test( + 'pyexample-json', + '_build/json/index.fjson', + [ + 'current_page_name', 'title', 'body', + 'toc', 'sourcename', 'rellinks', 'page_source_suffix', + ], + ) + + def test_no_generate_json_artifacts(self): + with self.assertRaises(IOError) as e: + self._run_test( + 'pyexample', + '_build/json/index.fjson', + ['current_page_name', 'title', 'body', 'toc'], + ) + self.assertIn( + "No such file or directory: '_build/json/index.fjson'", + str(e.exception) + ) diff --git a/tox.ini b/tox.ini index 201b919..33b28e3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,35,36}-sphinx{13,14,15,16} + py{27,35,36}-sphinx{13,14,15,16,17} lint [testenv] @@ -13,7 +13,8 @@ deps = sphinx13: Sphinx<1.4 sphinx14: Sphinx<1.5 sphinx15: Sphinx<1.6 - sphinx16: Sphinx==1.6b3 + sphinx16: Sphinx<1.7 + sphinx17: Sphinx==1.7.4 commands = py.test {posargs}