diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index c0951359..c55b4248 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -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) diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py index ef804e35..052f6b56 100644 --- a/mkdocs/commands/build.py +++ b/mkdocs/commands/build.py @@ -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 diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py index b93ff01f..8bcdbd7a 100644 --- a/mkdocs/config/defaults.py +++ b/mkdocs/config/defaults.py @@ -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. diff --git a/mkdocs/nav.py b/mkdocs/nav.py index 55d45a9f..db46c048 100644 --- a/mkdocs/nav.py +++ b/mkdocs/nav.py @@ -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): diff --git a/mkdocs/search.py b/mkdocs/search.py index 10a92b8a..a4021919 100644 --- a/mkdocs/search.py +++ b/mkdocs/search.py @@ -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): """ diff --git a/mkdocs/tests/base.py b/mkdocs/tests/base.py index 3a3d2f26..51cfee36 100644 --- a/mkdocs/tests/base.py +++ b/mkdocs/tests/base.py @@ -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 diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py index 607d4244..e25c26ba 100644 --- a/mkdocs/tests/build_tests.py +++ b/mkdocs/tests/build_tests.py @@ -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("""

Heading 1

@@ -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 = '

An internal link to another document.

' - 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 = '

First link second link.

' - 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 = '

An internal link to another document.

' - 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 = '

An internal link to another document.

' - 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 = '

The initial MkDocs layout

' 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 = '

The initial MkDocs layout

' 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("""

An HTML Anchor::

@@ -167,9 +181,9 @@ class BuildTests(unittest.TestCase): """) for page in site_navigation.walk_pages(): - markdown = 'An HTML Anchor::\n\n My example link\n' - html, _, _ = build.convert_markdown(markdown, load_config(), site_navigation=site_navigation) - self.assertEqual(dedent(html), expected) + page.markdown = 'An HTML Anchor::\n\n My example link\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, '

test

') + page.markdown = '[test](#test)' + page.render(config, site_navigation) + self.assertEqual(page.content, '

test

') def test_ignore_external_link(self): md_text = 'An [external link](http://example.com/external.md).' expected = '

An external link.

' - 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 = '

An internal link to another document.

' - 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 and an [link](mailto:example@example.com).' @@ -210,19 +225,21 @@ class BuildTests(unittest.TestCase): 'k@example.com', ' and an link.

' ]) - 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(""" @@ -245,54 +262,60 @@ class BuildTests(unittest.TestCase):
""") - 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("""
print 'foo'\n
""") - 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 = "

foobarbaz

" - 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 = "

foo__bar__baz

" - 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(), '

foo

') + 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(), '

foo

') 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("""

A Header

""") - 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) diff --git a/mkdocs/tests/nav_tests.py b/mkdocs/tests/nav_tests.py index ee997d5e..3eade7b7 100644 --- a/mkdocs/tests/nav_tests.py +++ b/mkdocs/tests/nav_tests.py @@ -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], diff --git a/mkdocs/tests/search_tests.py b/mkdocs/tests/search_tests.py index b4dbf911..e0a1947f 100644 --- a/mkdocs/tests/search_tests.py +++ b/mkdocs/tests/search_tests.py @@ -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) diff --git a/mkdocs/tests/utils/utils_tests.py b/mkdocs/tests/utils/utils_tests.py index fec697f7..c6539070 100644 --- a/mkdocs/tests/utils/utils_tests.py +++ b/mkdocs/tests/utils/utils_tests.py @@ -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" diff --git a/mkdocs/utils/__init__.py b/mkdocs/utils/__init__.py index 2b335111..1017180a 100644 --- a/mkdocs/utils/__init__.py +++ b/mkdocs/utils/__init__.py @@ -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 diff --git a/mkdocs/utils/meta.py b/mkdocs/utils/meta.py new file mode 100644 index 00000000..b976d9af --- /dev/null +++ b/mkdocs/utils/meta.py @@ -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[A-Za-z0-9_-]+):\s*(?P.*)') +META_MORE_RE = re.compile(r'^([ ]{4}|\t)(\s*)(?P.*)') +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)