Internal Refactor of Pages, Files, and Navigation (#1504)

Internal handling of pages, files and navigation has been completely refactored.
The changes included in the refactor are summarized below.

* Support for hidden pages. All Markdown pages are now included in the build
  regardless of whether they are included in the navigation configuration
  (fixes #699).
* The navigation can now include links to external sites (fixes #989, fixes #1373,
  & fixes #1406).
* Page data (including titles) is properly determined for all pages before any
  page is rendered (fixes #1347).
* Automatically populated navigation now sorts index pages to the top. In other
  words, The index page will be listed as the first child of a directory, while
  all other documents are sorted alphanumerically by file name after the index
  page (fixes #73 & fixes #1042).
* A `README.md` file is now treated as an index file within a directory and
  will be rendered to `index.html` (fixes #608).
* The URLs for all files are computed once and stored in a files collection.
  This ensures all internal links are always computed correctly regardless of
  the configuration. This also allows all internal links to be validated, not
  just links to other Markdown pages. (fixes #842 & fixes #872).
* An `on_files` plugin event has been added, which could be used to include
  files not in the `docs_dir`, exclude files, redefine page URLs (i.e.
  implement extensionless URLs), or to manipulate files in various other ways.

Backward incompatible changes are highlighted in the release notes included 
with this commit. Some notes regarding various decisions follow in no particular 
order:

This started out as the contents of the 'structure' dir from @tomchristie's 
work in #689.

All paths must be all Unicode all the time. When a byte string and a 
Unicode string are both passed to os.path (join ect) then returned value 
is always a byte string. Therefore, we need every path string to be 
Unicode. This ensures validation checks that and if the byte string uses 
the file system encoding, decodes it. For any other encoding, a 
validation error is raised.

Paths which start with a slash are assumed to be relative to the
docs_dir root. This behavior fixes #192. However, the slash not being
present in the output may surprise some users who are trying to create a
link relative to the server root when the mkdocs root is not at the
server root. The URLs available on a page are:

* Page.url is the url relative to the site_dir
* Page.canonical_url is the relative url joined with site_url or None if
  site_url is not defined (the default).
* Page.abs_url is the path component of the canonical url or None if
  canonical_url is None.

Note that new behavior is slightly different than before. Previously
abs_url ignored site_url and was always url with '' prepended. With the
new behavior, if site_url includes a subdir, that subdir will be
included in the abs_url.

When not on a server, there is no sensable "absolute_url" for a page.
Therefore, we shouldn't try to define one.

The thinking is that users generating docs to be browsed in the local
file system (`file://`) should leave the site_url setting unset, while
users who will be serving their docs from a server should be setting the
site_url. And if the site_url point sot a subdir of the server, the
abs_url will stil be absolute from the server root as it uses the "path"
of the canonical_url of the page.

Note that without the magical url context all URLs must be prepended by
`{{ base_url }}/` in the templates. While this requires a change in
third party themes, it is more consistent.

Links being ignored in raw HTML is now documented. Fixes #991.

All related tests that require temp dirs use the `mkdocs.tests.base.tempdir`
decorator. Note that any unrelated tests have not yet been updated. 
That can happen separately from this. The one test in 
`mkdocs.tests.structure.page_tests` (test_BOM) is unique enough to 
not use the decorator.
This commit is contained in:
Waylan Limberg
2018-06-28 15:08:17 -04:00
committed by GitHub
parent 3f0e4464a7
commit 34ef3ca6d0
51 changed files with 4195 additions and 2263 deletions
+2
View File
@@ -0,0 +1,2 @@
[report]
show_missing = True
+160 -35
View File
@@ -25,6 +25,131 @@ The current and past members of the MkDocs team.
### Major Additions to Development Version
#### Internal Refactor of Pages, Files, and Navigation
Internal handling of pages, files and navigation has been completely refactored.
The changes included in the refactor are summarized below.
* Support for hidden pages. All Markdown pages are now included in the build
regardless of whether they are included in the navigation configuration
(#699).
* The navigation can now include links to external sites (#989 #1373 & #1406).
* Page data (including titles) is properly determined for all pages before any
page is rendered (#1347).
* Automatically populated navigation now sorts index pages to the top. In other
words, The index page will be listed as the first child of a directory, while
all other documents are sorted alphanumerically by file name after the index
page (#73 & #1042).
* A `README.md` file is now treated as an index file within a directory and
will be rendered to `index.html` (#608).
* The URLs for all files are computed once and stored in a files collection.
This ensures all internal links are always computed correctly regardless of
the configuration. This also allows all internal links to be validated, not
just links to other Markdown pages. (#842 & #872).
* An [on_files] plugin event has been added, which could be used to include
files not in the `docs_dir`, exclude files, redefine page URLs (i.e.
implement extensionless URLs), or to manipulate files in various other ways.
[on_files]: ../user-guide/plugins.md#on_files
##### Backward Incompatible Changes
As part of the internal refactor, a number of backward incompatible changes have
been introduced, which are summarized below.
###### URLS have changed when `use_directory_urls` is `False`
Previously, all Markdown pages would be have their filenames altered to be index
pages regardless of how the [use_directory_urls] setting was configured.
However, the path munging is only needed when `use_directory_urls` is set to
`True` (the default). The path mungling no longer happens when
`use_directory_urls` is set to `False`, which will result in different URLs for
all pages that were not already index files. As this behavior only effects a
non-default configuration, and the most common user-case for setting the option
to `False` is for local file system (`file://`) browsing, its not likely to
effect most users. However, if you have `use_directory_urls` set to `False`
for a MkDocs site hosted on a web server, most of your URLs will now be broken.
As you can see below, the new URLs are much more sensible.
| Markdown file | Old URL | New URL |
| --------------- | -------------------- | -------------- |
| `index.md` | `index.html` | `index.html` |
| `foo.md` | `foo/index.html` | `foo.html` |
| `foo/bar.md` | `foo/bar/index.html` | `foo/bar.html` |
Note that there has been no change to URLs or file paths when
`use_directory_urls` is set to `True` (the default), except that MkDocs more
consistently includes an ending slash on all internally generated URLs.
[use_directory_urls]: ../user-guide/configuration.md#use_directory_urls
###### The `pages` configuration setting has been renamed to `nav`
The `pages` configuration setting is deprecated and will issue a warning if set
in the configuration file. The setting has been renamed `nav`. To update your
configuration, simply rename the setting to `nav`. In other words, if your
configuration looked like this:
```yaml
pages:
- Home: index.md
- User Guide: user-guide.md
```
Simply edit the configuration as follows:
```yaml
nav:
- Home: index.md
- User Guide: user-guide.md
```
In the current release, any configuration which includes a `pages` setting, but
no `nav` setting, the `pages` configuration will be copied to `nav` and a
warning will be issued. However, in a future release, that may no longer happen.
If both `pages` and `nav` are defined, the `pages` setting will be ignored.
###### Template variables and `base_url`
In previous versions of MkDocs some URLs expected the [base_url] template
variable to be prepended to the URL and others did not. That inconsistency has
been removed. All URLs must now be joined with the `base_url`. As previously, a
slash must be included between the `base_url` and the URL variable. For example,
a theme template might have previously included a link to the `site_name` as:
```django
<a href="{{ nav.homepage.url }}">{{ config.site_name }}</a>
```
And MkDocs would magically return a URL for the homepage which was relative to
the current page. That "magic" has been removed and the `base_url` must now be
explicitly included:
```django
<a href="{{ base_url }}/{{ nav.homepage.url }}">{{ config.site_name }}</a>
```
This change applies to any navigation items and pages, as well as the
`page.next_page` and `page.previous_page` attributes. For the time being, the
`extra_javascript` and `extra_css` variables continue to work as previously
(without `base_url`), but they have been deprecated and the corresponding
configuration values (`config.extra_javascript` and `config.extra_css`
respectively) should be used with `base_url` instead.
Note that navigation can now include links to external sites. Obviously, the
`base_url` should not be prepended to these items. Therefore, all navigation
items contain a `is_link` attribute which can be used to alter the behavior for
external links.
```django
<a href="{% if not nav_item.is_link %}{{ base_url }}/{% endif %}{{ nav_item.url }}">{{ nav_item.title }}</a>
```
Any other URL variables which should not be used with `base_url` are explicitly
documented as such.
[base_url]: ../user-guide/custom-themes.md#base_url
#### Path Based Settings are Relative to Configuration File (#543)
Previously any relative paths in the various configuration options were
@@ -181,7 +306,7 @@ template exists.
##### Context Variables
Page specific variable names in the template context have been refactored as
defined in [Custom Themes](../user-guide/custom-themes/#page). The
defined in [Custom Themes](../user-guide/custom-themes.md#page). The
old variable names issued a warning in version 0.16, but have been removed in
version 1.0.
@@ -199,14 +324,14 @@ user created and third-party templates:
| previous_page | [page.previous_page]|
| next_page | [page.next_page] |
[page]: ../user-guide/custom-themes/#page
[page.title]: ../user-guide/custom-themes/#pagetitle
[page.content]: ../user-guide/custom-themes/#pagecontent
[page.toc]: ../user-guide/custom-themes/#pagetoc
[page.meta]: ../user-guide/custom-themes/#pagemeta
[page.canonical_url]: ../user-guide/custom-themes/#pagecanonical_url
[page.previous_page]: ../user-guide/custom-themes/#pageprevious_page
[page.next_page]: ../user-guide/custom-themes/#pagenext_page
[page]: ../user-guide/custom-themes.md#page
[page.title]: ../user-guide/custom-themes.md#pagetitle
[page.content]: ../user-guide/custom-themes.md#pagecontent
[page.toc]: ../user-guide/custom-themes.md#pagetoc
[page.meta]: ../user-guide/custom-themes.md#pagemeta
[page.canonical_url]: ../user-guide/custom-themes.md#pagecanonical_url
[page.previous_page]: ../user-guide/custom-themes.md#pageprevious_page
[page.next_page]: ../user-guide/custom-themes.md#pagenext_page
Additionally, a number of global variables have been altered and/or removed
and user created and third-party templates should be updated as outlined below:
@@ -286,7 +411,7 @@ the `extra_css` or `extra_javascript` config settings going forward.
##### Page Context
Page specific variable names in the template context have been refactored as
defined in [Custom Themes](../user-guide/custom-themes/#page). The
defined in [Custom Themes](../user-guide/custom-themes.md#page). The
old variable names will issue a warning but continue to work for version 0.16,
but may be removed in a future version.
@@ -304,14 +429,14 @@ user created and third-party templates:
| previous_page | [page.previous_page]|
| next_page | [page.next_page] |
[page]: ../user-guide/custom-themes/#page
[page.title]: ../user-guide/custom-themes/#pagetitle
[page.content]: ../user-guide/custom-themes/#pagecontent
[page.toc]: ../user-guide/custom-themes/#pagetoc
[page.meta]: ../user-guide/custom-themes/#pagemeta
[page.canonical_url]: ../user-guide/custom-themes/#pagecanonical_url
[page.previous_page]: ../user-guide/custom-themes/#pageprevious_page
[page.next_page]: ../user-guide/custom-themes/#pagenext_page
[page]: ../user-guide/custom-themes.md#page
[page.title]: ../user-guide/custom-themes.md#pagetitle
[page.content]: ../user-guide/custom-themes.md#pagecontent
[page.toc]: ../user-guide/custom-themes.md#pagetoc
[page.meta]: ../user-guide/custom-themes.md#pagemeta
[page.canonical_url]: ../user-guide/custom-themes.md#pagecanonical_url
[page.previous_page]: ../user-guide/custom-themes.md#pageprevious_page
[page.next_page]: ../user-guide/custom-themes.md#pagenext_page
##### Global Context
@@ -400,7 +525,7 @@ overriding blocks in the same manner as the built-in themes. Third party themes
are encouraged to wrap the various pieces of their templates in blocks in order
to support such customization.
[blocks]: ../user-guide/styling-your-docs/#overriding-template-blocks
[blocks]: ../user-guide/styling-your-docs.md#overriding-template-blocks
#### Auto-Populated `extra_css` and `extra_javascript` Deprecated. (#986)
@@ -444,7 +569,7 @@ the `docs_dir` is set to the directory which contains your config file rather
than a child directory. You will need to rearrange you directory structure to
better conform with the documented [layout].
[layout]: ../user-guide/writing-your-docs/#file-layout
[layout]: ../user-guide/writing-your-docs.md#file-layout
### Other Changes and Additions to Version 0.16.0
@@ -522,8 +647,8 @@ See the documentation for [Styling your docs] for more information about using
and customizing themes and [Custom themes] for creating and distributing new
themes
[Styling your docs]: /user-guide/styling-your-docs.md
[Custom themes]: /user-guide/custom-themes.md
[Styling your docs]: ../user-guide/styling-your-docs.md
[Custom themes]: ../user-guide/custom-themes.md
### Other Changes and Additions to Version 0.15.0
@@ -544,9 +669,9 @@ themes
* Bugfix: Provide filename to Read the Docs. (#721 and RTD#1480)
* Bugfix: Silence Click's unicode_literals warning. (#708)
[site_description]: /user-guide/configuration.md#site_description
[site_author]: /user-guide/configuration.md#site_author
[ReadTheDocs]: /user-guide/styling-your-docs.md#readthedocs
[site_description]: ../user-guide/configuration.md#site_description
[site_author]: ../user-guide/configuration.md#site_author
[ReadTheDocs]: ../user-guide/styling-your-docs.md#readthedocs
## Version 0.14.0 (2015-06-09)
@@ -604,7 +729,7 @@ This new file is created on every MkDocs build (with `mkdocs build`) and
no configuration is needed to enable it.
[future release]: https://github.com/mkdocs/mkdocs/pull/481
[site_dir]: /user-guide/configuration.md#site_dir
[site_dir]: ../user-guide/configuration.md#site_dir
#### Change the pages configuration
@@ -612,8 +737,8 @@ Provide a [new way] to define pages, and specifically [nested pages], in the
mkdocs.yml file and deprecate the existing approach, support will be removed
with MkDocs 1.0.
[new way]: /user-guide/writing-your-docs.md#configure-pages-and-navigation
[nested pages]: /user-guide/writing-your-docs.md#multilevel-documentation
[new way]: ../user-guide/writing-your-docs.md#configure-pages-and-navigation
[nested pages]: ../user-guide/writing-your-docs.md#multilevel-documentation
#### Warn users about the removal of builtin themes
@@ -631,7 +756,7 @@ JavaScript library [lunr.js]. It has been added to both the `mkdocs` and
for adding it to your own themes.
[lunr.js]: http://lunrjs.com/
[supporting search]: /user-guide/styling-your-docs.md#search-and-themes
[supporting search]: ../user-guide/styling-your-docs.md#search-and-themes
#### New Command Line Interface
@@ -659,10 +784,10 @@ can also use Jinja2 syntax and take advantage of the [global variables].
By default MkDocs will use this approach to create a sitemap for the
documentation.
[extra_javascript]: /user-guide/configuration.md#extra_javascript
[extra_css]: /user-guide/configuration.md#extra_css
[extra_templates]: /user-guide/configuration.md#extra_templates
[global variables]: /user-guide/styling-your-docs.md#global-context
[extra_javascript]: ../user-guide/configuration.md#extra_javascript
[extra_css]: ../user-guide/configuration.md#extra_css
[extra_templates]: ../user-guide/configuration.md#extra_templates
[global variables]: ../user-guide/styling-your-docs.md#global-context
### Other Changes and Additions to Version 0.13.0
@@ -679,8 +804,8 @@ documentation.
called index.md (#535)
* Bugfix: Fix errors with Unicode filenames (#542).
[extra config]: /user-guide/configuration.md#extra
[Markdown extension configuration options]: /user-guide/configuration.md#markdown_extensions
[extra config]: ../user-guide/configuration.md#extra
[Markdown extension configuration options]: ../user-guide/configuration.md#markdown_extensions
[wheels]: http://pythonwheels.com/
## Version 0.12.2 (2015-04-22)
+55 -19
View File
@@ -162,24 +162,60 @@ This option can be overridden by a command line option in `gh-deploy`.
## Documentation layout
### pages
### nav
This setting is used to determine the set of pages that should be built for the
documentation. For example, the following would create Introduction, User Guide
and About pages, given the three source files `index.md`, `user-guide.md` and
`about.md`, respectively.
This setting is used to determine the format and layout of the global navigation
for the site. For example, the following would create "Introduction", "User
Guide" and "About" navigation items.
```yaml
pages:
nav:
- 'Introduction': 'index.md'
- 'User Guide': 'user-guide.md'
- 'About': 'about.md'
```
See the section on [configuring pages and navigation] for a more detailed
breakdown, including how to create sub-sections.
All paths must be relative to the `mkdocs.yml` configuration file. See the
section on [configuring pages and navigation] for a more detailed breakdown,
including how to create sub-sections.
**default**: By default `pages` will contain an alphanumerically sorted, nested
Navigation items may also include links to external sites. While titles are
optional for internal links, they are required for external links. An external
link may be a full URL or a relative URL. Any path which is not found in the
files is assumed to be an external link.
```yaml
nav:
- Home: index.md
- User Guide: user-guide.md
- Bug Tracker: https://example.com/
```
In the above example, the first two items point to local files while the third
points to an external site.
However, sometimes the MkDocs site is hosted in a subdirectory of a project's
site and you may want to link to other parts of the same site without including
the full domain. In that case, you may use and appropriate relative URL.
```yaml
site_url: http://example.com/foo/
nav:
- Home: ../
- User Guide: user-guide.md
- Bug Tracker: /bugs/
```
In the above example, two different styles of external links are used. First
note that the `site_url` indicates that the MkDocs site is hosted in the `/foo/`
subdirectory of the domain. Therefore, the `Home` navigation item is a relative
link which steps up one level to the server root and effectively points to
`http://example.com/`. The `Bug Tracker` item uses an absolute path from the
server root and effectively points to `http://example.com/bugs/`. Of course, the
`User Guide` points to a local MkDocs page.
**default**: By default `nav` will contain an alphanumerically sorted, nested
list of all the Markdown files found within the `docs_dir` and its
sub-directories. If none are found it will be `[]` (an empty list).
@@ -324,27 +360,26 @@ documentation.
The following table demonstrates how the URLs used on the site differ when
setting `use_directory_urls` to `true` or `false`.
Source file | Generated HTML | use_directory_urls: true | use_directory_urls: false
------------ | -------------------- | ------------------------ | ------------------------
index.md | index.html | / | /index.html
api-guide.md | api-guide/index.html | /api-guide/ | /api-guide/index.html
about.md | about/index.html | /about/ | /about/index.html
Source file | use_directory_urls: true | use_directory_urls: false
---------------- | ------------------------- | -------------------------
index.md | / | /index.html
api-guide.md | /api-guide/ | /api-guide.html
about/license.md | /about/license/ | /about/license.html
The default style of `use_directory_urls: true` creates more user friendly URLs,
and is usually what you'll want to use.
The alternate style can occasionally be useful if you want your documentation to
remain properly linked when opening pages directly from the file system, because
it create links that point directly to the target *file* rather than the target
it creates links that point directly to the target *file* rather than the target
*directory*.
**default**: `true`
### strict
Determines if a broken link to a page within the documentation is considered a
warning or an error (link to a page not listed in the pages setting). Set to
true to halt processing when a broken link is found, false prints a warning.
Determines how warnings are handled. Set to `true` to halt processing when a
warning is raised. Set to `false` to print a warning and continue processing.
**default**: `false`
@@ -519,7 +554,7 @@ You may [contribute additional languages].
any reason, a warning is issued. You may use the `--strict` flag when building
to cause such a failure to raise an error instead.
!!! Note
!!! Note
On smaller sites, using a pre-built index is not recommended as it creates a
significant increase is bandwidth requirements with little to no noticeable
@@ -545,3 +580,4 @@ You may [contribute additional languages].
[ISO 639-1]: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
[Lunr Languages]: https://github.com/MihaiValentin/lunr-languages#lunr-languages-----
[contribute additional languages]: https://github.com/MihaiValentin/lunr-languages/blob/master/CONTRIBUTING.md
[Node.js]: https://nodejs.org/
+209 -37
View File
@@ -126,6 +126,8 @@ used options include:
* [config.site_url](./configuration.md#site_url)
* [config.site_author](./configuration.md#site_author)
* [config.site_description](./configuration.md#site_description)
* [config.extra_javascript](./configuration.md#extra_javascript)
* [config.extra_css](./configuration.md#extra_css)
* [config.repo_url](./configuration.md#repo_url)
* [config.repo_name](./configuration.md#repo_name)
* [config.copyright](./configuration.md#copyright)
@@ -133,7 +135,29 @@ used options include:
#### nav
The `nav` variable is used to create the navigation for the documentation.
The `nav` variable is used to create the navigation for the documentation. The
`nav` object is an iterable of [navigation objects](#navigation-objects) as
defined by the [nav] configuration setting.
[nav]: configuration.md#nav
In addition to the iterable of [navigation objects](#navigation-objects), the
`nav` object contains the following attributes:
##### nav.homepage
The [page](#page) object for the homepage of the site.
##### nav.pages
A flat list of all [page](#page) objects contained in the navigation. This list
is not necessarily a complete list of all site pages as it does not contain
pages which are not included in the navigation. This list does match the list
and order of pages used for all "next page" and "previous page" links. For a
list of all pages, use the [pages](#pages) template variable.
##### Nav Example
Following is a basic usage example which outputs the first and second level
navigation as a nested list.
@@ -145,15 +169,15 @@ navigation as a nested list.
<li>{{ nav_item.title }}
<ul>
{% for nav_item in nav_item.children %}
<li class="{% if nav_item.active%}current{%endif%}">
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
<li class="{% if nav_item.active%}current{% endif %}">
<a href="{% if not nav_item.is_link %}{{ base_url }}/{% endif %}{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li class="{% if nav_item.active%}current{%endif%}">
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
<li class="{% if nav_item.active%}current{% endif %}">
<a href="{% if not nav_item.is_link %}{{ base_url }}/{% endif %}{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{% endif %}
{% endfor %}
@@ -161,9 +185,6 @@ navigation as a nested list.
{% endif %}
```
The `nav` object also contains a `homepage` object, which points to the `page`
object of the homepage. For example, you may want to access `nav.homepage.url`.
#### base_url
The `base_url` provides a relative path to the root of the MkDocs project.
@@ -175,22 +196,6 @@ folder on all pages you would do this:
<script src="{{ base_url }}/js/theme.js"></script>
```
#### extra_css
Contains a list of URLs to the style-sheets listed in the [extra_css]
config setting. Unlike the config setting, which contains local paths, this
variable contains absolute paths from the homepage.
[extra_css]: configuration.md#extra_css
#### extra_javascript
Contains a list of URLs to the scripts listed in the [extra_javascript] config
setting. Unlike the config setting, which contains local paths, this variable
contains absolute paths from the homepage.
[extra_javascript]: configuration.md#extra_javascript
#### mkdocs_version
Contains the current MkDocs version.
@@ -201,11 +206,23 @@ A Python datetime object that represents the date and time the documentation
was built in UTC. This is useful for showing how recently the documentation
was updated.
#### pages
A list of [page](#page) objects including *all* pages in the project. The list
is a flat list with all pages sorted alphanumerically by directory and file
name. Note that index pages sort to the top within a directory. This list can
contain pages not included in the global [navigation](#nav) and may not match
the order of pages within that navigation.
#### page
In templates which are not rendered from a Markdown source file, the `page`
variable is `None`. In templates which are rendered from a Markdown source file,
the `page` variable contains a page object with the following attributes:
the `page` variable contains a `page` object. The same `page` objects are used
as `page` [navigation objects](#navigation-objects) in the global
[navigation](#nav) and in the [pages](#pages) template variable.
All `page` objects contain the following attributes:
##### page.title
@@ -257,19 +274,43 @@ documentation page.
{% endfor %}
```
##### page.url
The URL of the page relative to the MkDocs `site_dir`. It is expected that this
be used with [base_url] to ensure the URL is relative to the current page.
```django
<a href="{{ base_url }}/{{ page.url }}">{{ page.title }}</a>
```
[base_url]: #base_url
##### page.abs_url
The absolute URL of the page from the server root as determined by the value
assigned to the [site_url] configuration setting. The value includes any
subdirectory included in the `site_url`, but not the domain. [base_url] should
not be used with this variable.
For example, if `site_url: http://example.com/`, then the value of
`page.abs_url` for the page `foo.md` would be `/foo/`. However, if
`site_url: http://example.com/bar/`, then the value of `page.abs_url` for the
page `foo.md` would be `/bar/foo/`.
[site_url]: ./configuration.md#site_url
##### page.canonical_url
The full, canonical URL to the current page. This includes the `site_url` from
the configuration.
The full, canonical URL to the current page as determined by the value assigned
to the [site_url] configuration setting. The value includes the domain and any
subdirectory included in the `site_url`. [base_url] should not be used with this
variable.
##### page.edit_url
The full URL to the input page in the source repository. Typically used to
provide a link to edit the source page.
##### page.url
The URL to the current page not including the `site_url` from the configuration.
The full URL to the source page in the source repository. Typically used to
provide a link to edit the source page. [base_url] should not be used with this
variable.
##### page.is_homepage
@@ -284,12 +325,143 @@ on the homepage:
##### page.previous_page
The page object for the previous page. The usage is the same as for
`page`.
The page object for the previous page or `None`. The value will be `None` if the
current page is the first item in the site navigation or if the current page is
not included in the navigation at all. When the value is a page object, the
usage is the same as for `page`.
##### page.next_page
The page object for the next page.The usage is the same as for `page`.
The page object for the next page or `None`. The value will be `None` if the
current page is the last item in the site navigation or if the current page is
not included in the navigation at all. When the value is a page object, the
usage is the same as for `page`.
##### page.parent
The immediate parent of the page in the [site navigation](#nav). `None` if the
page is at the top level.
##### page.children
Pages do not contain children and the attribute is always `None`.
##### page.active
When `True`, indicates that this page is the currently viewed page. Defaults
to `False`.
##### page.is_section
Indicates that the navigation object is a "section" object. Always `False` for
page objects.
##### page.is_page
Indicates that the navigation object is a "page" object. Always `True` for
page objects.
##### page.is_link
Indicates that the navigation object is a "link" object. Always `False` for
page objects.
### Navigation Objects
Navigation objects contained in the [nav](#nav) template variable may be one of
[section](#section) objects, [page](#page) objects, and [link](#link) objects.
While section objects may contain nested navigation objects, pages and links do
not.
Page objects are the full page object as used for the current [page](#page) with
all of the same attributes available. Section and Link objects contain a subset
of those attributes as defined below:
#### Section
A `section` navigation object defines a named section in the navigation and
contains a list of child navigation objects. Note that sections do not contain
URLs and are not links of any kind. However, by default, MkDocs sorts index
pages to the top and the first child might be used as the URL for a section if a
theme choses to do so.
The following attributes are available on `section` objects:
##### section.title
The title of the section.
##### section.parent
The immediate parent of the section or `None` if the section is at the top
level.
##### section.children
An iterable of all child navigation objects. Children may include nested
sections, pages and links.
##### section.active
When `True`, indicates that a child page of this section is the current page and
can be used to highlight the section as the currently viewed section. Defaults
to `False`.
##### section.is_section
Indicates that the navigation object is a "section" object. Always `True` for
section objects.
##### section.is_page
Indicates that the navigation object is a "page" object. Always `False` for
section objects.
##### section.is_link
Indicates that the navigation object is a "link" object. Always `False` for
section objects.
#### Link
A `link` navigation object contains a link which does not point to an internal
MkDocs page. The following attributes are available on `link` objects:
##### link.title
The title of the link. This would generally be used as the label of the link.
##### link.url
The URL that the link points to. The URL should always be an absolute URLs and
should not need to have `base_url` prepened.
##### link.parent
The immediate parent of the link. `None` if the link is at the top level.
##### link.children
Links do not contain children and the attribute is always `None`.
##### link.active
External links cannot be "active" and the attribute is always `False`.
##### link.is_section
Indicates that the navigation object is a "section" object. Always `False` for
link objects.
##### link.is_page
Indicates that the navigation object is a "page" object. Always `False` for
link objects.
##### link.is_link
Indicates that the navigation object is a "link" object. Always `True` for
link objects.
### Extra Context
@@ -607,4 +779,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
[theme]: ./configuration.md#theme
+2 -2
View File
@@ -108,7 +108,7 @@ public repository.
[rtd]: https://readthedocs.org/
[instructions]: https://read-the-docs.readthedocs.io/en/latest/getting_started.html#in-markdown
[features]: http://read-the-docs.readthedocs.io/en/latest/features.html
[theme]: /user-guide/styling-your-docs.md
[theme]: ./styling-your-docs.md#readthedocs
## Other Providers
@@ -153,4 +153,4 @@ deploying to [GitHub](#github-pages) but only on a custom domain. Other web
servers may be configured to use it but the feature won't always be available.
See the documentation for your server of choice for more information.
[site_dir]: ./configuration/#site_dir
[site_dir]: ./configuration.md#site_dir
+20 -4
View File
@@ -149,7 +149,7 @@ entire site.
: The `serve` event is only called when the `serve` command is used during
development. It is passed the `Server` instance which can be modified before
it is activated. For example, additional files or directories could be added
to the list of "watched" filed for auto-reloading.
to the list of "watched" files for auto-reloading.
Parameters:
: __server:__ `livereload.Server` instance
@@ -178,14 +178,30 @@ entire site.
Parameters:
: __config:__ global configuration object
##### on_files
: The `files` event is called after the files collection is populated from the
`docs_dir`. Use this event to add, remove, or alter files in the
collection. Note that Page objects have not yet been associated with the
file objects in the collection. Use [Page Events] to manipulate page
specific data.
Parameters:
: __files:__ global files collection
: __config:__ global configuration object
Returns:
: global files collection
##### on_nav
: The `nav` event is called after the site navigation is created and can
be used to alter the site navigation.
Parameters:
: __site_navigation:__ global navigation object
: __nav:__ global navigation object
: __config:__ global configuration object
: __files:__ global files collection
Returns:
: global navigation object
@@ -263,8 +279,8 @@ called after the [env] event and before any [page events].
#### Page Events
Page events are called once for each Markdown page included in the site. All
page events are called after the [post_template] event and before the [post_build]
event.
page events are called after the [post_template] event and before the
[post_build] event.
##### on_pre_page
+6 -7
View File
@@ -6,9 +6,8 @@ How to style and theme your documentation.
MkDocs includes a couple [built-in themes] as well as various [third party
themes], all of which can easily be customized with [extra CSS or
JavaScript][docs_dir] or overridden from the [theme directory][theme_dir]. You
can also create your own [custom theme] from the ground up for your
documentation.
JavaScript][docs_dir] or overridden from the theme's [custom_dir]. You can also
create your own [custom theme] from the ground up for your documentation.
To use a theme that is included in MkDocs, simply add this to your
`mkdocs.yml` config file.
@@ -261,21 +260,21 @@ 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
[Bootstrap]: http://getbootstrap.com/
[theme configuration options]: configuration.md#theme
[theme configuration options]: ./configuration.md#theme
[Read the Docs]: https://readthedocs.org/
[community wiki]: https://github.com/mkdocs/mkdocs/wiki/MkDocs-Themes
[custom theme]: ./custom-themes.md
[customize]: #customizing-a-theme
[docs_dir]: #using-the-docs_dir
[documentation directory]: ./configuration/#docs_dir
[documentation directory]: ./configuration.md#docs_dir
[extra_css]: ./configuration.md#extra_css
[extra_javascript]: ./configuration.md#extra_javascript
[Jinja documentation]: http://jinja.pocoo.org/docs/dev/templates/#template-inheritance
[mkdocs]: #mkdocs
[ReadTheDocs]: ./deploying-your-docs.md#readthedocs
[Template Variables]: ./custom-themes.md#template-variables
[custom_dir]: ./configuration/#custom_dir
[name]: ./configuration/#name
[custom_dir]: ./configuration.md#custom_dir
[name]: ./configuration.md#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
+76 -32
View File
@@ -22,7 +22,8 @@ docs/
By convention your project homepage should always be named `index`. Any of the
following extensions may be used for your Markdown source files: `markdown`,
`mdown`, `mkdn`, `mkd`, `md`.
`mdown`, `mkdn`, `mkd`, `md`. All Markdown files included in your documentation
directory will be rendered in the built site regardless of any settings.
You can also create multi-page documentation, by creating several Markdown
files:
@@ -65,55 +66,84 @@ nested URLs, like so:
/license/
```
### Index pages
When a directory is requested, by default, most web servers will return an index
file (usually named `index.html`) contained within that directory if one exists.
For that reason, the homepage in all of the examples above has been named
`index.md`, which MkDocs will render to `index.html` when building the site.
Many repository hosting sites provide special treatment for README files by
displaying the contents of the README file when browsing the contents of a
directory. Therefore, MkDocs will allow you to name your index pages as
`README.md` instead of `index.md`. In that way, when users are browsing your
source code, the repository host can display the index page of that directory as
it is a README file. However, when MkDocs renders your site, the file will be
renamed to `index.html` so that the server will serve it as a proper index file.
You should not include both an `index.md` file and a `README.md` file in the
same directory. It is suggested that you chose a convention for your project and
then stick to it.
### Configure Pages and Navigation
The [pages configuration](configuration.md#pages) in your `mkdocs.yml` defines
which pages are built by MkDocs and how they appear in the documentation
navigation. If not provided, the pages configuration will be automatically
created by discovering all the Markdown files in the [documentation
directory](configuration.md#docs_dir). An automatically created pages
configuration will always be sorted alphanumerically by file name. You will need
to manually define your pages configuration if you would like your pages sorted
differently.
The [nav](configuration.md#nav) configuration setting in your `mkdocs.yml` file
defines which pages are included in the global site navigation menu as well as
the structure of that menu. If not provided, the navigation will be
automatically created by discovering all the Markdown files in the
[documentation directory](configuration.md#docs_dir). An automatically created
navigation configuration will always be sorted alphanumerically by file name
(except that index files will always be listed first within a sub-section). You
will need to manually define your navigation configuration if you would like
your navigation menu sorted differently.
A simple pages configuration looks like this:
A simple navigation configuration looks like this:
```no-highlight
pages:
nav:
- 'index.md'
- 'about.md'
```
With this example we will build two pages at the top level and they will
automatically have their titles inferred from the filename. Assuming `docs_dir`
has the default value, `docs`, the source files for this documentation would be
`docs/index.md` and `docs/about.md`. To provide a custom name for these pages,
they can be added before the filename.
All paths in the navigation configuration must be relative to the `docs_dir`
configuration option. If that option is set to the default value, `docs`, the
source files for the above configuration would be located at `docs/index.md` and
`docs/about.md`.
The above example will result in two navigation items being created at the top
level and with their titles inferred from the contents of the file (or the
filename if no title is defined within the file). To define a custom title for
the pages, the title can be added before the filename.
```no-highlight
pages:
nav:
- Home: 'index.md'
- About: 'about.md'
```
Subsections can be created by listing related pages together under a section
title. For example:
Note that if a title is defined for a page in the navigation, that title will be
used throughout the site for that page and will override any title defined
within the page itself.
Navigation sub-sections can be created by listing related pages together under a
section title. For example:
```no-highlight
pages:
nav:
- Home: 'index.md'
- User Guide:
- 'Writing your docs': 'user-guide/writing-your-docs.md'
- 'Styling your docs': 'user-guide/styling-your-docs.md'
- 'Writing your docs': 'writing-your-docs.md'
- 'Styling your docs': 'styling-your-docs.md'
- About:
- 'License': 'about/license.md'
- 'Release Notes': 'about/release-notes.md'
- 'License': 'license.md'
- 'Release Notes': 'release-notes.md'
```
With the above configuration we have three top level sections: Home, User Guide
and About. Then under User Guide we have two pages, Writing your docs and
Styling your docs. Under the About section we also have two pages, License and
Release Notes.
With the above configuration we have three top level items: "Home", "User Guide"
and "About." "Home" is a link to the homepage for the site. Under the "User
Guide" section two pages are listed: "Writing your docs" and "Styling your
docs." Under the "About" section two more pages are listed: "License" and
"Release Notes."
Note that a section cannot have a page assigned to it. Sections are only
containers for child pages and sub-sections. You may nest sections as deeply as
@@ -121,6 +151,11 @@ you like. However, be careful that you don't make it too difficult for your
users to navigate through the site navigation by over-complicating the nesting.
While sections may mirror your directly structure, they do not have to.
Any pages not listed in your navigation configuration will still be rendered and
included with the built site, however, they will not be linked from the global
navigation and will not be included in the `previous` and `next` links. Such
pages will be "hidden" unless linked to directly.
## Writing with Markdown
MkDocs pages must be authored in [Markdown][md], a lightweight markup language
@@ -139,7 +174,7 @@ configuration setting for details on how to enable extensions.
MkDocs includes some extensions by default, which are highlighted below.
[Python-Markdown]: https://python-markdown.github.io/
[md]: http://daringfireball.net/projects/markdown/
[md]: https://daringfireball.net/projects/markdown/
[differences]: https://python-markdown.github.io/#differences
[syntax]: https://daringfireball.net/projects/markdown/syntax
[extensions]: https://python-markdown.github.io/extensions/
@@ -149,7 +184,7 @@ MkDocs includes some extensions by default, which are highlighted below.
MkDocs allows you to interlink your documentation by using regular Markdown
[links]. However, there are a few additional benefits to formatting those links
specifically for MkDocs as outlines below.
specifically for MkDocs as outlined below.
[links]: https://daringfireball.net/projects/markdown/syntax#link
@@ -280,6 +315,15 @@ also be previewed if you're working on the documentation with a Markdown editor.
[GitHub pages CNAME file]: https://help.github.com/articles/using-a-custom-domain-with-github-pages/
#### Linking from raw HTML
Markdown allows document authors to fall back to raw HTML when the Markdown
syntax does not meets the author's needs. MkDocs does not limit Markdown in this
regard. However, as all raw HTML is ignored by the Markdown parser, MkDocs is
not able to validate or convert links contained in raw HTML. When including
internal links within raw HTML, you will need to manually format the link
appropriately for the rendered document.
### Meta-Data
MkDocs includes support for [MultiMarkdown] style meta-data (often called
@@ -341,7 +385,7 @@ specific page. The following keys are supported:
MkDocs will attempt to determine the title of a document in the following
ways, in order:
1. A title defined in the [pages] configuration setting for a document.
1. A title defined in the [nav] configuration setting for a document.
2. A title defined in the `title` meta-data key of a document.
3. A level 1 Markdown header on the first line of the document body.
4. The filename of a document.
@@ -350,7 +394,7 @@ specific page. The following keys are supported:
additional sources in the above list.
[MultiMarkdown]: http://fletcherpenney.net/MultiMarkdown_Syntax_Guide#metadata
[pages]: configuration.md#pages
[nav]: configuration.md#nav
### Tables
+1 -1
View File
@@ -6,7 +6,7 @@ site_author: MkDocs Team
repo_url: https://github.com/mkdocs/mkdocs/
edit_uri: ""
pages:
nav:
- Home: index.md
- User Guide:
- Writing Your Docs: user-guide/writing-your-docs.md
+180 -176
View File
@@ -3,15 +3,17 @@
from __future__ import unicode_literals
from datetime import datetime
from calendar import timegm
import io
import logging
import os
import gzip
import io
from jinja2.exceptions import TemplateNotFound
import jinja2
from mkdocs import nav, utils
from mkdocs import utils
from mkdocs.structure.files import get_files
from mkdocs.structure.nav import get_navigation
import mkdocs
@@ -30,18 +32,17 @@ log = logging.getLogger(__name__)
log.addFilter(DuplicateFilter())
def get_context(nav, config, page=None):
def get_context(nav, files, config, page=None, base_url=''):
"""
Given the SiteNavigation and config, generate the context which is relevant
to app pages.
Return the template context for a given page or template.
"""
if nav is None:
return {'page', page}
if page is not None:
base_url = utils.get_relative_url('.', page.url)
extra_javascript = utils.create_media_urls(nav, config['extra_javascript'])
extra_javascript = utils.create_media_urls(config['extra_javascript'], page, base_url)
extra_css = utils.create_media_urls(nav, config['extra_css'])
extra_css = utils.create_media_urls(config['extra_css'], page, base_url)
# Support SOURCE_DATE_EPOCH environment variable for "reproducible" builds.
# See https://reproducible-builds.org/specs/source-date-epoch/
@@ -49,8 +50,9 @@ def get_context(nav, config, page=None):
return {
'nav': nav,
# base_url should never end with a slash.
'base_url': nav.url_context.make_relative('/').rstrip('/'),
'pages': files.documentation_pages(),
'base_url': base_url.rstrip('/'),
'extra_css': extra_css,
'extra_javascript': extra_javascript,
@@ -63,192 +65,169 @@ def get_context(nav, config, page=None):
}
def build_template(template_name, env, config, site_navigation=None):
def _build_template(name, template, files, config, nav):
"""
Return rendered output for given template as a string.
"""
# Run `pre_template` plugin events.
template = config['plugins'].run_event(
'pre_template', template, template_name=name, config=config
)
if utils.is_error_template(name):
# 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.
# See https://github.com/mkdocs/mkdocs/issues/77
base_url = utils.urlparse(config['site_url']).path
else:
base_url = utils.get_relative_url('.', name)
context = get_context(nav, files, config, base_url=base_url)
# Run `template_context` plugin events.
context = config['plugins'].run_event(
'template_context', context, template_name=name, config=config
)
output = template.render(context)
# Run `post_template` plugin events.
output = config['plugins'].run_event(
'post_template', output, template_name=name, config=config
)
return output
def _build_theme_template(template_name, env, files, config, nav):
""" Build a template using the theme environment. """
log.debug("Building template: %s", template_name)
log.debug("Building theme template: {}".format(template_name))
try:
template = env.get_template(template_name)
except TemplateNotFound:
log.info("Template skipped: '{}'. Not found in template directories.".format(template_name))
log.warn("Template skipped: '{}' not found in theme directories.".format(template_name))
return
# Run `pre_template` plugin events.
template = config['plugins'].run_event(
'pre_template', template, template_name=template_name, config=config
)
output = _build_template(template_name, template, files, config, nav)
context = get_context(site_navigation, config)
# Run `template_context` plugin events.
context = config['plugins'].run_event(
'template_context', context, template_name=template_name, config=config
)
output_content = template.render(context)
# Run `post_template` plugin events.
output_content = config['plugins'].run_event(
'post_template', output_content, template_name=template_name, config=config
)
if output_content.strip():
if output.strip():
output_path = os.path.join(config['site_dir'], template_name)
utils.write_file(output_content.encode('utf-8'), output_path)
utils.write_file(output.encode('utf-8'), output_path)
if template_name == 'sitemap.xml':
log.debug("Gzipping template: %s", template_name)
with gzip.open('{}.gz'.format(output_path), 'wb') as f:
f.write(output_content.encode('utf-8'))
f.write(output.encode('utf-8'))
else:
log.info("Template skipped: '{}'. Generated empty output.".format(template_name))
log.info("Template skipped: '{}' generated empty output.".format(template_name))
def build_error_template(template, env, config, site_navigation):
"""
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.
See https://github.com/mkdocs/mkdocs/issues/77
"""
site_navigation.url_context.force_abs_urls = True
default_base = site_navigation.url_context.base_path
site_navigation.url_context.base_path = utils.urlparse(config['site_url']).path
build_template(template, env, config, site_navigation)
# Reset nav behavior to the default
site_navigation.url_context.force_abs_urls = False
site_navigation.url_context.base_path = default_base
def _build_page(page, config, site_navigation, env, dirty=False):
""" Build a Markdown page and pass to theme template. """
# Run the `pre_page` plugin event
page = config['plugins'].run_event(
'pre_page', page, config=config, site_navigation=site_navigation
)
page.read_source(config=config)
# Run `page_markdown` plugin events.
page.markdown = config['plugins'].run_event(
'page_markdown', page.markdown, page=page, config=config, site_navigation=site_navigation
)
page.render(config, site_navigation)
# Run `page_content` plugin events.
page.content = config['plugins'].run_event(
'page_content', page.content, page=page, config=config, site_navigation=site_navigation
)
context = get_context(site_navigation, config, page)
# Allow 'template:' override in md source files.
if 'template' in page.meta:
template = env.get_template(page.meta['template'])
else:
template = env.get_template('main.html')
# Run `page_context` plugin events.
context = config['plugins'].run_event(
'page_context', context, page=page, config=config, site_navigation=site_navigation
)
# Render the template.
output_content = template.render(context)
# Run `post_page` plugin events.
output_content = config['plugins'].run_event(
'post_page', output_content, page=page, config=config
)
# Write the output file.
if output_content.strip():
utils.write_file(output_content.encode('utf-8'), page.abs_output_path)
else:
log.info("Page skipped: '{}'. Generated empty output.".format(page.title))
def build_extra_templates(extra_templates, config, site_navigation=None):
def _build_extra_template(template_name, files, config, nav):
""" Build user templates which are not part of the theme. """
log.debug("Building extra_templates pages")
log.debug("Building extra template: {}".format(template_name))
for extra_template in extra_templates:
file = files.get_file_from_path(template_name)
if file is None:
log.warn("Template skipped: '{}' not found in docs_dir.".format(template_name))
return
input_path = os.path.join(config['docs_dir'], extra_template)
try:
with io.open(file.abs_src_path, 'r', encoding='utf-8', errors='strict') as f:
template = jinja2.Template(f.read())
except Exception as e:
log.warn("Error reading template '{}': {}".format(template_name, e))
return
with io.open(input_path, 'r', encoding='utf-8') as template_file:
template = jinja2.Template(template_file.read())
output = _build_template(template_name, template, files, config, nav)
# Run `pre_template` plugin events.
template = config['plugins'].run_event(
'pre_template', template, template_name=extra_template, config=config
if output.strip():
utils.write_file(output.encode('utf-8'), file.abs_dest_path)
else:
log.info("Template skipped: '{}' generated empty output.".format(template_name))
def _populate_page(page, config, files, dirty=False):
""" Read page content from docs_dir and render Markdown. """
try:
# When --dirty is used, only read the page if the file has been modified since the
# previous build of the output.
if dirty and not page.file.is_modified():
return
# Run the `pre_page` plugin event
page = config['plugins'].run_event(
'pre_page', page, config=config, files=files
)
context = get_context(site_navigation, config)
page.read_source(config)
# Run `template_context` plugin events.
# Run `page_markdown` plugin events.
page.markdown = config['plugins'].run_event(
'page_markdown', page.markdown, page=page, config=config, files=files
)
page.render(config, files)
# Run `page_content` plugin events.
page.content = config['plugins'].run_event(
'page_content', page.content, page=page, config=config, files=files
)
except Exception as e:
log.error("Error reading page '{}': {}".format(page.file.src_path, e))
raise
def _build_page(page, config, files, nav, env, dirty=False):
""" Pass a Page to theme template and write output to site_dir. """
try:
# When --dirty is used, only build the page if the file has been modified since the
# previous build of the output.
if dirty and not page.file.is_modified():
return
log.debug("Building page {}".format(page.file.src_path))
# Activate page. Signals to theme that this is the current page.
page.active = True
context = get_context(nav, files, config, page)
# Allow 'template:' override in md source files.
if 'template' in page.meta:
template = env.get_template(page.meta['template'])
else:
template = env.get_template('main.html')
# Run `page_context` plugin events.
context = config['plugins'].run_event(
'template_context', context, template_name=extra_template, config=config
'page_context', context, page=page, config=config, nav=nav
)
output_content = template.render(context)
# Render the template.
output = template.render(context)
# Run `post_template` plugin events.
output_content = config['plugins'].run_event(
'post_template', output_content, template_name=extra_template, config=config
# Run `post_page` plugin events.
output = config['plugins'].run_event(
'post_page', output, page=page, config=config
)
if output_content.strip():
output_path = os.path.join(config['site_dir'], extra_template)
utils.write_file(output_content.encode('utf-8'), output_path)
# Write the output file.
if output.strip():
utils.write_file(output.encode('utf-8', errors='xmlcharrefreplace'), page.file.abs_dest_path)
else:
log.info("Template skipped: '{}'. Generated empty output.".format(extra_template))
log.info("Page skipped: '{}'. Generated empty output.".format(page.file.src_path))
def build_pages(config, dirty=False):
""" Build all pages and write them into the build directory. """
site_navigation = nav.SiteNavigation(config)
# Run `nav` plugin events.
site_navigation = config['plugins'].run_event('nav', site_navigation, config=config)
env = config['theme'].get_env()
# Run `env` plugin events.
env = config['plugins'].run_event(
'env', env, config=config, site_navigation=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
# previous build of the output.
if dirty and (utils.modified_time(page.abs_input_path) < utils.modified_time(page.abs_output_path)):
continue
log.debug("Building page %s", page.input_path)
_build_page(page, config, site_navigation, env)
except Exception:
log.error("Error building page %s", page.input_path)
raise
# Deactivate page
page.active = False
except Exception as e:
log.error("Error building page '{}': {}".format(page.file.src_path, e))
raise
def build(config, live_server=False, dirty=False):
@@ -263,29 +242,54 @@ def build(config, live_server=False, dirty=False):
if not dirty:
log.info("Cleaning site directory")
utils.clean_directory(config['site_dir'])
else:
else: # pragma: no cover
# Warn user about problems that may occur with --dirty option
log.warning("A 'dirty' build is being performed, this will likely lead to inaccurate navigation and other"
" links within your site. This option is designed for site development purposes only.")
if not live_server:
if not live_server: # pragma: no cover
log.info("Building documentation to directory: %s", config['site_dir'])
if dirty and site_directory_contains_stale_files(config['site_dir']):
log.info("The directory contains stale files. Use --clean to remove them.")
# 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'].dirs):
log.debug("Copying static assets from %s", theme_dir)
utils.copy_media_files(
theme_dir, config['site_dir'], exclude=['*.py', '*.pyc', '*.html', 'mkdocs_theme.yml'], dirty=dirty
)
# First gather all data from all files/pages to ensure all data is consistent across all pages.
log.debug("Copying static assets from the docs dir.")
utils.copy_media_files(config['docs_dir'], config['site_dir'], dirty=dirty)
files = get_files(config)
env = config['theme'].get_env()
files.add_files_from_theme(env, config)
build_pages(config, dirty=dirty)
# Run `files` plugin events.
files = config['plugins'].run_event('files', files, config=config)
nav = get_navigation(files, config)
# Run `nav` plugin events.
nav = config['plugins'].run_event('nav', nav, config=config, files=files)
log.debug("Reading markdown pages.")
for file in files.documentation_pages():
_populate_page(file.page, config, files, dirty)
# Run `env` plugin events.
env = config['plugins'].run_event(
'env', env, config=config, files=files
)
# Start writing files to site_dir now that all data is gathered. Note that order matters. Files
# with lower precedence get written first so that files with higher precedence can overwrite them.
log.debug("Copying static assets.")
files.copy_static_files(dirty=dirty)
for template in config['theme'].static_templates:
_build_theme_template(template, env, files, config, nav)
for template in config['extra_templates']:
_build_extra_template(template, files, config, nav)
log.debug("Building markdown pages.")
for file in files.documentation_pages():
_build_page(file.page, config, files, nav, env, dirty)
# Run `post_build` plugin events.
config['plugins'].run_event('post_build', config)
+8
View File
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import logging
import os
import sys
from mkdocs import exceptions
from mkdocs import utils
@@ -28,6 +29,13 @@ class Config(utils.UserDict):
self._schema = schema
self._schema_keys = set(dict(schema).keys())
# Ensure config_file_path is a Unicode string
if config_file_path is not None and not isinstance(config_file_path, utils.text_type):
try:
# Assume config_file_path is encoded with the file system encoding.
config_file_path = config_file_path.decode(encoding=sys.getfilesystemencoding())
except UnicodeDecodeError:
raise ValidationError("config_file_path is not a Unicode string.")
self.config_file_path = config_file_path
self.data = {}
+31 -58
View File
@@ -1,8 +1,8 @@
from __future__ import unicode_literals
from collections import Sequence
import os
from collections import namedtuple
import sys
from collections import Sequence, namedtuple
import markdown
from mkdocs import utils, theme, plugins
@@ -294,31 +294,29 @@ class FilesystemObject(Type):
def __init__(self, exists=False, **kwargs):
super(FilesystemObject, self).__init__(type_=utils.string_types, **kwargs)
self.exists = exists
self.config_dir = None
def pre_validation(self, config, key_name):
value = config[key_name]
if not value:
return
if os.path.isabs(value):
return
if config.config_file_path is None:
# Unable to determine absolute path of the config file; fall back
# to trusting the relative path
return
config_dir = os.path.dirname(config.config_file_path)
value = os.path.join(config_dir, value)
config[key_name] = value
self.config_dir = os.path.dirname(config.config_file_path) if config.config_file_path else None
def run_validation(self, value):
value = super(FilesystemObject, self).run_validation(value)
# PY2 only: Ensure value is a Unicode string. On PY3 byte strings fail
# the type test (super.run_validation) so we never get this far.
if not isinstance(value, utils.text_type):
try:
# Assume value is encoded with the file system encoding.
value = value.decode(encoding=sys.getfilesystemencoding())
except UnicodeDecodeError:
raise ValidationError("The path is not a Unicode string.")
if self.config_dir and not os.path.isabs(value):
value = os.path.join(self.config_dir, value)
if self.exists and not self.existence_test(value):
raise ValidationError("The path {path} isn't an existing {name}.".
format(path=value, name=self.name))
return os.path.abspath(value)
value = os.path.abspath(value)
assert isinstance(value, utils.text_type)
return value
class Dir(FilesystemObject):
@@ -392,6 +390,8 @@ class ThemeDir(Dir):
if config.get(key_name) is None:
return
super(ThemeDir, self).pre_validation(config, key_name)
warning = ('The configuration option {0} has been deprecated and will '
'be removed in a future release of MkDocs.')
self.warnings.append(warning)
@@ -518,15 +518,15 @@ class Extras(OptionallyRequired):
).format(key_name, "', '".join(actual_files)))
class Pages(OptionallyRequired):
class Nav(OptionallyRequired):
"""
Pages Config Option
Nav Config Option
Validate the pages config. Automatically add all markdown files if empty.
Validate the Nav config. Automatically add all markdown files if empty.
"""
def __init__(self, **kwargs):
super(Pages, self).__init__(**kwargs)
super(Nav, self).__init__(**kwargs)
self.file_match = utils.is_markdown_file
def run_validation(self, value):
@@ -546,42 +546,15 @@ class Pages(OptionallyRequired):
config_types, {utils.text_type, dict}
))
def walk_docs_dir(self, docs_dir):
if self.file_match is None:
raise StopIteration
for (dirpath, dirs, filenames) in os.walk(docs_dir, followlinks=True):
dirs.sort()
for filename in sorted(filenames):
fullpath = os.path.join(dirpath, filename)
# Some editors (namely Emacs) will create temporary symlinks
# for internal magic. We can just ignore these files.
if os.path.islink(fullpath):
local_fullpath = os.path.join(dirpath, os.readlink(fullpath))
if not os.path.exists(local_fullpath):
continue
relpath = os.path.normpath(os.path.relpath(fullpath, docs_dir))
if self.file_match(relpath):
yield relpath
def post_validation(self, config, key_name):
if config[key_name] is not None:
return
pages = []
for filename in self.walk_docs_dir(config['docs_dir']):
if os.path.splitext(filename)[0] == 'index':
pages.insert(0, filename)
else:
pages.append(filename)
config[key_name] = utils.nest_paths(pages)
# TODO: remove this when `pages` config setting is fully deprecated.
if key_name == 'pages' and config['pages'] is not None:
if config['nav'] is None:
# copy `pages` config to new 'nav' config setting
config['nav'] = config['pages']
warning = ("The 'pages' configuration option has been deprecated and will "
"be removed in a future release of MkDocs. Use 'nav' instead.")
self.warnings.append(warning)
class Private(OptionallyRequired):
+4 -3
View File
@@ -19,9 +19,10 @@ DEFAULT_SCHEMA = (
# The title to use for the documentation
('site_name', config_options.Type(utils.string_types, required=True)),
# Defines the structure of the navigation and which markdown files are
# included in the build.
('pages', config_options.Pages()),
# Defines the structure of the navigation.
('nav', config_options.Nav()),
# TODO: remove this when the `pages` config setting is fully deprecated.
('pages', config_options.Nav()),
# The full URL to where the documentation will be hosted
('site_url', config_options.URL()),
+3 -3
View File
@@ -69,17 +69,17 @@ class SearchIndex(object):
# Get the absolute URL for the page, this is then
# prepended to the urls of the sections
abs_url = page.abs_url
url = page.url
# Create an entry for the full page.
self._add_entry(
title=page.title,
text=self.strip_tags(page.content).rstrip('\n'),
loc=abs_url
loc=url
)
for section in parser.data:
self.create_entry_for_section(section, page.toc, abs_url)
self.create_entry_for_section(section, page.toc, url)
def create_entry_for_section(self, section, toc, abs_url):
"""
-416
View File
@@ -1,416 +0,0 @@
# coding: utf-8
"""
Deals with generating the site-wide navigation.
This consists of building a set of interlinked page and header objects.
"""
from __future__ import unicode_literals
import datetime
import logging
import markdown
import os
import io
from mkdocs import utils, exceptions, toc
from mkdocs.utils import meta
from mkdocs.relative_path_ext import RelativePathExtension
log = logging.getLogger(__name__)
def _filename_to_title(filename):
"""
Automatically generate a default title, given a filename.
"""
if utils.is_homepage(filename):
return 'Home'
return utils.filename_to_title(filename)
@meta.transformer()
def default(value):
""" By default, return all meta values as strings. """
return ' '.join(value)
class SiteNavigation(object):
def __init__(self, config):
self.url_context = URLContext()
self.file_context = FileContext()
self.nav_items, self.pages = _generate_site_navigation(
config, self.url_context)
self.homepage = self.pages[0] if self.pages else None
self.use_directory_urls = config['use_directory_urls']
def __str__(self):
return ''.join([str(item) for item in self])
def __iter__(self):
return iter(self.nav_items)
def __len__(self):
return len(self.nav_items)
def walk_pages(self):
"""
Returns each page in the site in turn.
Additionally this sets the active status of the pages and headers,
in the site navigation, so that the rendered navbar can correctly
highlight the currently active page and/or header item.
"""
page = self.homepage
page.set_active()
self.url_context.set_current_url(page.abs_url)
self.file_context.set_current_path(page.input_path)
yield page
while page.next_page:
page.set_active(False)
page = page.next_page
page.set_active()
self.url_context.set_current_url(page.abs_url)
self.file_context.set_current_path(page.input_path)
yield page
page.set_active(False)
@property
def source_files(self):
if not hasattr(self, '_source_files'):
self._source_files = set([page.input_path for page in self.pages])
return self._source_files
class URLContext(object):
"""
The URLContext is used to ensure that we can generate the appropriate
relative URLs to other pages from any given page in the site.
We use relative URLs so that static sites can be deployed to any location
without having to specify what the path component on the host will be
if the documentation is not hosted at the root path.
"""
def __init__(self):
self.base_path = '/'
self.force_abs_urls = False
def set_current_url(self, current_url):
self.base_path = os.path.dirname(current_url)
def make_relative(self, url):
"""
Given a URL path return it as a relative URL,
given the context of the current page.
"""
if self.force_abs_urls:
abs_url = '%s/%s' % (self.base_path.rstrip('/'), utils.path_to_url(url.lstrip('/')))
return abs_url
suffix = '/' if (url.endswith('/') and len(url) > 1) else ''
# Workaround for bug on `os.path.relpath()` in Python 2.6
if self.base_path == '/':
if url == '/':
# Workaround for static assets
return '.'
return url.lstrip('/')
# Under Python 2.6, relative_path adds an extra '/' at the end.
relative_path = os.path.relpath(url, start=self.base_path)
relative_path = relative_path.rstrip('/') + suffix
return utils.path_to_url(relative_path)
class FileContext(object):
"""
The FileContext is used to ensure that we can generate the appropriate
full path for other pages given their relative path from a particular page.
This is used when we have relative hyperlinks in the documentation, so that
we can ensure that they point to markdown documents that actually exist
in the `pages` config.
"""
def __init__(self):
self.current_file = None
self.base_path = ''
def set_current_path(self, current_path):
self.current_file = current_path
self.base_path = os.path.dirname(current_path)
def make_absolute(self, path):
"""
Given a relative file path return it as a POSIX-style
absolute filepath, given the context of the current page.
"""
return os.path.normpath(os.path.join(self.base_path, path))
class Page(object):
def __init__(self, title, path, url_context, config):
self._title = title
self.abs_url = utils.get_url_path(path, config['use_directory_urls'])
self.active = False
self.url_context = url_context
# Support SOURCE_DATE_EPOCH environment variable for "reproducible" builds.
# See https://reproducible-builds.org/specs/source-date-epoch/
if 'SOURCE_DATE_EPOCH' in os.environ:
self.update_date = datetime.datetime.utcfromtimestamp(
int(os.environ['SOURCE_DATE_EPOCH'])
).strftime("%Y-%m-%d")
else:
self.update_date = datetime.datetime.now().strftime("%Y-%m-%d")
# Relative and absolute paths to the input markdown file and output html file.
self.input_path = path
self.output_path = utils.get_html_path(path)
self.abs_input_path = os.path.join(config['docs_dir'], self.input_path)
self.abs_output_path = os.path.join(config['site_dir'], self.output_path)
self.canonical_url = None
if config['site_url']:
self._set_canonical_url(config['site_url'])
self.edit_url = None
if config['repo_url'] and config['edit_uri']:
self._set_edit_url(config['repo_url'], config['edit_uri'])
# Placeholders to be filled in later in the build
# process when we have access to the config.
self.markdown = ''
self.meta = {}
self.content = None
self.toc = None
self.previous_page = None
self.next_page = None
self.ancestors = []
def __eq__(self, other):
def sub_dict(d):
return dict((key, value) for key, value in d.items()
if key in ['title', 'input_path', 'abs_url'])
return (isinstance(other, self.__class__)
and sub_dict(self.__dict__) == sub_dict(other.__dict__))
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
return self.indent_print()
def __repr__(self):
return "nav.Page(title='{0}', input_path='{1}', url='{2}')".format(
self.title, self.input_path, self.abs_url)
@property
def title(self):
"""
Get the title for a Markdown document
Check these in order and return the first that has a valid title:
- self._title which is populated from the mkdocs.yml
- self.meta['title'] which comes from the page metadata
- self.markdown - look for the first H1
- self.input_path - create a title based on the filename
"""
if self._title is not None:
return self._title
elif 'title' in self.meta:
return self.meta['title']
title = utils.get_markdown_title(self.markdown)
if title is not None:
return title
return _filename_to_title(self.input_path.split(os.path.sep)[-1])
@property
def url(self):
return self.url_context.make_relative(self.abs_url)
@property
def is_homepage(self):
return utils.is_homepage(self.input_path)
@property
def is_top_level(self):
return len(self.ancestors) == 0
def read_source(self, config):
source = config['plugins'].run_event(
'page_read_source', None, config=config, page=self)
if source is None:
try:
with io.open(self.abs_input_path, 'r', encoding='utf-8-sig') as f:
source = f.read()
except IOError:
log.error('File not found: %s', self.abs_input_path)
raise
self.markdown, self.meta = meta.get_data(source)
def _set_canonical_url(self, base):
if not base.endswith('/'):
base += '/'
self.canonical_url = utils.urljoin(base, self.abs_url.lstrip('/'))
def _set_edit_url(self, repo_url, edit_uri):
# Normalize URL from Windows path '\\' -> '/'
input_path_url = self.input_path.replace('\\', '/')
self.edit_url = utils.urljoin(repo_url, edit_uri + input_path_url)
def indent_print(self, depth=0):
indent = ' ' * depth
active_marker = ' [*]' if self.active else ''
title = self.title if (self.title is not None) else '[blank]'
return '%s%s - %s%s\n' % (indent, title, self.abs_url, active_marker)
def set_active(self, active=True):
self.active = active
for ancestor in self.ancestors:
ancestor.set_active(active)
def render(self, config, site_navigation=None):
"""
Convert the Markdown source file to HTML as per the config and
site_navigation.
"""
extensions = [
RelativePathExtension(site_navigation, config['strict'])
] + config['markdown_extensions']
md = markdown.Markdown(
extensions=extensions,
extension_configs=config['mdx_configs'] or {}
)
self.content = md.convert(self.markdown)
self.toc = toc.TableOfContents(getattr(md, 'toc', ''))
class Header(object):
def __init__(self, title, children):
self.title, self.children = title, children
self.active = False
self.ancestors = []
def __str__(self):
return self.indent_print()
@property
def is_top_level(self):
return len(self.ancestors) == 0
def indent_print(self, depth=0):
indent = ' ' * depth
active_marker = ' [*]' if self.active else ''
ret = '%s%s%s\n' % (indent, self.title, active_marker)
for item in self.children:
ret += item.indent_print(depth + 1)
return ret
def set_active(self, active=True):
self.active = active
for ancestor in self.ancestors:
ancestor.set_active(active)
def _follow(config_line, url_context, config, header=None, title=None):
if isinstance(config_line, utils.string_types):
path = os.path.normpath(config_line)
page = Page(title, path, url_context, config)
if header:
page.ancestors = header.ancestors + [header, ]
header.children.append(page)
yield page
raise StopIteration
elif not isinstance(config_line, dict):
msg = ("Line in 'page' config is of type {0}, dict or string "
"expected. Config: {1}").format(type(config_line), config_line)
raise exceptions.ConfigurationError(msg)
if len(config_line) > 1:
raise exceptions.ConfigurationError(
"Page configs should be in the format 'name: markdown.md'. The "
"config contains an invalid entry: {0}".format(config_line))
elif len(config_line) == 0:
log.warning("Ignoring empty line in the pages config.")
raise StopIteration
next_cat_or_title, subpages_or_path = next(iter(config_line.items()))
if isinstance(subpages_or_path, utils.string_types):
path = subpages_or_path
for sub in _follow(path, url_context, config, header=header, title=next_cat_or_title):
yield sub
raise StopIteration
elif not isinstance(subpages_or_path, list):
msg = ("Line in 'page' config is of type {0}, list or string "
"expected for sub pages. Config: {1}"
).format(type(config_line), config_line)
raise exceptions.ConfigurationError(msg)
next_header = Header(title=next_cat_or_title, children=[])
if header:
next_header.ancestors = [header]
header.children.append(next_header)
yield next_header
subpages = subpages_or_path
for subpage in subpages:
for sub in _follow(subpage, url_context, config, next_header):
yield sub
def _generate_site_navigation(config, url_context):
"""
Returns a list of Page and Header instances that represent the
top level site navigation.
"""
nav_items = []
pages = []
previous = None
for config_line in config['pages']:
for page_or_header in _follow(
config_line, url_context, config):
if isinstance(page_or_header, Header):
if page_or_header.is_top_level:
nav_items.append(page_or_header)
elif isinstance(page_or_header, Page):
if page_or_header.is_top_level:
nav_items.append(page_or_header)
pages.append(page_or_header)
if previous:
page_or_header.previous_page = previous
previous.next_page = page_or_header
previous = page_or_header
if len(pages) == 0:
raise exceptions.ConfigurationError(
"No pages found in the pages config. "
"Remove it entirely to enable automatic page discovery.")
return (nav_items, pages)
+1 -1
View File
@@ -18,7 +18,7 @@ log = logging.getLogger('mkdocs.plugins')
EVENTS = (
'config', 'pre_build', 'nav', 'env', 'pre_template', 'template_context',
'config', 'pre_build', 'files', 'nav', 'env', 'pre_template', 'template_context',
'post_template', 'pre_page', 'page_read_source', 'page_markdown',
'page_content', 'page_context', 'post_page', 'post_build', 'serve'
)
View File
+266
View File
@@ -0,0 +1,266 @@
# coding: utf-8
from __future__ import unicode_literals
import fnmatch
import os
import logging
from functools import cmp_to_key
from mkdocs import utils
log = logging.getLogger(__name__)
class Files(object):
""" A collection of File objects. """
def __init__(self, files):
self._files = files
self.src_paths = {file.src_path: file for file in files}
def __iter__(self):
return iter(self._files)
def __len__(self):
return len(self._files)
def __contains__(self, path):
return path in self.src_paths
def get_file_from_path(self, path):
""" Return a File instance with File.src_path equal to path. """
return self.src_paths.get(os.path.normpath(path))
def append(self, file):
""" Append file to Files collection. """
self._files.append(file)
self.src_paths[file.src_path] = file
def copy_static_files(self, dirty=False):
""" Copy static files from source to destination. """
for file in self:
if not file.is_documentation_page():
file.copy_file(dirty)
def documentation_pages(self):
""" Return iterable of all Markdown page file objects. """
return [file for file in self if file.is_documentation_page()]
def static_pages(self):
""" Return iterable of all static page file objects. """
return [file for file in self if file.is_static_page()]
def media_files(self):
""" Return iterable of all file objects which are not documentation or static pages. """
return [file for file in self if file.is_media_file()]
def javascript_files(self):
""" Return iterable of all javascript file objects. """
return [file for file in self if file.is_javascript()]
def css_files(self):
""" Return iterable of all CSS file objects. """
return [file for file in self if file.is_css()]
def add_files_from_theme(self, env, config):
""" Retrieve static files from Jinja environment and add to collection. """
def filter(name):
patterns = ['.*', '*.py', '*.pyc', '*.html', 'mkdocs_theme.yml']
patterns.extend(config['theme'].static_templates)
for pattern in patterns:
if fnmatch.fnmatch(name, pattern):
return False
return True
for path in env.list_templates(filter_func=filter):
for dir in config['theme'].dirs:
# Find the first theme dir which contains path
if os.path.isfile(os.path.join(dir, path)):
self.append(File(path, dir, config['site_dir'], config['use_directory_urls']))
break
class File(object):
"""
A MkDocs File object.
Points to the source and destination locations of a file.
The `path` argument must be a path that exists relative to `src_dir`.
The `src_dir` and `dest_dir` must be absolute paths on the local file system.
The `use_directory_urls` argument controls how destination paths are generated. If `False`, a Markdown file is
mapped to an HTML file of the same name (the file extension is changed to `.html`). If True, a Markdown file is
mapped to an HTML index file (`index.html`) nested in a directory using the "name" of the file in `path`. The
`use_directory_urls` argument has no effect on non-Markdown files.
File objects have the following properties, which are Unicode strings:
File.src_path
The pure path of the source file relative to the source directory.
File.abs_src_path
The absolute concrete path of the source file.
File.dest_path
The pure path of the destination file relative to the destination directory.
File.abs_dest_path
The absolute concrete path of the destination file.
File.url
The url of the destination file relative to the destination directory as a string.
"""
def __init__(self, path, src_dir, dest_dir, use_directory_urls):
self.page = None
self.src_path = os.path.normpath(path)
self.abs_src_path = os.path.normpath(os.path.join(src_dir, self.src_path))
self.name = self._get_stem()
self.dest_path = self._get_dest_path(use_directory_urls)
self.abs_dest_path = os.path.normpath(os.path.join(dest_dir, self.dest_path))
self.url = self._get_url(use_directory_urls)
def __eq__(self, other):
def sub_dict(d):
return dict((key, value) for key, value in d.items() if key in ['src_path', 'abs_src_path', 'url'])
return (isinstance(other, self.__class__) and sub_dict(self.__dict__) == sub_dict(other.__dict__))
def __ne__(self, other):
return not self.__eq__(other)
def _get_stem(self):
""" Return the name of the file without it's extension. """
filename = os.path.basename(self.src_path)
stem, ext = os.path.splitext(filename)
return 'index' if stem in ('index', 'README') else stem
def _get_dest_path(self, use_directory_urls):
""" Return destination path based on source path. """
if self.is_documentation_page():
if use_directory_urls:
parent, filename = os.path.split(self.src_path)
if self.name == 'index':
# index.md or README.md => index.html
return os.path.join(parent, 'index.html')
else:
# foo.md => foo/index.html
return os.path.join(parent, self.name, 'index.html')
else:
# foo.md => foo.html
root, ext = os.path.splitext(self.src_path)
return root + '.html'
return self.src_path
def _get_url(self, use_directory_urls):
""" Return url based in destination path. """
url = self.dest_path.replace(os.path.sep, '/')
dirname, filename = os.path.split(url)
if use_directory_urls and filename == 'index.html':
if dirname == '':
url = '.'
else:
url = dirname + '/'
return url
def url_relative_to(self, other):
""" Return url for file relative to other file. """
return utils.get_relative_url(self.url, other.url if isinstance(other, File) else other)
def copy_file(self, dirty=False):
""" Copy source file to destination, ensuring parent directories exist. """
if dirty and not self.is_modified():
log.debug("Skip copying unmodified file: '{}'".format(self.src_path))
else:
log.debug("Copying media file: '{}'".format(self.src_path))
utils.copy_file(self.abs_src_path, self.abs_dest_path)
def is_modified(self):
if os.path.isfile(self.abs_dest_path):
return os.path.getmtime(self.abs_dest_path) < os.path.getmtime(self.abs_src_path)
return True
def is_documentation_page(self):
""" Return True if file is a Markdown page. """
return os.path.splitext(self.src_path)[1] in utils.markdown_extensions
def is_static_page(self):
""" Return True if file is a static page (html, xml, json). """
return os.path.splitext(self.src_path)[1] in (
'.html',
'.htm',
'.xml',
'.json',
)
def is_media_file(self):
""" Return True if file is not a documentation or static page. """
return not (self.is_documentation_page() or self.is_static_page())
def is_javascript(self):
""" Return True if file is a JavaScript file. """
return os.path.splitext(self.src_path)[1] in (
'.js',
'.javascript',
)
def is_css(self):
""" Return True if file is a CSS file. """
return os.path.splitext(self.src_path)[1] in (
'.css',
)
def get_files(config):
""" Walk the `docs_dir` and return a Files collection. """
files = []
exclude = ['.*', '/templates']
for source_dir, dirnames, filenames in os.walk(config['docs_dir'], followlinks=True):
relative_dir = os.path.relpath(source_dir, config['docs_dir'])
for dirname in list(dirnames):
path = os.path.normpath(os.path.join(relative_dir, dirname))
# Skip any excluded directories
if _filter_paths(basename=dirname, path=path, is_dir=True, exclude=exclude):
dirnames.remove(dirname)
dirnames.sort()
for filename in _sort_files(filenames):
path = os.path.normpath(os.path.join(relative_dir, filename))
# Skip any excluded files
if _filter_paths(basename=filename, path=path, is_dir=False, exclude=exclude):
continue
files.append(File(path, config['docs_dir'], config['site_dir'], config['use_directory_urls']))
return Files(files)
def _sort_files(filenames):
""" Always sort `index` as first filename in list. """
def compare(x, y):
if x == y:
return 0
if os.path.splitext(y)[0] == 'index':
return 1
if os.path.splitext(x)[0] == 'index' or x < y:
return -1
return 1
return sorted(filenames, key=cmp_to_key(compare))
def _filter_paths(basename, path, is_dir, exclude):
""" .gitignore style file filtering. """
for item in exclude:
# Items ending in '/' apply only to directories.
if item.endswith('/') and not is_dir:
continue
# Items starting with '/' apply to the whole path.
# In any other cases just the basename is used.
match = path if item.startswith('/') else basename
if fnmatch.fnmatch(match, item.strip('/')):
return True
return False
+182
View File
@@ -0,0 +1,182 @@
# coding: utf-8
from __future__ import unicode_literals
import logging
from mkdocs.structure.pages import Page
from mkdocs.utils import string_types, nest_paths
log = logging.getLogger(__name__)
class Navigation(object):
def __init__(self, items, pages):
self.items = items # Nested List with full navigation of Sections, Pages, and Links.
self.pages = pages # Flat List of subset of Pages in nav, in order.
self.homepage = None
for page in pages:
if page.is_homepage:
self.homepage = page
break
def __repr__(self):
return '\n'.join([item._indent_print() for item in self])
def __iter__(self):
return iter(self.items)
def __len__(self):
return len(self.items)
class Section(object):
def __init__(self, title, children):
self.title = title
self.children = children
self.parent = None
self.active = False
self.is_section = True
self.is_page = False
self.is_link = False
def __repr__(self):
return "Section(title='{0}')".format(self.title)
def _get_active(self):
""" Return active status of section. """
return self.__active
def _set_active(self, value):
""" Set active status of section and ancestors. """
self.__active = bool(value)
if self.parent is not None:
self.parent.active = bool(value)
active = property(_get_active, _set_active)
@property
def ancestors(self):
if self.parent is None:
return []
return [self.parent] + self.parent.ancestors
def _indent_print(self, depth=0):
ret = ['{}{}'.format(' ' * depth, repr(self))]
for item in self.children:
ret.append(item._indent_print(depth + 1))
return '\n'.join(ret)
class Link(object):
def __init__(self, title, url):
self.title = title
self.url = url
self.parent = None
# These should never change but are included for consistency with sections and pages.
self.children = None
self.active = False
self.is_section = False
self.is_page = False
self.is_link = True
def __repr__(self):
title = "'{}'".format(self.title) if (self.title is not None) else '[blank]'
return "Link(title={}, url='{}')".format(title, self.url)
@property
def ancestors(self):
if self.parent is None:
return []
return [self.parent] + self.parent.ancestors
def _indent_print(self, depth=0):
return '{}{}'.format(' ' * depth, repr(self))
def get_navigation(files, config):
""" Build site navigation from config and files."""
nav_config = config['nav'] or nest_paths(f.src_path for f in files.documentation_pages())
items = _data_to_navigation(nav_config, files, config)
if not isinstance(items, list):
items = [items]
# Get only the pages from the navigation, ignoring any sections and links.
pages = _get_by_type(items, Page)
# Include next, previous and parent links.
_add_previous_and_next_links(pages)
_add_parent_links(items)
missing_from_config = [file for file in files.documentation_pages() if file.page is None]
if missing_from_config:
log.info(
'The following pages exist in the docs directory, but are not '
'included in the "nav" configuration:\n - {}'.format(
'\n - '.join([file.src_path for file in missing_from_config]))
)
# Any documentation files not found in the nav should still have an associated page.
# However, these page objects are only accessable from File instances as `file.page`.
for file in missing_from_config:
Page(None, file, config)
links = _get_by_type(items, Link)
if links:
# Assume all links are external.
# TODO: warn or error on internal links?
log.info(
'The following paths are included in the "nav" configuration, '
'but do not exist in the docs directory:\n - {}'.format(
'\n - '.join([link.url for link in links]))
)
return Navigation(items, pages)
def _data_to_navigation(data, files, config):
if isinstance(data, dict):
return [
_data_to_navigation((key, value), files, config)
if isinstance(value, string_types) else
Section(title=key, children=_data_to_navigation(value, files, config))
for key, value in data.items()
]
elif isinstance(data, list):
return [
_data_to_navigation(item, files, config)[0]
if isinstance(item, dict) and len(item) == 1 else
_data_to_navigation(item, files, config)
for item in data
]
title, path = data if isinstance(data, tuple) else (None, data)
file = files.get_file_from_path(path)
if file:
return Page(title, file, config)
return Link(title, path)
def _get_by_type(nav, T):
ret = []
for item in nav:
if isinstance(item, T):
ret.append(item)
elif item.children:
ret.extend(_get_by_type(item.children, T))
return ret
def _add_parent_links(nav):
for item in nav:
if item.is_section:
for child in item.children:
child.parent = item
_add_parent_links(item.children)
def _add_previous_and_next_links(pages):
bookended = [None] + pages + [None]
zipped = zip(bookended[:-2], bookended[1:-1], bookended[2:])
for page0, page1, page2 in zipped:
page1.previous_page, page1.next_page = page0, page2
+266
View File
@@ -0,0 +1,266 @@
# coding: utf-8
from __future__ import unicode_literals
import os
import io
import datetime
import logging
import markdown
from markdown.extensions import Extension
from markdown.treeprocessors import Treeprocessor
from markdown.util import AMP_SUBSTITUTE
from mkdocs.structure.toc import get_toc
from mkdocs.utils import meta, urlparse, urlunparse, urljoin, get_markdown_title
from mkdocs.exceptions import MarkdownNotFound
log = logging.getLogger(__name__)
@meta.transformer()
def default(value):
""" By default, return all meta values as strings. """
return ' '.join(value)
class Page(object):
def __init__(self, title, file, config):
file.page = self
self.file = file
self.title = title
# Navigation attributes
self.parent = None
self.children = None
self.previous_page = None
self.next_page = None
self.active = False
self.is_section = False
self.is_page = True
self.is_link = False
# Support SOURCE_DATE_EPOCH environment variable for "reproducible" builds.
# See https://reproducible-builds.org/specs/source-date-epoch/
if 'SOURCE_DATE_EPOCH' in os.environ:
self.update_date = datetime.datetime.utcfromtimestamp(
int(os.environ['SOURCE_DATE_EPOCH'])
).strftime("%Y-%m-%d")
else:
self.update_date = datetime.datetime.now().strftime("%Y-%m-%d")
self._set_canonical_url(config.get('site_url', None))
self._set_edit_url(config.get('repo_url', None), config.get('edit_uri', None))
# Placeholders to be filled in later in the build process.
self.markdown = None
self.content = None
self.toc = []
self.meta = {}
def __eq__(self, other):
def sub_dict(d):
return dict((key, value) for key, value in d.items() if key in ['title', 'file'])
return (isinstance(other, self.__class__) and sub_dict(self.__dict__) == sub_dict(other.__dict__))
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
title = "'{}'".format(self.title) if (self.title is not None) else '[blank]'
return "Page(title={}, url='{}')".format(title, self.abs_url or self.file.url)
def _indent_print(self, depth=0):
return '{}{}'.format(' ' * depth, repr(self))
def _get_active(self):
""" Return active status of page. """
return self.__active
def _set_active(self, value):
""" Set active status of page and ancestors. """
self.__active = bool(value)
if self.parent is not None:
self.parent.active = bool(value)
active = property(_get_active, _set_active)
@property
def is_index(self):
return self.file.name == 'index'
@property
def is_top_level(self):
return self.parent is None
@property
def is_homepage(self):
return self.is_top_level and self.is_index
@property
def url(self):
return '' if self.file.url == '.' else self.file.url
@property
def ancestors(self):
if self.parent is None:
return []
return [self.parent] + self.parent.ancestors
def _set_canonical_url(self, base):
if base:
if not base.endswith('/'):
base += '/'
self.canonical_url = urljoin(base, self.url)
self.abs_url = urlparse(self.canonical_url).path
else:
self.canonical_url = None
self.abs_url = None
def _set_edit_url(self, repo_url, edit_uri):
if repo_url and edit_uri:
src_path = self.file.src_path.replace('\\', '/')
self.edit_url = urljoin(repo_url, edit_uri + src_path)
else:
self.edit_url = None
def read_source(self, config):
source = config['plugins'].run_event('page_read_source', None, config=config, page=self)
if source is None:
try:
with io.open(self.file.abs_src_path, 'r', encoding='utf-8-sig', errors='strict') as f:
source = f.read()
except IOError:
log.error('File not found: {}'.format(self.file.src_path))
raise
except ValueError:
log.error('Encoding error reading file: {}'.format(self.file.src_path))
raise
self.markdown, self.meta = meta.get_data(source)
self._set_title()
def _set_title(self):
"""
Set the title for a Markdown document.
Check these in order and use the first that returns a valid title:
- value provided on init (passed in from config)
- value of metadata 'title'
- content of the first H1 in Markdown content
- convert filename to title
"""
if self.title is not None:
return
if 'title' in self.meta:
self.title = self.meta['title']
return
title = get_markdown_title(self.markdown)
if title is None:
if self.is_homepage:
title = 'Home'
else:
title = self.file.name.replace('-', ' ').replace('_', ' ')
# Capitalize if the filename was all lowercase, otherwise leave it as-is.
if title.lower() == title:
title = title.capitalize()
self.title = title
def render(self, config, files):
"""
Convert the Markdown source file to HTML as per the config.
"""
extensions = [
_RelativePathExtension(self.file, files, config['strict'])
] + config['markdown_extensions']
md = markdown.Markdown(
extensions=extensions,
extension_configs=config['mdx_configs'] or {}
)
self.content = md.convert(self.markdown)
self.toc = get_toc(getattr(md, 'toc', ''))
class _RelativePathTreeprocessor(Treeprocessor):
def __init__(self, file, files, strict):
self.file = file
self.files = files
self.strict = strict
def run(self, root):
"""
Update urls on anchors and images to make them relative
Iterates through the full document tree looking for specific
tags and then makes them relative based on the site navigation
"""
for element in root.iter():
if element.tag == 'a':
key = 'href'
elif element.tag == 'img':
key = 'src'
else:
continue
url = element.get(key)
new_url = self.path_to_url(url)
element.set(key, new_url)
return root
def path_to_url(self, url):
scheme, netloc, path, params, query, fragment = urlparse(url)
if scheme or netloc or not path or AMP_SUBSTITUTE in url or '.' not in os.path.split(path)[-1]:
# Ignore URLs unless they are a relative link to a source file.
# AMP_SUBSTITUTE is used internally by Markdown only for email.
# No '.' in the last part of a path indicates path does not point to a file.
return url
# Determine the filepath of the target.
target_path = os.path.join(os.path.dirname(self.file.src_path), path)
target_path = os.path.normpath(target_path).lstrip(os.sep)
# Validate that the target exists in files collection.
if target_path not in self.files:
msg = (
"Documentation file '{}' contains a link to '{}' which does not exist "
"in the documentation directory.".format(self.file.src_path, target_path)
)
# In strict mode raise an error at this point.
if self.strict:
raise MarkdownNotFound(msg)
# Otherwise, when strict mode isn't enabled, log a warning
# to the user and leave the URL as it is.
log.warning(msg)
return url
target_file = self.files.get_file_from_path(target_path)
path = target_file.url_relative_to(self.file)
components = (scheme, netloc, path, params, query, fragment)
return urlunparse(components)
class _RelativePathExtension(Extension):
"""
The Extension class is what we pass to markdown, it then
registers the Treeprocessor.
"""
def __init__(self, file, files, strict):
self.file = file
self.files = files
self.strict = strict
def extendMarkdown(self, md, md_globals):
relpath = _RelativePathTreeprocessor(self.file, self.files, self.strict)
md.treeprocessors.add("relpath", relpath, "_end")
+131
View File
@@ -0,0 +1,131 @@
# coding: utf-8
"""
Deals with generating the per-page table of contents.
For the sake of simplicity we use an existing markdown extension to generate
an HTML table of contents, and then parse that into the underlying data.
"""
from __future__ import unicode_literals
try: # pragma: no cover
from html.parser import HTMLParser # noqa
except ImportError: # pragma: no cover
from HTMLParser import HTMLParser # noqa
def get_toc(toc_html):
items = _parse_html_table_of_contents(toc_html)
return TableOfContents(items)
class TableOfContents(object):
"""
Represents the table of contents for a given page.
"""
def __init__(self, items):
self.items = items
def __iter__(self):
return iter(self.items)
def __len__(self):
return len(self.items)
def __str__(self):
return ''.join([str(item) for item in self])
class AnchorLink(object):
"""
A single entry in the table of contents.
"""
def __init__(self, title, url):
self.title, self.url = title, url
self.children = []
def __str__(self):
return self.indent_print()
def indent_print(self, depth=0):
indent = ' ' * depth
ret = '%s%s - %s\n' % (indent, self.title, self.url)
for item in self.children:
ret += item.indent_print(depth + 1)
return ret
class _TOCParser(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.links = []
self.in_anchor = False
self.attrs = None
self.title = ''
# Prior to Python3.4 no convert_charrefs keyword existed.
# However, in Python3.5 the default was changed to True.
# We need the False behavior in all versions but can only
# set it if it exists.
if hasattr(self, 'convert_charrefs'): # pragma: no cover
self.convert_charrefs = False
def handle_starttag(self, tag, attrs):
if not self.in_anchor:
if tag == 'a':
self.in_anchor = True
self.attrs = dict(attrs)
def handle_endtag(self, tag):
if tag == 'a':
self.in_anchor = False
def handle_data(self, data):
if self.in_anchor:
self.title += data
def handle_charref(self, ref):
self.handle_entityref("#" + ref)
def handle_entityref(self, ref):
self.handle_data("&%s;" % ref)
def _parse_html_table_of_contents(html):
"""
Given a table of contents string that has been automatically generated by
the markdown library, parse it into a tree of AnchorLink instances.
Returns a list of all the parent AnchorLink instances.
"""
lines = html.splitlines()[2:-2]
parents = []
ret = []
for line in lines:
parser = _TOCParser()
parser.feed(line)
if parser.title:
try:
href = parser.attrs['href']
except KeyError:
continue
title = parser.title
nav = AnchorLink(title, href)
# Add the item to its parent if required. If it is a topmost
# item then instead append it to our return value.
if parents:
parents[-1].children.append(nav)
else:
ret.append(nav)
# If this item has children, store it as the current parent
if line.endswith('<ul>'):
parents.append(nav)
elif line.startswith('</ul>'):
if parents:
parents.pop()
# For the table of contents, always mark the first element as active
if ret:
ret[0].active = True
return ret
+99 -4
View File
@@ -5,9 +5,15 @@ import os
import logging
import collections
import unittest
from functools import wraps
try:
# py>=3.2
from tempfile import TemporaryDirectory
except ImportError:
from backports.tempfile import TemporaryDirectory
from mkdocs import toc
from mkdocs import config
from mkdocs import utils
@@ -16,11 +22,11 @@ def dedent(text):
return textwrap.dedent(text).strip()
def markdown_to_toc(markdown_source):
def get_markdown_toc(markdown_source):
""" Return TOC generated by Markdown parser from Markdown source text. """
md = markdown.Markdown(extensions=['toc'])
md.convert(markdown_source)
toc_output = md.toc
return toc.TableOfContents(toc_output)
return md.toc
def load_config(**cfg):
@@ -44,6 +50,95 @@ def load_config(**cfg):
return conf
def tempdir(files=None, **kw):
"""
A decorator for building a temporary directory with prepopulated files.
The temproary directory and files are created just before the wrapped function is called and are destroyed
imediately after the wrapped function returns.
The `files` keyword should be a dict of file paths as keys and strings of file content as values.
If `files` is a list, then each item is assumed to be a path of an empty file. All other
keywords are passed to `tempfile.TemporaryDirectory` to create the parent directory.
In the following example, two files are created in the temporary directory and then are destroyed when
the function exits:
@tempdir(files={
'foo.txt': 'foo content',
'bar.txt': 'bar content'
})
def example(self, tdir):
assert os.path.isfile(os.path.join(tdir, 'foo.txt'))
pth = os.path.join(tdir, 'bar.txt')
assert os.path.isfile(pth)
with io.open(pth, 'r', encoding='utf-8') as f:
assert f.read() == 'bar content'
"""
files = {f: '' for f in files} if isinstance(files, (list, tuple)) else files or {}
if 'prefix' not in kw:
kw['prefix'] = 'mkdocs_test-'
def decorator(fn):
@wraps(fn)
def wrapper(self, *args):
with TemporaryDirectory(**kw) as td:
for path, content in files.items():
pth = os.path.join(td, path)
utils.write_file(content.encode(encoding='utf-8'), pth)
return fn(self, td, *args)
return wrapper
return decorator
class PathAssertionMixin(object):
"""
Assertion methods for testing paths.
Each method accepts one or more strings, which are first joined using os.path.join.
"""
def assertPathsEqual(self, a, b, msg=None):
self.assertEqual(a.replace('\\', '/'), b.replace('\\', '/'))
def assertPathExists(self, *parts):
path = os.path.join(*parts)
if not os.path.exists(path):
msg = self._formatMessage(None, "The path '{}' does not exist".format(path))
raise self.failureException(msg)
def assertPathNotExists(self, *parts):
path = os.path.join(*parts)
if os.path.exists(path):
msg = self._formatMessage(None, "The path '{}' does exist".format(path))
raise self.failureException(msg)
def assertPathIsFile(self, *parts):
path = os.path.join(*parts)
if not os.path.isfile(path):
msg = self._formatMessage(None, "The path '{}' is not a file that exists".format(path))
raise self.failureException(msg)
def assertPathNotFile(self, *parts):
path = os.path.join(*parts)
if os.path.isfile(path):
msg = self._formatMessage(None, "The path '{}' is a file that exists".format(path))
raise self.failureException(msg)
def assertPathIsDir(self, *parts):
path = os.path.join(*parts)
if not os.path.isdir(path):
msg = self._formatMessage(None, "The path '{}' is not a directory that exists".format(path))
raise self.failureException(msg)
def assertPathNotDir(self, *parts):
path = os.path.join(*parts)
if os.path.isfile(path):
msg = self._formatMessage(None, "The path '{}' is a directory that exists".format(path))
raise self.failureException(msg)
# Backport unittest.TestCase.assertLogs for Python 2.7
# see https://github.com/python/cpython/blob/3.6/Lib/unittest/case.py
+439 -449
View File
@@ -2,482 +2,472 @@
# coding: utf-8
from __future__ import unicode_literals
import os
import unittest
import mock
import io
try:
from itertools import izip as zip
except ImportError:
# In Py3 use builtin zip function
pass
try:
# py>=3.2
from tempfile import TemporaryDirectory
except ImportError:
from backports.tempfile import TemporaryDirectory
from mkdocs import nav
from mkdocs.structure.pages import Page
from mkdocs.structure.files import File, Files
from mkdocs.structure.nav import get_navigation
from mkdocs.commands import build
from mkdocs.exceptions import MarkdownNotFound
from mkdocs.tests.base import dedent, load_config
from mkdocs.tests.base import load_config, LogTestCase, tempdir, PathAssertionMixin
from mkdocs.utils import meta
def build_page(title, path, config, md_src=None):
def build_page(title, path, config, md_src=''):
""" Helper which returns a Page object. """
sitenav = nav.SiteNavigation(config)
page = nav.Page(title, path, sitenav.url_context, config)
if md_src:
# Fake page.read_source()
page.markdown, page.meta = meta.get_data(md_src)
return page, sitenav
files = Files([File(path, config['docs_dir'], config['site_dir'], config['use_directory_urls'])])
page = Page(title, list(files)[0], config)
# Fake page.read_source()
page.markdown, page.meta = meta.get_data(md_src)
return page, files
class BuildTests(unittest.TestCase):
class BuildTests(PathAssertionMixin, LogTestCase):
def test_empty_document(self):
config = load_config(pages=[{'Home': 'index.md'}])
page, nav = build_page(None, 'index.md', config)
page.render(config, nav)
# Test build.get_context
self.assertEqual(page.content, '')
self.assertEqual(len(list(page.toc)), 0)
self.assertEqual(page.meta, {})
self.assertEqual(page.title, 'Home')
def test_convert_markdown(self):
"""
Ensure that basic Markdown -> HTML and TOC works.
"""
md_text = dedent("""
title: custom title
# Heading 1
This is some text.
# Heading 2
And some more text.
""")
config = load_config(pages=[{'Home': 'index.md'}])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
expected_html = dedent("""
<h1 id="heading-1">Heading 1</h1>
<p>This is some text.</p>
<h1 id="heading-2">Heading 2</h1>
<p>And some more text.</p>
""")
expected_toc = dedent("""
Heading 1 - #heading-1
Heading 2 - #heading-2
""")
expected_meta = {'title': 'custom title'}
self.assertEqual(page.content.strip(), expected_html)
self.assertEqual(str(page.toc).strip(), expected_toc)
self.assertEqual(page.meta, expected_meta)
self.assertEqual(page.title, 'custom title')
def test_convert_internal_link(self):
md_text = 'An [internal link](internal.md) to another document.'
expected = '<p>An <a href="internal/">internal link</a> to another document.</p>'
config = load_config(pages=['index.md', 'internal.md'])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected.strip())
def test_convert_multiple_internal_links(self):
md_text = '[First link](first.md) [second link](second.md).'
expected = '<p><a href="first/">First link</a> <a href="second/">second link</a>.</p>'
config = load_config(pages=['index.md', 'first.md', 'second.md'])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected.strip())
def test_convert_internal_link_differing_directory(self):
md_text = 'An [internal link](../internal.md) to another document.'
expected = '<p>An <a href="../internal/">internal link</a> to another document.</p>'
config = load_config(pages=['foo/bar.md', 'internal.md'])
page, nav = build_page(None, 'foo/bar.md', config, md_text)
page.render(config)
self.assertEqual(page.content.strip(), expected.strip())
def test_convert_internal_link_with_anchor(self):
md_text = 'An [internal link](internal.md#section1.1) to another document.'
expected = '<p>An <a href="internal/#section1.1">internal link</a> to another document.</p>'
config = load_config(pages=['index.md', 'internal.md'])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected.strip())
def test_convert_internal_media(self):
"""Test relative image URL's are the same for different base_urls"""
pages = [
'index.md',
'internal.md',
'sub/internal.md',
def test_context_base_url_homepage(self):
nav_cfg = [
{'Home': 'index.md'}
]
config = load_config(pages=pages)
site_navigation = nav.SiteNavigation(config)
expected_results = (
'./img/initial-layout.png',
'../img/initial-layout.png',
'../img/initial-layout.png',
)
template = '<p><img alt="The initial MkDocs layout" src="%s" /></p>'
for (page, expected) in zip(site_navigation.walk_pages(), expected_results):
page.markdown = '![The initial MkDocs layout](img/initial-layout.png)'
page.render(config, site_navigation)
self.assertEqual(page.content, template % expected)
def test_convert_internal_asbolute_media(self):
"""Test absolute image URL's are correct for different base_urls"""
pages = [
'index.md',
'internal.md',
'sub/internal.md',
]
config = load_config(pages=pages)
site_navigation = nav.SiteNavigation(config)
expected_results = (
'./img/initial-layout.png',
'../img/initial-layout.png',
'../../img/initial-layout.png',
)
template = '<p><img alt="The initial MkDocs layout" src="%s" /></p>'
for (page, expected) in zip(site_navigation.walk_pages(), expected_results):
page.markdown = '![The initial MkDocs layout](/img/initial-layout.png)'
page.render(config, site_navigation)
self.assertEqual(page.content, template % expected)
def test_dont_convert_code_block_urls(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
]
config = load_config(pages=pages)
site_navigation = nav.SiteNavigation(config)
expected = dedent("""
<p>An HTML Anchor::</p>
<pre><code>&lt;a href="index.md"&gt;My example link&lt;/a&gt;
</code></pre>
""")
for page in site_navigation.walk_pages():
page.markdown = 'An HTML Anchor::\n\n <a href="index.md">My example link</a>\n'
page.render(config, site_navigation)
self.assertEqual(page.content, expected)
def test_anchor_only_link(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
]
config = load_config(pages=pages)
site_navigation = nav.SiteNavigation(config)
for page in site_navigation.walk_pages():
page.markdown = '[test](#test)'
page.render(config, site_navigation)
self.assertEqual(page.content, '<p><a href="#test">test</a></p>')
def test_ignore_external_link(self):
md_text = 'An [external link](http://example.com/external.md).'
expected = '<p>An <a href="http://example.com/external.md">external link</a>.</p>'
config = load_config(pages=[{'Home': 'index.md'}])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected.strip())
def test_not_use_directory_urls(self):
md_text = 'An [internal link](internal.md) to another document.'
expected = '<p>An <a href="internal/index.html">internal link</a> to another document.</p>'
config = load_config(pages=['index.md', 'internal.md'], use_directory_urls=False)
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected.strip())
def test_ignore_email_links(self):
md_text = 'A <autolink@example.com> and an [link](mailto:example@example.com).'
expected = ''.join([
'<p>A <a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#97;&#117;&#116;',
'&#111;&#108;&#105;&#110;&#107;&#64;&#101;&#120;&#97;&#109;&#112;&#108;',
'&#101;&#46;&#99;&#111;&#109;">&#97;&#117;&#116;&#111;&#108;&#105;&#110;',
'&#107;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;',
'</a> and an <a href="mailto:example@example.com">link</a>.</p>'
cfg = load_config(nav=nav_cfg, use_directory_urls=False)
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
config = load_config(pages=[{'Home': 'index.md'}])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected.strip())
nav = get_navigation(files, cfg)
context = build.get_context(nav, files, cfg, nav.pages[0])
self.assertEqual(context['base_url'], '.')
def test_markdown_table_extension(self):
"""
Ensure that the table extension is supported.
"""
md_text = dedent("""
First Header | Second Header
-------------- | --------------
Content Cell 1 | Content Cell 2
Content Cell 3 | Content Cell 4
""")
expected_html = dedent("""
<table>
<thead>
<tr>
<th>First Header</th>
<th>Second Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>Content Cell 1</td>
<td>Content Cell 2</td>
</tr>
<tr>
<td>Content Cell 3</td>
<td>Content Cell 4</td>
</tr>
</tbody>
</table>
""")
config = load_config(pages=[{'Home': 'index.md'}])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected_html)
def test_markdown_fenced_code_extension(self):
"""
Ensure that the fenced code extension is supported.
"""
md_text = dedent("""
```
print 'foo'
```
""")
expected_html = dedent("""
<pre><code>print 'foo'\n</code></pre>
""")
config = load_config(pages=[{'Home': 'index.md'}])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected_html)
def test_markdown_custom_extension(self):
"""
Check that an extension applies when requested in the arguments to
`convert_markdown`.
"""
md_text = "foo__bar__baz"
# Check that the plugin is not active when not requested.
expected_without_smartstrong = "<p>foo<strong>bar</strong>baz</p>"
config = load_config(pages=[{'Home': 'index.md'}])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected_without_smartstrong)
# Check that the plugin is active when requested.
expected_with_smartstrong = "<p>foo__bar__baz</p>"
config = load_config(pages=[{'Home': 'index.md'}], markdown_extensions=['smart_strong'])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected_with_smartstrong)
def test_markdown_duplicate_custom_extension(self):
"""
Duplicated extension names should not cause problems.
"""
md_text = "foo"
config = load_config(pages=[{'Home': 'index.md'}], markdown_extensions=['toc'])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), '<p>foo</p>')
def test_copying_media(self):
with TemporaryDirectory() as docs_dir, TemporaryDirectory() as site_dir:
# Create a non-empty markdown file, image, html file, dot file and dot directory.
f = open(os.path.join(docs_dir, 'index.md'), 'w')
f.write(dedent("""
page_title: custom title
# Heading 1
This is some text.
# Heading 2
And some more text.
"""))
f.close()
open(os.path.join(docs_dir, 'img.jpg'), 'w').close()
open(os.path.join(docs_dir, 'example.html'), 'w').close()
open(os.path.join(docs_dir, '.hidden'), 'w').close()
os.mkdir(os.path.join(docs_dir, '.git'))
open(os.path.join(docs_dir, '.git/hidden'), 'w').close()
cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
build.build(cfg)
# Verify only the markdown (coverted to html) and the image are copied.
self.assertTrue(os.path.isfile(os.path.join(site_dir, 'index.html')))
self.assertTrue(os.path.isfile(os.path.join(site_dir, 'img.jpg')))
self.assertTrue(os.path.isfile(os.path.join(site_dir, 'example.html')))
self.assertFalse(os.path.isfile(os.path.join(site_dir, '.hidden')))
self.assertFalse(os.path.isfile(os.path.join(site_dir, '.git/hidden')))
def test_copy_theme_files(self):
with TemporaryDirectory() as docs_dir, TemporaryDirectory() as site_dir:
# Create a non-empty markdown file.
f = open(os.path.join(docs_dir, 'index.md'), 'w')
f.write(dedent("""
page_title: custom title
# Heading 1
This is some text.
"""))
f.close()
cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
build.build(cfg)
# Verify only theme media are copied, not templates or Python files.
self.assertTrue(os.path.isfile(os.path.join(site_dir, 'index.html')))
self.assertTrue(os.path.isdir(os.path.join(site_dir, 'js')))
self.assertTrue(os.path.isdir(os.path.join(site_dir, 'css')))
self.assertTrue(os.path.isdir(os.path.join(site_dir, 'img')))
self.assertFalse(os.path.isfile(os.path.join(site_dir, '__init__.py')))
self.assertFalse(os.path.isfile(os.path.join(site_dir, '__init__.pyc')))
self.assertFalse(os.path.isfile(os.path.join(site_dir, 'base.html')))
self.assertFalse(os.path.isfile(os.path.join(site_dir, 'content.html')))
self.assertFalse(os.path.isfile(os.path.join(site_dir, 'nav.html')))
def test_strict_mode_valid(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
def test_context_base_url_homepage_use_directory_urls(self):
nav_cfg = [
{'Home': 'index.md'}
]
cfg = load_config(nav=nav_cfg)
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
nav = get_navigation(files, cfg)
context = build.get_context(nav, files, cfg, nav.pages[0])
self.assertEqual(context['base_url'], '.')
md_text = "[test](internal.md)"
config = load_config(pages=pages, strict=False)
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
config = load_config(pages=pages, strict=True)
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
def test_strict_mode_invalid(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
def test_context_base_url_nested_page(self):
nav_cfg = [
{'Home': 'index.md'},
{'Nested': 'foo/bar.md'}
]
cfg = load_config(nav=nav_cfg, use_directory_urls=False)
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
])
nav = get_navigation(files, cfg)
context = build.get_context(nav, files, cfg, nav.pages[1])
self.assertEqual(context['base_url'], '..')
md_text = "[test](bad_link.md)"
config = load_config(pages=pages, strict=False)
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
config = load_config(pages=pages, strict=True)
page, nav = build_page(None, 'index.md', config, md_text)
self.assertRaises(
MarkdownNotFound,
page.render, config, nav)
def test_absolute_link(self):
pages = [
'index.md',
'sub/index.md',
def test_context_base_url_nested_page_use_directory_urls(self):
nav_cfg = [
{'Home': 'index.md'},
{'Nested': 'foo/bar.md'}
]
cfg = load_config(nav=nav_cfg)
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
])
nav = get_navigation(files, cfg)
context = build.get_context(nav, files, cfg, nav.pages[1])
self.assertEqual(context['base_url'], '../..')
md_text = "[test 1](/index.md) [test 2](/sub/index.md)"
config = load_config(pages=pages, strict=True)
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
def test_context_base_url_relative_no_page(self):
cfg = load_config(use_directory_urls=False)
context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
self.assertEqual(context['base_url'], '..')
def test_extension_config(self):
"""
Test that a dictionary of 'markdown_extensions' is recognized as
both a list of extensions and a dictionary of extnesion configs.
"""
md_text = dedent("""
# A Header
""")
def test_context_base_url_relative_no_page_use_directory_urls(self):
cfg = load_config()
context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
self.assertEqual(context['base_url'], '..')
expected_html = dedent("""
<h1 id="a-header">A Header<a class="headerlink" href="#a-header" title="Permanent link">&para;</a></h1>
""")
def test_context_base_url_absolute_no_page(self):
cfg = load_config(use_directory_urls=False)
context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/')
self.assertEqual(context['base_url'], '')
config = load_config(pages=[{'Home': 'index.md'}], markdown_extensions=[{'toc': {'permalink': True}}])
page, nav = build_page(None, 'index.md', config, md_text)
page.render(config, nav)
self.assertEqual(page.content.strip(), expected_html)
def test_context_base_url__absolute_no_page_use_directory_urls(self):
cfg = load_config()
context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/')
self.assertEqual(context['base_url'], '')
def test_context_base_url_absolute_nested_no_page(self):
cfg = load_config(use_directory_urls=False)
context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/')
self.assertEqual(context['base_url'], '/foo')
def test_context_base_url__absolute_nested_no_page_use_directory_urls(self):
cfg = load_config()
context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/')
self.assertEqual(context['base_url'], '/foo')
def test_context_extra_css_js_from_homepage(self):
nav_cfg = [
{'Home': 'index.md'}
]
cfg = load_config(
nav=nav_cfg,
extra_css=['style.css'],
extra_javascript=['script.js'],
use_directory_urls=False
)
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
nav = get_navigation(files, cfg)
context = build.get_context(nav, files, cfg, nav.pages[0])
self.assertEqual(context['extra_css'], ['style.css'])
self.assertEqual(context['extra_javascript'], ['script.js'])
def test_context_extra_css_js_from_nested_page(self):
nav_cfg = [
{'Home': 'index.md'},
{'Nested': 'foo/bar.md'}
]
cfg = load_config(
nav=nav_cfg,
extra_css=['style.css'],
extra_javascript=['script.js'],
use_directory_urls=False
)
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
])
nav = get_navigation(files, cfg)
context = build.get_context(nav, files, cfg, nav.pages[1])
self.assertEqual(context['extra_css'], ['../style.css'])
self.assertEqual(context['extra_javascript'], ['../script.js'])
def test_context_extra_css_js_from_nested_page_use_directory_urls(self):
nav_cfg = [
{'Home': 'index.md'},
{'Nested': 'foo/bar.md'}
]
cfg = load_config(
nav=nav_cfg,
extra_css=['style.css'],
extra_javascript=['script.js']
)
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
])
nav = get_navigation(files, cfg)
context = build.get_context(nav, files, cfg, nav.pages[1])
self.assertEqual(context['extra_css'], ['../../style.css'])
self.assertEqual(context['extra_javascript'], ['../../script.js'])
def test_context_extra_css_js_no_page(self):
cfg = load_config(extra_css=['style.css'], extra_javascript=['script.js'])
context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
self.assertEqual(context['extra_css'], ['../style.css'])
self.assertEqual(context['extra_javascript'], ['../script.js'])
def test_extra_context(self):
# Same as the default schema, but don't verify the docs_dir exists.
cfg = load_config(
site_name="Site",
extra={
'a': 1
}
)
context = build.get_context(mock.Mock(), cfg)
cfg = load_config(extra={'a': 1})
context = build.get_context(mock.Mock(), mock.Mock(), cfg)
self.assertEqual(context['config']['extra']['a'], 1)
def test_BOM(self):
with TemporaryDirectory() as docs_dir, TemporaryDirectory() as site_dir:
# Create an UTF-8 Encoded file with BOM (as Micorsoft editors do). See #1186.
f = io.open(os.path.join(docs_dir, 'index.md'), 'w', encoding='utf-8-sig')
f.write('# An UTF-8 encoded file with a BOM')
f.close()
# Test build._build_theme_template
cfg = load_config(
docs_dir=docs_dir,
site_dir=site_dir
)
build.build(cfg)
@mock.patch('mkdocs.utils.write_file')
@mock.patch('mkdocs.commands.build._build_template', return_value='some content')
def test_build_theme_template(self, mock_build_template, mock_write_file):
cfg = load_config()
env = cfg['theme'].get_env()
build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock())
mock_write_file.assert_called_once()
mock_build_template.assert_called_once()
# Verify that the file was generated properly.
# If the BOM is not removed, Markdown will return:
# `<p>\ufeff# An UTF-8 encoded file with a BOM</p>`.
f = io.open(os.path.join(site_dir, 'index.html'), 'r', encoding='utf-8')
output = f.read()
f.close()
self.assertTrue(
'<h1 id="an-utf-8-encoded-file-with-a-bom">An UTF-8 encoded file with a BOM</h1>' in output
)
@mock.patch('mkdocs.utils.write_file')
@mock.patch('mkdocs.commands.build._build_template', return_value='some content')
@mock.patch('gzip.open')
def test_build_sitemap_template(self, mock_gzip_open, mock_build_template, mock_write_file):
cfg = load_config()
env = cfg['theme'].get_env()
build._build_theme_template('sitemap.xml', env, mock.Mock(), cfg, mock.Mock())
mock_write_file.assert_called_once()
mock_build_template.assert_called_once()
mock_gzip_open.assert_called_once()
@mock.patch('mkdocs.utils.write_file')
@mock.patch('mkdocs.commands.build._build_template', return_value='')
def test_skip_missing_theme_template(self, mock_build_template, mock_write_file):
cfg = load_config()
env = cfg['theme'].get_env()
with self.assertLogs('mkdocs', level='WARN') as cm:
build._build_theme_template('missing.html', env, mock.Mock(), cfg, mock.Mock())
self.assertEqual(
cm.output,
["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in theme directories."]
)
mock_write_file.assert_not_called()
mock_build_template.assert_not_called()
@mock.patch('mkdocs.utils.write_file')
@mock.patch('mkdocs.commands.build._build_template', return_value='')
def test_skip_theme_template_empty_output(self, mock_build_template, mock_write_file):
cfg = load_config()
env = cfg['theme'].get_env()
with self.assertLogs('mkdocs', level='INFO') as cm:
build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock())
self.assertEqual(
cm.output,
["INFO:mkdocs.commands.build:Template skipped: 'main.html' generated empty output."]
)
mock_write_file.assert_not_called()
mock_build_template.assert_called_once()
# Test build._build_extra_template
@mock.patch('io.open', mock.mock_open(read_data='template content'))
def test_build_extra_template(self):
cfg = load_config()
files = Files([
File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
build._build_extra_template('foo.html', files, cfg, mock.Mock())
@mock.patch('io.open', mock.mock_open(read_data='template content'))
def test_skip_missing_extra_template(self):
cfg = load_config()
files = Files([
File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
with self.assertLogs('mkdocs', level='INFO') as cm:
build._build_extra_template('missing.html', files, cfg, mock.Mock())
self.assertEqual(
cm.output,
["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in docs_dir."]
)
@mock.patch('io.open', side_effect=IOError('Error message.'))
def test_skip_ioerror_extra_template(self, mock_open):
cfg = load_config()
files = Files([
File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
with self.assertLogs('mkdocs', level='INFO') as cm:
build._build_extra_template('foo.html', files, cfg, mock.Mock())
self.assertEqual(
cm.output,
["WARNING:mkdocs.commands.build:Error reading template 'foo.html': Error message."]
)
@mock.patch('io.open', mock.mock_open(read_data=''))
def test_skip_extra_template_empty_output(self):
cfg = load_config()
files = Files([
File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
with self.assertLogs('mkdocs', level='INFO') as cm:
build._build_extra_template('foo.html', files, cfg, mock.Mock())
self.assertEqual(
cm.output,
["INFO:mkdocs.commands.build:Template skipped: 'foo.html' generated empty output."]
)
# Test build._populate_page
@tempdir(files={'index.md': 'page content'})
def test_populate_page(self, docs_dir):
cfg = load_config(docs_dir=docs_dir)
file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
page = Page('Foo', file, cfg)
build._populate_page(page, cfg, Files([file]))
self.assertEqual(page.content, '<p>page content</p>')
@tempdir(files={'testing.html': '<p>page content</p>'})
def test_populate_page_dirty_modified(self, site_dir):
cfg = load_config(site_dir=site_dir)
file = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
page = Page('Foo', file, cfg)
build._populate_page(page, cfg, Files([file]), dirty=True)
self.assertTrue(page.markdown.startswith('# Welcome to MkDocs'))
self.assertTrue(page.content.startswith('<h1 id="welcome-to-mkdocs">Welcome to MkDocs</h1>'))
@tempdir(files={'index.md': 'page content'})
@tempdir(files={'index.html': '<p>page content</p>'})
def test_populate_page_dirty_not_modified(self, site_dir, docs_dir):
cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
page = Page('Foo', file, cfg)
build._populate_page(page, cfg, Files([file]), dirty=True)
# Content is empty as file read was skipped
self.assertEqual(page.markdown, None)
self.assertEqual(page.content, None)
@tempdir(files={'index.md': 'new page content'})
@mock.patch('io.open', side_effect=IOError('Error message.'))
def test_populate_page_read_error(self, docs_dir, mock_open):
cfg = load_config(docs_dir=docs_dir)
file = File('missing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
page = Page('Foo', file, cfg)
with self.assertLogs('mkdocs', level='ERROR') as cm:
self.assertRaises(IOError, build._populate_page, page, cfg, Files([file]))
self.assertEqual(
cm.output, [
'ERROR:mkdocs.structure.pages:File not found: missing.md',
"ERROR:mkdocs.commands.build:Error reading page 'missing.md': Error message."
]
)
mock_open.assert_called_once()
# Test build._build_page
@tempdir()
def test_build_page(self, site_dir):
cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
nav = get_navigation(files, cfg)
page = files.documentation_pages()[0].page
# Fake populate page
page.title = 'Title'
page.markdown = 'page content'
page.content = '<p>page content</p>'
build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
self.assertPathIsFile(site_dir, 'index.html')
# TODO: fix this. It seems that jinja2 chokes on the mock object. Not sure how to resolve.
# @tempdir()
# @mock.patch('jinja2.environment.Template')
# def test_build_page_empty(self, site_dir, mock_template):
# mock_template.render = mock.Mock(return_value='')
# cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
# files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
# nav = get_navigation(files, cfg)
# page = files.documentation_pages()[0].page
# # Fake populate page
# page.title = ''
# page.markdown = ''
# page.content = ''
# with self.assertLogs('mkdocs', level='INFO') as cm:
# build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
# self.assertEqual(
# cm.output,
# ["INFO:mkdocs.commands.build:Page skipped: 'index.md'. Generated empty output."]
# )
# mock_template.render.assert_called_once()
# self.assertPathNotFile(site_dir, 'index.html')
@tempdir(files={'index.md': 'page content'})
@tempdir(files={'index.html': '<p>page content</p>'})
@mock.patch('mkdocs.utils.write_file')
def test_build_page_dirty_modified(self, site_dir, docs_dir, mock_write_file):
cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, nav=['index.md'], plugins=[])
files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
nav = get_navigation(files, cfg)
page = files.documentation_pages()[0].page
# Fake populate page
page.title = 'Title'
page.markdown = 'new page content'
page.content = '<p>new page content</p>'
build._build_page(page, cfg, files, nav, cfg['theme'].get_env(), dirty=True)
mock_write_file.assert_not_called()
@tempdir(files={'testing.html': '<p>page content</p>'})
@mock.patch('mkdocs.utils.write_file')
def test_build_page_dirty_not_modified(self, site_dir, mock_write_file):
cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
files = Files([File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
nav = get_navigation(files, cfg)
page = files.documentation_pages()[0].page
# Fake populate page
page.title = 'Title'
page.markdown = 'page content'
page.content = '<p>page content</p>'
build._build_page(page, cfg, files, nav, cfg['theme'].get_env(), dirty=True)
mock_write_file.assert_called_once()
@tempdir()
def test_build_page_custom_template(self, site_dir):
cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
nav = get_navigation(files, cfg)
page = files.documentation_pages()[0].page
# Fake populate page
page.title = 'Title'
page.meta = {'template': '404.html'}
page.markdown = 'page content'
page.content = '<p>page content</p>'
build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
self.assertPathIsFile(site_dir, 'index.html')
@tempdir()
@mock.patch('mkdocs.utils.write_file', side_effect=IOError('Error message.'))
def test_build_page_error(self, site_dir, mock_write_file):
cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
nav = get_navigation(files, cfg)
page = files.documentation_pages()[0].page
# Fake populate page
page.title = 'Title'
page.markdown = 'page content'
page.content = '<p>page content</p>'
with self.assertLogs('mkdocs', level='ERROR') as cm:
self.assertRaises(IOError, build._build_page, page, cfg, files, nav, cfg['theme'].get_env())
self.assertEqual(
cm.output,
["ERROR:mkdocs.commands.build:Error building page 'index.md': Error message."]
)
mock_write_file.assert_called_once()
# Test build.build
@tempdir(files={
'index.md': 'page content',
'empty.md': '',
'img.jpg': '',
'static.html': 'content',
'.hidden': 'content',
'.git/hidden': 'content'
})
@tempdir()
def test_copying_media(self, site_dir, docs_dir):
cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
build.build(cfg)
# Verify that only non-empty md file (coverted to html), static HTML file and image are copied.
self.assertPathIsFile(site_dir, 'index.html')
self.assertPathIsFile(site_dir, 'img.jpg')
self.assertPathIsFile(site_dir, 'static.html')
self.assertPathNotExists(site_dir, 'empty.md')
self.assertPathNotExists(site_dir, '.hidden')
self.assertPathNotExists(site_dir, '.git/hidden')
@tempdir(files={'index.md': 'page content'})
@tempdir()
def test_copy_theme_files(self, site_dir, docs_dir):
cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
build.build(cfg)
# Verify only theme media are copied, not templates or Python files.
self.assertPathIsFile(site_dir, 'index.html')
self.assertPathIsFile(site_dir, '404.html')
self.assertPathIsDir(site_dir, 'js')
self.assertPathIsDir(site_dir, 'css')
self.assertPathIsDir(site_dir, 'img')
self.assertPathIsDir(site_dir, 'fonts')
self.assertPathNotExists(site_dir, '__init__.py')
self.assertPathNotExists(site_dir, '__init__.pyc')
self.assertPathNotExists(site_dir, 'base.html')
self.assertPathNotExists(site_dir, 'content.html')
self.assertPathNotExists(site_dir, 'main.html')
# Test build.site_directory_contains_stale_files
@tempdir(files=['index.html'])
def test_site_dir_contains_stale_files(self, site_dir):
self.assertTrue(build.site_directory_contains_stale_files(site_dir))
@tempdir()
def test_not_site_dir_contains_stale_files(self, site_dir):
self.assertFalse(build.site_directory_contains_stale_files(site_dir))
+3 -1
View File
@@ -9,7 +9,7 @@ try:
except ImportError:
from backports.tempfile import TemporaryDirectory
from mkdocs import exceptions
from mkdocs import exceptions, utils
from mkdocs.config import base, defaults
from mkdocs.config.config_options import BaseConfigOption
@@ -273,5 +273,7 @@ class ConfigBaseTests(unittest.TestCase):
self.assertTrue(isinstance(cfg, base.Config))
self.assertEqual(cfg['site_name'], 'MkDocs Test')
self.assertEqual(cfg['docs_dir'], docs_dir)
self.assertEqual(cfg.config_file_path, config_fname)
self.assertIsInstance(cfg.config_file_path, utils.text_type)
finally:
config_dir.cleanup()
+97 -9
View File
@@ -1,6 +1,9 @@
# coding=UTF-8
from __future__ import unicode_literals
import os
import sys
import unittest
from mock import patch
@@ -273,14 +276,99 @@ class DirTest(unittest.TestCase):
self.assertRaises(config_options.ValidationError,
option.validate, [])
def test_doc_dir_is_config_dir(self):
def test_dir_unicode(self):
cfg = Config(
[('docs_dir', config_options.Dir())],
[('dir', config_options.Dir())],
config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'),
)
test_config = {
'docs_dir': '.'
'dir': 'юникод'
}
cfg.load_dict(test_config)
fails, warns = cfg.validate()
self.assertEqual(len(fails), 0)
self.assertEqual(len(warns), 0)
self.assertIsInstance(cfg['dir'], utils.text_type)
def test_dir_filesystemencoding(self):
cfg = Config(
[('dir', config_options.Dir())],
config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'),
)
test_config = {
'dir': 'Übersicht'.encode(encoding=sys.getfilesystemencoding())
}
cfg.load_dict(test_config)
fails, warns = cfg.validate()
if utils.PY3:
# In PY3 string_types does not include byte strings so validation fails
self.assertEqual(len(fails), 1)
self.assertEqual(len(warns), 0)
else:
# In PY2 string_types includes byte strings so validation passes
# This test confirms that the byte string is properly decoded
self.assertEqual(len(fails), 0)
self.assertEqual(len(warns), 0)
self.assertIsInstance(cfg['dir'], utils.text_type)
def test_dir_bad_encoding_fails(self):
cfg = Config(
[('dir', config_options.Dir())],
config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'),
)
test_config = {
'dir': 'юникод'.encode(encoding='ISO 8859-5')
}
cfg.load_dict(test_config)
fails, warns = cfg.validate()
if sys.platform.startswith('win') and not utils.PY3:
# PY2 on Windows seems to be able to decode anything we give it.
# But that just means less possable errors for those users so we allow it.
self.assertEqual(len(fails), 0)
else:
self.assertEqual(len(fails), 1)
self.assertEqual(len(warns), 0)
def test_config_dir_prepended(self):
base_path = os.path.abspath('.')
cfg = Config(
[('dir', config_options.Dir())],
config_file_path=os.path.join(base_path, 'mkdocs.yml'),
)
test_config = {
'dir': 'foo'
}
cfg.load_dict(test_config)
fails, warns = cfg.validate()
self.assertEqual(len(fails), 0)
self.assertEqual(len(warns), 0)
self.assertIsInstance(cfg['dir'], utils.text_type)
self.assertEqual(cfg['dir'], os.path.join(base_path, 'foo'))
def test_dir_is_config_dir_fails(self):
cfg = Config(
[('dir', config_options.Dir())],
config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'),
)
test_config = {
'dir': '.'
}
cfg.load_dict(test_config)
@@ -438,11 +526,11 @@ class ThemeTest(unittest.TestCase):
option.validate, config)
class PagesTest(unittest.TestCase):
class NavTest(unittest.TestCase):
def test_old_format(self):
option = config_options.Pages()
option = config_options.Nav()
self.assertRaises(
config_options.ValidationError,
option.validate,
@@ -451,7 +539,7 @@ class PagesTest(unittest.TestCase):
def test_provided_dict(self):
option = config_options.Pages()
option = config_options.Nav()
value = option.validate([
'index.md',
{"Page": "page.md"}
@@ -462,7 +550,7 @@ class PagesTest(unittest.TestCase):
def test_provided_empty(self):
option = config_options.Pages()
option = config_options.Nav()
value = option.validate([])
self.assertEqual(None, value)
@@ -470,13 +558,13 @@ class PagesTest(unittest.TestCase):
def test_invalid_type(self):
option = config_options.Pages()
option = config_options.Nav()
self.assertRaises(config_options.ValidationError,
option.validate, {})
def test_invalid_config(self):
option = config_options.Pages()
option = config_options.Nav()
self.assertRaises(config_options.ValidationError,
option.validate, [[], 1])
+30 -49
View File
@@ -178,56 +178,37 @@ class ConfigTests(unittest.TestCase):
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):
with TemporaryDirectory() as tmp_dir:
open(os.path.join(tmp_dir, 'index.md'), 'w').close()
open(os.path.join(tmp_dir, 'about.md'), 'w').close()
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf.load_dict({
'site_name': 'Example',
'docs_dir': tmp_dir,
'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
})
conf.validate()
self.assertEqual(['index.md', 'about.md'], conf['pages'])
def test_empty_nav(self):
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf.load_dict({
'site_name': 'Example',
'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
})
conf.validate()
self.assertEqual(conf['nav'], None)
def test_default_pages_nested(self):
with TemporaryDirectory() as tmp_dir:
open(os.path.join(tmp_dir, 'index.md'), 'w').close()
open(os.path.join(tmp_dir, 'getting-started.md'), 'w').close()
open(os.path.join(tmp_dir, 'about.md'), 'w').close()
os.makedirs(os.path.join(tmp_dir, 'subA'))
open(os.path.join(tmp_dir, 'subA', 'index.md'), 'w').close()
os.makedirs(os.path.join(tmp_dir, 'subA', 'subA1'))
open(os.path.join(tmp_dir, 'subA', 'subA1', 'index.md'), 'w').close()
os.makedirs(os.path.join(tmp_dir, 'subC'))
open(os.path.join(tmp_dir, 'subC', 'index.md'), 'w').close()
os.makedirs(os.path.join(tmp_dir, 'subB'))
open(os.path.join(tmp_dir, 'subB', 'index.md'), 'w').close()
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf.load_dict({
'site_name': 'Example',
'docs_dir': tmp_dir,
'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
})
conf.validate()
self.assertEqual([
'index.md',
'about.md',
'getting-started.md',
{'subA': [
os.path.join('subA', 'index.md'),
{'subA1': [
os.path.join('subA', 'subA1', 'index.md')
]}
]},
{'subB': [
os.path.join('subB', 'index.md')
]},
{'subC': [
os.path.join('subC', 'index.md')
]}
], conf['pages'])
def test_copy_pages_to_nav(self):
# TODO: remove this when pages config setting is fully deprecated.
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf.load_dict({
'site_name': 'Example',
'pages': ['index.md', 'about.md'],
'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
})
conf.validate()
self.assertEqual(conf['nav'], ['index.md', 'about.md'])
def test_dont_overwrite_nav_with_pages(self):
# TODO: remove this when pages config setting is fully deprecated.
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf.load_dict({
'site_name': 'Example',
'pages': ['index.md', 'about.md'],
'nav': ['foo.md', 'bar.md'],
'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
})
conf.validate()
self.assertEqual(conf['nav'], ['foo.md', 'bar.md'])
def test_doc_dir_in_site_dir(self):
@@ -1,6 +1,6 @@
site_name: My Docs
pages:
nav:
- Home: index.md
- User Guide:
- Writing your docs: index.md
+1 -1
View File
@@ -1,6 +1,6 @@
site_name: MyTest
pages:
nav:
- 'testing.md'
site_author: "Tom Christie & Dougal Matthews"
@@ -1,4 +1,4 @@
# Test sub pages and referencing images
## Test sub pages and referencing images
## Reference an image in: /
@@ -0,0 +1,5 @@
title: A Page Title
# Welcome to MkDocs
Some page content goes here.
@@ -0,0 +1 @@
Page content.
@@ -0,0 +1 @@
Page content.
@@ -1,4 +1,4 @@
# Welcome to MkDocs
Welcome to MkDocs
For full documentation visit [mkdocs.org](http://mkdocs.org).
+1 -1
View File
@@ -1,4 +1,4 @@
# Welcome to MkDocs
Welcome to MkDocs
For full documentation visit [mkdocs.org](http://mkdocs.org).
-850
View File
@@ -1,850 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
from __future__ import unicode_literals
import mock
import os
import unittest
from mkdocs import nav
from mkdocs.exceptions import ConfigurationError
from mkdocs.tests.base import dedent, load_config
class SiteNavigationTests(unittest.TestCase):
def test_simple_toc(self):
pages = [
{'Home': 'index.md'},
{'About': 'about.md'}
]
expected = dedent("""
Home - /
About - /about/
""")
site_navigation = nav.SiteNavigation(load_config(pages=pages))
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.nav_items), 2)
self.assertEqual(len(site_navigation.pages), 2)
def test_empty_toc_item(self):
pages = [
'index.md',
{'About': 'about.md'}
]
expected = dedent("""
Home - /
About - /about/
""")
site_navigation = nav.SiteNavigation(load_config(pages=pages))
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.nav_items), 2)
self.assertEqual(len(site_navigation.pages), 2)
def test_indented_toc(self):
pages = [
{'Home': 'index.md'},
{'API Guide': [
{'Running': 'api-guide/running.md'},
{'Testing': 'api-guide/testing.md'},
{'Debugging': 'api-guide/debugging.md'},
]},
{'About': [
{'Release notes': 'about/release-notes.md'},
{'License': 'about/license.md'}
]}
]
expected = dedent("""
Home - /
API Guide
Running - /api-guide/running/
Testing - /api-guide/testing/
Debugging - /api-guide/debugging/
About
Release notes - /about/release-notes/
License - /about/license/
""")
site_navigation = nav.SiteNavigation(load_config(pages=pages))
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.nav_items), 3)
self.assertEqual(len(site_navigation.pages), 6)
def test_nested_ungrouped(self):
pages = [
{'Home': 'index.md'},
{'Contact': 'about/contact.md'},
{'License Title': 'about/sub/license.md'},
]
expected = dedent("""
Home - /
Contact - /about/contact/
License Title - /about/sub/license/
""")
site_navigation = nav.SiteNavigation(load_config(pages=pages))
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.nav_items), 3)
self.assertEqual(len(site_navigation.pages), 3)
def test_nested_ungrouped_no_titles(self):
pages = [
'index.md',
'about/contact.md',
'about/sub/license.md'
]
expected = dedent("""
Home - /
Contact - /about/contact/
License - /about/sub/license/
""")
site_navigation = nav.SiteNavigation(load_config(pages=pages))
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.nav_items), 3)
self.assertEqual(len(site_navigation.pages), 3)
@mock.patch.object(os.path, 'sep', '\\')
def test_nested_ungrouped_no_titles_windows(self):
pages = [
'index.md',
'about\\contact.md',
'about\\sub\\license.md',
]
expected = dedent("""
Home - /
Contact - /about/contact/
License - /about/sub/license/
""")
site_navigation = nav.SiteNavigation(load_config(pages=pages))
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.nav_items), 3)
self.assertEqual(len(site_navigation.pages), 3)
def test_walk_simple_toc(self):
pages = [
{'Home': 'index.md'},
{'About': 'about.md'}
]
expected = [
dedent("""
Home - / [*]
About - /about/
"""),
dedent("""
Home - /
About - /about/ [*]
""")
]
site_navigation = nav.SiteNavigation(load_config(pages=pages))
for index, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(str(site_navigation).strip(), expected[index])
def test_walk_empty_toc(self):
pages = [
'index.md',
{'About': 'about.md'}
]
expected = [
dedent("""
Home - / [*]
About - /about/
"""),
dedent("""
Home - /
About - /about/ [*]
""")
]
site_navigation = nav.SiteNavigation(load_config(pages=pages))
for index, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(str(site_navigation).strip(), expected[index])
def test_walk_indented_toc(self):
pages = [
{'Home': 'index.md'},
{'API Guide': [
{'Running': 'api-guide/running.md'},
{'Testing': 'api-guide/testing.md'},
{'Debugging': 'api-guide/debugging.md'},
]},
{'About': [
{'Release notes': 'about/release-notes.md'},
{'License': 'about/license.md'}
]}
]
expected = [
dedent("""
Home - / [*]
API Guide
Running - /api-guide/running/
Testing - /api-guide/testing/
Debugging - /api-guide/debugging/
About
Release notes - /about/release-notes/
License - /about/license/
"""),
dedent("""
Home - /
API Guide [*]
Running - /api-guide/running/ [*]
Testing - /api-guide/testing/
Debugging - /api-guide/debugging/
About
Release notes - /about/release-notes/
License - /about/license/
"""),
dedent("""
Home - /
API Guide [*]
Running - /api-guide/running/
Testing - /api-guide/testing/ [*]
Debugging - /api-guide/debugging/
About
Release notes - /about/release-notes/
License - /about/license/
"""),
dedent("""
Home - /
API Guide [*]
Running - /api-guide/running/
Testing - /api-guide/testing/
Debugging - /api-guide/debugging/ [*]
About
Release notes - /about/release-notes/
License - /about/license/
"""),
dedent("""
Home - /
API Guide
Running - /api-guide/running/
Testing - /api-guide/testing/
Debugging - /api-guide/debugging/
About [*]
Release notes - /about/release-notes/ [*]
License - /about/license/
"""),
dedent("""
Home - /
API Guide
Running - /api-guide/running/
Testing - /api-guide/testing/
Debugging - /api-guide/debugging/
About [*]
Release notes - /about/release-notes/
License - /about/license/ [*]
""")
]
site_navigation = nav.SiteNavigation(load_config(pages=pages))
for index, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(str(site_navigation).strip(), expected[index])
def test_base_url(self):
pages = [
'index.md'
]
site_navigation = nav.SiteNavigation(load_config(pages=pages, use_directory_urls=False))
base_url = site_navigation.url_context.make_relative('/')
self.assertEqual(base_url, '.')
def test_relative_md_links_have_slash(self):
pages = [
'index.md',
'user-guide/styling-your-docs.md'
]
site_navigation = nav.SiteNavigation(load_config(pages=pages, use_directory_urls=False))
site_navigation.url_context.base_path = "/user-guide/configuration"
url = site_navigation.url_context.make_relative('/user-guide/styling-your-docs/')
self.assertEqual(url, '../styling-your-docs/')
def test_generate_site_navigation(self):
"""
Verify inferring page titles based on the filename
"""
pages = [
'index.md',
'api-guide/running.md',
'about/notes.md',
'about/sub/license.md',
]
url_context = nav.URLContext()
nav_items, pages = nav._generate_site_navigation(load_config(pages=pages), url_context)
self.assertEqual([n.title for n in nav_items],
['Home', 'Running', 'Notes', 'License'])
self.assertEqual([n.url for n in nav_items], [
'.',
'api-guide/running/',
'about/notes/',
'about/sub/license/'
])
self.assertEqual([p.title for p in pages],
['Home', 'Running', 'Notes', 'License'])
@mock.patch.object(os.path, 'sep', '\\')
def test_generate_site_navigation_windows(self):
"""
Verify inferring page titles based on the filename with a windows path
"""
pages = [
'index.md',
'api-guide\\running.md',
'about\\notes.md',
'about\\sub\\license.md',
]
url_context = nav.URLContext()
nav_items, pages = nav._generate_site_navigation(load_config(pages=pages), url_context)
self.assertEqual([n.title for n in nav_items],
['Home', 'Running', 'Notes', 'License'])
self.assertEqual([n.url for n in nav_items], [
'.',
'api-guide/running/',
'about/notes/',
'about/sub/license/'
])
self.assertEqual([p.title for p in pages],
['Home', 'Running', 'Notes', 'License'])
def test_force_abs_urls(self):
"""
Verify force absolute URLs
"""
pages = [
'index.md',
'api-guide/running.md',
'about/notes.md',
'about/sub/license.md',
]
url_context = nav.URLContext()
url_context.force_abs_urls = True
nav_items, pages = nav._generate_site_navigation(load_config(pages=pages), url_context)
self.assertEqual([n.title for n in nav_items],
['Home', 'Running', 'Notes', 'License'])
self.assertEqual([n.url for n in nav_items], [
'/',
'/api-guide/running/',
'/about/notes/',
'/about/sub/license/'
])
def test_force_abs_urls_with_base(self):
"""
Verify force absolute URLs
"""
pages = [
'index.md',
'api-guide/running.md',
'about/notes.md',
'about/sub/license.md',
]
url_context = nav.URLContext()
url_context.force_abs_urls = True
url_context.base_path = '/foo/'
nav_items, pages = nav._generate_site_navigation(load_config(pages=pages), url_context)
self.assertEqual([n.title for n in nav_items],
['Home', 'Running', 'Notes', 'License'])
self.assertEqual([n.url for n in nav_items], [
'/foo/',
'/foo/api-guide/running/',
'/foo/about/notes/',
'/foo/about/sub/license/'
])
def test_invalid_pages_config(self):
bad_page = {"a": "index.md", "b": "index.md"} # extra key
def _test():
return nav._generate_site_navigation(load_config(pages=[bad_page, ]), None)
self.assertRaises(ConfigurationError, _test)
def test_pages_config(self):
bad_page = {} # empty
def _test():
return nav._generate_site_navigation(load_config(pages=[bad_page, ]), None)
self.assertRaises(ConfigurationError, _test)
def test_ancestors(self):
pages = [
{'Home': 'index.md'},
{'API Guide': [
{'Running': 'api-guide/running.md'},
{'Testing': 'api-guide/testing.md'},
{'Debugging': 'api-guide/debugging.md'},
{'Advanced': [
{'Part 1': 'api-guide/advanced/part-1.md'},
]},
]},
{'About': [
{'Release notes': 'about/release-notes.md'},
{'License': 'about/license.md'}
]}
]
site_navigation = nav.SiteNavigation(load_config(pages=pages))
ancestors = (
[],
[site_navigation.nav_items[1]],
[site_navigation.nav_items[1]],
[site_navigation.nav_items[1]],
[site_navigation.nav_items[1],
site_navigation.pages[4].ancestors[-1]],
[site_navigation.nav_items[2]],
[site_navigation.nav_items[2]],
)
self.assertEqual(len(site_navigation.pages), len(ancestors))
for i, (page, expected_ancestor) in enumerate(
zip(site_navigation.pages, ancestors)):
self.assertEqual(page.ancestors, expected_ancestor,
"Failed on ancestor test {0}".format(i))
def test_nesting(self):
pages = [
{'Home': 'index.md'},
{'Install': [
{'Pre-install': 'install/install-pre.md'},
{'The install': 'install/install-actual.md'},
{'Post install': 'install/install-post.md'},
]},
{'Guide': [
{'Tutorial': [
{'Getting Started': 'guide/tutorial/running.md'},
{'Advanced Features': 'guide/tutorial/testing.md'},
{'Further Reading': 'guide/tutorial/debugging.md'},
]},
{'API Reference': [
{'Feature 1': 'guide/api-ref/running.md'},
{'Feature 2': 'guide/api-ref/testing.md'},
{'Feature 3': 'guide/api-ref/debugging.md'},
]},
{'Testing': 'guide/testing.md'},
{'Deploying': 'guide/deploying.md'},
]}
]
site_navigation = nav.SiteNavigation(load_config(pages=pages))
self.assertEqual([n.title for n in site_navigation.nav_items],
['Home', 'Install', 'Guide'])
self.assertEqual(len(site_navigation.pages), 12)
expected = dedent("""
Home - /
Install
Pre-install - /install/install-pre/
The install - /install/install-actual/
Post install - /install/install-post/
Guide
Tutorial
Getting Started - /guide/tutorial/running/
Advanced Features - /guide/tutorial/testing/
Further Reading - /guide/tutorial/debugging/
API Reference
Feature 1 - /guide/api-ref/running/
Feature 2 - /guide/api-ref/testing/
Feature 3 - /guide/api-ref/debugging/
Testing - /guide/testing/
Deploying - /guide/deploying/
""")
self.maxDiff = None
self.assertEqual(str(site_navigation).strip(), expected)
def test_edit_uri(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
'sub1/sub2/internal.md',
]
# Basic test
repo_url = 'http://example.com/'
edit_uri = 'edit/master/docs/'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
repo_url + edit_uri + pages[1],
repo_url + edit_uri + pages[2],
repo_url + edit_uri + pages[3],
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_sub_dir(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
'sub1/sub2/internal.md',
]
# Basic test
repo_url = 'http://example.com/foo/'
edit_uri = 'edit/master/docs/'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
repo_url + edit_uri + pages[1],
repo_url + edit_uri + pages[2],
repo_url + edit_uri + pages[3],
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_missing_slash(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
'sub1/sub2/internal.md',
]
# Ensure the '/' is added to the repo_url and edit_uri
repo_url = 'http://example.com'
edit_uri = 'edit/master/docs'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + '/' + edit_uri + '/' + pages[0],
repo_url + '/' + edit_uri + '/' + pages[1],
repo_url + '/' + edit_uri + '/' + pages[2],
repo_url + '/' + edit_uri + '/' + pages[3],
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_sub_dir_missing_slash(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
'sub1/sub2/internal.md',
]
# Basic test
repo_url = 'http://example.com/foo'
edit_uri = 'edit/master/docs'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + '/' + edit_uri + '/' + pages[0],
repo_url + '/' + edit_uri + '/' + pages[1],
repo_url + '/' + edit_uri + '/' + pages[2],
repo_url + '/' + edit_uri + '/' + pages[3],
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_query_string(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
'sub1/sub2/internal.md',
]
# Ensure query strings are supported
repo_url = 'http://example.com'
edit_uri = '?query=edit/master/docs/'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
repo_url + edit_uri + pages[1],
repo_url + edit_uri + pages[2],
repo_url + edit_uri + pages[3],
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_fragment(self):
pages = [
'index.md',
'internal.md',
'sub/internal.md',
'sub1/sub2/internal.md',
]
# Ensure fragment strings are supported
repo_url = 'http://example.com'
edit_uri = '#fragment/edit/master/docs/'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
repo_url + edit_uri + pages[1],
repo_url + edit_uri + pages[2],
repo_url + edit_uri + pages[3],
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_windows(self):
pages = [
'index.md',
'internal.md',
'sub\\internal.md',
'sub1\\sub2\\internal.md',
]
# Basic test
repo_url = 'http://example.com/'
edit_uri = 'edit/master/docs/'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
repo_url + edit_uri + pages[1],
repo_url + edit_uri + pages[2].replace('\\', '/'),
repo_url + edit_uri + pages[3].replace('\\', '/'),
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_sub_dir_windows(self):
pages = [
'index.md',
'internal.md',
'sub\\internal.md',
'sub1\\sub2\\internal.md',
]
# Basic test
repo_url = 'http://example.com/foo/'
edit_uri = 'edit/master/docs/'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
repo_url + edit_uri + pages[1],
repo_url + edit_uri + pages[2].replace('\\', '/'),
repo_url + edit_uri + pages[3].replace('\\', '/'),
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_missing_slash_windows(self):
pages = [
'index.md',
'internal.md',
'sub\\internal.md',
'sub1\\sub2\\internal.md',
]
# Ensure the '/' is added to the repo_url and edit_uri
repo_url = 'http://example.com'
edit_uri = 'edit/master/docs'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + '/' + edit_uri + '/' + pages[0],
repo_url + '/' + edit_uri + '/' + pages[1],
repo_url + '/' + edit_uri + '/' + pages[2].replace('\\', '/'),
repo_url + '/' + edit_uri + '/' + pages[3].replace('\\', '/'),
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_sub_dir_missing_slash_windows(self):
pages = [
'index.md',
'internal.md',
'sub\\internal.md',
'sub1\\sub2\\internal.md',
]
# Ensure the '/' is added to the repo_url and edit_uri
repo_url = 'http://example.com/foo'
edit_uri = 'edit/master/docs'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + '/' + edit_uri + '/' + pages[0],
repo_url + '/' + edit_uri + '/' + pages[1],
repo_url + '/' + edit_uri + '/' + pages[2].replace('\\', '/'),
repo_url + '/' + edit_uri + '/' + pages[3].replace('\\', '/'),
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_query_string_windows(self):
pages = [
'index.md',
'internal.md',
'sub\\internal.md',
'sub1\\sub2\\internal.md',
]
# Ensure query strings are supported
repo_url = 'http://example.com'
edit_uri = '?query=edit/master/docs/'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
repo_url + edit_uri + pages[1],
repo_url + edit_uri + pages[2].replace('\\', '/'),
repo_url + edit_uri + pages[3].replace('\\', '/'),
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
def test_edit_uri_fragment_windows(self):
pages = [
'index.md',
'internal.md',
'sub\\internal.md',
'sub1\\sub2\\internal.md',
]
# Ensure fragment strings are supported
repo_url = 'http://example.com'
edit_uri = '#fragment/edit/master/docs/'
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
repo_url + edit_uri + pages[1],
repo_url + edit_uri + pages[2].replace('\\', '/'),
repo_url + edit_uri + pages[3].replace('\\', '/'),
)
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
+11 -10
View File
@@ -6,11 +6,13 @@ import unittest
import mock
import json
from mkdocs import nav
from mkdocs.structure.files import File
from mkdocs.structure.pages import Page
from mkdocs.structure.toc import get_toc
from mkdocs.contrib import search
from mkdocs.contrib.search import search_index
from mkdocs.config.config_options import ValidationError
from mkdocs.tests.base import dedent, markdown_to_toc, load_config
from mkdocs.tests.base import dedent, get_markdown_toc, load_config
def strip_whitespace(string):
@@ -248,7 +250,7 @@ class SearchIndexTests(unittest.TestCase):
## Heading 2
### Heading 3
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
toc_item = index._find_toc_by_id(toc, "heading-1")
self.assertEqual(toc_item.url, "#heading-1")
@@ -273,23 +275,22 @@ class SearchIndexTests(unittest.TestCase):
<p>Content 3</p>
"""
cfg = load_config()
pages = [
{'Home': 'index.md'},
{'About': 'about.md'},
Page('Home', File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
Page('About', File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg)
]
site_navigation = nav.SiteNavigation(load_config(pages=pages))
md = dedent("""
# Heading 1
## Heading 2
### Heading 3
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
full_content = ''.join("""Heading{0}Content{0}""".format(i) for i in range(1, 4))
for page in site_navigation:
for page in pages:
# Fake page.read_source() and page.render()
page.markdown = md
page.toc = toc
@@ -300,7 +301,7 @@ class SearchIndexTests(unittest.TestCase):
self.assertEqual(len(index._entries), 4)
loc = page.abs_url
loc = page.url
self.assertEqual(index._entries[0]['title'], page.title)
self.assertEqual(strip_whitespace(index._entries[0]['text']), full_content)
View File
+576
View File
@@ -0,0 +1,576 @@
import unittest
import os
import io
import mock
from mkdocs.structure.files import Files, File, get_files, _sort_files, _filter_paths
from mkdocs.tests.base import load_config, tempdir, PathAssertionMixin
class TestFiles(PathAssertionMixin, unittest.TestCase):
def test_file_eq(self):
file = File('a.md', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertTrue(file == File('a.md', '/path/to/docs', '/path/to/site', use_directory_urls=False))
def test_file_ne(self):
file = File('a.md', '/path/to/docs', '/path/to/site', use_directory_urls=False)
# Different filename
self.assertTrue(file != File('b.md', '/path/to/docs', '/path/to/site', use_directory_urls=False))
# Different src_path
self.assertTrue(file != File('a.md', '/path/to/other', '/path/to/site', use_directory_urls=False))
# Different URL
self.assertTrue(file != File('a.md', '/path/to/docs', '/path/to/site', use_directory_urls=True))
def test_sort_files(self):
self.assertEqual(
_sort_files(['b.md', 'bb.md', 'a.md', 'index.md', 'aa.md']),
['index.md', 'a.md', 'aa.md', 'b.md', 'bb.md']
)
self.assertEqual(
_sort_files(['b.md', 'index.html', 'a.md', 'index.md']),
['index.html', 'index.md', 'a.md', 'b.md']
)
self.assertEqual(
_sort_files(['a.md', 'index.md', 'b.md', 'index.html']),
['index.md', 'index.html', 'a.md', 'b.md']
)
self.assertEqual(
_sort_files(['.md', '_.md', 'a.md', 'index.md', '1.md']),
['index.md', '.md', '1.md', '_.md', 'a.md']
)
self.assertEqual(
_sort_files(['a.md', 'b.md', 'a.md']),
['a.md', 'a.md', 'b.md']
)
def test_md_file(self):
f = File('foo.md', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertPathsEqual(f.src_path, 'foo.md')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo.md')
self.assertPathsEqual(f.dest_path, 'foo.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo.html')
self.assertEqual(f.url, 'foo.html')
self.assertEqual(f.name, 'foo')
self.assertTrue(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_md_file_use_directory_urls(self):
f = File('foo.md', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertPathsEqual(f.src_path, 'foo.md')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo.md')
self.assertPathsEqual(f.dest_path, 'foo/index.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/index.html')
self.assertEqual(f.url, 'foo/')
self.assertEqual(f.name, 'foo')
self.assertTrue(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_md_file_nested(self):
f = File('foo/bar.md', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertPathsEqual(f.src_path, 'foo/bar.md')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.md')
self.assertPathsEqual(f.dest_path, 'foo/bar.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.html')
self.assertEqual(f.url, 'foo/bar.html')
self.assertEqual(f.name, 'bar')
self.assertTrue(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_md_file_nested_use_directory_urls(self):
f = File('foo/bar.md', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertPathsEqual(f.src_path, 'foo/bar.md')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.md')
self.assertPathsEqual(f.dest_path, 'foo/bar/index.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar/index.html')
self.assertEqual(f.url, 'foo/bar/')
self.assertEqual(f.name, 'bar')
self.assertTrue(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_md_index_file(self):
f = File('index.md', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertPathsEqual(f.src_path, 'index.md')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/index.md')
self.assertPathsEqual(f.dest_path, 'index.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/index.html')
self.assertEqual(f.url, 'index.html')
self.assertEqual(f.name, 'index')
self.assertTrue(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_md_index_file_use_directory_urls(self):
f = File('index.md', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertPathsEqual(f.src_path, 'index.md')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/index.md')
self.assertPathsEqual(f.dest_path, 'index.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/index.html')
self.assertEqual(f.url, '.')
self.assertEqual(f.name, 'index')
self.assertTrue(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_md_index_file_nested(self):
f = File('foo/index.md', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertPathsEqual(f.src_path, 'foo/index.md')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/index.md')
self.assertPathsEqual(f.dest_path, 'foo/index.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/index.html')
self.assertEqual(f.url, 'foo/index.html')
self.assertEqual(f.name, 'index')
self.assertTrue(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_md_index_file_nested_use_directory_urls(self):
f = File('foo/index.md', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertPathsEqual(f.src_path, 'foo/index.md')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/index.md')
self.assertPathsEqual(f.dest_path, 'foo/index.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/index.html')
self.assertEqual(f.url, 'foo/')
self.assertEqual(f.name, 'index')
self.assertTrue(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_static_file(self):
f = File('foo/bar.html', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertPathsEqual(f.src_path, 'foo/bar.html')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.html')
self.assertPathsEqual(f.dest_path, 'foo/bar.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.html')
self.assertEqual(f.url, 'foo/bar.html')
self.assertEqual(f.name, 'bar')
self.assertFalse(f.is_documentation_page())
self.assertTrue(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_static_file_use_directory_urls(self):
f = File('foo/bar.html', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertPathsEqual(f.src_path, 'foo/bar.html')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.html')
self.assertPathsEqual(f.dest_path, 'foo/bar.html')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.html')
self.assertEqual(f.url, 'foo/bar.html')
self.assertEqual(f.name, 'bar')
self.assertFalse(f.is_documentation_page())
self.assertTrue(f.is_static_page())
self.assertFalse(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_media_file(self):
f = File('foo/bar.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertPathsEqual(f.src_path, 'foo/bar.jpg')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.jpg')
self.assertPathsEqual(f.dest_path, 'foo/bar.jpg')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.jpg')
self.assertEqual(f.url, 'foo/bar.jpg')
self.assertEqual(f.name, 'bar')
self.assertFalse(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertTrue(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_media_file_use_directory_urls(self):
f = File('foo/bar.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertPathsEqual(f.src_path, 'foo/bar.jpg')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.jpg')
self.assertPathsEqual(f.dest_path, 'foo/bar.jpg')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.jpg')
self.assertEqual(f.url, 'foo/bar.jpg')
self.assertEqual(f.name, 'bar')
self.assertFalse(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertTrue(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertFalse(f.is_css())
def test_javascript_file(self):
f = File('foo/bar.js', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertPathsEqual(f.src_path, 'foo/bar.js')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.js')
self.assertPathsEqual(f.dest_path, 'foo/bar.js')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.js')
self.assertEqual(f.url, 'foo/bar.js')
self.assertEqual(f.name, 'bar')
self.assertFalse(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertTrue(f.is_media_file())
self.assertTrue(f.is_javascript())
self.assertFalse(f.is_css())
def test_javascript_file_use_directory_urls(self):
f = File('foo/bar.js', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertPathsEqual(f.src_path, 'foo/bar.js')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.js')
self.assertPathsEqual(f.dest_path, 'foo/bar.js')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.js')
self.assertEqual(f.url, 'foo/bar.js')
self.assertEqual(f.name, 'bar')
self.assertFalse(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertTrue(f.is_media_file())
self.assertTrue(f.is_javascript())
self.assertFalse(f.is_css())
def test_css_file(self):
f = File('foo/bar.css', '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertPathsEqual(f.src_path, 'foo/bar.css')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.css')
self.assertPathsEqual(f.dest_path, 'foo/bar.css')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.css')
self.assertEqual(f.url, 'foo/bar.css')
self.assertEqual(f.name, 'bar')
self.assertFalse(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertTrue(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertTrue(f.is_css())
def test_css_file_use_directory_urls(self):
f = File('foo/bar.css', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertPathsEqual(f.src_path, 'foo/bar.css')
self.assertPathsEqual(f.abs_src_path, '/path/to/docs/foo/bar.css')
self.assertPathsEqual(f.dest_path, 'foo/bar.css')
self.assertPathsEqual(f.abs_dest_path, '/path/to/site/foo/bar.css')
self.assertEqual(f.url, 'foo/bar.css')
self.assertEqual(f.name, 'bar')
self.assertFalse(f.is_documentation_page())
self.assertFalse(f.is_static_page())
self.assertTrue(f.is_media_file())
self.assertFalse(f.is_javascript())
self.assertTrue(f.is_css())
def test_files(self):
fs = [
File('index.md', '/path/to/docs', '/path/to/site', use_directory_urls=True),
File('foo/bar.md', '/path/to/docs', '/path/to/site', use_directory_urls=True),
File('foo/bar.html', '/path/to/docs', '/path/to/site', use_directory_urls=True),
File('foo/bar.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=True),
File('foo/bar.js', '/path/to/docs', '/path/to/site', use_directory_urls=True),
File('foo/bar.css', '/path/to/docs', '/path/to/site', use_directory_urls=True)
]
files = Files(fs)
self.assertEqual([f for f in files], fs)
self.assertEqual(len(files), 6)
self.assertEqual(files.documentation_pages(), [fs[0], fs[1]])
self.assertEqual(files.static_pages(), [fs[2]])
self.assertEqual(files.media_files(), [fs[3], fs[4], fs[5]])
self.assertEqual(files.javascript_files(), [fs[4]])
self.assertEqual(files.css_files(), [fs[5]])
self.assertEqual(files.get_file_from_path('foo/bar.jpg'), fs[3])
self.assertEqual(files.get_file_from_path('foo/bar.jpg'), fs[3])
self.assertEqual(files.get_file_from_path('missing.jpg'), None)
self.assertTrue(fs[2].src_path in files)
self.assertTrue(fs[2].src_path in files)
extra_file = File('extra.md', '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertFalse(extra_file.src_path in files)
files.append(extra_file)
self.assertEqual(len(files), 7)
self.assertTrue(extra_file.src_path in files)
self.assertEqual(files.documentation_pages(), [fs[0], fs[1], extra_file])
def test_filter_paths(self):
# Root level file
self.assertFalse(_filter_paths('foo.md', 'foo.md', False, ['bar.md']))
self.assertTrue(_filter_paths('foo.md', 'foo.md', False, ['foo.md']))
# Nested file
self.assertFalse(_filter_paths('foo.md', 'baz/foo.md', False, ['bar.md']))
self.assertTrue(_filter_paths('foo.md', 'baz/foo.md', False, ['foo.md']))
# Wildcard
self.assertFalse(_filter_paths('foo.md', 'foo.md', False, ['*.txt']))
self.assertTrue(_filter_paths('foo.md', 'foo.md', False, ['*.md']))
# Root level dir
self.assertFalse(_filter_paths('bar', 'bar', True, ['/baz']))
self.assertFalse(_filter_paths('bar', 'bar', True, ['/baz/']))
self.assertTrue(_filter_paths('bar', 'bar', True, ['/bar']))
self.assertTrue(_filter_paths('bar', 'bar', True, ['/bar/']))
# Nested dir
self.assertFalse(_filter_paths('bar', 'foo/bar', True, ['/bar']))
self.assertFalse(_filter_paths('bar', 'foo/bar', True, ['/bar/']))
self.assertTrue(_filter_paths('bar', 'foo/bar', True, ['bar/']))
# Files that look like dirs (no extension). Note that `is_dir` is `False`.
self.assertFalse(_filter_paths('bar', 'bar', False, ['bar/']))
self.assertFalse(_filter_paths('bar', 'foo/bar', False, ['bar/']))
def test_get_relative_url_use_directory_urls(self):
to_files = [
'index.md',
'foo/index.md',
'foo/bar/index.md',
'foo/bar/baz/index.md',
'foo.md',
'foo/bar.md',
'foo/bar/baz.md'
]
to_file_urls = [
'.',
'foo/',
'foo/bar/',
'foo/bar/baz/',
'foo/',
'foo/bar/',
'foo/bar/baz/'
]
from_file = File('img.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=True)
expected = [
'img.jpg', # img.jpg relative to .
'../img.jpg', # img.jpg relative to foo/
'../../img.jpg', # img.jpg relative to foo/bar/
'../../../img.jpg', # img.jpg relative to foo/bar/baz/
'../img.jpg', # img.jpg relative to foo
'../../img.jpg', # img.jpg relative to foo/bar
'../../../img.jpg' # img.jpg relative to foo/bar/baz
]
for i, filename in enumerate(to_files):
file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertEqual(from_file.url, 'img.jpg')
self.assertEqual(file.url, to_file_urls[i])
self.assertEqual(from_file.url_relative_to(file.url), expected[i])
self.assertEqual(from_file.url_relative_to(file), expected[i])
from_file = File('foo/img.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=True)
expected = [
'foo/img.jpg', # foo/img.jpg relative to .
'img.jpg', # foo/img.jpg relative to foo/
'../img.jpg', # foo/img.jpg relative to foo/bar/
'../../img.jpg', # foo/img.jpg relative to foo/bar/baz/
'img.jpg', # foo/img.jpg relative to foo
'../img.jpg', # foo/img.jpg relative to foo/bar
'../../img.jpg' # foo/img.jpg relative to foo/bar/baz
]
for i, filename in enumerate(to_files):
file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertEqual(from_file.url, 'foo/img.jpg')
self.assertEqual(file.url, to_file_urls[i])
self.assertEqual(from_file.url_relative_to(file.url), expected[i])
self.assertEqual(from_file.url_relative_to(file), expected[i])
from_file = File('index.html', '/path/to/docs', '/path/to/site', use_directory_urls=True)
expected = [
'.', # . relative to .
'..', # . relative to foo/
'../..', # . relative to foo/bar/
'../../..', # . relative to foo/bar/baz/
'..', # . relative to foo
'../..', # . relative to foo/bar
'../../..' # . relative to foo/bar/baz
]
for i, filename in enumerate(to_files):
file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertEqual(from_file.url, '.')
self.assertEqual(file.url, to_file_urls[i])
self.assertEqual(from_file.url_relative_to(file.url), expected[i])
self.assertEqual(from_file.url_relative_to(file), expected[i])
from_file = File('file.md', '/path/to/docs', '/path/to/site', use_directory_urls=True)
expected = [
'file/', # file relative to .
'../file/', # file relative to foo/
'../../file/', # file relative to foo/bar/
'../../../file/', # file relative to foo/bar/baz/
'../file/', # file relative to foo
'../../file/', # file relative to foo/bar
'../../../file/' # file relative to foo/bar/baz
]
for i, filename in enumerate(to_files):
file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=True)
self.assertEqual(from_file.url, 'file/')
self.assertEqual(file.url, to_file_urls[i])
self.assertEqual(from_file.url_relative_to(file.url), expected[i])
self.assertEqual(from_file.url_relative_to(file), expected[i])
def test_get_relative_url(self):
to_files = [
'index.md',
'foo/index.md',
'foo/bar/index.md',
'foo/bar/baz/index.md',
'foo.md',
'foo/bar.md',
'foo/bar/baz.md'
]
to_file_urls = [
'index.html',
'foo/index.html',
'foo/bar/index.html',
'foo/bar/baz/index.html',
'foo.html',
'foo/bar.html',
'foo/bar/baz.html'
]
from_file = File('img.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=False)
expected = [
'img.jpg', # img.jpg relative to .
'../img.jpg', # img.jpg relative to foo/
'../../img.jpg', # img.jpg relative to foo/bar/
'../../../img.jpg', # img.jpg relative to foo/bar/baz/
'img.jpg', # img.jpg relative to foo.html
'../img.jpg', # img.jpg relative to foo/bar.html
'../../img.jpg' # img.jpg relative to foo/bar/baz.html
]
for i, filename in enumerate(to_files):
file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertEqual(from_file.url, 'img.jpg')
self.assertEqual(file.url, to_file_urls[i])
self.assertEqual(from_file.url_relative_to(file.url), expected[i])
self.assertEqual(from_file.url_relative_to(file), expected[i])
from_file = File('foo/img.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=False)
expected = [
'foo/img.jpg', # foo/img.jpg relative to .
'img.jpg', # foo/img.jpg relative to foo/
'../img.jpg', # foo/img.jpg relative to foo/bar/
'../../img.jpg', # foo/img.jpg relative to foo/bar/baz/
'foo/img.jpg', # foo/img.jpg relative to foo.html
'img.jpg', # foo/img.jpg relative to foo/bar.html
'../img.jpg' # foo/img.jpg relative to foo/bar/baz.html
]
for i, filename in enumerate(to_files):
file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertEqual(from_file.url, 'foo/img.jpg')
self.assertEqual(file.url, to_file_urls[i])
self.assertEqual(from_file.url_relative_to(file.url), expected[i])
self.assertEqual(from_file.url_relative_to(file), expected[i])
from_file = File('index.html', '/path/to/docs', '/path/to/site', use_directory_urls=False)
expected = [
'index.html', # index.html relative to .
'../index.html', # index.html relative to foo/
'../../index.html', # index.html relative to foo/bar/
'../../../index.html', # index.html relative to foo/bar/baz/
'index.html', # index.html relative to foo.html
'../index.html', # index.html relative to foo/bar.html
'../../index.html' # index.html relative to foo/bar/baz.html
]
for i, filename in enumerate(to_files):
file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertEqual(from_file.url, 'index.html')
self.assertEqual(file.url, to_file_urls[i])
self.assertEqual(from_file.url_relative_to(file.url), expected[i])
self.assertEqual(from_file.url_relative_to(file), expected[i])
from_file = File('file.html', '/path/to/docs', '/path/to/site', use_directory_urls=False)
expected = [
'file.html', # file.html relative to .
'../file.html', # file.html relative to foo/
'../../file.html', # file.html relative to foo/bar/
'../../../file.html', # file.html relative to foo/bar/baz/
'file.html', # file.html relative to foo.html
'../file.html', # file.html relative to foo/bar.html
'../../file.html' # file.html relative to foo/bar/baz.html
]
for i, filename in enumerate(to_files):
file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=False)
self.assertEqual(from_file.url, 'file.html')
self.assertEqual(file.url, to_file_urls[i])
self.assertEqual(from_file.url_relative_to(file.url), expected[i])
self.assertEqual(from_file.url_relative_to(file), expected[i])
@tempdir(files=[
'index.md',
'bar.css',
'bar.html',
'bar.jpg',
'bar.js',
'bar.md',
'.dotfile',
'templates/foo.html'
])
def test_get_files(self, tdir):
config = load_config(docs_dir=tdir, extra_css=['bar.css'], extra_javascript=['bar.js'])
files = get_files(config)
expected = ['index.md', 'bar.css', 'bar.html', 'bar.jpg', 'bar.js', 'bar.md']
self.assertIsInstance(files, Files)
self.assertEqual(len(files), len(expected))
self.assertEqual([f.src_path for f in files], expected)
@tempdir()
@tempdir(files={'test.txt': 'source content'})
def test_copy_file(self, src_dir, dest_dir):
file = File('test.txt', src_dir, dest_dir, use_directory_urls=False)
dest_path = os.path.join(dest_dir, 'test.txt')
self.assertPathNotExists(dest_path)
file.copy_file()
self.assertPathIsFile(dest_path)
@tempdir(files={'test.txt': 'destination content'})
@tempdir(files={'test.txt': 'source content'})
def test_copy_file_clean_modified(self, src_dir, dest_dir):
file = File('test.txt', src_dir, dest_dir, use_directory_urls=False)
file.is_modified = mock.Mock(return_value=True)
dest_path = os.path.join(dest_dir, 'test.txt')
file.copy_file(dirty=False)
self.assertPathIsFile(dest_path)
with io.open(dest_path, 'r', encoding='utf-8') as f:
self.assertEqual(f.read(), 'source content')
@tempdir(files={'test.txt': 'destination content'})
@tempdir(files={'test.txt': 'source content'})
def test_copy_file_dirty_modified(self, src_dir, dest_dir):
file = File('test.txt', src_dir, dest_dir, use_directory_urls=False)
file.is_modified = mock.Mock(return_value=True)
dest_path = os.path.join(dest_dir, 'test.txt')
file.copy_file(dirty=True)
self.assertPathIsFile(dest_path)
with io.open(dest_path, 'r', encoding='utf-8') as f:
self.assertEqual(f.read(), 'source content')
@tempdir(files={'test.txt': 'destination content'})
@tempdir(files={'test.txt': 'source content'})
def test_copy_file_dirty_not_modified(self, src_dir, dest_dir):
file = File('test.txt', src_dir, dest_dir, use_directory_urls=False)
file.is_modified = mock.Mock(return_value=False)
dest_path = os.path.join(dest_dir, 'test.txt')
file.copy_file(dirty=True)
self.assertPathIsFile(dest_path)
with io.open(dest_path, 'r', encoding='utf-8') as f:
self.assertEqual(f.read(), 'destination content')
+343
View File
@@ -0,0 +1,343 @@
#!/usr/bin/env python
# coding: utf-8
from __future__ import unicode_literals
import sys
import unittest
from mkdocs.structure.nav import get_navigation
from mkdocs.structure.files import File, Files
from mkdocs.structure.pages import Page
from mkdocs.tests.base import dedent, load_config
class SiteNavigationTests(unittest.TestCase):
maxDiff = None
def test_simple_nav(self):
nav_cfg = [
{'Home': 'index.md'},
{'About': 'about.md'}
]
expected = dedent("""
Page(title='Home', url='/')
Page(title='About', url='/about/')
""")
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files(
[File(list(item.values())[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
for item in nav_cfg]
)
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 2)
self.assertEqual(len(site_navigation.pages), 2)
self.assertEqual(repr(site_navigation.homepage), "Page(title='Home', url='/')")
def test_nav_no_directory_urls(self):
nav_cfg = [
{'Home': 'index.md'},
{'About': 'about.md'}
]
expected = dedent("""
Page(title='Home', url='/index.html')
Page(title='About', url='/about.html')
""")
cfg = load_config(nav=nav_cfg, use_directory_urls=False, site_url='http://example.com/')
files = Files(
[File(list(item.values())[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
for item in nav_cfg]
)
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 2)
self.assertEqual(len(site_navigation.pages), 2)
def test_nav_missing_page(self):
nav_cfg = [
{'Home': 'index.md'}
]
expected = dedent("""
Page(title='Home', url='/')
""")
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('page_not_in_nav.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
])
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 1)
self.assertEqual(len(site_navigation.pages), 1)
for file in files:
self.assertIsInstance(file.page, Page)
def test_nav_no_title(self):
nav_cfg = [
'index.md',
{'About': 'about.md'}
]
expected = dedent("""
Page(title=[blank], url='/')
Page(title='About', url='/about/')
""")
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files([
File(nav_cfg[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File(nav_cfg[1]['About'], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
])
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 2)
self.assertEqual(len(site_navigation.pages), 2)
def test_nav_external_links(self):
nav_cfg = [
{'Home': 'index.md'},
{'Local': '/local.html'},
{'External': 'http://example.com/external.html'}
]
expected = dedent("""
Page(title='Home', url='/')
Link(title='Local', url='/local.html')
Link(title='External', url='http://example.com/external.html')
""")
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 3)
self.assertEqual(len(site_navigation.pages), 1)
def test_indented_nav(self):
nav_cfg = [
{'Home': 'index.md'},
{'API Guide': [
{'Running': 'api-guide/running.md'},
{'Testing': 'api-guide/testing.md'},
{'Debugging': 'api-guide/debugging.md'},
{'Advanced': [
{'Part 1': 'api-guide/advanced/part-1.md'},
]},
]},
{'About': [
{'Release notes': 'about/release-notes.md'},
{'License': '/license.html'}
]},
{'External': 'https://example.com/'}
]
expected = dedent("""
Page(title='Home', url='/')
Section(title='API Guide')
Page(title='Running', url='/api-guide/running/')
Page(title='Testing', url='/api-guide/testing/')
Page(title='Debugging', url='/api-guide/debugging/')
Section(title='Advanced')
Page(title='Part 1', url='/api-guide/advanced/part-1/')
Section(title='About')
Page(title='Release notes', url='/about/release-notes/')
Link(title='License', url='/license.html')
Link(title='External', url='https://example.com/')
""")
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/advanced/part-1.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 4)
self.assertEqual(len(site_navigation.pages), 6)
self.assertEqual(repr(site_navigation.homepage), "Page(title='Home', url='/')")
self.assertIsNone(site_navigation.items[0].parent)
self.assertEqual(site_navigation.items[0].ancestors, [])
self.assertIsNone(site_navigation.items[1].parent)
self.assertEqual(site_navigation.items[1].ancestors, [])
self.assertEqual(len(site_navigation.items[1].children), 4)
self.assertEqual(repr(site_navigation.items[1].children[0].parent), "Section(title='API Guide')")
self.assertEqual(site_navigation.items[1].children[0].ancestors, [site_navigation.items[1]])
self.assertEqual(repr(site_navigation.items[1].children[1].parent), "Section(title='API Guide')")
self.assertEqual(site_navigation.items[1].children[1].ancestors, [site_navigation.items[1]])
self.assertEqual(repr(site_navigation.items[1].children[2].parent), "Section(title='API Guide')")
self.assertEqual(site_navigation.items[1].children[2].ancestors, [site_navigation.items[1]])
self.assertEqual(repr(site_navigation.items[1].children[3].parent), "Section(title='API Guide')")
self.assertEqual(site_navigation.items[1].children[3].ancestors, [site_navigation.items[1]])
self.assertEqual(len(site_navigation.items[1].children[3].children), 1)
self.assertEqual(repr(site_navigation.items[1].children[3].children[0].parent), "Section(title='Advanced')")
self.assertEqual(site_navigation.items[1].children[3].children[0].ancestors,
[site_navigation.items[1].children[3], site_navigation.items[1]])
self.assertIsNone(site_navigation.items[2].parent)
self.assertEqual(len(site_navigation.items[2].children), 2)
self.assertEqual(repr(site_navigation.items[2].children[0].parent), "Section(title='About')")
self.assertEqual(site_navigation.items[2].children[0].ancestors, [site_navigation.items[2]])
self.assertEqual(repr(site_navigation.items[2].children[1].parent), "Section(title='About')")
self.assertEqual(site_navigation.items[2].children[1].ancestors, [site_navigation.items[2]])
self.assertIsNone(site_navigation.items[3].parent)
self.assertEqual(site_navigation.items[3].ancestors, [])
self.assertIsNone(site_navigation.items[3].children)
def test_nested_ungrouped_nav(self):
nav_cfg = [
{'Home': 'index.md'},
{'Contact': 'about/contact.md'},
{'License Title': 'about/sub/license.md'},
]
expected = dedent("""
Page(title='Home', url='/')
Page(title='Contact', url='/about/contact/')
Page(title='License Title', url='/about/sub/license/')
""")
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files(
[File(list(item.values())[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
for item in nav_cfg]
)
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 3)
self.assertEqual(len(site_navigation.pages), 3)
def test_nested_ungrouped_nav_no_titles(self):
nav_cfg = [
'index.md',
'about/contact.md',
'about/sub/license.md'
]
expected = dedent("""
Page(title=[blank], url='/')
Page(title=[blank], url='/about/contact/')
Page(title=[blank], url='/about/sub/license/')
""")
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files(
[File(item, cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) for item in nav_cfg]
)
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 3)
self.assertEqual(len(site_navigation.pages), 3)
self.assertEqual(repr(site_navigation.homepage), "Page(title=[blank], url='/')")
@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
def test_nested_ungrouped_no_titles_windows(self):
nav_cfg = [
'index.md',
'about\\contact.md',
'about\\sub\\license.md',
]
expected = dedent("""
Page(title=[blank], url='/')
Page(title=[blank], url='/about/contact/')
Page(title=[blank], url='/about/sub/license/')
""")
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files(
[File(item, cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) for item in nav_cfg]
)
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 3)
self.assertEqual(len(site_navigation.pages), 3)
def test_nav_from_files(self):
expected = dedent("""
Page(title=[blank], url='/')
Page(title=[blank], url='/about/')
""")
cfg = load_config(site_url='http://example.com/')
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
])
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 2)
self.assertEqual(len(site_navigation.pages), 2)
self.assertEqual(repr(site_navigation.homepage), "Page(title=[blank], url='/')")
def test_nav_from_nested_files(self):
expected = dedent("""
Page(title=[blank], url='/')
Section(title='About')
Page(title=[blank], url='/about/license/')
Page(title=[blank], url='/about/release-notes/')
Section(title='Api guide')
Page(title=[blank], url='/api-guide/debugging/')
Page(title=[blank], url='/api-guide/running/')
Page(title=[blank], url='/api-guide/testing/')
Section(title='Advanced')
Page(title=[blank], url='/api-guide/advanced/part-1/')
""")
cfg = load_config(site_url='http://example.com/')
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('about/license.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/advanced/part-1.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
site_navigation = get_navigation(files, cfg)
self.assertEqual(str(site_navigation).strip(), expected)
self.assertEqual(len(site_navigation.items), 3)
self.assertEqual(len(site_navigation.pages), 7)
self.assertEqual(repr(site_navigation.homepage), "Page(title=[blank], url='/')")
def test_active(self):
nav_cfg = [
{'Home': 'index.md'},
{'API Guide': [
{'Running': 'api-guide/running.md'},
{'Testing': 'api-guide/testing.md'},
{'Debugging': 'api-guide/debugging.md'},
{'Advanced': [
{'Part 1': 'api-guide/advanced/part-1.md'},
]},
]},
{'About': [
{'Release notes': 'about/release-notes.md'},
{'License': 'about/license.md'}
]}
]
cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
files = Files([
File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('api-guide/advanced/part-1.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
File('about/license.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
])
site_navigation = get_navigation(files, cfg)
# Confirm nothing is active
self.assertTrue(all(page.active is False for page in site_navigation.pages))
self.assertTrue(all(item.active is False for item in site_navigation.items))
# Activate
site_navigation.items[1].children[3].children[0].active = True
# Confirm ancestors are activated
self.assertTrue(site_navigation.items[1].children[3].children[0].active)
self.assertTrue(site_navigation.items[1].children[3].active)
self.assertTrue(site_navigation.items[1].active)
# Confirm non-ancestors are not activated
self.assertFalse(site_navigation.items[0].active)
self.assertFalse(site_navigation.items[1].children[0].active)
self.assertFalse(site_navigation.items[1].children[1].active)
self.assertFalse(site_navigation.items[1].children[2].active)
self.assertFalse(site_navigation.items[2].active)
self.assertFalse(site_navigation.items[2].children[0].active)
self.assertFalse(site_navigation.items[2].children[1].active)
# Deactivate
site_navigation.items[1].children[3].children[0].active = False
# Confirm ancestors are deactivated
self.assertFalse(site_navigation.items[1].children[3].children[0].active)
self.assertFalse(site_navigation.items[1].children[3].active)
self.assertFalse(site_navigation.items[1].active)
+789
View File
@@ -0,0 +1,789 @@
from __future__ import unicode_literals
import unittest
import os
import sys
import mock
import io
try:
# py>=3.2
from tempfile import TemporaryDirectory
except ImportError:
from backports.tempfile import TemporaryDirectory
from mkdocs.structure.pages import Page
from mkdocs.structure.files import File, Files
from mkdocs.tests.base import load_config, dedent, LogTestCase
from mkdocs.exceptions import MarkdownNotFound
class PageTests(unittest.TestCase):
DOCS_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../integration/subpages/docs')
def test_homepage(self):
cfg = load_config(docs_dir=self.DOCS_DIR)
fl = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
self.assertIsNone(fl.page)
pg = Page('Foo', fl, cfg)
self.assertEqual(fl.page, pg)
self.assertEqual(pg.url, '')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertTrue(pg.is_homepage)
self.assertTrue(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertEqual(pg.markdown, None)
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Foo')
self.assertEqual(pg.toc, [])
def test_nested_index_page(self):
cfg = load_config(docs_dir=self.DOCS_DIR)
fl = File('sub1/index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
pg.parent = 'foo'
self.assertEqual(pg.url, 'sub1/')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertTrue(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertFalse(pg.is_top_level)
self.assertEqual(pg.markdown, None)
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, 'foo')
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Foo')
self.assertEqual(pg.toc, [])
def test_nested_nonindex_page(self):
cfg = load_config(docs_dir=self.DOCS_DIR)
fl = File('sub1/non-index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
pg.parent = 'foo'
self.assertEqual(pg.url, 'sub1/non-index/')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertFalse(pg.is_top_level)
self.assertEqual(pg.markdown, None)
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, 'foo')
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Foo')
self.assertEqual(pg.toc, [])
def test_page_defaults(self):
cfg = load_config()
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertRegexpMatches(pg.update_date, r'\d{4}-\d{2}-\d{2}')
self.assertEqual(pg.url, 'testing/')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertEqual(pg.markdown, None)
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Foo')
self.assertEqual(pg.toc, [])
def test_page_no_directory_url(self):
cfg = load_config(use_directory_urls=False)
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertEqual(pg.url, 'testing.html')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertEqual(pg.markdown, None)
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Foo')
self.assertEqual(pg.toc, [])
def test_page_canonical_url(self):
cfg = load_config(site_url='http://example.com')
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertEqual(pg.url, 'testing/')
self.assertEqual(pg.abs_url, '/testing/')
self.assertEqual(pg.canonical_url, 'http://example.com/testing/')
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertEqual(pg.markdown, None)
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Foo')
self.assertEqual(pg.toc, [])
def test_page_canonical_url_nested(self):
cfg = load_config(site_url='http://example.com/foo/')
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertEqual(pg.url, 'testing/')
self.assertEqual(pg.abs_url, '/foo/testing/')
self.assertEqual(pg.canonical_url, 'http://example.com/foo/testing/')
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertEqual(pg.markdown, None)
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Foo')
self.assertEqual(pg.toc, [])
def test_page_canonical_url_nested_no_slash(self):
cfg = load_config(site_url='http://example.com/foo')
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertEqual(pg.url, 'testing/')
self.assertEqual(pg.abs_url, '/foo/testing/')
self.assertEqual(pg.canonical_url, 'http://example.com/foo/testing/')
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertEqual(pg.markdown, None)
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Foo')
self.assertEqual(pg.toc, [])
def test_predefined_page_title(self):
cfg = load_config()
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Page Title', fl, cfg)
pg.read_source(cfg)
self.assertEqual(pg.url, 'testing/')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertTrue(pg.markdown.startswith('# Welcome to MkDocs\n'))
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Page Title')
self.assertEqual(pg.toc, [])
def test_page_title_from_markdown(self):
cfg = load_config()
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page(None, fl, cfg)
pg.read_source(cfg)
self.assertEqual(pg.url, 'testing/')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertTrue(pg.markdown.startswith('# Welcome to MkDocs\n'))
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Welcome to MkDocs')
self.assertEqual(pg.toc, [])
def test_page_title_from_meta(self):
cfg = load_config(docs_dir=self.DOCS_DIR)
fl = File('metadata.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page(None, fl, cfg)
pg.read_source(cfg)
self.assertEqual(pg.url, 'metadata/')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertTrue(pg.markdown.startswith('# Welcome to MkDocs\n'))
self.assertEqual(pg.meta, {'title': 'A Page Title'})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'A Page Title')
self.assertEqual(pg.toc, [])
def test_page_title_from_filename(self):
cfg = load_config(docs_dir=self.DOCS_DIR)
fl = File('page-title.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page(None, fl, cfg)
pg.read_source(cfg)
self.assertEqual(pg.url, 'page-title/')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertTrue(pg.markdown.startswith('Page content.\n'))
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Page title')
self.assertEqual(pg.toc, [])
def test_page_title_from_capitalized_filename(self):
cfg = load_config(docs_dir=self.DOCS_DIR)
fl = File('pageTitle.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page(None, fl, cfg)
pg.read_source(cfg)
self.assertEqual(pg.url, 'pageTitle/')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertFalse(pg.is_homepage)
self.assertFalse(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertTrue(pg.markdown.startswith('Page content.\n'))
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'pageTitle')
self.assertEqual(pg.toc, [])
def test_page_title_from_homepage_filename(self):
cfg = load_config(docs_dir=self.DOCS_DIR)
fl = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page(None, fl, cfg)
pg.read_source(cfg)
self.assertEqual(pg.url, '')
self.assertEqual(pg.abs_url, None)
self.assertEqual(pg.canonical_url, None)
self.assertEqual(pg.edit_url, None)
self.assertEqual(pg.file, fl)
self.assertEqual(pg.content, None)
self.assertTrue(pg.is_homepage)
self.assertTrue(pg.is_index)
self.assertTrue(pg.is_page)
self.assertFalse(pg.is_section)
self.assertTrue(pg.is_top_level)
self.assertTrue(pg.markdown.startswith('## Test'))
self.assertEqual(pg.meta, {})
self.assertEqual(pg.next_page, None)
self.assertEqual(pg.parent, None)
self.assertEqual(pg.previous_page, None)
self.assertEqual(pg.title, 'Home')
self.assertEqual(pg.toc, [])
def test_page_eq(self):
cfg = load_config()
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertTrue(pg == Page('Foo', fl, cfg))
def test_page_ne(self):
cfg = load_config()
f1 = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
f2 = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', f1, cfg)
# Different Title
self.assertTrue(pg != Page('Bar', f1, cfg))
# Different File
self.assertTrue(pg != Page('Foo', f2, cfg))
def test_BOM(self):
md_src = '# An UTF-8 encoded file with a BOM'
with TemporaryDirectory() as docs_dir:
# We don't use mkdocs.tests.base.tempdir decorator here due to uniqueness of this test.
cfg = load_config(docs_dir=docs_dir)
fl = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page(None, fl, cfg)
# Create an UTF-8 Encoded file with BOM (as Micorsoft editors do). See #1186
with io.open(fl.abs_src_path, 'w', encoding='utf-8-sig') as f:
f.write(md_src)
# Now read the file.
pg.read_source(cfg)
# Ensure the BOM (`\ufeff`) is removed
self.assertNotIn('\ufeff', pg.markdown)
self.assertEqual(pg.markdown, md_src)
self.assertEqual(pg.meta, {})
def test_page_edit_url(self):
configs = [
{
'repo_url': 'http://github.com/mkdocs/mkdocs'
},
{
'repo_url': 'https://github.com/mkdocs/mkdocs/'
}, {
'repo_url': 'http://example.com'
}, {
'repo_url': 'http://example.com',
'edit_uri': 'edit/master'
}, {
'repo_url': 'http://example.com',
'edit_uri': '/edit/master'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': '/edit/master/'
}, {
'repo_url': 'http://example.com/foo',
'edit_uri': '/edit/master/'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': '/edit/master'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': 'edit/master/'
}, {
'repo_url': 'http://example.com/foo',
'edit_uri': 'edit/master/'
}, {
'repo_url': 'http://example.com',
'edit_uri': '?query=edit/master'
}, {
'repo_url': 'http://example.com/',
'edit_uri': '?query=edit/master/'
}, {
'repo_url': 'http://example.com',
'edit_uri': '#edit/master'
}, {
'repo_url': 'http://example.com/',
'edit_uri': '#edit/master/'
}, {
'repo_url': 'http://example.com',
'edit_uri': '' # Set to blank value
}, {
# Nothing defined
}
]
expected = [
'http://github.com/mkdocs/mkdocs/edit/master/docs/testing.md',
'https://github.com/mkdocs/mkdocs/edit/master/docs/testing.md',
None,
'http://example.com/edit/master/testing.md',
'http://example.com/edit/master/testing.md',
'http://example.com/edit/master/testing.md',
'http://example.com/edit/master/testing.md',
'http://example.com/edit/master/testing.md',
'http://example.com/foo/edit/master/testing.md',
'http://example.com/foo/edit/master/testing.md',
'http://example.com?query=edit/master/testing.md',
'http://example.com/?query=edit/master/testing.md',
'http://example.com#edit/master/testing.md',
'http://example.com/#edit/master/testing.md',
None,
None
]
for i, c in enumerate(configs):
cfg = load_config(**c)
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertEqual(pg.url, 'testing/')
self.assertEqual(pg.edit_url, expected[i])
def test_nested_page_edit_url(self):
configs = [
{
'repo_url': 'http://github.com/mkdocs/mkdocs'
},
{
'repo_url': 'https://github.com/mkdocs/mkdocs/'
}, {
'repo_url': 'http://example.com'
}, {
'repo_url': 'http://example.com',
'edit_uri': 'edit/master'
}, {
'repo_url': 'http://example.com',
'edit_uri': '/edit/master'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': '/edit/master/'
}, {
'repo_url': 'http://example.com/foo',
'edit_uri': '/edit/master/'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': '/edit/master'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': 'edit/master/'
}, {
'repo_url': 'http://example.com/foo',
'edit_uri': 'edit/master/'
}, {
'repo_url': 'http://example.com',
'edit_uri': '?query=edit/master'
}, {
'repo_url': 'http://example.com/',
'edit_uri': '?query=edit/master/'
}, {
'repo_url': 'http://example.com',
'edit_uri': '#edit/master'
}, {
'repo_url': 'http://example.com/',
'edit_uri': '#edit/master/'
}
]
expected = [
'http://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
'https://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
None,
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/foo/edit/master/sub1/non-index.md',
'http://example.com/foo/edit/master/sub1/non-index.md',
'http://example.com?query=edit/master/sub1/non-index.md',
'http://example.com/?query=edit/master/sub1/non-index.md',
'http://example.com#edit/master/sub1/non-index.md',
'http://example.com/#edit/master/sub1/non-index.md'
]
for i, c in enumerate(configs):
c['docs_dir'] = self.DOCS_DIR
cfg = load_config(**c)
fl = File('sub1/non-index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertEqual(pg.url, 'sub1/non-index/')
self.assertEqual(pg.edit_url, expected[i])
@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
def test_nested_page_edit_url_windows(self):
configs = [
{
'repo_url': 'http://github.com/mkdocs/mkdocs'
},
{
'repo_url': 'https://github.com/mkdocs/mkdocs/'
}, {
'repo_url': 'http://example.com'
}, {
'repo_url': 'http://example.com',
'edit_uri': 'edit/master'
}, {
'repo_url': 'http://example.com',
'edit_uri': '/edit/master'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': '/edit/master/'
}, {
'repo_url': 'http://example.com/foo',
'edit_uri': '/edit/master/'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': '/edit/master'
}, {
'repo_url': 'http://example.com/foo/',
'edit_uri': 'edit/master/'
}, {
'repo_url': 'http://example.com/foo',
'edit_uri': 'edit/master/'
}, {
'repo_url': 'http://example.com',
'edit_uri': '?query=edit/master'
}, {
'repo_url': 'http://example.com/',
'edit_uri': '?query=edit/master/'
}, {
'repo_url': 'http://example.com',
'edit_uri': '#edit/master'
}, {
'repo_url': 'http://example.com/',
'edit_uri': '#edit/master/'
}
]
expected = [
'http://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
'https://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
None,
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/edit/master/sub1/non-index.md',
'http://example.com/foo/edit/master/sub1/non-index.md',
'http://example.com/foo/edit/master/sub1/non-index.md',
'http://example.com?query=edit/master/sub1/non-index.md',
'http://example.com/?query=edit/master/sub1/non-index.md',
'http://example.com#edit/master/sub1/non-index.md',
'http://example.com/#edit/master/sub1/non-index.md'
]
for i, c in enumerate(configs):
c['docs_dir'] = self.DOCS_DIR
cfg = load_config(**c)
fl = File('sub1\\non-index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertEqual(pg.url, 'sub1/non-index/')
self.assertEqual(pg.edit_url, expected[i])
def test_page_render(self):
cfg = load_config()
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
pg.read_source(cfg)
self.assertEqual(pg.content, None)
self.assertEqual(pg.toc, [])
pg.render(cfg, [fl])
self.assertTrue(pg.content.startswith(
'<h1 id="welcome-to-mkdocs">Welcome to MkDocs</h1>\n'
))
self.assertEqual(str(pg.toc).strip(), dedent("""
Welcome to MkDocs - #welcome-to-mkdocs
Commands - #commands
Project layout - #project-layout
"""))
def test_missing_page(self):
cfg = load_config()
fl = File('missing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertRaises(IOError, pg.read_source, cfg)
class SourceDateEpochTests(unittest.TestCase):
def setUp(self):
self.default = os.environ.get('SOURCE_DATE_EPOCH', None)
os.environ['SOURCE_DATE_EPOCH'] = '0'
def test_source_date_epoch(self):
cfg = load_config()
fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
pg = Page('Foo', fl, cfg)
self.assertEqual(pg.update_date, '1970-01-01')
def tearDown(self):
if self.default is not None:
os.environ['SOURCE_DATE_EPOCH'] = self.default
else:
del os.environ['SOURCE_DATE_EPOCH']
class RelativePathExtensionTests(LogTestCase):
DOCS_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../integration/subpages/docs')
def get_rendered_result(self, files, strict=False):
cfg = load_config(docs_dir=self.DOCS_DIR, strict=strict)
fs = []
for f in files:
fs.append(File(f.replace('/', os.sep), cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']))
pg = Page('Foo', fs[0], cfg)
pg.read_source(cfg)
pg.render(cfg, Files(fs))
return pg.content
@mock.patch('io.open', mock.mock_open(read_data='[link](non-index.md)'))
def test_relative_html_link(self):
self.assertEqual(
self.get_rendered_result(['index.md', 'non-index.md']),
'<p><a href="non-index/">link</a></p>' # No trailing /
)
@mock.patch('io.open', mock.mock_open(read_data='[link](index.md)'))
def test_relative_html_link_index(self):
self.assertEqual(
self.get_rendered_result(['non-index.md', 'index.md']),
'<p><a href="..">link</a></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='[link](sub2/index.md)'))
def test_relative_html_link_sub_index(self):
self.assertEqual(
self.get_rendered_result(['index.md', 'sub2/index.md']),
'<p><a href="sub2/">link</a></p>' # No trailing /
)
@mock.patch('io.open', mock.mock_open(read_data='[link](sub2/non-index.md)'))
def test_relative_html_link_sub_page(self):
self.assertEqual(
self.get_rendered_result(['index.md', 'sub2/non-index.md']),
'<p><a href="sub2/non-index/">link</a></p>' # No trailing /
)
@mock.patch('io.open', mock.mock_open(read_data='[link](../index.md)'))
def test_relative_html_link_parent_index(self):
self.assertEqual(
self.get_rendered_result(['sub2/non-index.md', 'index.md']),
'<p><a href="../..">link</a></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='[link](non-index.md#hash)'))
def test_relative_html_link_hash(self):
self.assertEqual(
self.get_rendered_result(['index.md', 'non-index.md']),
'<p><a href="non-index/#hash">link</a></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='[link](sub2/index.md#hash)'))
def test_relative_html_link_sub_index_hash(self):
self.assertEqual(
self.get_rendered_result(['index.md', 'sub2/index.md']),
'<p><a href="sub2/#hash">link</a></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='[link](sub2/non-index.md#hash)'))
def test_relative_html_link_sub_page_hash(self):
self.assertEqual(
self.get_rendered_result(['index.md', 'sub2/non-index.md']),
'<p><a href="sub2/non-index/#hash">link</a></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='[link](#hash)'))
def test_relative_html_link_hash_only(self):
self.assertEqual(
self.get_rendered_result(['index.md']),
'<p><a href="#hash">link</a></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='![image](image.png)'))
def test_relative_image_link_from_homepage(self):
self.assertEqual(
self.get_rendered_result(['index.md', 'image.png']),
'<p><img alt="image" src="image.png" /></p>' # no opening ./
)
@mock.patch('io.open', mock.mock_open(read_data='![image](../image.png)'))
def test_relative_image_link_from_subpage(self):
self.assertEqual(
self.get_rendered_result(['sub2/non-index.md', 'image.png']),
'<p><img alt="image" src="../../image.png" /></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='![image](image.png)'))
def test_relative_image_link_from_sibling(self):
self.assertEqual(
self.get_rendered_result(['non-index.md', 'image.png']),
'<p><img alt="image" src="../image.png" /></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='*__not__ a link*.'))
def test_no_links(self):
self.assertEqual(
self.get_rendered_result(['index.md'], strict=True),
'<p><em><strong>not</strong> a link</em>.</p>'
)
@mock.patch('io.open', mock.mock_open(read_data='[link](non-existant.md)'))
def test_bad_relative_html_link(self):
with self.assertLogs('mkdocs', level='WARNING') as cm:
self.assertEqual(
self.get_rendered_result(['index.md']),
'<p><a href="non-existant.md">link</a></p>'
)
self.assertEqual(
cm.output,
["WARNING:mkdocs.structure.pages:Documentation file 'index.md' contains a link "
"to 'non-existant.md' which does not exist in the documentation directory."]
)
@mock.patch('io.open', mock.mock_open(read_data='[link](non-existant.md)'))
def test_bad_relative_html_link_strict(self):
self.assertRaises(MarkdownNotFound, self.get_rendered_result, ['index.md'], strict=True)
@mock.patch('io.open', mock.mock_open(read_data='[external link](http://example.com/index.md)'))
def test_external_link(self):
self.assertEqual(
self.get_rendered_result(['index.md'], strict=True),
'<p><a href="http://example.com/index.md">external link</a></p>'
)
@mock.patch('io.open', mock.mock_open(read_data='<mail@example.com>'))
def test_email_link(self):
self.assertEqual(
self.get_rendered_result(['index.md'], strict=True),
# Markdown's default behavior is to obscure email addresses by entity-encoding them.
# The following is equivalent to: '<p><a href="mailto:mail@example.com">mail@example.com</a></p>'
'<p><a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#109;&#97;&#105;&#108;&#64;&#101;'
'&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;">&#109;&#97;&#105;&#108;&#64;'
'&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;</a></p>'
)
@@ -3,12 +3,29 @@
from __future__ import unicode_literals
import unittest
from mkdocs.tests.base import dedent, markdown_to_toc
from mkdocs.structure.toc import get_toc
from mkdocs.tests.base import dedent, get_markdown_toc
class TableOfContentsTests(unittest.TestCase):
def test_html_toc(self):
html = dedent("""
<div class="toc">
<ul>
<li><a href="#foo">Heading 1</a></li>
<li><a href="#bar">Heading 2</a></li>
</ul>
</div>
""")
expected = dedent("""
Heading 1 - #foo
Heading 2 - #bar
""")
toc = get_toc(html)
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 2)
def test_indented_toc(self):
md = dedent("""
# Heading 1
@@ -20,8 +37,9 @@ class TableOfContentsTests(unittest.TestCase):
Heading 2 - #heading-2
Heading 3 - #heading-3
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 1)
def test_indented_toc_html(self):
md = dedent("""
@@ -34,8 +52,9 @@ class TableOfContentsTests(unittest.TestCase):
Heading 2 - #heading-2
Heading 3 - #heading-3
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 1)
def test_flat_toc(self):
md = dedent("""
@@ -48,8 +67,9 @@ class TableOfContentsTests(unittest.TestCase):
Heading 2 - #heading-2
Heading 3 - #heading-3
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 3)
def test_flat_h2_toc(self):
md = dedent("""
@@ -62,8 +82,9 @@ class TableOfContentsTests(unittest.TestCase):
Heading 2 - #heading-2
Heading 3 - #heading-3
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 3)
def test_mixed_toc(self):
md = dedent("""
@@ -80,8 +101,9 @@ class TableOfContentsTests(unittest.TestCase):
Heading 4 - #heading-4
Heading 5 - #heading-5
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 2)
def test_mixed_html(self):
md = dedent("""
@@ -98,8 +120,9 @@ class TableOfContentsTests(unittest.TestCase):
Heading 4 - #heading-4
Heading 5 - #heading-5
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 2)
def test_nested_anchor(self):
md = dedent("""
@@ -116,8 +139,9 @@ class TableOfContentsTests(unittest.TestCase):
Heading 4 - #heading-4
Heading 5 - #heading-5
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 2)
def test_entityref(self):
md = dedent("""
@@ -130,5 +154,27 @@ class TableOfContentsTests(unittest.TestCase):
Heading &gt; 2 - #heading-2
Heading &lt; 3 - #heading-3
""")
toc = markdown_to_toc(md)
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 1)
def test_charref(self):
md = '# &#64;Header'
expected = '&#64;Header - #header'
toc = get_toc(get_markdown_toc(md))
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 1)
def test_skip_no_href(self):
html = dedent("""
<div class="toc">
<ul>
<li><a>Header 1</a></li>
<li><a href="#foo">Header 2</a></li>
</ul>
</div>
""")
expected = 'Header 2 - #foo'
toc = get_toc(html)
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 1)
+100 -60
View File
@@ -10,7 +10,9 @@ import tempfile
import shutil
import stat
from mkdocs import nav, utils, exceptions
from mkdocs import utils, exceptions
from mkdocs.structure.files import File
from mkdocs.structure.pages import Page
from mkdocs.tests.base import dedent, load_config
@@ -60,78 +62,116 @@ class UtilsTests(unittest.TestCase):
self.assertEqual(is_html, expected_result)
def test_create_media_urls(self):
pages = [
{'Home': 'index.md'},
{'About': 'about.md'},
{'Sub': [
{'Sub Home': 'index.md'},
{'Sub About': 'about.md'},
]}
]
expected_results = {
'https://media.cdn.org/jq.js': 'https://media.cdn.org/jq.js',
'http://media.cdn.org/jquery.js': 'http://media.cdn.org/jquery.js',
'//media.cdn.org/jquery.js': '//media.cdn.org/jquery.js',
'media.cdn.org/jquery.js': './media.cdn.org/jquery.js',
'local/file/jquery.js': './local/file/jquery.js',
'local\\windows\\file\\jquery.js': './local/windows/file/jquery.js',
'image.png': './image.png',
'style.css?v=20180308c': './style.css?v=20180308c'
'https://media.cdn.org/jq.js': [
'https://media.cdn.org/jq.js',
'https://media.cdn.org/jq.js',
'https://media.cdn.org/jq.js'
],
'http://media.cdn.org/jquery.js': [
'http://media.cdn.org/jquery.js',
'http://media.cdn.org/jquery.js',
'http://media.cdn.org/jquery.js'
],
'//media.cdn.org/jquery.js': [
'//media.cdn.org/jquery.js',
'//media.cdn.org/jquery.js',
'//media.cdn.org/jquery.js'
],
'media.cdn.org/jquery.js': [
'media.cdn.org/jquery.js',
'media.cdn.org/jquery.js',
'../media.cdn.org/jquery.js'
],
'local/file/jquery.js': [
'local/file/jquery.js',
'local/file/jquery.js',
'../local/file/jquery.js'
],
'local\\windows\\file\\jquery.js': [
'local/windows/file/jquery.js',
'local/windows/file/jquery.js',
'../local/windows/file/jquery.js'
],
'image.png': [
'image.png',
'image.png',
'../image.png'
],
'style.css?v=20180308c': [
'style.css?v=20180308c',
'style.css?v=20180308c',
'../style.css?v=20180308c'
]
}
site_navigation = nav.SiteNavigation(load_config(pages=pages))
for path, expected_result in expected_results.items():
urls = utils.create_media_urls(site_navigation, [path])
self.assertEqual(urls[0], expected_result)
def test_create_relative_media_url_sub_index(self):
'''
test special case where there's a sub/index.md page
'''
cfg = load_config(use_directory_urls=False)
pages = [
{'Home': 'index.md'},
{'Sub': [
{'Sub Home': '/subpage/index.md'},
]}
Page('Home', File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
Page('About', File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
Page('FooBar', File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg)
]
site_navigation = nav.SiteNavigation(load_config(pages=pages))
site_navigation.url_context.set_current_url('/subpage/')
site_navigation.file_context.current_file = "subpage/index.md"
def assertPathGenerated(declared, expected):
url = utils.create_relative_media_url(site_navigation, declared)
self.assertEqual(url, expected)
for i, page in enumerate(pages):
urls = utils.create_media_urls(expected_results.keys(), page)
self.assertEqual([v[i] for v in expected_results.values()], urls)
assertPathGenerated("img.png", "./img.png")
assertPathGenerated("./img.png", "./img.png")
assertPathGenerated("/img.png", "../img.png")
def test_create_media_urls_use_directory_urls(self):
def test_create_relative_media_url_sub_index_windows(self):
'''
test special case where there's a sub/index.md page and we are on Windows.
current_file paths uses backslash in Windows
'''
expected_results = {
'https://media.cdn.org/jq.js': [
'https://media.cdn.org/jq.js',
'https://media.cdn.org/jq.js',
'https://media.cdn.org/jq.js'
],
'http://media.cdn.org/jquery.js': [
'http://media.cdn.org/jquery.js',
'http://media.cdn.org/jquery.js',
'http://media.cdn.org/jquery.js'
],
'//media.cdn.org/jquery.js': [
'//media.cdn.org/jquery.js',
'//media.cdn.org/jquery.js',
'//media.cdn.org/jquery.js'
],
'media.cdn.org/jquery.js': [
'media.cdn.org/jquery.js',
'../media.cdn.org/jquery.js',
'../../media.cdn.org/jquery.js'
],
'local/file/jquery.js': [
'local/file/jquery.js',
'../local/file/jquery.js',
'../../local/file/jquery.js'
],
'local\\windows\\file\\jquery.js': [
'local/windows/file/jquery.js',
'../local/windows/file/jquery.js',
'../../local/windows/file/jquery.js'
],
'image.png': [
'image.png',
'../image.png',
'../../image.png'
],
'style.css?v=20180308c': [
'style.css?v=20180308c',
'../style.css?v=20180308c',
'../../style.css?v=20180308c'
]
}
cfg = load_config(use_directory_urls=True)
pages = [
{'Home': 'index.md'},
{'Sub': [
{'Sub Home': '/level1/level2/index.md'},
]}
Page('Home', File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
Page('About', File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
Page('FooBar', File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg)
]
site_navigation = nav.SiteNavigation(load_config(pages=pages))
site_navigation.url_context.set_current_url('/level1/level2')
site_navigation.file_context.current_file = "level1\\level2\\index.md"
def assertPathGenerated(declared, expected):
url = utils.create_relative_media_url(site_navigation, declared)
self.assertEqual(url, expected)
assertPathGenerated("img.png", "./img.png")
assertPathGenerated("./img.png", "./img.png")
assertPathGenerated("/img.png", "../img.png")
for i, page in enumerate(pages):
urls = utils.create_media_urls(expected_results.keys(), page)
self.assertEqual([v[i] for v in expected_results.values()], urls)
def test_reduce_list(self):
self.assertEqual(
+1 -1
View File
@@ -1,6 +1,6 @@
{%- if not nav_item.children %}
<li {% if nav_item.active %}class="active"{% endif %}>
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
<a href="{% if not nav_item.is_link %}{{ base_url }}/{% endif %}{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{%- else %}
<li class="dropdown-submenu">
+4 -4
View File
@@ -14,7 +14,7 @@
{%- endif %}
{%- block site_name %}
<a class="navbar-brand" href="{{ nav.homepage.url }}">{{ config.site_name }}</a>
<a class="navbar-brand" href="{{ base_url }}/{{ nav.homepage.url }}">{{ config.site_name }}</a>
{%- endblock %}
</div>
@@ -36,7 +36,7 @@
</li>
{%- else %}
<li {% if nav_item.active %}class="active"{% endif %}>
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
<a href="{% if not nav_item.is_link %}{{ base_url }}/{% endif %}{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{%- endif %}
{%- endfor %}
@@ -58,12 +58,12 @@
{%- block next_prev %}
{%- if page and (page.next_page or page.previous_page) %}
<li {% if not page.previous_page %}class="disabled"{% endif %}>
<a rel="next" {% if page.previous_page %}href="{{ page.previous_page.url }}"{% endif %}>
<a rel="next" {% if page.previous_page %}href="{{ base_url }}/{{ page.previous_page.url }}"{% endif %}>
<i class="fa fa-arrow-left"></i> Previous
</a>
</li>
<li {% if not page.next_page %}class="disabled"{% endif %}>
<a rel="prev" {% if page.next_page %}href="{{ page.next_page.url }}"{% endif %}>
<a rel="prev" {% if page.next_page %}href="{{ base_url }}/{{ page.next_page.url }}"{% endif %}>
Next <i class="fa fa-arrow-right"></i>
</a>
</li>
+3 -3
View File
@@ -32,7 +32,7 @@
<script>
// Current page data
var mkdocs_page_name = {{ page.title|tojson|safe }};
var mkdocs_page_input_path = {{ page.input_path|tojson|safe }};
var mkdocs_page_input_path = {{ page.file.src_path|string|tojson|safe }};
var mkdocs_page_url = {{ page.abs_url|tojson|safe }};
</script>
{% endif %}
@@ -66,7 +66,7 @@
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-nav-search">
{%- block site_name %}
<a href="{{ nav.homepage.url }}" class="icon icon-home"> {{ config.site_name }}</a>
<a href="{{ base_url }}/{{ nav.homepage.url }}" class="icon icon-home"> {{ config.site_name }}</a>
{%- endblock %}
{%- block search_button %}
{% if 'search' in config['plugins'] %}{% include "searchbox.html" %}{% endif %}
@@ -93,7 +93,7 @@
{# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="{{ nav.homepage.url }}">{{ config.site_name }}</a>
<a href="{{ base_url }}/{{ nav.homepage.url }}">{{ config.site_name }}</a>
</nav>
{# PAGE CONTENT #}
+1 -1
View File
@@ -1,6 +1,6 @@
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="{{ nav.homepage.url }}">Docs</a> &raquo;</li>
<li><a href="{{ base_url }}/{{ nav.homepage.url }}">Docs</a> &raquo;</li>
{% if page %}
{% for doc in page.ancestors %}
{% if doc.link %}
+2 -2
View File
@@ -3,10 +3,10 @@
{% if page and page.next_page or page.previous_page %}
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
{% if page.next_page %}
<a href="{{ page.next_page.url }}" class="btn btn-neutral float-right" title="{{ page.next_page.title }}">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="{{ base_url }}/{{ page.next_page.url }}" class="btn btn-neutral float-right" title="{{ page.next_page.title }}">Next <span class="icon icon-circle-arrow-right"></span></a>
{% endif %}
{% if page.previous_page %}
<a href="{{ page.previous_page.url }}" class="btn btn-neutral" title="{{ page.previous_page.title }}"><span class="icon icon-circle-arrow-left"></span> Previous</a>
<a href="{{ base_url }}/{{ page.previous_page.url }}" class="btn btn-neutral" title="{{ page.previous_page.title }}"><span class="icon icon-circle-arrow-left"></span> Previous</a>
{% endif %}
</div>
{% endif %}
+1 -1
View File
@@ -1,5 +1,5 @@
{%- if nav_item.url %}
<a class="{% if nav_item.active%}current{%endif%}" href="{{ nav_item.url }}">{{ nav_item.title }}</a>
<a class="{% if nav_item.active%}current{%endif%}" href="{% if not nav_item.is_link %}{{ base_url }}/{% endif %}{{ nav_item.url }}">{{ nav_item.title }}</a>
{%- else %}
<span class="caption-text">{{ nav_item.title }}</span>
{%- endif %}
+2 -2
View File
@@ -8,10 +8,10 @@
<a href="{{ config.repo_url }}" class="icon icon-gitlab" style="float: left; color: #fcfcfc"> GitLab</a>
{% endif %}
{% if page.previous_page %}
<span><a href="{{ page.previous_page.url }}" style="color: #fcfcfc;">&laquo; Previous</a></span>
<span><a href="{{ base_url }}/{{ page.previous_page.url }}" style="color: #fcfcfc;">&laquo; Previous</a></span>
{% endif %}
{% if page.next_page %}
<span style="margin-left: 15px"><a href="{{ page.next_page.url }}" style="color: #fcfcfc">Next &raquo;</a></span>
<span style="margin-left: 15px"><a href="{{ base_url }}/{{ page.next_page.url }}" style="color: #fcfcfc">Next &raquo;</a></span>
{% endif %}
</span>
</div>
+22 -7
View File
@@ -17,6 +17,7 @@ import re
import sys
import yaml
import fnmatch
import posixpath
from mkdocs import exceptions
@@ -285,23 +286,37 @@ def is_error_template(path):
return bool(_ERROR_TEMPLATE_RE.match(path))
def create_media_urls(nav, path_list):
def get_relative_url(url, other):
"""
Return a list of URLs that have been processed correctly for inclusion in
a page.
Return given url relative to other.
"""
if other != '.':
# Remove filename from other url if it has one.
parts = posixpath.split(other)
other = parts[0] if '.' in parts[1] else other
relurl = posixpath.relpath(url, other)
return relurl + '/' if url.endswith('/') else relurl
def create_media_urls(path_list, page=None, base=''):
"""
Return a list of URLs relative to the given page or using the base.
"""
final_urls = []
for path in path_list:
# Allow links to fully qualified URL's
path = path_to_url(path)
# Allow links to be fully qualified URL's
parsed = urlparse(path)
if parsed.netloc:
final_urls.append(path)
continue
# We must be looking at a local path.
url = path_to_url(path)
relative_url = '%s/%s' % (nav.url_context.make_relative('/').rstrip('/'), url)
final_urls.append(relative_url)
if page is not None:
url = get_relative_url(path, page.url)
else:
url = posixpath.join(base, path)
final_urls.append(url)
return final_urls
+1 -1
View File
@@ -61,7 +61,7 @@ setup(
'livereload>=2.5.1',
'Markdown>=2.3.1',
'PyYAML>=3.10',
'tornado>=5.0',
'tornado>=5.0'
],
python_requires='>=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*',
entry_points={