mirror of
https://github.com/mkdocs/mkdocs.git
synced 2026-03-27 09:58:31 +07:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
169
mkdocs/nav.py
169
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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ''
|
||||
html, _, _ = build.convert_markdown(md_text, load_config(), site_navigation=site_navigation)
|
||||
self.assertEqual(html, template % expected)
|
||||
page.markdown = ''
|
||||
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 = ''
|
||||
html, _, _ = build.convert_markdown(md_text, load_config(), site_navigation=site_navigation)
|
||||
self.assertEqual(html, template % expected)
|
||||
page.markdown = ''
|
||||
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):
|
||||
'k@example.com',
|
||||
'</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">¶</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)
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
176
mkdocs/utils/meta.py
Normal 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)
|
||||
Reference in New Issue
Block a user