1from __future__ import annotations
2
3import os
4import pickle
5import types
6from os import path
7from typing import TYPE_CHECKING
8
9from sphinx.application import ENV_PICKLE_FILENAME, Sphinx
10from sphinx.builders.html import BuildInfo, StandaloneHTMLBuilder
11from sphinx.locale import get_translation
12from sphinx.util.osutil import SEP, copyfile, ensuredir, os_path
13
14from sphinxcontrib.serializinghtml import jsonimpl
15
16if TYPE_CHECKING:
17 from collections.abc import Sequence
18 from typing import Any, Protocol
19
20 class SerialisingImplementation(Protocol):
21 def dump(self, obj: Any, file: Any, *args: Any, **kwargs: Any) -> None: ...
22 def dumps(self, obj: Any, *args: Any, **kwargs: Any) -> str | bytes: ...
23 def load(self, file: Any, *args: Any, **kwargs: Any) -> Any: ...
24 def loads(self, data: Any, *args: Any, **kwargs: Any) -> Any: ...
25
26__version__ = '2.0.0'
27__version_info__ = (2, 0, 0)
28
29package_dir = path.abspath(path.dirname(__file__))
30
31__ = get_translation(__name__, 'console')
32
33
34#: the filename for the "last build" file (for serializing builders)
35LAST_BUILD_FILENAME = 'last_build'
36
37
38class SerializingHTMLBuilder(StandaloneHTMLBuilder):
39 """
40 An abstract builder that serializes the generated HTML.
41 """
42 #: the serializing implementation to use. Set this to a module that
43 #: implements a `dump`, `load`, `dumps` and `loads` functions
44 #: (pickle, json etc.)
45 implementation: SerialisingImplementation
46 implementation_dumps_unicode = False
47 #: additional arguments for dump()
48 additional_dump_args: Sequence[Any] = ()
49
50 #: the filename for the global context file
51 globalcontext_filename: str = ''
52
53 supported_image_types = ['image/svg+xml', 'image/png',
54 'image/gif', 'image/jpeg']
55
56 def init(self) -> None:
57 self.build_info = BuildInfo(self.config, self.tags)
58 self.imagedir = '_images'
59 self.current_docname = ''
60 self.theme = None # type: ignore[assignment] # no theme necessary
61 self.templates = None # no template bridge necessary
62 self.init_templates()
63 self.init_highlighter()
64 self.init_css_files()
65 self.init_js_files()
66 self.use_index = self.get_builder_config('use_index', 'html')
67
68 def get_target_uri(self, docname: str, typ: str | None = None) -> str:
69 if docname == 'index':
70 return ''
71 if docname.endswith(SEP + 'index'):
72 return docname[:-5] # up to sep
73 return docname + SEP
74
75 def dump_context(self, context: dict[str, Any], filename: str | os.PathLike[str]) -> None:
76 context = context.copy()
77 if 'css_files' in context:
78 context['css_files'] = [css.filename for css in context['css_files']]
79 if 'script_files' in context:
80 context['script_files'] = [js.filename for js in context['script_files']]
81 if self.implementation_dumps_unicode:
82 with open(filename, 'w', encoding='utf-8') as ft:
83 self.implementation.dump(context, ft, *self.additional_dump_args)
84 else:
85 with open(filename, 'wb') as fb:
86 self.implementation.dump(context, fb, *self.additional_dump_args)
87
88 def handle_page(self, pagename: str, ctx: dict[str, Any], templatename: str = 'page.html',
89 outfilename: str | None = None, event_arg: Any = None) -> None:
90 ctx['current_page_name'] = pagename
91 ctx.setdefault('pathto', lambda p: p)
92 self.add_sidebars(pagename, ctx)
93
94 if not outfilename:
95 outfilename = path.join(self.outdir,
96 os_path(pagename) + self.out_suffix)
97
98 # we're not taking the return value here, since no template is
99 # actually rendered
100 self.app.emit('html-page-context', pagename, templatename, ctx, event_arg)
101
102 # make context object serializable
103 for key in list(ctx):
104 if isinstance(ctx[key], types.FunctionType):
105 del ctx[key]
106
107 ensuredir(path.dirname(outfilename))
108 self.dump_context(ctx, outfilename)
109
110 # if there is a source file, copy the source file for the
111 # "show source" link
112 if ctx.get('sourcename'):
113 source_name = path.join(self.outdir, '_sources',
114 os_path(ctx['sourcename']))
115 ensuredir(path.dirname(source_name))
116 copyfile(self.env.doc2path(pagename), source_name)
117
118 def handle_finish(self) -> None:
119 # dump the global context
120 outfilename = path.join(self.outdir, self.globalcontext_filename)
121 self.dump_context(self.globalcontext, outfilename)
122
123 # super here to dump the search index
124 super().handle_finish()
125
126 # copy the environment file from the doctree dir to the output dir
127 # as needed by the web app
128 copyfile(path.join(self.doctreedir, ENV_PICKLE_FILENAME),
129 path.join(self.outdir, ENV_PICKLE_FILENAME))
130
131 # touch 'last build' file, used by the web application to determine
132 # when to reload its environment and clear the cache
133 open(path.join(self.outdir, LAST_BUILD_FILENAME), 'w').close()
134
135
136class PickleHTMLBuilder(SerializingHTMLBuilder):
137 """
138 A Builder that dumps the generated HTML into pickle files.
139 """
140 name = 'pickle'
141 epilog = __('You can now process the pickle files in %(outdir)s.')
142
143 implementation = pickle
144 implementation_dumps_unicode = False
145 additional_dump_args: tuple[Any] = (pickle.HIGHEST_PROTOCOL,)
146 indexer_format = pickle
147 indexer_dumps_unicode = False
148 out_suffix = '.fpickle'
149 globalcontext_filename = 'globalcontext.pickle'
150 searchindex_filename = 'searchindex.pickle'
151
152
153class JSONHTMLBuilder(SerializingHTMLBuilder):
154 """
155 A builder that dumps the generated HTML into JSON files.
156 """
157 name = 'json'
158 epilog = __('You can now process the JSON files in %(outdir)s.')
159
160 implementation = jsonimpl
161 implementation_dumps_unicode = True
162 indexer_format = jsonimpl
163 indexer_dumps_unicode = True
164 out_suffix = '.fjson'
165 globalcontext_filename = 'globalcontext.json'
166 searchindex_filename = 'searchindex.json'
167
168
169def setup(app: Sphinx) -> dict[str, Any]:
170 app.require_sphinx('5.0')
171 app.setup_extension('sphinx.builders.html')
172 app.add_builder(JSONHTMLBuilder)
173 app.add_builder(PickleHTMLBuilder)
174 app.add_message_catalog(__name__, path.join(package_dir, 'locales'))
175
176 return {
177 'version': __version__,
178 'parallel_read_safe': True,
179 'parallel_write_safe': True,
180 }