Theme refactor.

Themes now have `theme` objects, and theme specific configs.
Themes can inherit from other themes. Users (and theme authors)
can define custom static templates and variables.
This commit is contained in:
Waylan Limberg
2017-03-27 23:06:48 -04:00
parent 7985c72c2e
commit 75350da44c
17 changed files with 619 additions and 142 deletions

View File

@@ -1,5 +1,5 @@
include README.md
include LICENSE
recursive-include mkdocs *.ico *.js *.css *.png *.html *.eot *.svg *.ttf *.woff *.woff2 *.xml *.mustache
recursive-include mkdocs *.ico *.js *.css *.png *.html *.eot *.svg *.ttf *.woff *.woff2 *.xml *.mustache *mkdocs_theme.yml
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

View File

@@ -25,6 +25,40 @@ The current and past members of the MkDocs team.
### Major Additions to Version 1.0.0
#### Theme Customization. (#1164)
Support had been added to provide theme specific customizations. Theme authors
can define default options as documented in [Theme Configuration]. A theme can
now inherit from another theme, define various static templates to be rendered,
and define arbitrary default variables to control behavior in the templates.
Users can override those defaults under the [theme] configuration option, which
now accepts nested options. One such nested option is the [custom_dir] option,
which replaces the now deprecated `theme_dir` option. If users had previously
set the `theme_dir` option, a warning will be issued, with an error expected in
a future release.
If a configuration previously defined a `theme_dir` like this:
```yaml
theme: mkdocs
theme_dir: custom
```
Then the configuration should be adjusted as follows:
```yaml
theme:
name: mkdocs
custom_dir: custom
```
See the [theme] configuration option documentation for details.
[Theme Configuration]: ../user-guide/custom-themes.md#theme-configuration
[theme]: ../user-guide/configuration.md#theme
[custom_dir]: ../user-guide/configuration.md#custom_dir
#### Previously deprecated Template variables removed. (#1168)
##### Page Template

View File

@@ -4,7 +4,7 @@ div.col-md-9 h1:first-of-type {
font-weight: 300;
}
div.col-md-9 p:first-of-type {
div.col-md-9>p:first-of-type {
text-align: center;
}

View File

@@ -170,25 +170,57 @@ sub-directories. If none are found it will be `[]` (an empty list).
### theme
Sets the theme of your documentation site, for a list of available themes visit
[styling your docs].
Sets the theme and theme specific configuration of your documentation site.
May be either a string or a set of key/value pairs.
If a string, it must be the string name of a known installed theme. For a list
of available themes visit [styling your docs].
An example set of key/value pairs might look something like this:
```yaml
theme:
name: mkdocs
custom_dir: my_theme_customizations/
static_templates:
- sitemap.html
include_sidebar: false
```
If a set of key/value pairs, the following nested keys can be defined:
!!! block ""
#### name:
The string name of a known installed theme. For a list of available themes
visit [styling your docs].
#### custom_dir:
A directory to custom a theme. This can either be a relative directory, in
which case it is resolved relative to the directory containing your
configuration file, or it can be an absolute directory path.
See [styling your docs][theme_dir] for details if you would like to tweak an
existing theme.
See [custom themes] if you would like to build your own theme from the
ground up.
#### static_templates:
A list of templates to render as static pages. The templates must be located
in either the theme's template directory or in the `custom_dir` defined in
the theme configuration.
#### (theme specific keywords)
Any additional keywords supported by the theme can also be defined. See the
documentation for the theme you are using for details.
**default**: `'mkdocs'`
### theme_dir
Lets you set a directory to a custom theme. This can either be a relative
directory, in which case it is resolved relative to the directory containing
your configuration file, or it can be an absolute directory path.
See [styling your docs][theme_dir] for details if you would like to tweak an
existing theme.
See [custom themes] if you would like to build your own theme from the ground
up.
**default**: `null`
### docs_dir
Lets you set the directory containing the documentation source markdown files.

View File

@@ -19,12 +19,12 @@ and their usage.
## Creating a custom theme
The bare minimum required for a custom theme is a `main.html` [Jinja2
template] file. This should be placed in a directory which will be the
`theme_dir` and it should be created next to the `mkdocs.yml` configuration
file. Within `mkdocs.yml`, specify the `theme_dir` option and set it to the
name of the directory containing `main.html`. For example, given this example
project layout:
The bare minimum required for a custom theme is a `main.html` [Jinja2 template]
file. This should be placed in a directory which will be the `theme_dir` and it
should be created next to the `mkdocs.yml` configuration file. Within
`mkdocs.yml`, specify the theme `custom_dir` option and set it to the name of
the directory containing `main.html`. For example, given this example project
layout:
mkdocs.yml
docs/
@@ -37,23 +37,24 @@ project layout:
You would include the following settings in `mkdocs.yml` to use the custom theme
directory:
theme: null
theme_dir: 'custom_theme'
theme:
name: null
custom_dir: 'custom_theme'
!!! Note
Generally, when building your own custom theme, the `theme` configuration
setting would be set to `null`. However, if used in combination with the
`theme_dir` configuration value a custom theme can be used to replace only
specific parts of a built-in theme. For example, with the above layout and
if you set `theme: "mkdocs"` then the `main.html` file in the `theme_dir`
would replace that in the theme but otherwise the `mkdocs` theme would
remain the same. This is useful if you want to make small adjustments to an
existing theme.
Generally, when building your own custom theme, the theme `name`
configuration setting would be set to `null`. However, if used in
combination with the `custom_dir` configuration value a custom theme can be
used to replace only specific parts of a built-in theme. For example, with
the above layout and if you set `name: "mkdocs"` then the `main.html` file
in the `custom_dir` would replace that in the theme but otherwise the
`mkdocs` theme would remain the same. This is useful if you want to make
small adjustments to an existing theme.
For more specific information, see [styling your docs].
[styling your docs]: ./styling-your-docs.md#using-the-theme_dir
[styling your docs]: ./styling-your-docs.md#using-the-theme-custom_dir
## Basic theme
@@ -84,7 +85,7 @@ the [built-in themes] and modify it accordingly.
power of Jinja, including [template inheritance]. You may notice that the
themes included with MkDocs make extensive use of template inheritance and
blocks, allowing users to easily override small bits and pieces of the
templates from the [theme_dir]. Therefore, the built-in themes are
templates from the theme [custom_dir]. Therefore, the built-in themes are
implemented in a `base.html` file, which `main.html` extends. Although not
required, third party template authors are encouraged to follow a similar
pattern and may want to define the same [blocks] as are used in the built-in
@@ -380,11 +381,11 @@ Bootswatch theme].
!!! Note
It is not strictly necessary to package a theme, as the entire theme
can be contained in the `theme_dir`. If you have created a "one-off theme,"
that should be sufficent. However, if you intend to distribute your theme
can be contained in the `custom_dir`. If you have created a "one-off theme,"
that should be sufficient. However, if you intend to distribute your theme
for others to use, packaging the theme has some advantages. By packaging
your theme, your users can more easily install it and they can them take
advantage of the [theme_dir] to make tweaks to your theme to better suit
your theme, your users can more easily install it and they can then take
advantage of the [custom_dir] to make tweaks to your theme to better suit
their needs.
[Python packaging]: https://packaging.python.org/en/latest/
@@ -394,14 +395,16 @@ Bootswatch theme].
### Package Layout
The following layout is recommended for themes. Two files at the top level
directory called `MANIFEST.in` amd `setup.py` beside the theme directory which
contains an empty `__init__.py` file and your template and media files.
directory called `MANIFEST.in` and `setup.py` beside the theme directory which
contains an empty `__init__.py` file, a theme configuration file
(`mkdocs-theme.yml`), and your template and media files.
```no-highlight
.
|-- MANIFEST.in
|-- theme_name
| |-- __init__.py
| |-- mkdocs-theme.yml
| |-- main.html
| |-- styles.css
`-- setup.py
@@ -462,6 +465,57 @@ it includes a `main.html` for the theme. It **must** also include a
`__init__.py` file which should be empty, this file tells Python that the
directory is a package.
### Theme Configuration
A packaged theme is required to include a configuration file named
`mkdocs.theme.yml` which is placed in the root of your template files. The file
should contain default configuration options for the theme. However, if the
theme offers no configuration options, the file is still required and can be
left blank.
The theme author is free to define any arbitrary options deemed necessary and
those options will be made available in the templates to control behavior.
For example, a theme might want to make a sidebar optional and include the
following in the `mkdocs-theme.yml` file:
```yaml
show_sidebar: true
```
Then in a template, that config option could be referenced:
```django
{% if config.theme.show_sidebar %}
<div id="sidebar">...</div>
{% endif %}
```
And the user could override the default in their project's `mkdocs.yml` config
file:
```yaml
theme:
name: themename
show_sidebar: false
```
In addition to arbitrary options defined by the theme, MkDocs defines a few
special options which alters its behavior:
!!! block ""
#### static_templates
This option mirrors the [theme] config option of the same name and allows
some defaults to be set by the theme. Note that while the user can add
templates to this list, the user cannot remove templates included in the
theme's config.
#### extends
Defines a parent theme that this theme inherits from. The value should be
the string name of the parent theme. Normal Jinja inheritance rules apply.
### Distributing Themes
With the above changes, your theme should now be ready to install. This can be
@@ -481,3 +535,4 @@ For a much more detailed guide, see the official Python packaging
documentation for [Packaging and Distributing Projects].
[Packaging and Distributing Projects]: https://packaging.python.org/en/latest/distributing/
[theme]: ./configuration/#theme

View File

@@ -41,8 +41,8 @@ have created your own, please feel free to add it to the list.
If you would like to make a few tweaks to an existing theme, there is no need to
create your own theme from scratch. For minor tweaks which only require some CSS
and/or JavaScript, you can use the [docs_dir]. However, for more complex
customizations, including overriding templates, you will need to use the
[theme_dir].
customizations, including overriding templates, you will need to use the theme
[custom_dir] setting.
### Using the docs_dir
@@ -78,24 +78,25 @@ changes were automatically picked up and the documentation will be updated.
Any extra CSS or JavaScript files will be added to the generated HTML
document after the page content. If you desire to include a JavaScript
library, you may have better success including the library by using the
[theme_dir].
theme [custom_dir].
### Using the theme_dir
### Using the theme custom_dir
The [theme_dir] configuration option can be used to point to a directory of
files which override the files in the theme set on the [theme] configuration
option. Any file in the `theme_dir` with the same name as a file in the `theme`
will replace the file of the same name in the `theme`. Any additional files in
the `theme_dir` will be added to the `theme`. The contents of the `theme_dir`
should mirror the directory structure of the `theme`. You may include templates,
JavaScript files, CSS files, images, fonts, or any other media included in a
theme.
The theme.[custom_dir] configuration option can be used to point to a directory
of files which override the files in a parent theme. The parent theme would be
the theme defined in the theme.[name] configuration option. Any file in the
`custom_dir` with the same name as a file in the parent theme will replace the
file of the same name in the parent theme. Any additional files in the
`custom_dir` will be added to the parent theme. The contents of the `custom_dir`
should mirror the directory structure of the parent theme. You may include
templates, JavaScript files, CSS files, images, fonts, or any other media
included in a theme.
!!! Note
For this to work, the `theme` setting must be set to a known installed theme.
If the `theme` setting is instead set to `null` (or not defined), then there
is no theme to override and the contents of the `theme_dir` must be a
For this to work, the theme `name` setting must be set to a known installed theme.
If the `name` setting is instead set to `null` (or not defined), then there
is no theme to override and the contents of the `custom_dir` must be a
complete, standalone theme. See [Custom Themes][custom theme] for more
information.
@@ -127,7 +128,9 @@ mkdir custom_theme
And then point your `mkdocs.yml` configuration file at the new directory:
```yaml
theme_dir: custom_theme
theme:
name: mkdocs
custom_dir: custom_theme
```
To override the 404 error page ("file not found"), add a new template file named
@@ -156,16 +159,17 @@ Your directory structure should now look like this:
!!! Note
Any files included in the `theme` but not included in the `theme_dir` will
still be utilized. The `theme_dir` will only override/replace files in the
`theme`. If you want to remove files, or build a theme from scratch, then
you should review the documentation for building a [custom theme].
Any files included in the parent theme (defined in `name`) but not included
in the `custom_dir` will still be utilized. The `custom_dir` will only
override/replace files in the parent theme. If you want to remove files, or
build a theme from scratch, then you should review the documentation for
building a [custom theme].
#### Overriding Template Blocks
The built-in themes implement many of their parts inside template blocks which
can be individually overridden in the `main.html` template. Simply create a
`main.html` template file in your `theme_dir` and define replacement blocks
`main.html` template file in your `custom_dir` and define replacement blocks
within that file. Just make sure that the `main.html` extends `base.html`. For
example, to alter the title of the MkDocs theme, your replacement `main.html`
template would contain the following:
@@ -205,11 +209,11 @@ following blocks:
You may need to view the source template files to ensure your modifications will
work with the structure of the site. See [Template Variables] for a list of
variables you can use within your custom blocks. For a more complete
explaination of blocks, consult the [Jinja documentation].
explanation of blocks, consult the [Jinja documentation].
#### Combining the theme_dir and Template Blocks
#### Combining the custom_dir and Template Blocks
Adding a JavaScript library to the `theme_dir` will make it available, but
Adding a JavaScript library to the `custom_dir` will make it available, but
won't include it in the pages generated by MkDocs. Therefore, a link needs to
be added to the library from the HTML.
@@ -243,8 +247,8 @@ Note that the [base_url] template variable was used to ensure that the link is
always relative to the current page.
Now the generated pages will include links to the template provided libraries as
well as the library included in the `theme_dir`. The same would be required for
any additional CSS files included in the `theme_dir`.
well as the library included in the `custom_dir`. The same would be required for
any additional CSS files included in the `custom_dir`.
[browse source]: https://github.com/mkdocs/mkdocs/tree/master/mkdocs/themes/mkdocs
[built-in themes]: #built-in-themes
@@ -259,8 +263,8 @@ any additional CSS files included in the `theme_dir`.
[mkdocs]: #mkdocs
[ReadTheDocs]: ./deploying-your-docs.md#readthedocs
[Template Variables]: ./custom-themes.md#template-variables
[theme]: ./configuration/#theme
[theme_dir]: ./configuration/#theme_dir
[custom_dir]: ./configuration/#custom_dir
[name]: ./configuration/#name
[third party themes]: #third-party-themes
[super block]: http://jinja.pocoo.org/docs/dev/templates/#super-blocks
[base_url]: ./custom-themes.md#base_url

View File

@@ -11,7 +11,6 @@ from jinja2.exceptions import TemplateNotFound
import jinja2
from mkdocs import nav, search, utils
from mkdocs.utils import filters
import mkdocs
@@ -81,9 +80,9 @@ def build_template(template_name, env, config, site_navigation=None):
return True
def build_error_templates(templates, env, config, site_navigation):
def build_error_template(template, env, config, site_navigation):
"""
Build error templates.
Build error template.
Force absolute URLs in the nav of error pages and account for the
possability that the docs root might be different than the server root.
@@ -94,8 +93,7 @@ def build_error_templates(templates, env, config, site_navigation):
default_base = site_navigation.url_context.base_path
site_navigation.url_context.base_path = utils.urlparse(config['site_url']).path
for template in templates:
build_template(template, env, config, site_navigation)
build_template(template, env, config, site_navigation)
# Reset nav behavior to the default
site_navigation.url_context.force_abs_urls = False
@@ -147,23 +145,19 @@ def build_pages(config, dirty=False):
""" Build all pages and write them into the build directory. """
site_navigation = nav.SiteNavigation(config)
loader = jinja2.FileSystemLoader(config['theme_dir'] + [config['mkdocs_templates'], ])
env = jinja2.Environment(loader=loader)
env = config['theme'].get_env()
env.filters['tojson'] = filters.tojson
search_index = search.SearchIndex()
build_error_templates(['404.html'], env, config, site_navigation)
if not build_template('search.html', env, config, site_navigation):
log.debug("Search is enabled but the theme doesn't contain a "
"search.html file. Assuming the theme implements search "
"within a modal.")
build_template('sitemap.xml', env, config, site_navigation)
for template in config['theme'].static_templates:
if utils.is_error_template(template):
build_error_template(template, env, config, site_navigation)
else:
build_template(template, env, config, site_navigation)
build_extra_templates(config['extra_templates'], config, site_navigation)
log.debug("Building markdown pages.")
for page in site_navigation.walk_pages():
try:
# When --dirty is used, only build the page if the markdown has been modified since the
@@ -202,16 +196,15 @@ def build(config, live_server=False, dirty=False):
# Reversed as we want to take the media files from the builtin theme
# and then from the custom theme_dir so that the custom versions take
# precedence.
for theme_dir in reversed(config['theme_dir']):
log.debug("Copying static assets from theme: %s", theme_dir)
for theme_dir in reversed(config['theme'].dirs):
log.debug("Copying static assets from %s", theme_dir)
utils.copy_media_files(
theme_dir, config['site_dir'], exclude=['*.py', '*.pyc', '*.html'], dirty=dirty
theme_dir, config['site_dir'], exclude=['*.py', '*.pyc', '*.html', 'mkdocs_theme.yml'], dirty=dirty
)
log.debug("Copying static assets from the docs dir.")
utils.copy_media_files(config['docs_dir'], config['site_dir'], dirty=dirty)
log.debug("Building markdown pages.")
build_pages(config, dirty=dirty)

View File

@@ -49,7 +49,7 @@ def _livereload(host, port, config, builder, site_dir):
server.watch(config['docs_dir'], builder)
server.watch(config['config_file_path'], builder)
for d in config['theme_dir']:
for d in config['theme'].dirs:
server.watch(d, builder)
server.serve(root=site_dir, host=host, port=port, restart_delay=0)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import os
from collections import namedtuple
from mkdocs import utils
from mkdocs import utils, theme
from mkdocs.config.base import Config, ValidationError
@@ -311,56 +311,77 @@ class SiteDir(Dir):
class ThemeDir(Dir):
"""
ThemeDir Config Option
Post validation, verify the theme_dir and do some path munging.
TODO: This could probably be improved and/or moved from here. It's a tad
gross really.
ThemeDir Config Option. Deprecated
"""
def pre_validation(self, config, key_name):
if config.get(key_name) is None:
return
warning = ('The configuration option {0} has been deprecated and will '
'be removed in a future release of MkDocs.')
self.warnings.append(warning)
def post_validation(self, config, key_name):
theme_in_config = any(['theme' in c for c in config.user_configs])
package_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..'))
theme_dir = [utils.get_theme_dir(config['theme']), ]
config['mkdocs_templates'] = os.path.join(package_dir, 'templates')
if config['theme_dir'] is not None:
# If the user has given us a custom theme but not a
# builtin theme name then we don't want to merge them.
if not theme_in_config:
theme_dir = []
theme_dir.insert(0, config['theme_dir'])
config['theme_dir'] = theme_dir
# Add the search assets to the theme_dir, this means that
# they will then we copied into the output directory but can
# be overwritten by themes if needed.
search_assets = os.path.join(package_dir, 'assets', 'search')
config['theme_dir'].append(search_assets)
# The validation in the parent class this inherits from is not relevant here.
pass
class Theme(OptionallyRequired):
class Theme(BaseConfigOption):
"""
Theme Config Option
Validate that the theme is one of the builtin Mkdocs theme names.
Validate that the theme exists and build Theme instance.
"""
def run_validation(self, value):
def __init__(self, default=None):
super(Theme, self).__init__()
self.default = default
def validate(self, value):
if value is None and self.default is not None:
value = {'name': self.default}
if isinstance(value, utils.string_types):
value = {'name': value}
themes = utils.get_theme_names()
if value in themes:
return value
if isinstance(value, dict):
if 'name' in value:
if value['name'] is None or value['name'] in themes:
return value
raise ValidationError(
"Unrecognised theme '{0}'. The available installed themes "
"are: {1}".format(value, ', '.join(themes))
)
raise ValidationError(
"Unrecognised theme name: '{0}'. The available installed themes "
"are: {1}".format(value['name'], ', '.join(themes))
)
raise ValidationError("No theme name set.")
raise ValidationError('Invalid type "{0}". Expected a string or key/value pairs.'.format(type(value)))
def post_validation(self, config, key_name):
theme_config = config[key_name]
# TODO: Remove when theme_dir is fully deprecated.
if config['theme_dir'] is not None:
if 'custom_dir' not in theme_config:
# Only pass in 'theme_dir' if it is set and 'custom_dir' is not set.
theme_config['custom_dir'] = config['theme_dir']
if not any(['theme' in c for c in config.user_configs]):
# If the user did not define a theme, but did define theme_dir, then remove default set in validate.
theme_config['name'] = None
if not theme_config['name'] and 'custom_dir' not in theme_config:
raise ValidationError("At least one of 'theme.name' or 'theme.custom_dir' must be defined.")
# Ensure custom_dir is an absolute path
if 'custom_dir' in theme_config and not os.path.isabs(theme_config['custom_dir']):
theme_config['custom_dir'] = os.path.abspath(theme_config['custom_dir'])
config[key_name] = theme.Theme(**theme_config)
class Extras(OptionallyRequired):

View File

@@ -339,18 +339,78 @@ class SiteDirTest(unittest.TestCase):
class ThemeTest(unittest.TestCase):
def test_theme(self):
def test_theme_as_string(self):
option = config_options.Theme()
value = option.validate("mkdocs")
self.assertEqual("mkdocs", value)
self.assertEqual({'name': 'mkdocs'}, value)
def test_theme_invalid(self):
def test_uninstalled_theme_as_string(self):
option = config_options.Theme()
self.assertRaises(config_options.ValidationError,
option.validate, "mkdocs2")
def test_theme_default(self):
option = config_options.Theme(default='mkdocs')
value = option.validate(None)
self.assertEqual({'name': 'mkdocs'}, value)
def test_theme_as_simple_config(self):
config = {
'name': 'mkdocs'
}
option = config_options.Theme()
value = option.validate(config)
self.assertEqual(config, value)
def test_theme_as_complex_config(self):
config = {
'name': 'mkdocs',
'custom_dir': 'custom',
'static_templates': ['sitemap.html'],
'show_sidebar': False
}
option = config_options.Theme()
value = option.validate(config)
self.assertEqual(config, value)
def test_theme_name_is_none(self):
config = {
'name': None
}
option = config_options.Theme()
value = option.validate(config)
self.assertEqual(config, value)
def test_theme_config_missing_name(self):
config = {
'custom_dir': 'custom',
}
option = config_options.Theme()
self.assertRaises(config_options.ValidationError,
option.validate, config)
def test_uninstalled_theme_as_config(self):
config = {
'name': 'mkdocs2'
}
option = config_options.Theme()
self.assertRaises(config_options.ValidationError,
option.validate, config)
def test_theme_invalid_type(self):
config = ['mkdocs2']
option = config_options.Theme()
self.assertRaises(config_options.ValidationError,
option.validate, config)
class ExtrasTest(unittest.TestCase):

View File

@@ -7,6 +7,7 @@ import shutil
import tempfile
import unittest
import mkdocs
from mkdocs import config
from mkdocs import utils
from mkdocs.config import config_options
@@ -102,19 +103,59 @@ class ConfigTests(unittest.TestCase):
{"theme": "readthedocs"}, # builtin theme
{"theme_dir": mytheme}, # custom only
{"theme": "readthedocs", "theme_dir": custom}, # builtin and custom
{"theme": {'name': 'readthedocs'}}, # builtin as complex
{"theme": {'name': None, 'custom_dir': mytheme}}, # custom only as complex
{"theme": {'name': 'readthedocs', 'custom_dir': custom}}, # builtin and custom as complex
{ # user defined variables
'theme': {
'name': 'mkdocs',
'static_templates': ['foo.html'],
'show_sidebar': False,
'some_var': 'bar'
}
}
]
abs_path = os.path.abspath(os.path.dirname(__file__))
mkdocs_dir = os.path.abspath(os.path.join(abs_path, '..', '..'))
mkdocs_dir = os.path.abspath(os.path.dirname(mkdocs.__file__))
mkdocs_templates_dir = os.path.join(mkdocs_dir, 'templates')
theme_dir = os.path.abspath(os.path.join(mkdocs_dir, 'themes'))
search_asset_dir = os.path.abspath(os.path.join(
mkdocs_dir, 'assets', 'search'))
results = (
[os.path.join(theme_dir, 'mkdocs'), search_asset_dir],
[os.path.join(theme_dir, 'readthedocs'), search_asset_dir],
[mytheme, search_asset_dir],
[custom, os.path.join(theme_dir, 'readthedocs'), search_asset_dir],
{
'dirs': [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir, search_asset_dir],
'static_templates': ['404.html', 'sitemap.xml'],
'vars': {}
}, {
'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir, search_asset_dir],
'static_templates': ['search.html', 'sitemap.xml'],
'vars': {}
}, {
'dirs': [mytheme, mkdocs_templates_dir, search_asset_dir],
'static_templates': ['sitemap.xml'],
'vars': {}
}, {
'dirs': [custom, os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir, search_asset_dir],
'static_templates': ['search.html', 'sitemap.xml'],
'vars': {}
}, {
'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir, search_asset_dir],
'static_templates': ['search.html', 'sitemap.xml'],
'vars': {}
}, {
'dirs': [mytheme, mkdocs_templates_dir, search_asset_dir],
'static_templates': ['sitemap.xml'],
'vars': {}
}, {
'dirs': [custom, os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir, search_asset_dir],
'static_templates': ['search.html', 'sitemap.xml'],
'vars': {}
}, {
'dirs': [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir, search_asset_dir],
'static_templates': ['404.html', 'sitemap.xml', 'foo.html'],
'vars': {'show_sidebar': False, 'some_var': 'bar'}
}
)
for config_contents, result in zip(configs, results):
@@ -124,8 +165,11 @@ class ConfigTests(unittest.TestCase):
('theme_dir', config_options.ThemeDir(exists=True)),
))
c.load_dict(config_contents)
c.validate()
self.assertEqual(c['theme_dir'], result)
errors, warnings = c.validate()
self.assertEqual(len(errors), 0)
self.assertEqual(c['theme'].dirs, result['dirs'])
self.assertEqual(c['theme'].static_templates, set(result['static_templates']))
self.assertEqual(dict([(k, c['theme'][k]) for k in iter(c['theme'])]), result['vars'])
def test_default_pages(self):
tmp_dir = tempfile.mkdtemp()

View File

@@ -15,8 +15,9 @@ site_url: http://www.mkdocs.org/
docs_dir: documentation
site_dir: output
theme: mkdocs
theme_dir: theme_tweaks
theme:
name: mkdocs
custom_dir: theme_tweaks
copyright: "Dougal Matthews"
google_analytics: ["1", "2"]

View File

@@ -0,0 +1,97 @@
from __future__ import unicode_literals
import os
import tempfile
import unittest
import mock
import mkdocs
from mkdocs.theme import Theme
abs_path = os.path.abspath(os.path.dirname(__file__))
mkdocs_dir = os.path.abspath(os.path.dirname(mkdocs.__file__))
mkdocs_templates_dir = os.path.join(mkdocs_dir, 'templates')
theme_dir = os.path.abspath(os.path.join(mkdocs_dir, 'themes'))
search_asset_dir = os.path.abspath(os.path.join(mkdocs_dir, 'assets', 'search'))
def get_vars(theme):
""" Return dict of theme vars. """
return dict([(k, theme[k]) for k in iter(theme)])
class ThemeTests(unittest.TestCase):
def test_simple_theme(self):
theme = Theme(name='mkdocs')
self.assertEqual(
theme.dirs,
[os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir, search_asset_dir]
)
self.assertEqual(theme.static_templates, set(['404.html', 'sitemap.xml']))
self.assertEqual(get_vars(theme), {})
def test_custom_dir(self):
custom = tempfile.mkdtemp()
theme = Theme(name='mkdocs', custom_dir=custom)
self.assertEqual(
theme.dirs,
[
custom,
os.path.join(theme_dir, 'mkdocs'),
mkdocs_templates_dir,
search_asset_dir
]
)
def test_custom_dir_only(self):
custom = tempfile.mkdtemp()
theme = Theme(name=None, custom_dir=custom)
self.assertEqual(
theme.dirs,
[custom, mkdocs_templates_dir, search_asset_dir]
)
def static_templates(self):
theme = Theme(name='mkdocs', static_templates='foo.html')
self.assertEqual(
theme.static_templates,
set(['404.html', 'sitemap.xml', 'foo.html'])
)
def test_vars(self):
theme = Theme(name='mkdocs', foo='bar', baz=True)
self.assertEqual(theme['foo'], 'bar')
self.assertEqual(theme['baz'], True)
self.assertTrue('new' not in theme)
self.assertRaises(KeyError, lambda t, k: t[k], theme, 'new')
theme['new'] = 42
self.assertTrue('new' in theme)
self.assertEqual(theme['new'], 42)
@mock.patch('mkdocs.utils.yaml_load', return_value={})
def test_no_theme_config(self, m):
theme = Theme(name='mkdocs')
self.assertEqual(m.call_count, 1)
self.assertEqual(theme.static_templates, set(['sitemap.xml']))
def test_inherited_theme(self):
m = mock.Mock(side_effect=[
{'extends': 'readthedocs', 'static_templates': ['child.html']},
{'static_templates': ['parent.html']}
])
with mock.patch('mkdocs.utils.yaml_load', m) as m:
theme = Theme(name='mkdocs')
self.assertEqual(m.call_count, 2)
self.assertEqual(
theme.dirs,
[
os.path.join(theme_dir, 'mkdocs'),
os.path.join(theme_dir, 'readthedocs'),
mkdocs_templates_dir,
search_asset_dir
]
)
self.assertEqual(
theme.static_templates, set(['sitemap.xml', 'child.html', 'parent.html'])
)

118
mkdocs/theme.py Normal file
View File

@@ -0,0 +1,118 @@
# coding: utf-8
from __future__ import unicode_literals
import os
import jinja2
import logging
from mkdocs import utils
from mkdocs.utils import filters
from mkdocs.config.base import ValidationError
log = logging.getLogger(__name__)
class Theme(object):
"""
A Theme object.
Keywords:
name: The name of the theme as defined by its entrypoint.
custom_dir: User defined directory for custom templates.
static_templates: A list of templates to render as static pages.
All other keywords are passed as-is and made available as a key/value mapping.
"""
def __init__(self, name=None, **user_config):
self.name = name
self._vars = {}
# MkDocs provided static templates are always included
package_dir = os.path.abspath(os.path.dirname(__file__))
mkdocs_templates = os.path.join(package_dir, 'templates')
self.static_templates = set(os.listdir(mkdocs_templates))
# Build self.dirs from various sources in order of precedence
self.dirs = []
if 'custom_dir' in user_config:
self.dirs.append(user_config.pop('custom_dir'))
if self.name:
self._load_theme_config(name)
# Include templates provided directly by MkDocs (outside any theme)
self.dirs.append(mkdocs_templates)
# Add the search assets to the theme_dir, so that they will be copied
# into the output directory but can still be overwritten by themes.
self.dirs.append(os.path.join(package_dir, 'assets', 'search'))
# Handle remaining user configs. Override theme configs (if set)
self.static_templates.update(user_config.pop('static_templates', []))
self._vars.update(user_config)
def __repr__(self):
return "{0}(name='{1}', dirs={2}, static_templates={3}, {4})".format(
self.__class__.__name__, self.name, self.dirs, list(self.static_templates),
', '.join('{0}={1}'.format(k, repr(v)) for k, v in self._vars.items())
)
def __getitem__(self, key):
return self._vars[key]
def __setitem__(self, key, value):
self._vars[key] = value
def __contains__(self, item):
return item in self._vars
def __iter__(self):
return iter(self._vars)
def _load_theme_config(self, name):
""" Recursively load theme and any parent themes. """
theme_dir = utils.get_theme_dir(name)
self.dirs.append(theme_dir)
try:
file_path = os.path.join(theme_dir, 'mkdocs_theme.yml')
with open(file_path, 'rb') as f:
theme_config = utils.yaml_load(f)
except IOError as e:
log.debug(e)
# TODO: Change this warning to an error in a future version
log.warning(
"The theme '{0}' does not appear to have a configuration file. "
"Please upgrade to a current version of the theme.".format(name)
)
return
log.debug("Loaded theme configuration for '%s' from '%s': %s", name, file_path, theme_config)
parent_theme = theme_config.pop('extends', None)
if parent_theme:
themes = utils.get_theme_names()
if parent_theme not in themes:
raise ValidationError(
"The theme '{0}' inherits from '{1}', which does not appear to be installed. "
"The available installed themes are: {2}".format(name, parent_theme, ', '.join(themes))
)
self._load_theme_config(parent_theme)
self.static_templates.update(theme_config.pop('static_templates', []))
self._vars.update(theme_config)
def get_env(self):
""" Return a Jinja environment for the theme. """
loader = jinja2.FileSystemLoader(self.dirs)
env = jinja2.Environment(loader=loader)
env.filters['tojson'] = filters.tojson
return env

View File

@@ -0,0 +1,4 @@
# Config options for 'mkdocs' theme
static_templates:
- 404.html

View File

@@ -0,0 +1,4 @@
# Config options for 'readthedocs' theme
static_templates:
- search.html

View File

@@ -275,6 +275,16 @@ def is_template_file(path):
]
_ERROR_TEMPLATE_RE = re.compile(r'^\d{3}\.html?$')
def is_error_template(path):
"""
Return True if the given file path is an HTTP error template.
"""
return bool(_ERROR_TEMPLATE_RE.match(path))
def create_media_urls(nav, path_list):
"""
Return a list of URLs that have been processed correctly for inclusion in