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