summaryrefslogtreecommitdiff
path: root/sphinx/builders
diff options
context:
space:
mode:
authorGeorg Brandl <georg@python.org>2012-05-08 12:48:33 +0200
committerGeorg Brandl <georg@python.org>2012-05-08 12:48:33 +0200
commitb4d6f785f297ae22a7b41b5dc669a19e15f354a2 (patch)
tree87e9316d18fc22d2f808d4a7846b685e35a31265 /sphinx/builders
parentb5de8e7728023a7721d3537959487358b2c6e6f4 (diff)
parent047a8d573affc71be3a6f2749c6182428f387f54 (diff)
downloadsphinx-b4d6f785f297ae22a7b41b5dc669a19e15f354a2.tar.gz
Merge with stable
Diffstat (limited to 'sphinx/builders')
-rw-r--r--sphinx/builders/epub.py131
-rw-r--r--sphinx/builders/htmlhelp.py1
-rw-r--r--sphinx/builders/linkcheck.py72
3 files changed, 184 insertions, 20 deletions
diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py
index e946e0d3..b4c3b277 100644
--- a/sphinx/builders/epub.py
+++ b/sphinx/builders/epub.py
@@ -18,12 +18,21 @@ import codecs
import zipfile
from os import path
+try:
+ from PIL import Image
+except ImportError:
+ try:
+ import Image
+ except ImportError:
+ Image = None
+
from docutils import nodes
from sphinx import addnodes
from sphinx.builders.html import StandaloneHTMLBuilder
-from sphinx.util.osutil import EEXIST
+from sphinx.util.osutil import ensuredir, EEXIST
from sphinx.util.smartypants import sphinx_smarty_pants as ssp
+from sphinx.util.console import brown
# (Fragment) templates from which the metainfo files content.opf, toc.ncx,
@@ -95,6 +104,9 @@ _content_template = u'''\
<spine toc="ncx">
%(spine)s
</spine>
+ <guide>
+%(guide)s
+ </guide>
</package>
'''
@@ -112,12 +124,21 @@ _file_template = u'''\
_spine_template = u'''\
<itemref idref="%(idref)s" />'''
+_guide_template = u'''\
+ <reference type="%(type)s" title="%(title)s" href="%(uri)s" />'''
+
_toctree_template = u'toctree-l%d'
_link_target_template = u' [%(uri)s]'
_css_link_target_class = u'link-target'
+# XXX These strings should be localized according to epub_language
+_guide_titles = {
+ 'toc': u'Table of Contents',
+ 'cover': u'Cover Page'
+}
+
_media_types = {
'.html': 'application/xhtml+xml',
'.css': 'text/css',
@@ -165,7 +186,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
self.playorder = 0
def get_theme_config(self):
- return self.config.epub_theme, {}
+ return self.config.epub_theme, self.config.epub_theme_options
# generic support functions
def make_id(self, name):
@@ -206,11 +227,11 @@ class EpubBuilder(StandaloneHTMLBuilder):
return result
def get_toc(self):
- """Get the total table of contents, containg the master_doc
+ """Get the total table of contents, containing the master_doc
and pre and post files not managed by sphinx.
"""
doctree = self.env.get_and_resolve_doctree(self.config.master_doc,
- self, prune_toctrees=False)
+ self, prune_toctrees=False, includehidden=True)
self.refnodes = self.get_refnodes(doctree, [])
master_dir = os.path.dirname(self.config.master_doc)
if master_dir:
@@ -301,6 +322,57 @@ class EpubBuilder(StandaloneHTMLBuilder):
subentrylinks[i] = (ismain,
self.fix_fragment(m.group(1), m.group(2)))
+ def copy_image_files_pil(self):
+ """Copy images using the PIL.
+ The method tries to read and write the files with the PIL,
+ converting the format and resizing the image if necessary/possible.
+ """
+ ensuredir(path.join(self.outdir, '_images'))
+ for src in self.status_iterator(self.images, 'copying images... ',
+ brown, len(self.images)):
+ dest = self.images[src]
+ try:
+ img = Image.open(path.join(self.srcdir, src))
+ except IOError:
+ self.warn('cannot read image file %r: copying it instead' %
+ (path.join(self.srcdir, src), ))
+ try:
+ copyfile(path.join(self.srcdir, src),
+ path.join(self.outdir, '_images', dest))
+ except Exception, err:
+ self.warn('cannot copy image file %r: %s' %
+ (path.join(self.srcdir, src), err))
+ continue
+ if self.config.epub_fix_images:
+ if img.mode in ('P',):
+ # See PIL documentation for Image.convert()
+ img = img.convert()
+ if self.config.epub_max_image_width > 0:
+ (width, height) = img.size
+ nw = self.config.epub_max_image_width
+ if width > nw:
+ nh = (height * nw) / width
+ img = img.resize((nw, nh), Image.BICUBIC)
+ try:
+ img.save(path.join(self.outdir, '_images', dest))
+ except IOError, err:
+ self.warn('cannot write image file %r: %s' %
+ (path.join(self.srcdir, src), err))
+
+ def copy_image_files(self):
+ """Copy image files to destination directory.
+ This overwritten method can use the PIL to convert image files.
+ """
+ if self.images:
+ if self.config.epub_fix_images or self.config.epub_max_image_width:
+ if not Image:
+ self.warn('PIL not found - copying image files')
+ super(EpubBuilder, self).copy_image_files()
+ else:
+ self.copy_image_files_pil()
+ else:
+ super(EpubBuilder, self).copy_image_files()
+
def handle_page(self, pagename, addctx, templatename='page.html',
outfilename=None, event_arg=None):
"""Create a rendered page.
@@ -348,7 +420,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
finally:
f.close()
- def content_metadata(self, files, spine):
+ def content_metadata(self, files, spine, guide):
"""Create a dictionary with all metadata for the content.opf
file properly escaped.
"""
@@ -364,6 +436,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
metadata['date'] = self.esc(time.strftime('%Y-%m-%d'))
metadata['files'] = files
metadata['spine'] = spine
+ metadata['guide'] = guide
return metadata
def build_content(self, outdir, outname):
@@ -419,14 +492,15 @@ class EpubBuilder(StandaloneHTMLBuilder):
# add the optional cover
content_tmpl = _content_template
+ html_tmpl = None
if self.config.epub_cover:
- image, tmpl = self.config.epub_cover
+ image, html_tmpl = self.config.epub_cover
mpos = content_tmpl.rfind('</metadata>')
cpos = content_tmpl.rfind('\n', 0 , mpos) + 1
content_tmpl = content_tmpl[:cpos] + \
_cover_template % {'cover': self.esc(self.make_id(image))} + \
content_tmpl[cpos:]
- if tmpl:
+ if html_tmpl:
spine.insert(0, _spine_template % {
'idref': self.esc(self.make_id(_coverpage_name))})
if _coverpage_name not in self.files:
@@ -439,16 +513,46 @@ class EpubBuilder(StandaloneHTMLBuilder):
})
ctx = {'image': self.esc(image), 'title': self.config.project}
self.handle_page(
- os.path.splitext(_coverpage_name)[0], ctx, tmpl)
-
+ os.path.splitext(_coverpage_name)[0], ctx, html_tmpl)
+
+ guide = []
+ auto_add_cover = True
+ auto_add_toc = True
+ if self.config.epub_guide:
+ for type, uri, title in self.config.epub_guide:
+ file = uri.split('#')[0]
+ if file not in self.files:
+ self.files.append(file)
+ if type == 'cover':
+ auto_add_cover = False
+ if type == 'toc':
+ auto_add_toc = False
+ guide.append(_guide_template % {
+ 'type': self.esc(type),
+ 'title': self.esc(title),
+ 'uri': self.esc(uri)
+ })
+ if auto_add_cover and html_tmpl:
+ guide.append(_guide_template % {
+ 'type': 'cover',
+ 'title': _guide_titles['cover'],
+ 'uri': self.esc(_coverpage_name)
+ })
+ if auto_add_toc and self.refnodes:
+ guide.append(_guide_template % {
+ 'type': 'toc',
+ 'title': _guide_titles['toc'],
+ 'uri': self.esc(self.refnodes[0]['refuri'])
+ })
projectfiles = '\n'.join(projectfiles)
spine = '\n'.join(spine)
+ guide = '\n'.join(guide)
# write the project file
f = codecs.open(path.join(outdir, outname), 'w', 'utf-8')
try:
f.write(content_tmpl % \
- self.content_metadata(projectfiles, spine))
+ self.content_metadata(projectfiles, spine, guide))
finally:
f.close()
@@ -529,7 +633,12 @@ class EpubBuilder(StandaloneHTMLBuilder):
"""Write the metainfo file toc.ncx."""
self.info('writing %s file...' % outname)
- navpoints = self.build_navpoints(self.refnodes)
+ doctree = self.env.get_and_resolve_doctree(self.config.master_doc,
+ self, prune_toctrees=False, includehidden=False)
+ refnodes = self.get_refnodes(doctree, [])
+ if not refnodes:
+ refnodes = self.refnodes
+ navpoints = self.build_navpoints(refnodes)
level = max(item['level'] for item in self.refnodes)
level = min(level, self.config.epub_tocdepth)
f = codecs.open(path.join(outdir, outname), 'w', 'utf-8')
diff --git a/sphinx/builders/htmlhelp.py b/sphinx/builders/htmlhelp.py
index fdf25cc8..a1dcef23 100644
--- a/sphinx/builders/htmlhelp.py
+++ b/sphinx/builders/htmlhelp.py
@@ -142,6 +142,7 @@ chm_locales = {
'lt': (0x427, 'cp1257'),
'lv': (0x426, 'cp1257'),
'nl': (0x413, 'cp1252'),
+ 'no_NB': (0x414, 'cp1252'),
'pl': (0x415, 'cp1250'),
'pt_BR': (0x416, 'cp1252'),
'ru': (0x419, 'cp1251'),
diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py
index ad15b55d..a8adcdac 100644
--- a/sphinx/builders/linkcheck.py
+++ b/sphinx/builders/linkcheck.py
@@ -15,7 +15,8 @@ import Queue
import socket
import threading
from os import path
-from urllib2 import build_opener, Request
+from urllib2 import build_opener, unquote, Request
+from HTMLParser import HTMLParser, HTMLParseError
from docutils import nodes
@@ -33,6 +34,41 @@ class HeadRequest(Request):
return 'HEAD'
+class AnchorCheckParser(HTMLParser):
+ """Specialized HTML parser that looks for a specific anchor."""
+
+ def __init__(self, search_anchor):
+ HTMLParser.__init__(self)
+
+ self.search_anchor = search_anchor
+ self.found = False
+
+ def handle_starttag(self, tag, attrs):
+ for key, value in attrs:
+ if key in ('id', 'name') and value == self.search_anchor:
+ self.found = True
+
+
+def check_anchor(f, hash):
+ """Reads HTML data from a filelike object 'f' searching for anchor 'hash'.
+ Returns True if anchor was found, False otherwise.
+ """
+ parser = AnchorCheckParser(hash)
+ try:
+ # Read file in chunks of 8192 bytes. If we find a matching anchor, we
+ # break the loop early in hopes not to have to download the whole thing.
+ chunk = f.read(8192)
+ while chunk and not parser.found:
+ parser.feed(chunk)
+ chunk = f.read(8192)
+ parser.close()
+ except HTMLParseError:
+ # HTMLParser is usually pretty good with sloppy HTML, but it tends to
+ # choke on EOF. But we're done then anyway.
+ pass
+ return parser.found
+
+
class CheckExternalLinksBuilder(Builder):
"""
Checks for broken external links.
@@ -66,7 +102,7 @@ class CheckExternalLinksBuilder(Builder):
def check():
# check for various conditions without bothering the network
- if len(uri) == 0 or uri[0:7] == 'mailto:' or uri[0:4] == 'ftp:':
+ if len(uri) == 0 or uri[0] == '#' or uri[0:7] == 'mailto:' or uri[0:4] == 'ftp:':
return 'unchecked', ''
elif not (uri[0:5] == 'http:' or uri[0:6] == 'https:'):
return 'local', ''
@@ -80,19 +116,39 @@ class CheckExternalLinksBuilder(Builder):
if rex.match(uri):
return 'ignored', ''
+ if '#' in uri:
+ req_url, hash = uri.split('#', 1)
+ else:
+ req_url = uri
+ hash = None
+
# need to actually check the URI
try:
- f = opener.open(HeadRequest(uri), **kwargs)
- f.close()
+ if hash and self.app.config.linkcheck_anchors:
+ # Read the whole document and see if #hash exists
+ f = opener.open(Request(req_url), **kwargs)
+ found = check_anchor(f, unquote(hash))
+ f.close()
+
+ if not found:
+ raise Exception("Anchor '%s' not found" % hash)
+ else:
+ f = opener.open(HeadRequest(req_url), **kwargs)
+ f.close()
+
except Exception, err:
self.broken[uri] = str(err)
return 'broken', str(err)
- if f.url.rstrip('/') == uri.rstrip('/'):
+ if f.url.rstrip('/') == req_url.rstrip('/'):
self.good.add(uri)
return 'working', 'new'
else:
- self.redirected[uri] = f.url
- return 'redirected', f.url
+ new_url = f.url
+ if hash:
+ new_url += '#' + hash
+
+ self.redirected[uri] = new_url
+ return 'redirected', new_url
while True:
uri, docname, lineno = self.wqueue.get()
@@ -142,8 +198,6 @@ class CheckExternalLinksBuilder(Builder):
if 'refuri' not in node:
continue
uri = node['refuri']
- if '#' in uri:
- uri = uri.split('#')[0]
lineno = None
while lineno is None:
node = node.parent