mirror of
https://github.com/mkdocs/mkdocs.git
synced 2026-03-27 09:58:31 +07:00
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:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
|
||||
97
mkdocs/tests/theme_tests.py
Normal file
97
mkdocs/tests/theme_tests.py
Normal 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
118
mkdocs/theme.py
Normal 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
|
||||
4
mkdocs/themes/mkdocs/mkdocs_theme.yml
Normal file
4
mkdocs/themes/mkdocs/mkdocs_theme.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
# Config options for 'mkdocs' theme
|
||||
|
||||
static_templates:
|
||||
- 404.html
|
||||
4
mkdocs/themes/readthedocs/mkdocs_theme.yml
Normal file
4
mkdocs/themes/readthedocs/mkdocs_theme.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
# Config options for 'readthedocs' theme
|
||||
|
||||
static_templates:
|
||||
- search.html
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user