1"""
2    sphinx.builders.epub3
3    ~~~~~~~~~~~~~~~~~~~~~
4
5    Build epub3 files.
6    Originally derived from epub.py.
7
8    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
9    :license: BSD, see LICENSE for details.
10"""
11
12import html
13import warnings
14from collections import namedtuple
15from os import path
16from typing import Any, Dict, List, Set, Tuple
17
18from sphinx import package_dir
19from sphinx.application import Sphinx
20from sphinx.builders import _epub_base
21from sphinx.config import ENUM, Config
22from sphinx.deprecation import RemovedInSphinx40Warning
23from sphinx.locale import __
24from sphinx.util import logging, xmlname_checker
25from sphinx.util.fileutil import copy_asset_file
26from sphinx.util.i18n import format_date
27from sphinx.util.osutil import make_filename
28
29logger = logging.getLogger(__name__)
30
31
32NavPoint = namedtuple('NavPoint', ['text', 'refuri', 'children'])
33
34# writing modes
35PAGE_PROGRESSION_DIRECTIONS = {
36    'horizontal': 'ltr',
37    'vertical': 'rtl',
38}
39IBOOK_SCROLL_AXIS = {
40    'horizontal': 'vertical',
41    'vertical': 'horizontal',
42}
43THEME_WRITING_MODES = {
44    'vertical': 'vertical-rl',
45    'horizontal': 'horizontal-tb',
46}
47
48DOCTYPE = '''<!DOCTYPE html>'''
49
50HTML_TAG = (
51    '<html xmlns="http://www.w3.org/1999/xhtml" '
52    'xmlns:epub="http://www.idpf.org/2007/ops">'
53)
54
55
56class Epub3Builder(_epub_base.EpubBuilder):
57    """
58    Builder that outputs epub3 files.
59
60    It creates the metainfo files content.opf, nav.xhtml, toc.ncx, mimetype,
61    and META-INF/container.xml. Afterwards, all necessary files are zipped to
62    an epub file.
63    """
64    name = 'epub'
65    epilog = __('The ePub file is in %(outdir)s.')
66
67    supported_remote_images = False
68    template_dir = path.join(package_dir, 'templates', 'epub3')
69    doctype = DOCTYPE
70    html_tag = HTML_TAG
71    use_meta_charset = True
72
73    # Finish by building the epub file
74    def handle_finish(self) -> None:
75        """Create the metainfo files and finally the epub."""
76        self.get_toc()
77        self.build_mimetype()
78        self.build_container()
79        self.build_content()
80        self.build_navigation_doc()
81        self.build_toc()
82        self.build_epub()
83
84    def validate_config_value(self) -> None:
85        warnings.warn('Epub3Builder.validate_config_value() is deprecated.',
86                      RemovedInSphinx40Warning, stacklevel=2)
87
88    def content_metadata(self) -> Dict:
89        """Create a dictionary with all metadata for the content.opf
90        file properly escaped.
91        """
92        writing_mode = self.config.epub_writing_mode
93
94        metadata = super().content_metadata()
95        metadata['description'] = html.escape(self.config.epub_description)
96        metadata['contributor'] = html.escape(self.config.epub_contributor)
97        metadata['page_progression_direction'] = PAGE_PROGRESSION_DIRECTIONS.get(writing_mode)
98        metadata['ibook_scroll_axis'] = IBOOK_SCROLL_AXIS.get(writing_mode)
99        metadata['date'] = html.escape(format_date("%Y-%m-%dT%H:%M:%SZ"))
100        metadata['version'] = html.escape(self.config.version)
101        metadata['epub_version'] = self.config.epub_version
102        return metadata
103
104    def prepare_writing(self, docnames: Set[str]) -> None:
105        super().prepare_writing(docnames)
106
107        writing_mode = self.config.epub_writing_mode
108        self.globalcontext['theme_writing_mode'] = THEME_WRITING_MODES.get(writing_mode)
109        self.globalcontext['html_tag'] = self.html_tag
110        self.globalcontext['use_meta_charset'] = self.use_meta_charset
111        self.globalcontext['skip_ua_compatible'] = True
112
113    def build_navlist(self, navnodes: List[Dict[str, Any]]) -> List[NavPoint]:
114        """Create the toc navigation structure.
115
116        This method is almost same as build_navpoints method in epub.py.
117        This is because the logical navigation structure of epub3 is not
118        different from one of epub2.
119
120        The difference from build_navpoints method is templates which are used
121        when generating navigation documents.
122        """
123        navstack = []  # type: List[NavPoint]
124        navstack.append(NavPoint('', '', []))
125        level = 0
126        for node in navnodes:
127            if not node['text']:
128                continue
129            file = node['refuri'].split('#')[0]
130            if file in self.ignored_files:
131                continue
132            if node['level'] > self.config.epub_tocdepth:
133                continue
134
135            navpoint = NavPoint(node['text'], node['refuri'], [])
136            if node['level'] == level:
137                navstack.pop()
138                navstack[-1].children.append(navpoint)
139                navstack.append(navpoint)
140            elif node['level'] == level + 1:
141                level += 1
142                navstack[-1].children.append(navpoint)
143                navstack.append(navpoint)
144            elif node['level'] < level:
145                while node['level'] < len(navstack):
146                    navstack.pop()
147                level = node['level']
148                navstack[-1].children.append(navpoint)
149                navstack.append(navpoint)
150            else:
151                raise RuntimeError('Should never reach here. It might be a bug.')
152
153        return navstack[0].children
154
155    def navigation_doc_metadata(self, navlist: List[NavPoint]) -> Dict:
156        """Create a dictionary with all metadata for the nav.xhtml file
157        properly escaped.
158        """
159        metadata = {}  # type: Dict
160        metadata['lang'] = html.escape(self.config.epub_language)
161        metadata['toc_locale'] = html.escape(self.guide_titles['toc'])
162        metadata['navlist'] = navlist
163        return metadata
164
165    def build_navigation_doc(self, outdir: str = None, outname: str = 'nav.xhtml') -> None:
166        """Write the metainfo file nav.xhtml."""
167        if outdir:
168            warnings.warn('The arguments of Epub3Builder.build_navigation_doc() '
169                          'is deprecated.', RemovedInSphinx40Warning, stacklevel=2)
170        else:
171            outdir = self.outdir
172
173        logger.info(__('writing %s file...'), outname)
174
175        if self.config.epub_tocscope == 'default':
176            doctree = self.env.get_and_resolve_doctree(
177                self.config.master_doc, self,
178                prune_toctrees=False, includehidden=False)
179            refnodes = self.get_refnodes(doctree, [])
180            self.toc_add_files(refnodes)
181        else:
182            # 'includehidden'
183            refnodes = self.refnodes
184        navlist = self.build_navlist(refnodes)
185        copy_asset_file(path.join(self.template_dir, 'nav.xhtml_t'),
186                        path.join(outdir, outname),
187                        self.navigation_doc_metadata(navlist))
188
189        # Add nav.xhtml to epub file
190        if outname not in self.files:
191            self.files.append(outname)
192
193
194def validate_config_values(app: Sphinx) -> None:
195    if app.builder.name != 'epub':
196        return
197
198    # <package> lang attribute, dc:language
199    if not app.config.epub_language:
200        logger.warning(__('conf value "epub_language" (or "language") '
201                          'should not be empty for EPUB3'))
202    # <package> unique-identifier attribute
203    if not xmlname_checker().match(app.config.epub_uid):
204        logger.warning(__('conf value "epub_uid" should be XML NAME for EPUB3'))
205    # dc:title
206    if not app.config.epub_title:
207        logger.warning(__('conf value "epub_title" (or "html_title") '
208                          'should not be empty for EPUB3'))
209    # dc:creator
210    if not app.config.epub_author:
211        logger.warning(__('conf value "epub_author" should not be empty for EPUB3'))
212    # dc:contributor
213    if not app.config.epub_contributor:
214        logger.warning(__('conf value "epub_contributor" should not be empty for EPUB3'))
215    # dc:description
216    if not app.config.epub_description:
217        logger.warning(__('conf value "epub_description" should not be empty for EPUB3'))
218    # dc:publisher
219    if not app.config.epub_publisher:
220        logger.warning(__('conf value "epub_publisher" should not be empty for EPUB3'))
221    # dc:rights
222    if not app.config.epub_copyright:
223        logger.warning(__('conf value "epub_copyright" (or "copyright")'
224                          'should not be empty for EPUB3'))
225    # dc:identifier
226    if not app.config.epub_identifier:
227        logger.warning(__('conf value "epub_identifier" should not be empty for EPUB3'))
228    # meta ibooks:version
229    if not app.config.version:
230        logger.warning(__('conf value "version" should not be empty for EPUB3'))
231
232
233def convert_epub_css_files(app: Sphinx, config: Config) -> None:
234    """This converts string styled epub_css_files to tuple styled one."""
235    epub_css_files = []  # type: List[Tuple[str, Dict]]
236    for entry in config.epub_css_files:
237        if isinstance(entry, str):
238            epub_css_files.append((entry, {}))
239        else:
240            try:
241                filename, attrs = entry
242                epub_css_files.append((filename, attrs))
243            except Exception:
244                logger.warning(__('invalid css_file: %r, ignored'), entry)
245                continue
246
247    config.epub_css_files = epub_css_files  # type: ignore
248
249
250def setup(app: Sphinx) -> Dict[str, Any]:
251    app.add_builder(Epub3Builder)
252
253    # config values
254    app.add_config_value('epub_basename', lambda self: make_filename(self.project), None)
255    app.add_config_value('epub_version', 3.0, 'epub')  # experimental
256    app.add_config_value('epub_theme', 'epub', 'epub')
257    app.add_config_value('epub_theme_options', {}, 'epub')
258    app.add_config_value('epub_title', lambda self: self.project, 'epub')
259    app.add_config_value('epub_author', lambda self: self.author, 'epub')
260    app.add_config_value('epub_language', lambda self: self.language or 'en', 'epub')
261    app.add_config_value('epub_publisher', lambda self: self.author, 'epub')
262    app.add_config_value('epub_copyright', lambda self: self.copyright, 'epub')
263    app.add_config_value('epub_identifier', 'unknown', 'epub')
264    app.add_config_value('epub_scheme', 'unknown', 'epub')
265    app.add_config_value('epub_uid', 'unknown', 'env')
266    app.add_config_value('epub_cover', (), 'env')
267    app.add_config_value('epub_guide', (), 'env')
268    app.add_config_value('epub_pre_files', [], 'env')
269    app.add_config_value('epub_post_files', [], 'env')
270    app.add_config_value('epub_css_files', lambda config: config.html_css_files, 'epub')
271    app.add_config_value('epub_exclude_files', [], 'env')
272    app.add_config_value('epub_tocdepth', 3, 'env')
273    app.add_config_value('epub_tocdup', True, 'env')
274    app.add_config_value('epub_tocscope', 'default', 'env')
275    app.add_config_value('epub_fix_images', False, 'env')
276    app.add_config_value('epub_max_image_width', 0, 'env')
277    app.add_config_value('epub_show_urls', 'inline', 'epub')
278    app.add_config_value('epub_use_index', lambda self: self.html_use_index, 'epub')
279    app.add_config_value('epub_description', 'unknown', 'epub')
280    app.add_config_value('epub_contributor', 'unknown', 'epub')
281    app.add_config_value('epub_writing_mode', 'horizontal', 'epub',
282                         ENUM('horizontal', 'vertical'))
283
284    # event handlers
285    app.connect('config-inited', convert_epub_css_files, priority=800)
286    app.connect('builder-inited', validate_config_values)
287
288    return {
289        'version': 'builtin',
290        'parallel_read_safe': True,
291        'parallel_write_safe': True,
292    }
293