Refactor Markdown handling. (#1180)

All Markdown handling is now contained within the `Page` object, which
external code no longer needs to know the internals of.
A slightly different approach to the work started in #713.

Include the multimarkdown meta-data parser from docdata
See: https://github.com/waylan/docdata

Tests have been updated. However, as noted in #713, they could use
some refactoring. The existing tests all pass. We can leave refactoring
as a seperate matter from this.

Closes #713.
This commit is contained in:
Waylan Limberg
2017-03-24 11:11:43 -04:00
parent b4cc6c3367
commit b9af8b1fba
12 changed files with 611 additions and 380 deletions

View File

@@ -87,6 +87,7 @@ and user created and third-party templates should be updated as outlined below:
### Other Changes and Additions to Version 1.0.0
* Internal refactor of Markdown processing (#713)
* Removed special error message for mkdocs-bootstrap and mkdocs-bootswatch
themes (#1168)
* The legacy pages config is no longer supported (#1168)

View File

@@ -12,7 +12,6 @@ import jinja2
from mkdocs import nav, search, utils
from mkdocs.utils import filters
from mkdocs.relative_path_ext import RelativePathExtension
import mkdocs
@@ -31,40 +30,15 @@ log = logging.getLogger(__name__)
log.addFilter(DuplicateFilter())
def get_complete_paths(config, page):
"""
Return the complete input/output paths for the supplied page.
"""
input_path = os.path.join(config['docs_dir'], page.input_path)
output_path = os.path.join(config['site_dir'], page.output_path)
return input_path, output_path
def convert_markdown(markdown_source, config, site_navigation=None):
"""
Convert the Markdown source file to HTML as per the config and
site_navigation. Return a tuple of the HTML as a string, the parsed table
of contents, and a dictionary of any metadata that was specified in the
Markdown file.
"""
extensions = [
RelativePathExtension(site_navigation, config['strict'])
] + config['markdown_extensions']
return utils.convert_markdown(
markdown_source=markdown_source,
extensions=extensions,
extension_configs=config['mdx_configs']
)
def get_global_context(nav, config):
def get_context(nav, config, page=None):
"""
Given the SiteNavigation and config, generate the context which is relevant
to app pages.
"""
if nav is None:
return {'page', page}
extra_javascript = utils.create_media_urls(nav, config['extra_javascript'])
extra_css = utils.create_media_urls(nav, config['extra_css'])
@@ -85,27 +59,10 @@ def get_global_context(nav, config):
'build_date_utc': datetime.utcfromtimestamp(timestamp),
'config': config,
'page': page,
}
def get_page_context(page, content, toc, meta, config):
"""
Generate the page context by extending the global context and adding page
specific variables.
"""
if config['site_url']:
page.set_canonical_url(config['site_url'])
if config['repo_url']:
page.set_edit_url(config['repo_url'], config['edit_uri'])
page.content = content
page.toc = toc
page.meta = meta
return {'page': page}
def build_template(template_name, env, config, site_navigation=None):
log.debug("Building template: %s", template_name)
@@ -115,9 +72,7 @@ def build_template(template_name, env, config, site_navigation=None):
except TemplateNotFound:
return False
context = {'page': None}
if site_navigation is not None:
context.update(get_global_context(site_navigation, config))
context = get_context(site_navigation, config)
output_content = template.render(context)
output_path = os.path.join(config['site_dir'], template_name)
@@ -127,31 +82,15 @@ def build_template(template_name, env, config, site_navigation=None):
def _build_page(page, config, site_navigation, env, dirty=False):
# Get the input/output paths
input_path, output_path = get_complete_paths(config, page)
# Read the input file
try:
input_content = io.open(input_path, 'r', encoding='utf-8').read()
except IOError:
log.error('file not found: %s', input_path)
raise
# Process the markdown text
html_content, table_of_contents, meta = convert_markdown(
markdown_source=input_content,
config=config,
site_navigation=site_navigation
)
page.load_markdown()
page.render(config, site_navigation)
context = get_global_context(site_navigation, config)
context.update(get_page_context(
page, html_content, table_of_contents, meta, config
))
context = get_context(site_navigation, config, page)
# Allow 'template:' override in md source files.
if 'template' in meta:
template = env.get_template(meta['template'][0])
if 'template' in page.meta:
template = env.get_template(page.meta['template'])
else:
template = env.get_template('main.html')
@@ -159,9 +98,7 @@ def _build_page(page, config, site_navigation, env, dirty=False):
output_content = template.render(context)
# Write the output file.
utils.write_file(output_content.encode('utf-8'), output_path)
return html_content, table_of_contents, meta
utils.write_file(output_content.encode('utf-8'), page.abs_output_path)
def build_extra_templates(extra_templates, config, site_navigation=None):
@@ -175,9 +112,7 @@ def build_extra_templates(extra_templates, config, site_navigation=None):
with io.open(input_path, 'r', encoding='utf-8') as template_file:
template = jinja2.Template(template_file.read())
context = {'page': None}
if site_navigation is not None:
context.update(get_global_context(site_navigation, config))
context = get_context(site_navigation, config)
output_content = template.render(context)
output_path = os.path.join(config['site_dir'], extra_template)
@@ -188,7 +123,7 @@ def build_pages(config, dirty=False):
"""
Builds all the pages and writes them into the build directory.
"""
site_navigation = nav.SiteNavigation(config['pages'], config['use_directory_urls'])
site_navigation = nav.SiteNavigation(config)
loader = jinja2.FileSystemLoader(config['theme_dir'] + [config['mkdocs_templates'], ])
env = jinja2.Environment(loader=loader)
@@ -218,18 +153,14 @@ def build_pages(config, dirty=False):
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.
input_path, output_path = get_complete_paths(config, page)
if dirty and (utils.modified_time(input_path) < utils.modified_time(output_path)):
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_result = _build_page(page, config, site_navigation, env)
html_content, table_of_contents, _ = build_result
search_index.add_entry_from_context(
page, html_content, table_of_contents)
_build_page(page, config, site_navigation, env)
search_index.add_entry_from_context(page)
except Exception:
log.error("Error building page %s", page.input_path)
raise

View File

@@ -90,7 +90,7 @@ DEFAULT_SCHEMA = (
# PyMarkdown extension names.
('markdown_extensions', config_options.MarkdownExtensions(
builtins=['meta', 'toc', 'tables', 'fenced_code'],
builtins=['toc', 'tables', 'fenced_code'],
configkey='mdx_configs', default=[])),
# PyMarkdown Extension Configs. For internal use only.

View File

@@ -9,14 +9,18 @@ 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
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):
def _filename_to_title(filename):
"""
Automatically generate a default title, given a filename.
"""
@@ -26,14 +30,20 @@ def filename_to_title(filename):
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, pages_config, use_directory_urls=True):
def __init__(self, config):
self.url_context = URLContext()
self.file_context = FileContext()
self.nav_items, self.pages = _generate_site_navigation(
pages_config, self.url_context, use_directory_urls)
config, self.url_context)
self.homepage = self.pages[0] if self.pages else None
self.use_directory_urls = use_directory_urls
self.use_directory_urls = config['use_directory_urls']
def __str__(self):
return ''.join([str(item) for item in self])
@@ -139,10 +149,10 @@ class FileContext(object):
class Page(object):
def __init__(self, title, url, path, url_context):
def __init__(self, title, path, url_context, config):
self.title = title
self.abs_url = url
self._title = title
self.abs_url = utils.get_url_path(path, config['use_directory_urls'])
self.active = False
self.url_context = url_context
@@ -155,22 +165,71 @@ class Page(object):
else:
self.update_date = datetime.datetime.now().strftime("%Y-%m-%d")
# Relative paths to the input markdown file and output html file.
# 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']:
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
# Links to related pages
self.previous_page = None
self.next_page = None
self.ancestors = []
# Placeholders to be filled in later in the build
# process when we have access to the config.
self.canonical_url = None
self.edit_url = None
self.content = None
self.meta = None
self.toc = None
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):
@@ -184,8 +243,29 @@ class Page(object):
def is_top_level(self):
return len(self.ancestors) == 0
def __str__(self):
return self.indent_print()
def load_markdown(self):
try:
input_content = io.open(self.abs_input_path, 'r', encoding='utf-8').read()
except IOError:
log.error('file not found: %s', self.abs_input_path)
raise
self.markdown, self.meta = meta.get_data(input_content)
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):
if not edit_uri:
self.edit_url = repo_url
else:
# 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
@@ -198,20 +278,23 @@ class Page(object):
for ancestor in self.ancestors:
ancestor.set_active(active)
def set_canonical_url(self, base):
if not base.endswith('/'):
base += '/'
self.canonical_url = utils.urljoin(base, self.abs_url.lstrip('/'))
def render(self, config, site_navigation=None):
"""
Convert the Markdown source file to HTML as per the config and
site_navigation.
def set_edit_url(self, repo_url, edit_uri):
if not edit_uri:
self.edit_url = repo_url
else:
# Normalize URL from Windows path '\\' -> '/'
input_path_url = self.input_path.replace('\\', '/')
self.edit_url = utils.urljoin(
repo_url,
edit_uri + input_path_url)
"""
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):
@@ -241,19 +324,11 @@ class Header(object):
ancestor.set_active(active)
def _path_to_page(path, title, url_context, use_directory_urls):
if title is None:
title = filename_to_title(path.split(os.path.sep)[-1])
url = utils.get_url_path(path, use_directory_urls)
return Page(title=title, url=url, path=path,
url_context=url_context)
def _follow(config_line, url_context, use_dir_urls, header=None, title=None):
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 = _path_to_page(path, title, url_context, use_dir_urls)
page = Page(title, path, url_context, config)
if header:
page.ancestors = header.ancestors + [header, ]
@@ -279,7 +354,7 @@ def _follow(config_line, url_context, use_dir_urls, header=None, title=None):
if isinstance(subpages_or_path, utils.string_types):
path = subpages_or_path
for sub in _follow(path, url_context, use_dir_urls, header=header, title=next_cat_or_title):
for sub in _follow(path, url_context, config, header=header, title=next_cat_or_title):
yield sub
raise StopIteration
@@ -298,11 +373,11 @@ def _follow(config_line, url_context, use_dir_urls, header=None, title=None):
subpages = subpages_or_path
for subpage in subpages:
for sub in _follow(subpage, url_context, use_dir_urls, next_header):
for sub in _follow(subpage, url_context, config, next_header):
yield sub
def _generate_site_navigation(pages_config, url_context, use_dir_urls=True):
def _generate_site_navigation(config, url_context):
"""
Returns a list of Page and Header instances that represent the
top level site navigation.
@@ -312,10 +387,10 @@ def _generate_site_navigation(pages_config, url_context, use_dir_urls=True):
previous = None
for config_line in pages_config:
for config_line in config['pages']:
for page_or_header in _follow(
config_line, url_context, use_dir_urls):
config_line, url_context, config):
if isinstance(page_or_header, Header):

View File

@@ -41,7 +41,7 @@ class SearchIndex(object):
'location': loc
})
def add_entry_from_context(self, page, content, toc):
def add_entry_from_context(self, page):
"""
Create a set of entries in the index for a page. One for
the page itself and then one for each of its' heading
@@ -52,7 +52,7 @@ class SearchIndex(object):
# full page. This handles all the parsing and prepares
# us to iterate through it.
parser = ContentParser()
parser.feed(content)
parser.feed(page.content)
parser.close()
# Get the absolute URL for the page, this is then
@@ -62,12 +62,12 @@ class SearchIndex(object):
# Create an entry for the full page.
self._add_entry(
title=page.title,
text=self.strip_tags(content).rstrip('\n'),
text=self.strip_tags(page.content).rstrip('\n'),
loc=abs_url
)
for section in parser.data:
self.create_entry_for_section(section, toc, abs_url)
self.create_entry_for_section(section, page.toc, abs_url)
def create_entry_for_section(self, section, toc, abs_url):
"""

View File

@@ -1,8 +1,10 @@
from __future__ import unicode_literals
import textwrap
import markdown
import os
from mkdocs import toc
from mkdocs import config
def dedent(text):
@@ -14,3 +16,24 @@ def markdown_to_toc(markdown_source):
md.convert(markdown_source)
toc_output = md.toc
return toc.TableOfContents(toc_output)
def load_config(**cfg):
""" Helper to build a simple config for testing. """
path_base = os.path.join(
os.path.abspath(os.path.dirname(__file__)), 'integration', 'minimal'
)
cfg = cfg or {}
if 'site_name' not in cfg:
cfg['site_name'] = 'Example'
if 'config_file_path' not in cfg:
cfg['config_file_path'] = os.path.join(path_base, 'mkdocs.yml')
if 'docs_dir' not in cfg:
# Point to an actual dir to avoid a 'does not exist' error on validation.
cfg['docs_dir'] = os.path.join(path_base, 'docs')
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf.load_dict(cfg)
errors_warnings = conf.validate()
assert(errors_warnings == ([], [])), errors_warnings
return conf

View File

@@ -15,44 +15,42 @@ except ImportError:
pass
from mkdocs import nav, config
from mkdocs import nav
from mkdocs.commands import build
from mkdocs.exceptions import MarkdownNotFound
from mkdocs.tests.base import dedent
from mkdocs.tests.base import dedent, load_config
from mkdocs.utils import meta
def load_config(cfg=None):
""" Helper to build a simple config for testing. """
cfg = cfg or {}
if 'site_name' not in cfg:
cfg['site_name'] = 'Example'
if 'config_file_path' not in cfg:
cfg['config_file_path'] = os.path.join(os.path.abspath('.'), 'mkdocs.yml')
if 'extra_css' not in cfg:
cfg['extra_css'] = ['css/extra.css']
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf.load_dict(cfg)
def build_page(title, path, config, md_src=None):
""" Helper which returns a Page object. """
errors_warnings = conf.validate()
assert(errors_warnings == ([], [])), errors_warnings
return conf
sitenav = nav.SiteNavigation(config)
page = nav.Page(title, path, sitenav.url_context, config)
if md_src:
# Fake page.load_markdown()
page.markdown, page.meta = meta.get_data(md_src)
return page, sitenav
class BuildTests(unittest.TestCase):
def test_empty_document(self):
html, toc, meta = build.convert_markdown("", load_config())
config = load_config(pages=[{'Home': 'index.md'}])
page, nav = build_page(None, 'index.md', config)
page.render(config, nav)
self.assertEqual(html, '')
self.assertEqual(len(list(toc)), 0)
self.assertEqual(meta, {})
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.
"""
html, toc, meta = build.convert_markdown(dedent("""
page_title: custom title
md_text = dedent("""
title: custom title
# Heading 1
@@ -61,7 +59,11 @@ class BuildTests(unittest.TestCase):
# Heading 2
And some more text.
"""), load_config())
""")
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>
@@ -75,35 +77,44 @@ class BuildTests(unittest.TestCase):
Heading 2 - #heading-2
""")
expected_meta = {'page_title': ['custom title']}
expected_meta = {'title': 'custom title'}
self.assertEqual(html.strip(), expected_html)
self.assertEqual(str(toc).strip(), expected_toc)
self.assertEqual(meta, expected_meta)
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>'
html, toc, meta = build.convert_markdown(md_text, load_config())
self.assertEqual(html.strip(), expected.strip())
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>'
html, toc, meta = build.convert_markdown(md_text, load_config())
self.assertEqual(html.strip(), expected.strip())
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>'
html, toc, meta = build.convert_markdown(md_text, load_config())
self.assertEqual(html.strip(), expected.strip())
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>'
html, toc, meta = build.convert_markdown(md_text, load_config())
self.assertEqual(html.strip(), expected.strip())
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"""
@@ -113,7 +124,8 @@ class BuildTests(unittest.TestCase):
'sub/internal.md',
]
site_navigation = nav.SiteNavigation(pages)
config = load_config(pages=pages)
site_navigation = nav.SiteNavigation(config)
expected_results = (
'./img/initial-layout.png',
@@ -124,9 +136,9 @@ class BuildTests(unittest.TestCase):
template = '<p><img alt="The initial MkDocs layout" src="%s" /></p>'
for (page, expected) in zip(site_navigation.walk_pages(), expected_results):
md_text = '![The initial MkDocs layout](img/initial-layout.png)'
html, _, _ = build.convert_markdown(md_text, load_config(), site_navigation=site_navigation)
self.assertEqual(html, template % expected)
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"""
@@ -136,7 +148,8 @@ class BuildTests(unittest.TestCase):
'sub/internal.md',
]
site_navigation = nav.SiteNavigation(pages)
config = load_config(pages=pages)
site_navigation = nav.SiteNavigation(config)
expected_results = (
'./img/initial-layout.png',
@@ -147,9 +160,9 @@ class BuildTests(unittest.TestCase):
template = '<p><img alt="The initial MkDocs layout" src="%s" /></p>'
for (page, expected) in zip(site_navigation.walk_pages(), expected_results):
md_text = '![The initial MkDocs layout](/img/initial-layout.png)'
html, _, _ = build.convert_markdown(md_text, load_config(), site_navigation=site_navigation)
self.assertEqual(html, template % expected)
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 = [
@@ -158,7 +171,8 @@ class BuildTests(unittest.TestCase):
'sub/internal.md',
]
site_navigation = nav.SiteNavigation(pages)
config = load_config(pages=pages)
site_navigation = nav.SiteNavigation(config)
expected = dedent("""
<p>An HTML Anchor::</p>
@@ -167,9 +181,9 @@ class BuildTests(unittest.TestCase):
""")
for page in site_navigation.walk_pages():
markdown = 'An HTML Anchor::\n\n <a href="index.md">My example link</a>\n'
html, _, _ = build.convert_markdown(markdown, load_config(), site_navigation=site_navigation)
self.assertEqual(dedent(html), expected)
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 = [
@@ -178,28 +192,29 @@ class BuildTests(unittest.TestCase):
'sub/internal.md',
]
site_navigation = nav.SiteNavigation(pages)
config = load_config(pages=pages)
site_navigation = nav.SiteNavigation(config)
for page in site_navigation.walk_pages():
markdown = '[test](#test)'
html, _, _ = build.convert_markdown(markdown, load_config(), site_navigation=site_navigation)
self.assertEqual(html, '<p><a href="#test">test</a></p>')
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>'
html, toc, meta = build.convert_markdown(md_text, load_config())
self.assertEqual(html.strip(), expected.strip())
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>'
pages = [
'internal.md',
]
site_navigation = nav.SiteNavigation(pages, use_directory_urls=False)
html, toc, meta = build.convert_markdown(md_text, load_config(), site_navigation=site_navigation)
self.assertEqual(html.strip(), expected.strip())
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).'
@@ -210,19 +225,21 @@ class BuildTests(unittest.TestCase):
'&#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>'
])
html, toc, meta = build.convert_markdown(md_text, load_config())
self.assertEqual(html.strip(), expected.strip())
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_markdown_table_extension(self):
"""
Ensure that the table extension is supported.
"""
html, toc, meta = build.convert_markdown(dedent("""
md_text = dedent("""
First Header | Second Header
-------------- | --------------
Content Cell 1 | Content Cell 2
Content Cell 3 | Content Cell 4
"""), load_config())
""")
expected_html = dedent("""
<table>
@@ -245,54 +262,60 @@ class BuildTests(unittest.TestCase):
</table>
""")
self.assertEqual(html.strip(), expected_html)
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.
"""
html, toc, meta = build.convert_markdown(dedent("""
md_text = dedent("""
```
print 'foo'
```
"""), load_config())
""")
expected_html = dedent("""
<pre><code>print 'foo'\n</code></pre>
""")
self.assertEqual(html.strip(), expected_html)
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_input = "foo__bar__baz"
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>"
html_base, _, _ = build.convert_markdown(md_input, load_config())
self.assertEqual(html_base.strip(), expected_without_smartstrong)
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.
cfg = load_config({
'markdown_extensions': ['smart_strong']
})
expected_with_smartstrong = "<p>foo__bar__baz</p>"
html_ext, _, _ = build.convert_markdown(md_input, cfg)
self.assertEqual(html_ext.strip(), expected_with_smartstrong)
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.
"""
cfg = load_config({
'markdown_extensions': ['toc']
})
md_input = "foo"
html_ext, _, _ = build.convert_markdown(md_input, cfg)
self.assertEqual(html_ext.strip(), '<p>foo</p>')
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):
docs_dir = tempfile.mkdtemp()
@@ -318,10 +341,7 @@ class BuildTests(unittest.TestCase):
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
})
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.
@@ -349,10 +369,7 @@ class BuildTests(unittest.TestCase):
"""))
f.close()
cfg = load_config({
'docs_dir': docs_dir,
'site_dir': site_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.
@@ -375,11 +392,16 @@ class BuildTests(unittest.TestCase):
'internal.md',
'sub/internal.md',
]
site_nav = nav.SiteNavigation(pages)
valid = "[test](internal.md)"
build.convert_markdown(valid, load_config({'strict': False}), site_nav)
build.convert_markdown(valid, load_config({'strict': True}), site_nav)
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 = [
@@ -387,55 +409,58 @@ class BuildTests(unittest.TestCase):
'internal.md',
'sub/internal.md',
]
site_nav = nav.SiteNavigation(pages)
invalid = "[test](bad_link.md)"
build.convert_markdown(invalid, load_config({'strict': False}), site_nav)
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,
build.convert_markdown, invalid, load_config({'strict': True}), site_nav)
page.render, config, nav)
def test_absolute_link(self):
pages = [
'index.md',
'sub/index.md',
]
site_nav = nav.SiteNavigation(pages)
markdown = "[test 1](/index.md) [test 2](/sub/index.md)"
cfg = load_config({'strict': True})
build.convert_markdown(markdown, cfg, site_nav)
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_extension_config(self):
"""
Test that a dictionary of 'markdown_extensions' is recognized as
both a list of extensions and a dictionary of extnesion configs.
"""
cfg = load_config({
'markdown_extensions': [{'toc': {'permalink': True}}]
})
html, toc, meta = build.convert_markdown(dedent("""
md_text = dedent("""
# A Header
"""), cfg)
""")
expected_html = dedent("""
<h1 id="a-header">A Header<a class="headerlink" href="#a-header" title="Permanent link">&para;</a></h1>
""")
self.assertEqual(html.strip(), expected_html)
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_extra_context(self):
# Same as the default schema, but don't verify the docs_dir exists.
cfg = load_config({
'site_name': "Site",
'extra': {
cfg = load_config(
site_name="Site",
extra={
'a': 1
}
})
)
context = build.get_global_context(mock.Mock(), cfg)
context = build.get_context(mock.Mock(), cfg)
self.assertEqual(context['config']['extra']['a'], 1)

View File

@@ -8,7 +8,7 @@ import unittest
from mkdocs import nav
from mkdocs.exceptions import ConfigurationError
from mkdocs.tests.base import dedent
from mkdocs.tests.base import dedent, load_config
class SiteNavigationTests(unittest.TestCase):
@@ -21,7 +21,7 @@ class SiteNavigationTests(unittest.TestCase):
Home - /
About - /about/
""")
site_navigation = nav.SiteNavigation(pages)
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)
@@ -35,7 +35,7 @@ class SiteNavigationTests(unittest.TestCase):
Home - /
About - /about/
""")
site_navigation = nav.SiteNavigation(pages)
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)
@@ -63,7 +63,7 @@ class SiteNavigationTests(unittest.TestCase):
Release notes - /about/release-notes/
License - /about/license/
""")
site_navigation = nav.SiteNavigation(pages)
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)
@@ -79,7 +79,7 @@ class SiteNavigationTests(unittest.TestCase):
Contact - /about/contact/
License Title - /about/sub/license/
""")
site_navigation = nav.SiteNavigation(pages)
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)
@@ -96,7 +96,7 @@ class SiteNavigationTests(unittest.TestCase):
License - /about/sub/license/
""")
site_navigation = nav.SiteNavigation(pages)
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)
@@ -114,7 +114,7 @@ class SiteNavigationTests(unittest.TestCase):
License - /about/sub/license/
""")
site_navigation = nav.SiteNavigation(pages)
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)
@@ -134,7 +134,7 @@ class SiteNavigationTests(unittest.TestCase):
About - /about/ [*]
""")
]
site_navigation = nav.SiteNavigation(pages)
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])
@@ -153,7 +153,7 @@ class SiteNavigationTests(unittest.TestCase):
About - /about/ [*]
""")
]
site_navigation = nav.SiteNavigation(pages)
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])
@@ -232,7 +232,7 @@ class SiteNavigationTests(unittest.TestCase):
License - /about/license/ [*]
""")
]
site_navigation = nav.SiteNavigation(pages)
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])
@@ -240,7 +240,7 @@ class SiteNavigationTests(unittest.TestCase):
pages = [
'index.md'
]
site_navigation = nav.SiteNavigation(pages, use_directory_urls=False)
site_navigation = nav.SiteNavigation(load_config(pages=pages, use_directory_urls=False))
base_url = site_navigation.url_context.make_relative('/')
self.assertEqual(base_url, '.')
@@ -249,7 +249,7 @@ class SiteNavigationTests(unittest.TestCase):
'index.md',
'user-guide/styling-your-docs.md'
]
site_navigation = nav.SiteNavigation(pages, use_directory_urls=False)
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/')
@@ -267,7 +267,7 @@ class SiteNavigationTests(unittest.TestCase):
]
url_context = nav.URLContext()
nav_items, pages = nav._generate_site_navigation(pages, url_context)
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'])
@@ -293,7 +293,7 @@ class SiteNavigationTests(unittest.TestCase):
]
url_context = nav.URLContext()
nav_items, pages = nav._generate_site_navigation(pages, url_context)
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'])
@@ -320,7 +320,7 @@ class SiteNavigationTests(unittest.TestCase):
url_context = nav.URLContext()
url_context.force_abs_urls = True
nav_items, pages = nav._generate_site_navigation(pages, url_context)
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'])
@@ -346,7 +346,7 @@ class SiteNavigationTests(unittest.TestCase):
url_context = nav.URLContext()
url_context.force_abs_urls = True
url_context.base_path = '/foo/'
nav_items, pages = nav._generate_site_navigation(pages, url_context)
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'])
@@ -367,7 +367,7 @@ class SiteNavigationTests(unittest.TestCase):
for bad_page in bad_pages:
def _test():
return nav._generate_site_navigation((bad_page, ), None)
return nav._generate_site_navigation({'pages': (bad_page, )}, None)
self.assertRaises(ConfigurationError, _test)
@@ -376,7 +376,7 @@ class SiteNavigationTests(unittest.TestCase):
bad_page = {} # empty
def _test():
return nav._generate_site_navigation((bad_page, ), None)
return nav._generate_site_navigation({'pages': (bad_page, )}, None)
self.assertRaises(ConfigurationError, _test)
@@ -397,7 +397,7 @@ class SiteNavigationTests(unittest.TestCase):
{'License': 'about/license.md'}
]}
]
site_navigation = nav.SiteNavigation(pages)
site_navigation = nav.SiteNavigation(load_config(pages=pages))
ancestors = (
[],
@@ -419,7 +419,7 @@ class SiteNavigationTests(unittest.TestCase):
def test_nesting(self):
pages_config = [
pages = [
{'Home': 'index.md'},
{'Install': [
{'Pre-install': 'install/install-pre.md'},
@@ -442,7 +442,7 @@ class SiteNavigationTests(unittest.TestCase):
]}
]
site_navigation = nav.SiteNavigation(pages_config)
site_navigation = nav.SiteNavigation(load_config(pages=pages))
self.assertEqual([n.title for n in site_navigation.nav_items],
['Home', 'Install', 'Guide'])
@@ -486,15 +486,15 @@ class SiteNavigationTests(unittest.TestCase):
repo_url = 'http://example.com/'
edit_uri = 'edit/master/docs/'
site_navigation = nav.SiteNavigation({
'pages': pages,
'repo_url': repo_url,
'edit_uri': edit_uri,
'docs_dir': 'docs',
'site_dir': 'site',
'site_url': '',
'use_directory_urls': True
})
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
docs_dir='docs',
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
@@ -510,15 +510,15 @@ class SiteNavigationTests(unittest.TestCase):
repo_url = 'http://example.com'
edit_uri = 'edit/master/docs'
site_navigation = nav.SiteNavigation({
'pages': pages,
'repo_url': repo_url,
'edit_uri': edit_uri,
'docs_dir': 'docs',
'site_dir': 'site',
'site_url': '',
'use_directory_urls': True
})
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
docs_dir='docs',
site_dir='site',
site_url='',
use_directory_urls=True
))
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
@@ -527,15 +527,15 @@ class SiteNavigationTests(unittest.TestCase):
repo_url = 'http://example.com'
edit_uri = '?query=edit/master/docs/'
site_navigation = nav.SiteNavigation({
'pages': pages,
'repo_url': repo_url,
'edit_uri': edit_uri,
'docs_dir': 'docs',
'site_dir': 'site',
'site_url': '',
'use_directory_urls': True
})
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
docs_dir='docs',
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
@@ -551,15 +551,15 @@ class SiteNavigationTests(unittest.TestCase):
repo_url = 'http://example.com'
edit_uri = '#fragment/edit/master/docs/'
site_navigation = nav.SiteNavigation({
'pages': pages,
'repo_url': repo_url,
'edit_uri': edit_uri,
'docs_dir': 'docs',
'site_dir': 'site',
'site_url': '',
'use_directory_urls': True
})
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
docs_dir='docs',
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
@@ -587,15 +587,15 @@ class SiteNavigationTests(unittest.TestCase):
repo_url = 'http://example.com/'
edit_uri = 'edit/master/docs/'
site_navigation = nav.SiteNavigation({
'pages': pages,
'repo_url': repo_url,
'edit_uri': edit_uri,
'docs_dir': 'docs',
'site_dir': 'site',
'site_url': '',
'use_directory_urls': True
})
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
docs_dir='docs',
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
@@ -611,15 +611,15 @@ class SiteNavigationTests(unittest.TestCase):
repo_url = 'http://example.com'
edit_uri = 'edit/master/docs'
site_navigation = nav.SiteNavigation({
'pages': pages,
'repo_url': repo_url,
'edit_uri': edit_uri,
'docs_dir': 'docs',
'site_dir': 'site',
'site_url': '',
'use_directory_urls': True
})
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
docs_dir='docs',
site_dir='site',
site_url='',
use_directory_urls=True
))
for idx, page in enumerate(site_navigation.walk_pages()):
self.assertEqual(page.edit_url, expected_results[idx])
@@ -628,15 +628,15 @@ class SiteNavigationTests(unittest.TestCase):
repo_url = 'http://example.com'
edit_uri = '?query=edit/master/docs/'
site_navigation = nav.SiteNavigation({
'pages': pages,
'repo_url': repo_url,
'edit_uri': edit_uri,
'docs_dir': 'docs',
'site_dir': 'site',
'site_url': '',
'use_directory_urls': True
})
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
docs_dir='docs',
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],
@@ -652,15 +652,15 @@ class SiteNavigationTests(unittest.TestCase):
repo_url = 'http://example.com'
edit_uri = '#fragment/edit/master/docs/'
site_navigation = nav.SiteNavigation({
'pages': pages,
'repo_url': repo_url,
'edit_uri': edit_uri,
'docs_dir': 'docs',
'site_dir': 'site',
'site_url': '',
'use_directory_urls': True
})
site_navigation = nav.SiteNavigation(load_config(
pages=pages,
repo_url=repo_url,
edit_uri=edit_uri,
docs_dir='docs',
site_dir='site',
site_url='',
use_directory_urls=True
))
expected_results = (
repo_url + edit_uri + pages[0],

View File

@@ -6,7 +6,7 @@ import unittest
from mkdocs import nav
from mkdocs import search
from mkdocs.tests.base import dedent, markdown_to_toc
from mkdocs.tests.base import dedent, markdown_to_toc, load_config
def strip_whitespace(string):
@@ -111,7 +111,8 @@ class SearchTests(unittest.TestCase):
{'Home': 'index.md'},
{'About': 'about.md'},
]
site_navigation = nav.SiteNavigation(pages)
site_navigation = nav.SiteNavigation(load_config(pages=pages))
md = dedent("""
# Heading 1
@@ -123,9 +124,13 @@ class SearchTests(unittest.TestCase):
full_content = ''.join("""Heading{0}Content{0}""".format(i) for i in range(1, 4))
for page in site_navigation:
# Fake page.load_markdown() and page.render()
page.markdown = md
page.toc = toc
page.content = html_content
index = search.SearchIndex()
index.add_entry_from_context(page, html_content, toc)
index.add_entry_from_context(page)
self.assertEqual(len(index._entries), 4)

View File

@@ -11,7 +11,7 @@ import shutil
import stat
from mkdocs import nav, utils, exceptions
from mkdocs.tests.base import dedent
from mkdocs.tests.base import dedent, load_config
class UtilsTests(unittest.TestCase):
@@ -77,7 +77,7 @@ class UtilsTests(unittest.TestCase):
'local/file/jquery.js': './local/file/jquery.js',
'image.png': './image.png',
}
site_navigation = nav.SiteNavigation(pages)
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)
@@ -87,13 +87,14 @@ class UtilsTests(unittest.TestCase):
test special case where there's a sub/index.md page
'''
site_navigation = nav.SiteNavigation([
pages = [
{'Home': 'index.md'},
{'Sub': [
{'Sub Home': '/subpage/index.md'},
]}
])
]
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"
@@ -111,13 +112,14 @@ class UtilsTests(unittest.TestCase):
current_file paths uses backslash in Windows
'''
site_navigation = nav.SiteNavigation([
pages = [
{'Home': 'index.md'},
{'Sub': [
{'Sub Home': '/level1/level2/index.md'},
]}
])
]
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"

View File

@@ -10,7 +10,6 @@ and structure of the site and pages in the site.
from __future__ import unicode_literals
import logging
import markdown
import os
import pkg_resources
import shutil
@@ -19,7 +18,7 @@ import sys
import yaml
import fnmatch
from mkdocs import toc, exceptions
from mkdocs import exceptions
try: # pragma: no cover
from urllib.parse import urlparse, urlunparse, urljoin # noqa
@@ -357,31 +356,6 @@ def path_to_url(path):
return pathname2url(path)
def convert_markdown(markdown_source, extensions=None, extension_configs=None):
"""
Convert the Markdown source file to HTML content, and additionally
return the parsed table of contents, and a dictionary of any metadata
that was specified in the Markdown file.
`extensions` is an optional sequence of Python Markdown extensions to add
to the default set.
"""
md = markdown.Markdown(
extensions=extensions or [],
extension_configs=extension_configs or {}
)
html_content = md.convert(markdown_source)
# On completely blank markdown files, no Meta or tox properties are added
# to the generated document.
meta = getattr(md, 'Meta', {})
toc_html = getattr(md, 'toc', '')
# Post process the generated table of contents into a data structure
table_of_contents = toc.TableOfContents(toc_html)
return (html_content, table_of_contents, meta)
def get_theme_dir(name):
""" Return the directory of an installed theme by name. """
@@ -441,6 +415,25 @@ def dirname_to_title(dirname):
return title
def get_markdown_title(markdown_src):
"""
Get the title of a Markdown document. The title in this case is considered
to be a H1 that occurs before any other content in the document.
The procedure is then to iterate through the lines, stopping at the first
non-whitespace content. If it is a title, return that, otherwise return
None.
"""
lines = markdown_src.replace('\r\n', '\n').replace('\r', '\n').split('\n')
while lines:
line = lines.pop(0).strip()
if not line.strip():
continue
if not line.startswith('# '):
return
return line.lstrip('# ')
def find_or_create_node(branch, key):
"""
Given a list, look for dictionary with a key matching key and return it's

176
mkdocs/utils/meta.py Normal file
View File

@@ -0,0 +1,176 @@
"""
Copyright (c) 2015, Waylan Limberg
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.
MultiMarkdown Meta-Data
Extracts, parses and transforms MultiMarkdown style data from documents.
"""
import re
#####################################################################
# Transformer Collection #
#####################################################################
class TransformerCollection(object):
"""
A collecton of transformers.
A transformer is a callable that accepts a single argument (the value to be transformed)
and returns a transformed value.
"""
def __init__(self, items=None, default=None):
"""
Create a transformer collection.
`items`: A dictionary which points to a transformer for each key (optional).
`default`: The default transformer (optional). If no default is provided,
then the values of unknown keys are returned unaltered.
"""
self._registery = items or {}
self.default = default or (lambda v: v)
def register(self, key=None):
"""
Decorator which registers a transformer for the given key.
If no key is provided, a "default" transformer is registered.
"""
def wrap(fn):
if key:
self._registery[key] = fn
else:
self.default = fn
return fn
return wrap
def transform(self, key, value):
"""
Calls the transformer for the given key and returns the transformed value.
"""
if key in self._registery:
return self._registery[key](value)
return self.default(value)
def transform_dict(self, data):
"""
Calls the transformer for each item in a dictionary and returns a new dictionary.
"""
newdata = {}
for k, v in data.items():
newdata[k] = self.transform(k, v)
return newdata
# The global default transformer collection.
tc = TransformerCollection()
def transformer(key=None):
"""
Decorator which registers a transformer for the given key.
If no key is provided, a "default" transformer is registered.
"""
def wrap(fn):
tc.register(key)(fn)
return fn
return wrap
#####################################################################
# Data Parser #
#####################################################################
BEGIN_RE = re.compile(r'^-{3}(\s.*)?')
META_RE = re.compile(r'^[ ]{0,3}(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*)')
META_MORE_RE = re.compile(r'^([ ]{4}|\t)(\s*)(?P<value>.*)')
END_RE = re.compile(r'^(-{3}|\.{3})(\s.*)?')
def get_raw_data(doc):
"""
Extract raw meta-data from a text document.
Returns a tuple of document and a data dict.
"""
lines = doc.replace('\r\n', '\n').replace('\r', '\n').split('\n')
if lines and BEGIN_RE.match(lines[0]):
lines.pop(0)
data = {}
key = None
while lines:
line = lines.pop(0)
if line.strip() == '' or END_RE.match(line):
break # blank line or end deliminator - done
m1 = META_RE.match(line)
if m1:
key = m1.group('key').lower().strip()
value = m1.group('value').strip()
try:
data[key].append(value)
except KeyError:
data[key] = [value]
else:
m2 = META_MORE_RE.match(line)
if m2 and key:
# Add another line to existing key
data[key].append(m2.group('value').strip())
else:
lines.insert(0, line)
break # no meta data - done
return '\n'.join(lines).lstrip('\n'), data
def get_data(doc, transformers=tc):
"""
Extract meta-data from a text document.
`transformers`: A TransformerCollection used to transform data values.
Returns a tuple of document and a (transformed) data dict.
"""
doc, rawdata = get_raw_data(doc)
return doc, transformers.transform_dict(rawdata)