Create an EPUB version of the calibre User Manual (linked to in the online documentation)

This commit is contained in:
Kovid Goyal 2009-08-21 12:01:07 -06:00
parent ce2fe95dbb
commit 50d66d6f3a
9 changed files with 324 additions and 32 deletions

View File

@ -107,7 +107,7 @@ class EPUBOutput(OutputFormatPlugin):
''' '''
TITLEPAGE = '''\ TITLEPAGE = '''\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head> <head>
<title>%(title)s</title> <title>%(title)s</title>
<style type="text/css"> <style type="text/css">

View File

@ -91,12 +91,15 @@ html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
#html_use_smartypants = True html_use_smartypants = True
# Overall title of the documentation # Overall title of the documentation
html_title = 'calibre User Manual' html_title = 'calibre User Manual'
html_short_title = 'Start' html_short_title = 'Start'
html_logo = 'resources/logo.png' html_logo = 'resources/logo.png'
epub_titlepage = 'resources/titlepage.html'
epub_logo = 'resources/logo.png'
epub_author = 'Kovid Goyal'
# Custom sidebar templates, maps document names to template names. # Custom sidebar templates, maps document names to template names.
#html_sidebars = {} #html_sidebars = {}

View File

@ -3,7 +3,7 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os, inspect, re, textwrap import sys, os, inspect, re, textwrap, cStringIO
from sphinx.builder import StandaloneHTMLBuilder from sphinx.builder import StandaloneHTMLBuilder
from qthelp import QtHelpBuilder from qthelp import QtHelpBuilder
@ -116,7 +116,7 @@ $groups
CLI_CMD += CLI_GROUPS CLI_CMD += CLI_GROUPS
def generate_ebook_convert_help(): def generate_ebook_convert_help(info):
from calibre.ebooks.conversion.cli import create_option_parser from calibre.ebooks.conversion.cli import create_option_parser
from calibre.customize.ui import input_format_plugins, output_format_plugins from calibre.customize.ui import input_format_plugins, output_format_plugins
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
@ -139,22 +139,26 @@ def generate_ebook_convert_help():
''') ''')
for i, ip in enumerate(input_format_plugins()): for i, ip in enumerate(input_format_plugins()):
with open(os.path.join('cli', 'ebook-convert-%d.rst'%i), 'wb') as f: f = cStringIO.StringIO()
f.write(sec_templ.format(ip.name)) path = os.path.join('cli', 'ebook-convert-%d.rst'%i)
toc[ip.name] = 'ebook-convert-%d'%i f.write(sec_templ.format(ip.name))
for op in output_format_plugins(): toc[ip.name] = 'ebook-convert-%d'%i
title = ip.name + ' to ' + op.name for op in output_format_plugins():
parser, plumber = create_option_parser(['ebook-convert', title = ip.name + ' to ' + op.name
'dummyi.'+list(ip.file_types)[0], parser, plumber = create_option_parser(['ebook-convert',
'dummyo.'+op.file_type, '-h'], default_log) 'dummyi.'+list(ip.file_types)[0],
groups = [(None, None, parser.option_list)] 'dummyo.'+op.file_type, '-h'], default_log)
for grp in parser.option_groups: groups = [(None, None, parser.option_list)]
groups.append((grp.title, grp.description, grp.option_list)) for grp in parser.option_groups:
template = str(CLI_GROUPS) groups.append((grp.title, grp.description, grp.option_list))
template = TextTemplate(template[template.find('||'):]) template = str(CLI_GROUPS)
options = template.generate(groups=groups).render() template = TextTemplate(template[template.find('||'):])
f.write(title+'\n------------------------------------------------------') options = template.generate(groups=groups).render()
f.write('\n\n'+options.replace('||', '\n')) f.write(title+'\n------------------------------------------------------')
f.write('\n\n'+options.replace('||', '\n'))
if not os.path.exists(path):
info('creating '+path)
open(path, 'wb').write(f.getvalue())
toct = '||||.. toctree::|| :maxdepth: 2||||' toct = '||||.. toctree::|| :maxdepth: 2||||'
for ip in sorted(toc): for ip in sorted(toc):
@ -204,7 +208,7 @@ def cli_docs(app):
for grp in parser.option_groups: for grp in parser.option_groups:
groups.append((grp.title, grp.description, grp.option_list)) groups.append((grp.title, grp.description, grp.option_list))
if cmd == 'ebook-convert': if cmd == 'ebook-convert':
groups = generate_ebook_convert_help() groups = generate_ebook_convert_help(info)
templ = TextTemplate(EBOOK_CONVERT) templ = TextTemplate(EBOOK_CONVERT)
raw = templ.generate(cmd=cmd, cmdline=cmdline, usage=usage, groups=groups).render() raw = templ.generate(cmd=cmd, cmdline=cmdline, usage=usage, groups=groups).render()
raw = raw.replace('||', '\n').replace('&lt;', '<').replace('&gt;', '>') raw = raw.replace('||', '\n').replace('&lt;', '<').replace('&gt;', '>')
@ -265,6 +269,9 @@ def auto_member(dirname, arguments, options, content, lineno,
return list(node) return list(node)
def setup(app): def setup(app):
app.add_config_value('epub_titlepage', None, False)
app.add_config_value('epub_author', '', False)
app.add_config_value('epub_logo', None, False)
app.add_builder(CustomBuilder) app.add_builder(CustomBuilder)
app.add_builder(CustomQtBuild) app.add_builder(CustomQtBuild)
app.add_builder(EPUBHelpBuilder) app.add_builder(EPUBHelpBuilder)

View File

@ -112,3 +112,8 @@ Metadata plugins
------------------- -------------------
Metadata plugins add the ability to read/write metadata from ebook files to |app|. See :ref:`pluginsMetadataPlugin` for details. Metadata plugins add the ability to read/write metadata from ebook files to |app|. See :ref:`pluginsMetadataPlugin` for details.
.. toctree::
:hidden:
plugins

View File

@ -6,10 +6,88 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os, mimetypes, uuid, shutil
from datetime import datetime
from docutils import nodes
from xml.sax.saxutils import escape, quoteattr
from urlparse import urldefrag
from zipfile import ZipFile, ZIP_STORED, ZipInfo
from sphinx import addnodes
from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.builders.html import StandaloneHTMLBuilder
NCX = '''\
<?xml version="1.0" encoding="UTF-8"?>
<ncx version="2005-1"
xml:lang="en"
xmlns="http://www.daisy.org/z3986/2005/ncx/"
xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata"
>
<head>
<meta name="dtb:uid" content="{uid}"/>
<meta name="dtb:depth" content="{depth}"/>
<meta name="dtb:generator" content="sphinx"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle><text>Table of Contents</text></docTitle>
<navMap>
{navpoints}
</navMap>
</ncx>
'''
OPF = '''\
<?xml version="1.0" encoding="UTF-8"?>
<package version="2.0"
xmlns="http://www.idpf.org/2007/opf"
unique-identifier="sphinx_id"
>
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata">
<dc:title>{title}</dc:title>
<dc:creator opf:role="aut">{author}</dc:creator>
<dc:contributor opf:role="bkp">Sphinx</dc:contributor>
<dc:identifier opf:scheme="sphinx" id="sphinx_id">{uid}</dc:identifier>
<dc:date>{date}</dc:date>
<meta name="calibre:publication_type" content="sphinx_manual" />
</metadata>
<manifest>
{manifest}
</manifest>
<spine toc="ncx">
{spine}
</spine>
<guide>
{guide}
</guide>
</package>
'''
CONTAINER='''\
<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="{0}" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
'''
class TOC(list):
def __init__(self, title=None, href=None):
list.__init__(self)
self.title, self.href = title, href
def create_child(self, title, href):
self.append(TOC(title, href))
return self[-1]
def depth(self):
try:
return max(node.depth() for node in self)+1
except ValueError:
return 1
class EPUBHelpBuilder(StandaloneHTMLBuilder): class EPUBHelpBuilder(StandaloneHTMLBuilder):
""" """
Builder that also outputs Qt help project, contents and index files. Builder that also outputs Qt help project, contents and index files.
@ -29,11 +107,174 @@ class EPUBHelpBuilder(StandaloneHTMLBuilder):
def init(self): def init(self):
StandaloneHTMLBuilder.init(self) StandaloneHTMLBuilder.init(self)
# the output files for HTML help must be .html only self.out_suffix = '.html'
self.out_suffix = '.xhtml' self.link_suffix = '.html'
self.outdir = os.path.join(self.outdir, 'src') self.html_outdir = self.outdir = os.path.join(self.outdir, 'src')
#self.config.html_style = 'traditional.css' self.conf = self.config
def finish(self): def finish(self):
StandaloneHTMLBuilder.finish(self) StandaloneHTMLBuilder.finish(self)
print 11111111 self.create_titlepage()
self.outdir = os.path.dirname(self.outdir)
cwd = os.getcwd()
os.chdir(self.html_outdir)
try:
self.generate_manifest()
self.generate_toc()
self.render_opf()
self.render_epub()
finally:
os.chdir(cwd)
def render_epub(self):
container = CONTAINER.format('content.opf')
path = os.path.abspath('..'+os.sep+self.conf.project+'.epub')
zf = ZipFile(path, 'w')
zi = ZipInfo('mimetype')
zi.compress_type = ZIP_STORED
zf.writestr(zi, 'application/epub+zip')
zf.writestr('META-INF/container.xml', container)
for url in self.manifest:
fp = os.path.join(self.html_outdir, *url.split('/'))
zf.write(fp, url)
zf.close()
self.info('EPUB created at: '+path)
def render_opf(self):
manifest = []
for href in self.manifest:
mt, id = self.manifest[href]
manifest.append(' '*8 + '<item id=%s href=%s media-type=%s />'%\
tuple(map(quoteattr, (id, href, mt))))
manifest = '\n'.join(manifest)
spine = [' '*8+'<itemref idref=%s />'%quoteattr(x) for x in self.spine]
spine = '\n'.join(spine)
guide = ''
if self.conf.epub_titlepage:
guide = ' '*8 + '<reference type="cover" href="_static/titlepage.html" />'
opf = OPF.format(title=escape(self.conf.html_title),
author=escape(self.conf.epub_author), uid=str(uuid.uuid4()),
date=datetime.now().isoformat(), manifest=manifest, spine=spine,
guide=guide)
open('content.opf', 'wb').write(opf)
self.manifest['content.opf'] = ('application/oebps-package+xml', 'opf')
def create_titlepage(self):
if self.conf.epub_titlepage:
img = ''
if self.conf.epub_logo:
img = '_static/epub_logo'+os.path.splitext(self.conf.epub_logo)[1]
shutil.copyfile(self.conf.epub_logo,
os.path.join(self.html_outdir, *img.split('/')))
raw = open(self.conf.epub_titlepage, 'rb').read()
raw = raw%dict(title=self.conf.html_title,
version=self.conf.version,
img=img.split('/')[-1],
author=self.conf.epub_author)
open(os.path.join(self.html_outdir, '_static', 'titlepage.html'), 'wb').write(raw)
def generate_manifest(self):
self.manifest = {}
id = 1
for dirpath, dirnames, filenames in os.walk('.'):
for fname in filenames:
if fname == '.buildinfo':
continue
fpath = os.path.abspath(os.path.join(dirpath, fname))
url = os.path.relpath(fpath).replace(os.sep, '/')
self.manifest[url] = mimetypes.guess_type(url, False)[0]
if self.manifest[url] is None:
self.warn('Unknown mimetype for: ' + url)
self.manifest[url] = 'application/octet-stream'
if self.manifest[url] == 'text/html':
self.manifest[url] = 'application/xhtml+xml'
self.manifest[url] = (self.manifest[url], 'id'+str(id))
id += 1
def isdocnode(self, node):
if not isinstance(node, nodes.list_item):
return False
if len(node.children) != 2:
return False
if not isinstance(node.children[0], addnodes.compact_paragraph):
return False
if not isinstance(node.children[0][0], nodes.reference):
return False
if not isinstance(node.children[1], nodes.bullet_list):
return False
return True
def generate_toc(self):
tocdoc = self.env.get_and_resolve_doctree(self.config.master_doc, self,
prune_toctrees=False)
istoctree = lambda node: (
isinstance(node, addnodes.compact_paragraph)
and node.has_key('toctree'))
toc = TOC()
for node in tocdoc.traverse(istoctree):
self.extend_toc(toc, node)
self._parts = []
self._po = 0
self._po_map = {}
self.spine_map = {}
self.spine = []
self.render_toc(toc)
navpoints = '\n'.join(self._parts).strip()
ncx = NCX.format(uid=str(uuid.uuid4()), depth=toc.depth(),
navpoints=navpoints)
open('toc.ncx', 'wb').write(ncx)
self.manifest['toc.ncx'] = ('application/x-dtbncx+xml', 'ncx')
self.spine.insert(0, self.manifest[self.conf.master_doc+'.html'][1])
if self.conf.epub_titlepage:
self.spine.insert(0, self.manifest['_static/titlepage.html'][1])
def add_to_spine(self, href):
href = urldefrag(href)[0]
if href not in self.spine_map:
for url in self.manifest:
if url == href:
self.spine_map[href]= self.manifest[url][1]
self.spine.append(self.spine_map[href])
def render_toc(self, toc, level=2):
for child in toc:
if child.title and child.href:
href = child.href
self.add_to_spine(href)
title = escape(child.title)
if isinstance(title, unicode):
title = title.encode('utf-8')
if child.href in self._po_map:
po = self._po_map[child.href]
else:
self._po += 1
po = self._po
self._parts.append(' '*(level*4)+
'<navPoint id="%s" playOrder="%d">'%(uuid.uuid4(),
po))
self._parts.append(' '*((level+1)*4)+
'<navLabel><text>%s</text></navLabel>'%title)
self._parts.append(' '*((level+1)*4)+
'<content src=%s />'%quoteattr(href))
self.render_toc(child, level+1)
self._parts.append(' '*(level*4)+'</navPoint>')
def extend_toc(self, toc, node):
if self.isdocnode(node):
refnode = node.children[0][0]
parent = toc.create_child(refnode.astext(), refnode['refuri'])
for subnode in node.children[1]:
self.extend_toc(parent, subnode)
elif isinstance(node, (nodes.list_item, nodes.bullet_list,
addnodes.compact_paragraph)):
for subnode in node:
self.extend_toc(toc, subnode)
elif isinstance(node, nodes.reference):
parent = toc.create_child(node.astext(), node['refuri'])

View File

@ -17,6 +17,10 @@ To get started with more advanced usage, you should read about the :ref:`Graphic
You will find the list of :ref:`Frequently Asked Questions <faq>` useful as well. You will find the list of :ref:`Frequently Asked Questions <faq>` useful as well.
.. only:: html and online
An e-book version of this User Manual is available in `EPUB format <calibre.epub>`_. Because the User Manual uses advanced formatting, it is only suitable for use with the |app| e-book viewer.
Sections Sections
------------ ------------
@ -25,15 +29,12 @@ Sections
gui gui
news news
cli/cli-index
conversion conversion
metadata metadata
faq faq
xpath xpath
customize customize
cli/cli-index
glossary glossary
.. toctree::
:hidden:
plugins

View File

@ -120,6 +120,7 @@ class QtHelpBuilder(StandaloneHTMLBuilder):
StandaloneHTMLBuilder.init(self) StandaloneHTMLBuilder.init(self)
# the output files for HTML help must be .html only # the output files for HTML help must be .html only
self.out_suffix = '.html' self.out_suffix = '.html'
self.link_suffix = '.html'
#self.config.html_style = 'traditional.css' #self.config.html_style = 'traditional.css'
def handle_finish(self): def handle_finish(self):

View File

@ -0,0 +1,29 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>%(title)s</title>
<style type="text/css">
body {
text-align: center;
vertical-align: center;
overflow: hidden;
font-size: 16pt;
}
.logo {
text-align:center;
font-size: 1pt;
overflow:hidden;
}
h1 { font-family: serif; }
h2, h4 { font-family: monospace; }
</style>
</head>
<body>
<h1>%(title)s</h1>
<h4 style="font-family:monospace">%(version)s</h4>
<div style="text-align:center">
<img class="logo" src="%(img)s" alt="calibre logo" />
</div>
<h2>%(author)s</h2>
</body>
</html>

View File

@ -123,8 +123,13 @@ class manual(OptionlessCommand):
os.makedirs(d) os.makedirs(d)
if not os.path.exists('.build'+os.sep+'html'): if not os.path.exists('.build'+os.sep+'html'):
os.makedirs('.build'+os.sep+'html') os.makedirs('.build'+os.sep+'html')
check_call(['sphinx-build', '-b', 'custom', '-d', check_call(['sphinx-build', '-b', 'custom', '-d', '-t', 'online',
'.build/doctrees', '.', '.build/html']) '.build/doctrees', '.', '.build/html'])
check_call(['sphinx-build', '-b', 'epub', '-d',
'.build/doctrees', '.', '.build/epub'])
j = os.path.join
shutil.copyfile(j('.build', 'epub', 'calibre.epub'), j('.build',
'html', 'calibre.epub'))
finally: finally:
os.chdir(cwd) os.chdir(cwd)