diff options
| author | Georg Brandl <georg@python.org> | 2012-05-08 12:48:33 +0200 |
|---|---|---|
| committer | Georg Brandl <georg@python.org> | 2012-05-08 12:48:33 +0200 |
| commit | b4d6f785f297ae22a7b41b5dc669a19e15f354a2 (patch) | |
| tree | 87e9316d18fc22d2f808d4a7846b685e35a31265 /sphinx/builders | |
| parent | b5de8e7728023a7721d3537959487358b2c6e6f4 (diff) | |
| parent | 047a8d573affc71be3a6f2749c6182428f387f54 (diff) | |
| download | sphinx-b4d6f785f297ae22a7b41b5dc669a19e15f354a2.tar.gz | |
Merge with stable
Diffstat (limited to 'sphinx/builders')
| -rw-r--r-- | sphinx/builders/epub.py | 131 | ||||
| -rw-r--r-- | sphinx/builders/htmlhelp.py | 1 | ||||
| -rw-r--r-- | sphinx/builders/linkcheck.py | 72 |
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 |
