diff --git a/.travis.yml b/.travis.yml index a56d9f6..0bfc4ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ python: sudo: false env: - TOX_ENV=lint + - TOX_ENV=py27 + - TOX_ENV=py34 install: - pip install tox script: diff --git a/MANIFEST.in b/MANIFEST.in index a037f16..0f931dd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ recursive-include readthedocs_ext *.css recursive-include readthedocs_ext *.js_t recursive-include readthedocs_ext *.js +recursive-include readthedocs_ext *.tmpl include *.rst diff --git a/readthedocs_ext/_static/readthedocs-data.js_t b/readthedocs_ext/_static/readthedocs-data.js_t index b7e56ef..e5bebfa 100644 --- a/readthedocs_ext/_static/readthedocs-data.js_t +++ b/readthedocs_ext/_static/readthedocs-data.js_t @@ -2,7 +2,6 @@ project: "{{ slug }}", version: "{{ current_version }}", language: "{{ rtd_language }}", - page: "{{ pagename }}", theme: "{{ html_theme }}", builder: "sphinx", docroot: "{{ conf_py_path }}", @@ -10,9 +9,3 @@ api_host: "{{ api_host }}", commit: "{{ commit }}" } - - // Old variables - var doc_version = "{{ current_version }}"; - var doc_slug = "{{ slug }}"; - var page_name = "{{ pagename }}"; - var html_theme = "{{ html_theme }}"; \ No newline at end of file diff --git a/readthedocs_ext/_templates/readthedocs-insert.html.tmpl b/readthedocs_ext/_templates/readthedocs-insert.html.tmpl new file mode 100644 index 0000000..48fb501 --- /dev/null +++ b/readthedocs_ext/_templates/readthedocs-insert.html.tmpl @@ -0,0 +1,34 @@ + + + +{%- if pagename == "index" %} + {%- set canonical_page = "" %} +{%- elif pagename.endswith("/index") %} + {%- set canonical_page = pagename[:-("/index"|length)] + "/" %} +{%- else %} + {%- set ending = "/" if builder == "readthedocsdirhtml" else ".html" %} + {%- set canonical_page = pagename + ending %} +{%- endif %} + + + + + + + + + + + + + + + diff --git a/readthedocs_ext/comments/builder.py b/readthedocs_ext/comments/builder.py new file mode 100644 index 0000000..f390017 --- /dev/null +++ b/readthedocs_ext/comments/builder.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import + +from collections import defaultdict + +from sphinx.builders.html import StandaloneHTMLBuilder, DirectoryHTMLBuilder + +from . import backend, translator + + +def finalize_comment_media(app): + + if 'comments' not in app.builder.name: + return + builder = app.builder + # Pull project data from conf.py if it exists + builder.storage = backend.WebStorage(builder=builder) + builder.page_hash_mapping = defaultdict(list) + builder.metadata_mapping = defaultdict(list) + try: + builder.comment_metadata = builder.storage.get_project_metadata( + builder.config.html_context['slug'])['results'] + for obj in builder.comment_metadata: + builder.metadata_mapping[obj['node']['page']].append(obj['node']) + except: + builder.comment_metadata = {} + + context = builder.config.html_context + MEDIA_URL = context.get('MEDIA_URL', 'https://media.readthedocs.org/') + + # add our custom bits + builder.script_files.append('_static/jquery.pageslide.js') + # builder.script_files.append('_static/websupport2-bundle.js') + builder.script_files.append( + '%sjavascript/websupport2-bundle.js' % MEDIA_URL) + builder.css_files.append('_static/websupport2.css') + builder.css_files.append('_static/sphinxweb.css') + builder.css_files.append('_static/jquery.pageslide.css') + + +class ReadtheDocsBuilderComments(StandaloneHTMLBuilder): + + """ + Comment Builders. + + Sets the translator class, + which handles adding a content-specific hash to each text node object. + """ + name = 'readthedocs-comments' + versioning_method = 'commentable' + + def init(self): + StandaloneHTMLBuilder.init(self) + finalize_comment_media(self) + + def init_translator_class(self): + self.translator_class = translator.UUIDTranslator + + +class ReadtheDocsDirectoryHTMLBuilderComments(DirectoryHTMLBuilder): + + """ Adds specific media files to script_files and css_files. """ + name = 'readthedocsdirhtml-comments' + versioning_method = 'commentable' + + def init(self): + DirectoryHTMLBuilder.init(self) + finalize_comment_media(self) + + def init_translator_class(self): + self.translator_class = translator.UUIDTranslator diff --git a/readthedocs_ext/readthedocs.py b/readthedocs_ext/readthedocs.py index 6d06b24..d8dc06c 100644 --- a/readthedocs_ext/readthedocs.py +++ b/readthedocs_ext/readthedocs.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- + +from __future__ import absolute_import import os -from collections import defaultdict +import types from sphinx.builders.html import StandaloneHTMLBuilder, DirectoryHTMLBuilder, SingleFileHTMLBuilder from sphinx.util import copy_static_entry from sphinx.util.console import bold -from .comments import backend, translator, directive +from .comments.builder import (finalize_comment_media, ReadtheDocsBuilderComments, + ReadtheDocsDirectoryHTMLBuilderComments) +from .comments.directive import CommentConfigurationDirective from .embed import EmbedDirective MEDIA_MAPPING = { @@ -22,8 +26,95 @@ ] +def finalize_media(app): + """ Point media files at our media server. """ + + if (app.builder.name == 'readthedocssinglehtmllocalmedia' or + app.builder.format != 'html' or + not hasattr(app.builder, 'script_files')): + return # Use local media for downloadable files + # Pull project data from conf.py if it exists + context = app.builder.config.html_context + MEDIA_URL = context.get('MEDIA_URL', 'https://media.readthedocs.org/') + + # Put in our media files instead of putting them in the docs. + for index, file in enumerate(app.builder.script_files): + if file in MEDIA_MAPPING.keys(): + app.builder.script_files[index] = MEDIA_MAPPING[file] % MEDIA_URL + if file == "_static/jquery.js": + app.builder.script_files.insert( + index + 1, "%sjavascript/jquery/jquery-migrate-1.2.1.min.js" % MEDIA_URL) + app.builder.script_files.append( + '%sjavascript/readthedocs-doc-embed.js' % MEDIA_URL + ) + + +def update_body(app, pagename, templatename, context, doctree): + """ + Add Read the Docs content to Sphinx body content. + + This is the most reliable way to inject our content into the page. + """ + + MEDIA_URL = context.get('MEDIA_URL', 'https://media.readthedocs.org/') + if app.builder.name == 'readthedocssinglehtmllocalmedia': + if 'html_theme' in context and context['html_theme'] == 'sphinx_rtd_theme': + theme_css = '_static/css/theme.css' + else: + theme_css = '_static/css/badge_only.css' + elif app.builder.name in ['readthedocs', 'readthedocsdirhtml']: + if 'html_theme' in context and context['html_theme'] == 'sphinx_rtd_theme': + theme_css = '%scss/sphinx_rtd_theme.css' % MEDIA_URL + else: + theme_css = '%scss/badge_only.css' % MEDIA_URL + else: + # Only insert on our HTML builds + return + + # This is monkey patched on the signal because we can't know what the user + # has done with their `app.builder.templates` before now. + + if not hasattr(app.builder.templates.render, '_patched'): + # Janky monkey patch of template rendering to add our content + old_render = app.builder.templates.render + + def rtd_render(self, template, render_context): + """ + A decorator that renders the content with the users template renderer, + then adds the Read the Docs HTML content at the end of body. + """ + # Render Read the Docs content + template_context = render_context.copy() + template_context['theme_css'] = theme_css + template_context['rtd_css_url'] = '%scss/readthedocs-doc-embed.css' % MEDIA_URL + source = os.path.join( + os.path.abspath(os.path.dirname(__file__)), + '_templates', + 'readthedocs-insert.html.tmpl' + ) + templ = open(source).read() + rtd_content = app.builder.templates.render_string(templ, template_context) + + # Handle original render function + content = old_render(template, render_context) + end_body = content.lower().find('') + + # Insert our content at the end of the body. + if end_body != -1: + content = content[:end_body] + rtd_content + content[end_body:] + else: + app.debug("File doesn't look like HTML. Skipping RTD content addition") + + return content + + rtd_render._patched = True + app.builder.templates.render = types.MethodType(rtd_render, + app.builder.templates) + + def copy_media(app, exception): - if app.builder.name == 'readthedocs' and not exception: + """ Move our dynamically generated files after build. """ + if app.builder.name in ['readthedocs', 'readthedocsdirhtml'] and not exception: for file in ['readthedocs-dynamic-include.js_t', 'readthedocs-data.js_t', 'searchtools.js_t']: app.info(bold('Copying %s... ' % file), nonl=True) @@ -61,69 +152,6 @@ def copy_media(app, exception): app.info('done') -def finalize_media(builder, local=False): - # Pull project data from conf.py if it exists - context = builder.config.html_context - MEDIA_URL = context.get('MEDIA_URL', 'https://media.readthedocs.org/') - - # Put in our media files instead of putting them in the docs. - for index, file in enumerate(builder.script_files): - if file in MEDIA_MAPPING.keys(): - builder.script_files[index] = MEDIA_MAPPING[file] % MEDIA_URL - if file == "_static/jquery.js": - builder.script_files.insert( - index + 1, "%sjavascript/jquery/jquery-migrate-1.2.1.min.js" % MEDIA_URL) - - if local: - if 'html_theme' in context and context['html_theme'] == 'sphinx_rtd_theme': - builder.css_files.insert(0, '_static/css/theme.css') - else: - builder.css_files.insert(0, '_static/css/badge_only.css') - else: - if 'html_theme' in context and context['html_theme'] == 'sphinx_rtd_theme': - builder.css_files.insert( - 0, '%scss/sphinx_rtd_theme.css' % MEDIA_URL) - else: - builder.css_files.insert(0, '%scss/badge_only.css' % MEDIA_URL) - - # Analytics codes - # builder.script_files.append('_static/readthedocs-data.js') - # builder.script_files.append('_static/readthedocs-dynamic-include.js') - # We include the media servers version here so we can update rtd.js across all - # documentation without rebuilding every one. - # If this script is embedded in each build, - # then updating the file across all docs is basically impossible. - builder.script_files.append( - '%sjavascript/readthedocs-doc-embed.js' % MEDIA_URL) - builder.css_files.append('%scss/readthedocs-doc-embed.css' % MEDIA_URL) - - -def finalize_comment_media(builder): - # Pull project data from conf.py if it exists - builder.storage = backend.WebStorage(builder=builder) - builder.page_hash_mapping = defaultdict(list) - builder.metadata_mapping = defaultdict(list) - try: - builder.comment_metadata = builder.storage.get_project_metadata( - builder.config.html_context['slug'])['results'] - for obj in builder.comment_metadata: - builder.metadata_mapping[obj['node']['page']].append(obj['node']) - except: - builder.comment_metadata = {} - - context = builder.config.html_context - MEDIA_URL = context.get('MEDIA_URL', 'https://media.readthedocs.org/') - - # add our custom bits - builder.script_files.append('_static/jquery.pageslide.js') - # builder.script_files.append('_static/websupport2-bundle.js') - builder.script_files.append( - '%sjavascript/websupport2-bundle.js' % MEDIA_URL) - builder.css_files.append('_static/websupport2.css') - builder.css_files.append('_static/sphinxweb.css') - builder.css_files.append('_static/jquery.pageslide.css') - - class ReadtheDocsBuilder(StandaloneHTMLBuilder): """ @@ -131,30 +159,6 @@ class ReadtheDocsBuilder(StandaloneHTMLBuilder): """ name = 'readthedocs' - def init(self): - StandaloneHTMLBuilder.init(self) - finalize_media(self) - - -class ReadtheDocsBuilderComments(StandaloneHTMLBuilder): - - """ - Comment Builders. - - Sets the translator class, - which handles adding a content-specific hash to each text node object. - """ - name = 'readthedocs-comments' - versioning_method = 'commentable' - - def init(self): - StandaloneHTMLBuilder.init(self) - finalize_media(self) - finalize_comment_media(self) - - def init_translator_class(self): - self.translator_class = translator.UUIDTranslator - class ReadtheDocsDirectoryHTMLBuilder(DirectoryHTMLBuilder): @@ -163,39 +167,13 @@ class ReadtheDocsDirectoryHTMLBuilder(DirectoryHTMLBuilder): """ name = 'readthedocsdirhtml' - def init(self): - DirectoryHTMLBuilder.init(self) - finalize_media(self) - - -class ReadtheDocsDirectoryHTMLBuilderComments(DirectoryHTMLBuilder): - - """ - Adds specific media files to script_files and css_files. - """ - name = 'readthedocsdirhtml-comments' - versioning_method = 'commentable' - - def init(self): - DirectoryHTMLBuilder.init(self) - finalize_media(self) - finalize_comment_media(self) - - def init_translator_class(self): - self.translator_class = translator.UUIDTranslator - class ReadtheDocsSingleFileHTMLBuilder(SingleFileHTMLBuilder): - """ Adds specific media files to script_files and css_files. """ name = 'readthedocssinglehtml' - def init(self): - SingleFileHTMLBuilder.init(self) - finalize_media(self) - class ReadtheDocsSingleFileHTMLBuilderLocalMedia(SingleFileHTMLBuilder): @@ -204,22 +182,21 @@ class ReadtheDocsSingleFileHTMLBuilderLocalMedia(SingleFileHTMLBuilder): """ name = 'readthedocssinglehtmllocalmedia' - def init(self): - SingleFileHTMLBuilder.init(self) - finalize_media(self, local=True) - def setup(app): app.add_builder(ReadtheDocsBuilder) app.add_builder(ReadtheDocsDirectoryHTMLBuilder) app.add_builder(ReadtheDocsSingleFileHTMLBuilder) app.add_builder(ReadtheDocsSingleFileHTMLBuilderLocalMedia) + app.connect('builder-inited', finalize_media) + app.connect('builder-inited', finalize_comment_media) + app.connect('html-page-context', update_body) app.connect('build-finished', copy_media) # Comments # app.connect('env-updated', add_comments_to_doctree) app.add_directive( - 'comment-configure', directive.CommentConfigurationDirective) + 'comment-configure', CommentConfigurationDirective) app.add_builder(ReadtheDocsBuilderComments) app.add_builder(ReadtheDocsDirectoryHTMLBuilderComments) app.add_config_value( diff --git a/requirements.txt b/requirements.txt index 5eb2dce..b24ab50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ sphinx==1.3.4 +pytest==2.8.5 diff --git a/setup.py b/setup.py index 5452691..0e6c0d8 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ # trying to add files... include_package_data=True, package_data={ - '': ['_static/*.js', '_static/*.js_t', '_static/*.css'], + '': ['_static/*.js', '_static/*.js_t', '_static/*.css', '_templates/*.tmpl'], }, **extra_setup ) diff --git a/tests/pyexample/conf.py b/tests/pyexample/conf.py new file mode 100644 index 0000000..9ad6817 --- /dev/null +++ b/tests/pyexample/conf.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +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' diff --git a/tests/pyexample/index.rst b/tests/pyexample/index.rst new file mode 100644 index 0000000..1ea26b8 --- /dev/null +++ b/tests/pyexample/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 new file mode 100644 index 0000000..8213e49 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,55 @@ +import os +import shutil +import unittest +import io + +from sphinx.application import Sphinx + + +class LanguageIntegrationTests(unittest.TestCase): + + def _run_test(self, test_dir, test_file, test_string, builder='html'): + os.chdir('tests/{0}'.format(test_dir)) + try: + app = Sphinx( + srcdir='.', + confdir='.', + outdir='_build/%s' % builder, + doctreedir='_build/.doctrees', + buildername='%s' % builder, + ) + app.build(force_all=True) + with io.open(test_file, encoding="utf-8") as fin: + text = fin.read().strip() + self.assertIn(test_string, text) + finally: + shutil.rmtree('_build') + os.chdir('../..') + + +class IntegrationTests(LanguageIntegrationTests): + + def test_integration(self): + self._run_test( + 'pyexample', + '_build/readthedocs/index.html', + 'Hey there friend!', + builder='readthedocs', + ) + + def test_media_integration(self): + self._run_test( + 'pyexample', + '_build/readthedocs/index.html', + 'media.readthedocs.org', + builder='readthedocs', + ) + + def test_included_js(self): + self._run_test( + 'pyexample', + '_build/readthedocs/index.html', + 'readthedocs-dynamic-include.js', + builder='readthedocs', + ) + diff --git a/tox.ini b/tox.ini index 8ea61db..e47de19 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,12 @@ [tox] -envlist = lint +envlist = py27,py34,lint + +[testenv] +setenv = + LANG=C +deps = -r{toxinidir}/requirements.txt +commands = + py.test {posargs} [testenv:lint] deps =