Basic theme infrastructure.

This commit is contained in:
Georg Brandl 2009-01-05 20:22:30 +01:00
parent 249f9544b7
commit beb987bd5b
10 changed files with 136 additions and 58 deletions

View File

@ -75,7 +75,8 @@ pygments_style = 'friendly'
# The style sheet to use for HTML and HTML Help pages. A file of that name # The style sheet to use for HTML and HTML Help pages. A file of that name
# must exist either in Sphinx' static/ path, or in one of the custom paths # must exist either in Sphinx' static/ path, or in one of the custom paths
# given in html_static_path. # given in html_static_path.
html_style = 'sphinxdoc.css' #html_style = 'sphinxdoc.css'
html_theme = 'sphinxdoc'
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,

View File

@ -72,16 +72,20 @@ class Builder(object):
def init_templates(self): def init_templates(self):
""" """
Initialize the template system. Initialize the theme and template system.
Call this method from init() if you need templates in your builder. Call this method from init() if you need templates in your builder.
""" """
from sphinx.theming import Theme
Theme.init_themes(self)
self.theme = Theme(self.config.html_theme)
if self.config.template_bridge: if self.config.template_bridge:
self.templates = self.app.import_object( self.templates = self.app.import_object(
self.config.template_bridge, 'template_bridge setting')() self.config.template_bridge, 'template_bridge setting')()
else: else:
from sphinx.jinja2glue import BuiltinTemplates from sphinx.jinja2glue import BuiltinTemplateLoader
self.templates = BuiltinTemplates() self.templates = BuiltinTemplateLoader()
self.templates.init(self) self.templates.init(self)
def get_target_uri(self, docname, typ=None): def get_target_uri(self, docname, typ=None):

View File

@ -62,7 +62,6 @@ class StandaloneHTMLBuilder(Builder):
script_files = ['_static/jquery.js', '_static/doctools.js'] script_files = ['_static/jquery.js', '_static/doctools.js']
def init(self): def init(self):
"""Load templates."""
self.init_templates() self.init_templates()
self.init_translator_class() self.init_translator_class()
if self.config.html_file_suffix: if self.config.html_file_suffix:
@ -382,7 +381,8 @@ class StandaloneHTMLBuilder(Builder):
shutil.copyfile(jsfile, path.join(self.outdir, '_static', shutil.copyfile(jsfile, path.join(self.outdir, '_static',
'translations.js')) 'translations.js'))
# then, copy over all user-supplied static files # then, copy over all user-supplied static files
staticdirnames = [path.join(package_dir, 'static')] + \ staticdirnames = [path.join(themepath, 'static')
for themepath in self.theme.get_dirchain()[::-1]] + \
[path.join(self.confdir, spath) [path.join(self.confdir, spath)
for spath in self.config.html_static_path] for spath in self.config.html_static_path]
for staticdirname in staticdirnames: for staticdirname in staticdirnames:

View File

@ -53,11 +53,13 @@ class Config(object):
keep_warnings = (False, True), keep_warnings = (False, True),
# HTML options # HTML options
html_theme = ('default', False),
html_theme_path = ([], False),
html_title = (lambda self: '%s v%s documentation' % html_title = (lambda self: '%s v%s documentation' %
(self.project, self.release), (self.project, self.release),
False), False),
html_short_title = (lambda self: self.html_title, False), html_short_title = (lambda self: self.html_title, False),
html_style = ('default.css', False), html_style = ('default.css', False), # XXX
html_logo = (None, False), html_logo = (None, False),
html_favicon = (None, False), html_favicon = (None, False),
html_static_path = ([], False), html_static_path = ([], False),

View File

@ -18,63 +18,31 @@ from sphinx.util import mtimes_of_files
from sphinx.application import TemplateBridge from sphinx.application import TemplateBridge
class SphinxLoader(jinja2.BaseLoader): class BuiltinTemplateLoader(TemplateBridge, jinja2.BaseLoader):
""" """
A jinja2 reimplementation of `sphinx._jinja.SphinxFileSystemLoader`. Interfaces the rendering environment of jinja2 for use in Sphinx.
""" """
def __init__(self, basepath, extpaths, encoding='utf-8'): # TemplateBridge interface
"""
Create a new loader for sphinx.
*extpaths* is a list of directories, which provide additional templates
to sphinx.
*encoding* is used to decode the templates into unicode strings.
Defaults to utf-8.
If *basepath* is set, this path is used to load sphinx core templates.
If False, these templates are loaded from the sphinx package.
"""
self.core_loader = jinja2.FileSystemLoader(basepath)
self.all_loaders = jinja2.ChoiceLoader(
[jinja2.FileSystemLoader(extpath) for extpath in extpaths] +
[self.core_loader])
def get_source(self, environment, template):
# exclamation mark forces loading from core
if template.startswith('!'):
return self.core_loader.get_source(environment, template[1:])
# check if the template is probably an absolute path
fs_path = template.replace('/', path.sep)
if path.isabs(fs_path):
if not path.exists(fs_path):
raise jinja2.TemplateNotFound(template)
f = codecs.open(fs_path, 'r', self.encoding)
try:
mtime = path.getmtime(path)
return (f.read(), fs_path,
lambda: mtime == path.getmtime(path))
finally:
f.close()
# finally try to load from custom templates
return self.all_loaders.get_source(environment, template)
class BuiltinTemplates(TemplateBridge):
"""
Interfaces the rendering environment of jinja2 for use in sphinx.
"""
def init(self, builder): def init(self, builder):
base_templates_path = path.join(path.dirname(__file__), 'templates') self.theme = builder.theme
ext_templates_path = [path.join(builder.confdir, dir) # create a chain of paths to search:
for dir in builder.config.templates_path] # the theme's own dir and its bases' dirs
self.templates_path = [base_templates_path] + ext_templates_path chain = self.theme.get_dirchain()
loader = SphinxLoader(base_templates_path, ext_templates_path) # then the theme parent paths (XXX doc)
chain.extend(self.theme.themepath)
# prepend explicit template paths
if builder.config.templates_path:
chain[0:0] = builder.config.templates_path
# make the paths into loaders
self.loaders = map(jinja2.FileSystemLoader, chain)
use_i18n = builder.translator is not None use_i18n = builder.translator is not None
extensions = use_i18n and ['jinja2.ext.i18n'] or [] extensions = use_i18n and ['jinja2.ext.i18n'] or []
self.environment = jinja2.Environment(loader=loader, self.environment = jinja2.Environment(loader=self,
extensions=extensions) extensions=extensions)
if use_i18n: if use_i18n:
self.environment.install_gettext_translations(builder.translator) self.environment.install_gettext_translations(builder.translator)
@ -83,4 +51,19 @@ class BuiltinTemplates(TemplateBridge):
return self.environment.get_template(template).render(context) return self.environment.get_template(template).render(context)
def newest_template_mtime(self): def newest_template_mtime(self):
return max(mtimes_of_files(self.templates_path, '.html')) return max(mtimes_of_files(self.theme.themepath, '.html'))
# Loader interface
def get_source(self, environment, template):
loaders = self.loaders
# exclamation mark starts search from base
if template.startswith('!'):
loaders = loaders[1:]
template = template[1:]
for loader in loaders:
try:
return loader.get_source(environment, template)
except jinja2.TemplateNotFound:
pass
raise jinja2.TemplateNotFound(template)

View File

@ -0,0 +1,4 @@
[theme]
inherit = none
stylesheet = basic.css
pygments_style = sphinx

View File

@ -0,0 +1,3 @@
[theme]
inherit = basic
stylesheet = default.css

View File

@ -0,0 +1,3 @@
[theme]
inherit = basic
stylesheet = sphinxdoc.css

View File

@ -0,0 +1,3 @@
[theme]
inherit = basic
stylesheet = traditional.css

75
sphinx/theming.py Normal file
View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""
sphinx.theming
~~~~~~~~~~~~~~
Theming support for HTML builders.
:copyright: 2007-2009 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import os
import ConfigParser
from os import path
from sphinx.application import SphinxError
THEMECONF = 'theme.conf'
class ThemeError(SphinxError):
category = 'Theme error'
class Theme(object):
"""
Represents the theme chosen in the configuration.
"""
@classmethod
def init_themes(cls, builder):
"""Search all theme paths for available themes."""
cls.themes = {}
cls.themepath = list(builder.config.html_theme_path)
cls.themepath.append(
path.join(path.abspath(path.dirname(__file__)), 'themes'))
for themedir in cls.themepath[::-1]:
themedir = path.join(builder.confdir, themedir)
if not path.isdir(themedir):
continue
for theme in os.listdir(themedir):
if not path.isfile(path.join(themedir, theme, THEMECONF)):
continue
cls.themes[theme] = path.join(themedir, theme)
def __init__(self, name):
if name not in self.themes:
raise ThemeError('no theme named %r found' % name)
self.name = name
self.themedir = self.themes[name]
self.themeconf = ConfigParser.RawConfigParser()
self.themeconf.read(path.join(self.themedir, THEMECONF))
inherit = self.themeconf.get('theme', 'inherit')
if inherit == 'none':
self.base = None
elif inherit not in self.themes:
raise ThemeError('no theme named %r found, inherited by %r' %
(inherit, name))
else:
self.base = Theme(inherit)
def get_dirchain(self):
"""
Return a list of theme directories, beginning with this theme's,
then the base theme's, then that one's base theme's, etc.
"""
chain = [self.themedir]
base = self.base
while base is not None:
chain.append(base.themedir)
base = base.base
return chain