diff --git a/doc/conf.py b/doc/conf.py index 709d6f750..a826f4c2a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -75,7 +75,8 @@ pygments_style = 'friendly' # 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 # 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, # relative to this directory. They are copied after the builtin static files, diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 8847b6dca..5179cba0c 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -72,16 +72,20 @@ class Builder(object): 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. """ + from sphinx.theming import Theme + Theme.init_themes(self) + self.theme = Theme(self.config.html_theme) + if self.config.template_bridge: self.templates = self.app.import_object( self.config.template_bridge, 'template_bridge setting')() else: - from sphinx.jinja2glue import BuiltinTemplates - self.templates = BuiltinTemplates() + from sphinx.jinja2glue import BuiltinTemplateLoader + self.templates = BuiltinTemplateLoader() self.templates.init(self) def get_target_uri(self, docname, typ=None): diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 91682aa20..55cf2de2a 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -62,7 +62,6 @@ class StandaloneHTMLBuilder(Builder): script_files = ['_static/jquery.js', '_static/doctools.js'] def init(self): - """Load templates.""" self.init_templates() self.init_translator_class() if self.config.html_file_suffix: @@ -382,7 +381,8 @@ class StandaloneHTMLBuilder(Builder): shutil.copyfile(jsfile, path.join(self.outdir, '_static', 'translations.js')) # 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) for spath in self.config.html_static_path] for staticdirname in staticdirnames: diff --git a/sphinx/config.py b/sphinx/config.py index 428c0bf40..a3ae34c51 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -53,11 +53,13 @@ class Config(object): keep_warnings = (False, True), # HTML options + html_theme = ('default', False), + html_theme_path = ([], False), html_title = (lambda self: '%s v%s documentation' % (self.project, self.release), 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_favicon = (None, False), html_static_path = ([], False), diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index 0c7c5d721..fc3efc017 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -18,63 +18,31 @@ from sphinx.util import mtimes_of_files 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'): - """ - 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. - """ + # TemplateBridge interface def init(self, builder): - base_templates_path = path.join(path.dirname(__file__), 'templates') - ext_templates_path = [path.join(builder.confdir, dir) - for dir in builder.config.templates_path] - self.templates_path = [base_templates_path] + ext_templates_path - loader = SphinxLoader(base_templates_path, ext_templates_path) + self.theme = builder.theme + # create a chain of paths to search: + # the theme's own dir and its bases' dirs + chain = self.theme.get_dirchain() + # 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 extensions = use_i18n and ['jinja2.ext.i18n'] or [] - self.environment = jinja2.Environment(loader=loader, + self.environment = jinja2.Environment(loader=self, extensions=extensions) if use_i18n: self.environment.install_gettext_translations(builder.translator) @@ -83,4 +51,19 @@ class BuiltinTemplates(TemplateBridge): return self.environment.get_template(template).render(context) 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) diff --git a/sphinx/themes/basic/theme.conf b/sphinx/themes/basic/theme.conf new file mode 100644 index 000000000..160cac40d --- /dev/null +++ b/sphinx/themes/basic/theme.conf @@ -0,0 +1,4 @@ +[theme] +inherit = none +stylesheet = basic.css +pygments_style = sphinx diff --git a/sphinx/themes/default/theme.conf b/sphinx/themes/default/theme.conf new file mode 100644 index 000000000..0d5ab8eb2 --- /dev/null +++ b/sphinx/themes/default/theme.conf @@ -0,0 +1,3 @@ +[theme] +inherit = basic +stylesheet = default.css diff --git a/sphinx/themes/sphinxdoc/theme.conf b/sphinx/themes/sphinxdoc/theme.conf new file mode 100644 index 000000000..cd74cb8a3 --- /dev/null +++ b/sphinx/themes/sphinxdoc/theme.conf @@ -0,0 +1,3 @@ +[theme] +inherit = basic +stylesheet = sphinxdoc.css diff --git a/sphinx/themes/traditional/theme.conf b/sphinx/themes/traditional/theme.conf new file mode 100644 index 000000000..02b77833e --- /dev/null +++ b/sphinx/themes/traditional/theme.conf @@ -0,0 +1,3 @@ +[theme] +inherit = basic +stylesheet = traditional.css diff --git a/sphinx/theming.py b/sphinx/theming.py new file mode 100644 index 000000000..9493481c2 --- /dev/null +++ b/sphinx/theming.py @@ -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