298 lines
10 KiB
Python
298 lines
10 KiB
Python
import docutils
|
|
import os
|
|
import sphinx
|
|
import warnings
|
|
|
|
from sphinx.environment.collectors import EnvironmentCollector
|
|
from sphinx.errors import ExtensionError
|
|
|
|
from . import version
|
|
from .utils import replace_uris
|
|
|
|
|
|
class BaseURIError(ExtensionError):
|
|
"""Exception for malformed base URI."""
|
|
pass
|
|
|
|
|
|
# https://www.sphinx-doc.org/en/stable/extdev/appapi.html#event-html-collect-pages
|
|
def html_collect_pages(app):
|
|
"""
|
|
Create a ``404.html`` page.
|
|
|
|
Uses ``notfound_template`` as a template to be rendered with
|
|
``notfound_context`` for its context. The resulting file generated is
|
|
``notfound_pagename``.html.
|
|
|
|
If the user already defined a page with pagename title
|
|
``notfound_pagename``, we don't generate this page.
|
|
|
|
:param app: Sphinx Application
|
|
:type app: sphinx.application.Sphinx
|
|
"""
|
|
if app.config.notfound_pagename in app.env.titles:
|
|
# There is already a ``404.rst`` file rendered.
|
|
# Skip generating our default one.
|
|
return []
|
|
|
|
return [(
|
|
app.config.notfound_pagename,
|
|
app.config.notfound_context,
|
|
app.config.notfound_template,
|
|
)]
|
|
|
|
|
|
# https://www.sphinx-doc.org/en/stable/extdev/appapi.html#event-html-page-context
|
|
def finalize_media(app, pagename, templatename, context, doctree):
|
|
"""
|
|
Point media files at our media server.
|
|
|
|
Generate absolute URLs for resources (js, images, css, etc) to point to the
|
|
right. For example, if a URL in the page is ``_static/js/custom.js`` it will
|
|
be replaced by ``<notfound_urls_prefix>/_static/js/custom.js``.
|
|
|
|
On the other hand, if ``notfound_no_urls_prefix`` is set, it will be
|
|
replaced by ``/_static/js/custom.js``.
|
|
|
|
Also, all the links from the sidebar (toctree) are replaced with their
|
|
absolute version. For example, ``../section/pagename.html`` will be replaced
|
|
by ``/section/pagename.html``.
|
|
|
|
:param app: Sphinx Application
|
|
:type app: sphinx.application.Sphinx
|
|
|
|
:param pagename: name of the page being rendered
|
|
:type pagename: str
|
|
|
|
:param templatename: template used to render the page
|
|
:type templatename: str
|
|
|
|
:param context: context used to render the page
|
|
:type context: dict
|
|
|
|
:param doctree: doctree of the page being rendered
|
|
:type doctree: docutils.nodes.document
|
|
"""
|
|
|
|
# https://github.com/sphinx-doc/sphinx/blob/7138d03ba033e384f1e7740f639849ba5f2cc71d/sphinx/builders/html.py#L1054-L1065
|
|
def pathto(otheruri, resource=False, baseuri=None):
|
|
"""
|
|
Hack pathto to display absolute URL's.
|
|
|
|
Instead of calling ``relative_url`` function, we call
|
|
``app.builder.get_target_uri`` to get the absolut URL.
|
|
|
|
.. note::
|
|
|
|
If ``otheruri`` is a external ``resource`` it does not modify it.
|
|
"""
|
|
if resource and '://' in otheruri:
|
|
# allow non-local resources given by scheme
|
|
return otheruri
|
|
|
|
if not resource:
|
|
otheruri = app.builder.get_target_uri(otheruri)
|
|
|
|
if baseuri is None:
|
|
if app.config.notfound_no_urls_prefix:
|
|
baseuri = '/'
|
|
else:
|
|
baseuri = '{prefix}'.format(
|
|
prefix=app.config.notfound_urls_prefix or '/',
|
|
)
|
|
|
|
if not baseuri.startswith('/'):
|
|
raise BaseURIError('"baseuri" must be absolute')
|
|
|
|
if otheruri and not otheruri.startswith('/'):
|
|
otheruri = '/' + otheruri
|
|
|
|
if otheruri:
|
|
if baseuri.endswith('/'):
|
|
baseuri = baseuri[:-1]
|
|
otheruri = baseuri + otheruri
|
|
|
|
uri = otheruri or '#'
|
|
return uri
|
|
|
|
# https://github.com/sphinx-doc/sphinx/blob/2adeb68af1763be46359d5e808dae59d708661b1/sphinx/builders/html.py#L1081
|
|
def toctree(*args, **kwargs):
|
|
try:
|
|
# Sphinx >= 1.6
|
|
from sphinx.environment.adapters.toctree import TocTree
|
|
get_toctree_for = TocTree(app.env).get_toctree_for
|
|
except ImportError:
|
|
# Sphinx < 1.6
|
|
get_toctree_for = app.env.get_toctree_for
|
|
|
|
toc = get_toctree_for(
|
|
app.config.notfound_pagename,
|
|
app.builder,
|
|
collapse=kwargs.pop('collapse', False),
|
|
includehidden=kwargs.pop('includehidden', False),
|
|
**kwargs # not using trailing comma here makes this compatible with
|
|
# Python2 syntax
|
|
)
|
|
|
|
# If no TOC is found, just return ``None`` instead of failing here
|
|
if not toc:
|
|
return None
|
|
|
|
replace_uris(app, toc, docutils.nodes.reference, 'refuri')
|
|
return app.builder.render_partial(toc)['fragment']
|
|
|
|
# Apply our custom manipulation to 404.html page only
|
|
if pagename == app.config.notfound_pagename:
|
|
# Override the ``pathto`` helper function from the context to use a custom ones
|
|
# https://www.sphinx-doc.org/en/master/templating.html#pathto
|
|
context['pathto'] = pathto
|
|
|
|
# Override the ``toctree`` helper function from context to use a custom
|
|
# one and generate valid links on not found page.
|
|
# https://www.sphinx-doc.org/en/master/templating.html#toctree
|
|
# NOTE: not used on ``singlehtml`` builder for RTD Sphinx theme
|
|
context['toctree'] = toctree
|
|
|
|
|
|
# https://www.sphinx-doc.org/en/stable/extdev/appapi.html#event-doctree-resolved
|
|
def doctree_resolved(app, doctree, docname):
|
|
"""
|
|
Generate and override URLs for ``.. image::`` Sphinx directive.
|
|
|
|
When ``.. image::`` is used in the ``404.rst`` file, this function will
|
|
override the URLs to point to the right place.
|
|
|
|
:param app: Sphinx Application
|
|
:type app: sphinx.application.Sphinx
|
|
:param doctree: doctree representing the document
|
|
:type doctree: docutils.nodes.document
|
|
:param docname: name of the document
|
|
:type docname: str
|
|
"""
|
|
|
|
if docname == app.config.notfound_pagename:
|
|
# Replace image ``uri`` to its absolute version
|
|
replace_uris(app, doctree, docutils.nodes.image, 'uri')
|
|
|
|
|
|
class OrphanMetadataCollector(EnvironmentCollector):
|
|
"""
|
|
Force the 404 page to be ``orphan``.
|
|
|
|
This way we remove the WARNING that Sphinx raises saying the page is not
|
|
included in any toctree.
|
|
|
|
This collector has the same effect than ``:orphan:`` at the top of the page.
|
|
"""
|
|
|
|
def clear_doc(self, app, env, docname):
|
|
return None
|
|
|
|
def process_doc(self, app, doctree):
|
|
metadata = app.env.metadata[app.config.notfound_pagename]
|
|
metadata.update({'orphan': True})
|
|
if sphinx.version_info >= (3, 0, 0):
|
|
metadata.update({'nosearch': True})
|
|
|
|
|
|
def handle_deprecated_configs(app, *args, **kwargs):
|
|
"""
|
|
Handle deprecated configurations.
|
|
|
|
Looks for old deprecated configurations, define the new ones and triggers
|
|
warnings for old configs.
|
|
"""
|
|
default, rebuild, types = app.config.values.get('notfound_urls_prefix')
|
|
if app.config.notfound_urls_prefix == default:
|
|
language = app.config.notfound_default_language
|
|
version = app.config.notfound_default_version
|
|
app.config.notfound_urls_prefix = '/{language}/{version}/'.format(
|
|
language=language,
|
|
version=version,
|
|
)
|
|
|
|
deprecated_configs = [
|
|
'notfound_default_language',
|
|
'notfound_default_version',
|
|
'notfound_no_urls_prefix',
|
|
]
|
|
for config in deprecated_configs:
|
|
default, rebuild, types = app.config.values.get(config)
|
|
if getattr(app.config, config) != default:
|
|
message = '{config} is deprecated. Use "notfound_urls_prefix" instead.'.format(
|
|
config=config,
|
|
)
|
|
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
|
|
|
|
|
def validate_configs(app, *args, **kwargs):
|
|
"""
|
|
Validate configs.
|
|
|
|
Shows a warning if one of the configs is not valid.
|
|
"""
|
|
default, rebuild, types = app.config.values.get('notfound_urls_prefix')
|
|
if app.config.notfound_urls_prefix != default:
|
|
if app.config.notfound_urls_prefix and not all([
|
|
app.config.notfound_urls_prefix.startswith('/'),
|
|
app.config.notfound_urls_prefix.endswith('/'),
|
|
]):
|
|
message = 'notfound_urls_prefix should start and end with "/" (slash)'
|
|
warnings.warn(message, UserWarning, stacklevel=2)
|
|
|
|
|
|
def setup(app):
|
|
default_context = {
|
|
'title': 'Page not found',
|
|
'body': '<h1>Page not found</h1>\n\nThanks for trying.',
|
|
}
|
|
|
|
# https://github.com/sphinx-doc/sphinx/blob/master/sphinx/themes/basic/page.html
|
|
app.add_config_value('notfound_template', 'page.html', 'html')
|
|
app.add_config_value('notfound_context', default_context, 'html')
|
|
app.add_config_value('notfound_pagename', '404', 'html')
|
|
|
|
# TODO: get these values from Project's settings
|
|
default_version = os.environ.get('READTHEDOCS_VERSION', 'latest')
|
|
|
|
app.add_config_value('notfound_default_language', 'en', 'html')
|
|
app.add_config_value('notfound_default_version', default_version, 'html')
|
|
app.add_config_value('notfound_no_urls_prefix', False, 'html')
|
|
|
|
# This config should replace the previous three
|
|
app.add_config_value(
|
|
'notfound_urls_prefix',
|
|
'/en/{default_version}/'.format(
|
|
default_version=default_version,
|
|
),
|
|
'html',
|
|
)
|
|
|
|
if sphinx.version_info > (1, 8, 0):
|
|
app.connect('config-inited', handle_deprecated_configs)
|
|
app.connect('config-inited', validate_configs)
|
|
else:
|
|
app.connect('builder-inited', handle_deprecated_configs)
|
|
app.connect('builder-inited', validate_configs)
|
|
|
|
app.connect('html-collect-pages', html_collect_pages)
|
|
app.connect('html-page-context', finalize_media)
|
|
app.connect('doctree-resolved', doctree_resolved)
|
|
|
|
# Sphinx injects some javascript files using ``add_js_file``. The path for
|
|
# this file is rendered in the template using ``js_tag`` instead of
|
|
# ``pathto``. The ``js_tag`` uses ``pathto`` internally to resolve these
|
|
# paths, we call again the setup function for this tag *after* the context
|
|
# was overriden by our extension with the patched ``pathto`` function.
|
|
if sphinx.version_info >= (1, 8):
|
|
from sphinx.builders.html import setup_js_tag_helper
|
|
app.connect('html-page-context', setup_js_tag_helper)
|
|
|
|
app.add_env_collector(OrphanMetadataCollector)
|
|
|
|
return {
|
|
'version': version,
|
|
'parallel_read_safe': True,
|
|
'parallel_write_safe': True,
|
|
}
|