# -*- coding: utf-8 -*-""" sphinx.builders.html ~~~~~~~~~~~~~~~~~~~~ Several HTML builders. :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details."""importosimportcodecsimportshutilimportposixpathimportcPickleaspicklefromosimportpathtry:fromhashlibimportmd5exceptImportError:# 2.4 compatibilityfrommd5importmd5fromdocutilsimportnodesfromdocutils.ioimportDocTreeInput,StringOutputfromdocutils.coreimportpublish_partsfromdocutils.utilsimportnew_documentfromdocutils.frontendimportOptionParserfromdocutils.readers.doctreeimportReaderasDoctreeReaderfromsphinximportpackage_dir,__version__fromsphinx.utilimportSEP,os_path,relative_uri,ensuredir, \
movefile,ustrftime,copy_static_entryfromsphinx.errorsimportSphinxErrorfromsphinx.searchimportjs_indexfromsphinx.themingimportThemefromsphinx.buildersimportBuilder,ENV_PICKLE_FILENAMEfromsphinx.highlightingimportPygmentsBridgefromsphinx.util.consoleimportboldfromsphinx.writers.htmlimportHTMLWriter,HTMLTranslator, \
SmartyPantsHTMLTranslatortry:importjsonexceptImportError:try:importsimplejsonasjsonexceptImportError:json=None#: the filename for the inventory of objectsINVENTORY_FILENAME='objects.inv'#: the filename for the "last build" file (for serializing builders)LAST_BUILD_FILENAME='last_build'classStandaloneHTMLBuilder(Builder):""" Builds standalone HTML docs. """name='html'format='html'copysource=Trueout_suffix='.html'link_suffix='.html'# defaults to matching out_suffixindexer_format=js_indexsupported_image_types=['image/svg+xml','image/png','image/gif','image/jpeg']searchindex_filename='searchindex.js'add_permalinks=Trueembedded=False# for things like HTML help or Qt help: suppresses sidebar# This is a class attribute because it is mutated by Sphinx.add_javascript.script_files=['_static/jquery.js','_static/doctools.js']definit(self):# a hash of all config values that, if changed, cause a full rebuildself.config_hash=''self.tags_hash=''# section numbers for headings in the currently visited documentself.secnumbers={}self.init_templates()self.init_highlighter()self.init_translator_class()ifself.config.html_file_suffix:self.out_suffix=self.config.html_file_suffixifself.config.html_link_suffixisnotNone:self.link_suffix=self.config.html_link_suffixelse:self.link_suffix=self.out_suffixifself.config.languageisnotNone:jsfile=path.join(package_dir,'locale',self.config.language,'LC_MESSAGES','sphinx.js')ifpath.isfile(jsfile):self.script_files.append('_static/translations.js')definit_templates(self):Theme.init_themes(self)self.theme=Theme(self.config.html_theme)self.create_template_bridge()self.templates.init(self,self.theme)definit_highlighter(self):# determine Pygments style and create the highlighterifself.config.pygments_styleisnotNone:style=self.config.pygments_styleelifself.theme:style=self.theme.get_confstr('theme','pygments_style','none')else:style='sphinx'self.highlighter=PygmentsBridge('html',style)definit_translator_class(self):ifself.config.html_translator_class:self.translator_class=self.app.import_object(self.config.html_translator_class,'html_translator_class setting')elifself.config.html_use_smartypants:self.translator_class=SmartyPantsHTMLTranslatorelse:self.translator_class=HTMLTranslatordefget_outdated_docs(self):cfgdict=dict((name,self.config[name])for(name,desc)inself.config.values.iteritems()ifdesc[1]=='html')self.config_hash=md5(str(cfgdict)).hexdigest()self.tags_hash=md5(str(sorted(self.tags))).hexdigest()old_config_hash=old_tags_hash=''try:fp=open(path.join(self.outdir,'.buildinfo'))version=fp.readline()ifversion.rstrip()!='# Sphinx build info version 1':raiseValueErrorfp.readline()# skip commentarycfg,old_config_hash=fp.readline().strip().split(': ')ifcfg!='config':raiseValueErrortag,old_tags_hash=fp.readline().strip().split(': ')iftag!='tags':raiseValueErrorfp.close()exceptValueError:self.warn('unsupported build info format in %r, building all'%path.join(self.outdir,'.buildinfo'))exceptException:passifold_config_hash!=self.config_hashor \
old_tags_hash!=self.tags_hash:fordocnameinself.env.found_docs:yielddocnamereturnifself.templates:template_mtime=self.templates.newest_template_mtime()else:template_mtime=0fordocnameinself.env.found_docs:ifdocnamenotinself.env.all_docs:yielddocnamecontinuetargetname=self.env.doc2path(docname,self.outdir,self.out_suffix)try:targetmtime=path.getmtime(targetname)exceptException:targetmtime=0try:srcmtime=max(path.getmtime(self.env.doc2path(docname)),template_mtime)ifsrcmtime>targetmtime:yielddocnameexceptEnvironmentError:# source doesn't exist anymorepassdefrender_partial(self,node):"""Utility: Render a lone doctree node."""doc=new_document('<partial node>')doc.append(node)returnpublish_parts(doc,source_class=DocTreeInput,reader=DoctreeReader(),writer=HTMLWriter(self),settings_overrides={'output_encoding':'unicode'})defprepare_writing(self,docnames):fromsphinx.searchimportIndexBuilderself.indexer=IndexBuilder(self.env)self.load_indexer(docnames)self.docwriter=HTMLWriter(self)self.docsettings=OptionParser(defaults=self.env.settings,components=(self.docwriter,)).get_default_values()# format the "last updated on" string, only once is enough since it# typically doesn't include the time of daylufmt=self.config.html_last_updated_fmtiflufmtisnotNone:self.last_updated=ustrftime(lufmtor_('%b %d, %Y'))else:self.last_updated=Nonelogo=self.config.html_logoand \
path.basename(self.config.html_logo)or''favicon=self.config.html_faviconand \
path.basename(self.config.html_favicon)or''iffaviconandos.path.splitext(favicon)[1]!='.ico':self.warn('html_favicon is not an .ico file')ifnotisinstance(self.config.html_use_opensearch,basestring):self.warn('html_use_opensearch config value must now be a string')self.relations=self.env.collect_relations()rellinks=[]ifself.config.html_use_index:rellinks.append(('genindex',_('General Index'),'I',_('index')))ifself.config.html_use_modindexandself.env.modules:rellinks.append(('modindex',_('Global Module Index'),'M',_('modules')))ifself.config.html_styleisnotNone:stylename=self.config.html_styleelifself.theme:stylename=self.theme.get_confstr('theme','stylesheet')else:stylename='default.css'self.globalcontext=dict(embedded=self.embedded,project=self.config.project,release=self.config.release,version=self.config.version,last_updated=self.last_updated,copyright=self.config.copyright,master_doc=self.config.master_doc,use_opensearch=self.config.html_use_opensearch,docstitle=self.config.html_title,shorttitle=self.config.html_short_title,show_sphinx=self.config.html_show_sphinx,has_source=self.config.html_copy_source,show_source=self.config.html_show_sourcelink,file_suffix=self.out_suffix,script_files=self.script_files,sphinx_version=__version__,style=stylename,rellinks=rellinks,builder=self.name,parents=[],logo=logo,favicon=favicon,)ifself.theme:self.globalcontext.update(('theme_'+key,val)for(key,val)inself.theme.get_options(self.config.html_theme_options).iteritems())self.globalcontext.update(self.config.html_context)defget_doc_context(self,docname,body,metatags):"""Collect items for the template context of a page."""# find out relationsprev=next=Noneparents=[]rellinks=self.globalcontext['rellinks'][:]related=self.relations.get(docname)titles=self.env.titlesifrelatedandrelated[2]:try:next={'link':self.get_relative_uri(docname,related[2]),'title':self.render_partial(titles[related[2]])['title']}rellinks.append((related[2],next['title'],'N',_('next')))exceptKeyError:next=Noneifrelatedandrelated[1]:try:prev={'link':self.get_relative_uri(docname,related[1]),'title':self.render_partial(titles[related[1]])['title']}rellinks.append((related[1],prev['title'],'P',_('previous')))exceptKeyError:# the relation is (somehow) not in the TOC tree, handle# that gracefullyprev=Nonewhilerelatedandrelated[0]:try:parents.append({'link':self.get_relative_uri(docname,related[0]),'title':self.render_partial(titles[related[0]])['title']})exceptKeyError:passrelated=self.relations.get(related[0])ifparents:parents.pop()# remove link to the master file; we have a generic# "back to index" link alreadyparents.reverse()# title rendered as HTMLtitle=titles.get(docname)title=titleandself.render_partial(title)['title']or''# the name for the copied sourcesourcename=self.config.html_copy_sourceanddocname+'.txt'or''# metadata for the documentmeta=self.env.metadata.get(docname)# local TOC and global TOC treetoc=self.render_partial(self.env.get_toc_for(docname))['fragment']returndict(parents=parents,prev=prev,next=next,title=title,meta=meta,body=body,metatags=metatags,rellinks=rellinks,sourcename=sourcename,toc=toc,# only display a TOC if there's more than one item to showdisplay_toc=(self.env.toc_num_entries[docname]>1),)defwrite_doc(self,docname,doctree):destination=StringOutput(encoding='utf-8')doctree.settings=self.docsettingsself.secnumbers=self.env.toc_secnumbers.get(docname,{})self.imgpath=relative_uri(self.get_target_uri(docname),'_images')self.post_process_images(doctree)self.dlpath=relative_uri(self.get_target_uri(docname),'_downloads')self.docwriter.write(doctree,destination)self.docwriter.assemble_parts()body=self.docwriter.parts['fragment']metatags=self.docwriter.clean_metactx=self.get_doc_context(docname,body,metatags)self.index_page(docname,doctree,ctx.get('title',''))self.handle_page(docname,ctx,event_arg=doctree)deffinish(self):self.info(bold('writing additional files...'),nonl=1)# the global general indexifself.config.html_use_index:# the total count of lines for each index letter, used to distribute# the entries into two columnsgenindex=self.env.create_index(self)indexcounts=[]for_,entriesingenindex:indexcounts.append(sum(1+len(subitems)for_,(_,subitems)inentries))genindexcontext=dict(genindexentries=genindex,genindexcounts=indexcounts,split_index=self.config.html_split_index,)self.info(' genindex',nonl=1)ifself.config.html_split_index:self.handle_page('genindex',genindexcontext,'genindex-split.html')self.handle_page('genindex-all',genindexcontext,'genindex.html')for(key,entries),countinzip(genindex,indexcounts):ctx={'key':key,'entries':entries,'count':count,'genindexentries':genindex}self.handle_page('genindex-'+key,ctx,'genindex-single.html')else:self.handle_page('genindex',genindexcontext,'genindex.html')# the global module indexifself.config.html_use_modindexandself.env.modules:# the sorted list of all modules, for the global module indexmodules=sorted(((mn,(self.get_relative_uri('modindex',fn)+'#module-'+mn,sy,pl,dep))for(mn,(fn,sy,pl,dep))inself.env.modules.iteritems()),key=lambdax:x[0].lower())# collect all platformsplatforms=set()# sort out collapsable modulesmodindexentries=[]letters=[]pmn=''num_toplevels=0num_collapsables=0cg=0# collapse groupfl=''# first letterformn,(fn,sy,pl,dep)inmodules:pl=plandpl.split(', ')or[]platforms.update(pl)ignore=self.env.config['modindex_common_prefix']ignore=sorted(ignore,key=len,reverse=True)foriinignore:ifmn.startswith(i):mn=mn[len(i):]stripped=ibreakelse:stripped=''iffl!=mn[0].lower()andmn[0]!='_':# headingletter=mn[0].upper()ifletternotinletters:modindexentries.append(['',False,0,False,letter,'',[],False,''])letters.append(letter)tn=mn.split('.')[0]iftn!=mn:# submoduleifpmn==tn:# first submodule - make parent collapsablemodindexentries[-1][1]=Truenum_collapsables+=1elifnotpmn.startswith(tn):# submodule without parent in list, add dummy entrycg+=1modindexentries.append([tn,True,cg,False,'','',[],False,stripped])else:num_toplevels+=1cg+=1modindexentries.append([mn,False,cg,(tn!=mn),fn,sy,pl,dep,stripped])pmn=mnfl=mn[0].lower()platforms=sorted(platforms)# apply heuristics when to collapse modindex at page load:# only collapse if number of toplevel modules is larger than# number of submodulescollapse=len(modules)-num_toplevels<num_toplevels# As some parts of the module names may have been stripped, those# names have changed, thus it is necessary to sort the entries.ifignore:defsorthelper(entry):name=entry[0]ifname=='':# headingname=entry[4]returnname.lower()modindexentries.sort(key=sorthelper)letters.sort()modindexcontext=dict(modindexentries=modindexentries,platforms=platforms,letters=letters,collapse_modindex=collapse,)self.info(' modindex',nonl=1)self.handle_page('modindex',modindexcontext,'modindex.html')# the search pageifself.name!='htmlhelp':self.info(' search',nonl=1)self.handle_page('search',{},'search.html')# additional pages from conf.pyforpagename,templateinself.config.html_additional_pages.items():self.info(' '+pagename,nonl=1)self.handle_page(pagename,{},template)ifself.config.html_use_opensearchandself.name!='htmlhelp':self.info(' opensearch',nonl=1)fn=path.join(self.outdir,'_static','opensearch.xml')self.handle_page('opensearch',{},'opensearch.xml',outfilename=fn)self.info()# copy image filesifself.images:self.info(bold('copying images...'),nonl=True)ensuredir(path.join(self.outdir,'_images'))forsrc,destinself.images.iteritems():self.info(' '+src,nonl=1)shutil.copyfile(path.join(self.srcdir,src),path.join(self.outdir,'_images',dest))self.info()# copy downloadable filesifself.env.dlfiles:self.info(bold('copying downloadable files...'),nonl=True)ensuredir(path.join(self.outdir,'_downloads'))forsrc,(_,dest)inself.env.dlfiles.iteritems():self.info(' '+src,nonl=1)shutil.copyfile(path.join(self.srcdir,src),path.join(self.outdir,'_downloads',dest))self.info()# copy static filesself.info(bold('copying static files... '),nonl=True)ensuredir(path.join(self.outdir,'_static'))# first, create pygments style filef=open(path.join(self.outdir,'_static','pygments.css'),'w')f.write(self.highlighter.get_stylesheet())f.close()# then, copy translations JavaScript fileifself.config.languageisnotNone:jsfile=path.join(package_dir,'locale',self.config.language,'LC_MESSAGES','sphinx.js')ifpath.isfile(jsfile):shutil.copyfile(jsfile,path.join(self.outdir,'_static','translations.js'))# then, copy over all user-supplied static filesifself.theme:staticdirnames=[path.join(themepath,'static')forthemepathinself.theme.get_dirchain()[::-1]]else:staticdirnames=[]staticdirnames+=[path.join(self.confdir,spath)forspathinself.config.html_static_path]forstaticdirnameinstaticdirnames:ifnotpath.isdir(staticdirname):self.warn('static directory %r does not exist'%staticdirname)continueforfilenameinos.listdir(staticdirname):iffilename.startswith('.'):continuefullname=path.join(staticdirname,filename)targetname=path.join(self.outdir,'_static',filename)copy_static_entry(fullname,targetname,self,self.globalcontext)# last, copy logo file (handled differently)ifself.config.html_logo:logobase=path.basename(self.config.html_logo)shutil.copyfile(path.join(self.confdir,self.config.html_logo),path.join(self.outdir,'_static',logobase))# write build info filefp=open(path.join(self.outdir,'.buildinfo'),'w')try:fp.write('# Sphinx build info version 1\n''# This file hashes the configuration used when building'' these files. When it is not found, a full rebuild will'' be done.\nconfig: %s\ntags: %s\n'%(self.config_hash,self.tags_hash))finally:fp.close()self.info('done')# dump the search indexself.handle_finish()defcleanup(self):# clean up theme stuffifself.theme:self.theme.cleanup()defpost_process_images(self,doctree):""" Pick the best candiate for an image and link down-scaled images to their high res version. """Builder.post_process_images(self,doctree)fornodeindoctree.traverse(nodes.image):ifnotnode.has_key('scale')or \
isinstance(node.parent,nodes.reference):# docutils does unfortunately not preserve the# ``target`` attribute on images, so we need to check# the parent node here.continueuri=node['uri']reference=nodes.reference()ifuriinself.images:reference['refuri']=posixpath.join(self.imgpath,self.images[uri])else:reference['refuri']=urinode.replace_self(reference)reference.append(node)defload_indexer(self,docnames):keep=set(self.env.all_docs)-set(docnames)try:f=open(path.join(self.outdir,self.searchindex_filename),'rb')try:self.indexer.load(f,self.indexer_format)finally:f.close()except(IOError,OSError,ValueError):ifkeep:self.warn('search index couldn\'t be loaded, but not all ''documents will be built: the index will be ''incomplete.')# delete all entries for files that will be rebuiltself.indexer.prune(keep)defindex_page(self,pagename,doctree,title):# only index pages with titleifself.indexerisnotNoneandtitle:self.indexer.feed(pagename,title,doctree)def_get_local_toctree(self,docname,collapse=True):returnself.render_partial(self.env.get_toctree_for(docname,self,collapse))['fragment']defget_outfilename(self,pagename):returnpath.join(self.outdir,os_path(pagename)+self.out_suffix)# --------- these are overwritten by the serialization builderdefget_target_uri(self,docname,typ=None):returndocname+self.link_suffixdefhandle_page(self,pagename,addctx,templatename='page.html',outfilename=None,event_arg=None):ctx=self.globalcontext.copy()# current_page_name is backwards compatibilityctx['pagename']=ctx['current_page_name']=pagenamedefpathto(otheruri,resource=False,baseuri=self.get_target_uri(pagename)):ifnotresource:otheruri=self.get_target_uri(otheruri)returnrelative_uri(baseuri,otheruri)ctx['pathto']=pathtoctx['hasdoc']=lambdaname:nameinself.env.all_docsctx['customsidebar']=self.config.html_sidebars.get(pagename)ctx['toctree']=lambda**kw:self._get_local_toctree(pagename,**kw)ctx.update(addctx)self.app.emit('html-page-context',pagename,templatename,ctx,event_arg)output=self.templates.render(templatename,ctx)ifnotoutfilename:outfilename=self.get_outfilename(pagename)# outfilename's path is in general different from self.outdirensuredir(path.dirname(outfilename))try:f=codecs.open(outfilename,'w','utf-8')try:f.write(output)finally:f.close()except(IOError,OSError),err:self.warn("Error writing file %s: %s"%(outfilename,err))ifself.copysourceandctx.get('sourcename'):# copy the source file for the "show source" linksource_name=path.join(self.outdir,'_sources',os_path(ctx['sourcename']))ensuredir(path.dirname(source_name))shutil.copyfile(self.env.doc2path(pagename),source_name)defhandle_finish(self):self.info(bold('dumping search index... '),nonl=True)self.indexer.prune(self.env.all_docs)searchindexfn=path.join(self.outdir,self.searchindex_filename)# first write to a temporary file, so that if dumping fails,# the existing index won't be overwrittenf=open(searchindexfn+'.tmp','wb')try:self.indexer.dump(f,self.indexer_format)finally:f.close()movefile(searchindexfn+'.tmp',searchindexfn)self.info('done')self.info(bold('dumping object inventory... '),nonl=True)f=open(path.join(self.outdir,INVENTORY_FILENAME),'w')try:f.write('# Sphinx inventory version 1\n')f.write('# Project: %s\n'%self.config.project.encode('utf-8'))f.write('# Version: %s\n'%self.config.version)formodname,infoinself.env.modules.iteritems():f.write('%s mod %s\n'%(modname,self.get_target_uri(info[0])))forrefname,(docname,desctype)inself.env.descrefs.iteritems():f.write('%s%s%s\n'%(refname,desctype,self.get_target_uri(docname)))finally:f.close()self.info('done')classDirectoryHTMLBuilder(StandaloneHTMLBuilder):""" A StandaloneHTMLBuilder that creates all HTML pages as "index.html" in a directory given by their pagename, so that generated URLs don't have ``.html`` in them. """name='dirhtml'defget_target_uri(self,docname,typ=None):ifdocname=='index':return''ifdocname.endswith(SEP+'index'):returndocname[:-5]# up to sepreturndocname+SEPdefget_outfilename(self,pagename):ifpagename=='index'orpagename.endswith(SEP+'index'):outfilename=path.join(self.outdir,os_path(pagename)+self.out_suffix)else:outfilename=path.join(self.outdir,os_path(pagename),'index'+self.out_suffix)returnoutfilenameclassSerializingHTMLBuilder(StandaloneHTMLBuilder):""" An abstract builder that serializes the generated HTML. """#: the serializing implementation to use. Set this to a module that#: implements a `dump`, `load`, `dumps` and `loads` functions#: (pickle, simplejson etc.)implementation=None#: the filename for the global context fileglobalcontext_filename=Nonesupported_image_types=['image/svg+xml','image/png','image/gif','image/jpeg']definit(self):self.config_hash=''self.tags_hash=''self.theme=None# no theme necessaryself.templates=None# no template bridge necessaryself.init_translator_class()self.init_highlighter()defget_target_uri(self,docname,typ=None):ifdocname=='index':return''ifdocname.endswith(SEP+'index'):returndocname[:-5]# up to sepreturndocname+SEPdefhandle_page(self,pagename,ctx,templatename='page.html',outfilename=None,event_arg=None):ctx['current_page_name']=pagenamesidebarfile=self.config.html_sidebars.get(pagename)ifsidebarfile:ctx['customsidebar']=sidebarfileifnotoutfilename:outfilename=path.join(self.outdir,os_path(pagename)+self.out_suffix)self.app.emit('html-page-context',pagename,templatename,ctx,event_arg)ensuredir(path.dirname(outfilename))f=open(outfilename,'wb')try:self.implementation.dump(ctx,f,2)finally:f.close()# if there is a source file, copy the source file for the# "show source" linkifctx.get('sourcename'):source_name=path.join(self.outdir,'_sources',os_path(ctx['sourcename']))ensuredir(path.dirname(source_name))shutil.copyfile(self.env.doc2path(pagename),source_name)defhandle_finish(self):# dump the global contextoutfilename=path.join(self.outdir,self.globalcontext_filename)f=open(outfilename,'wb')try:self.implementation.dump(self.globalcontext,f,2)finally:f.close()# super here to dump the search indexStandaloneHTMLBuilder.handle_finish(self)# copy the environment file from the doctree dir to the output dir# as needed by the web appshutil.copyfile(path.join(self.doctreedir,ENV_PICKLE_FILENAME),path.join(self.outdir,ENV_PICKLE_FILENAME))# touch 'last build' file, used by the web application to determine# when to reload its environment and clear the cacheopen(path.join(self.outdir,LAST_BUILD_FILENAME),'w').close()classPickleHTMLBuilder(SerializingHTMLBuilder):""" A Builder that dumps the generated HTML into pickle files. """implementation=pickleindexer_format=picklename='pickle'out_suffix='.fpickle'globalcontext_filename='globalcontext.pickle'searchindex_filename='searchindex.pickle'# compatibility aliasWebHTMLBuilder=PickleHTMLBuilderclassJSONHTMLBuilder(SerializingHTMLBuilder):""" A builder that dumps the generated HTML into JSON files. """implementation=jsonindexer_format=jsonname='json'out_suffix='.fjson'globalcontext_filename='globalcontext.json'searchindex_filename='searchindex.json'definit(self):ifjsonisNone:raiseSphinxError('The module simplejson (or json in Python >= 2.6) ''is not available. The JSONHTMLBuilder builder will not work.')SerializingHTMLBuilder.init(self)