From a6fc4f9420d81982a34fb87a1b6e9a37de5145c6 Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Fri, 15 May 2015 00:07:55 -0400 Subject: [PATCH] Refactor Markdown Extension Options Config. Config validation now handles all extension processing: * Builtin extensions are defined and within the default scheme. * User extensions are defined only as a list in the config. Note, this is a backward incompatable change from the previous (short-lived) release. * The users extensions are added to the list of builtins. * Any duplicates are accounted for in validation. * Extension options are supported by a child key/value pair on the ext name. * All extension options are compiled into a format Markdown accepts within the validation process and are saved to the internal `mdx_configs` config setting. * The `mdx_configs` setting is private and raises an error if set by the user. * A whole suite of tests were added to test all aspects of ext validation. All relevant build tests were updated to pass the config to `build.convert_markdown` as the config now handles all extension data. The relevant documentation was updated to reflect the changes. While I was at it, I spellchecked the entire document and made a few additional formatting changes. This fixes #519 plus a lot more. --- docs/user-guide/configuration.md | 73 ++++++--- mkdocs.yml | 4 +- mkdocs/build.py | 36 ++--- mkdocs/config/config_options.py | 53 +++++++ mkdocs/config/defaults.py | 8 +- mkdocs/tests/build_tests.py | 91 ++++++----- mkdocs/tests/config/config_options_tests.py | 167 ++++++++++++++++++++ mkdocs/utils.py | 7 +- 8 files changed, 345 insertions(+), 94 deletions(-) diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index 924e6207..4b9c5b8d 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -8,7 +8,7 @@ Guide to all available configuration settings. Project settings are always configured by using a YAML configuration file in the project directory named `mkdocs.yml`. -As a miniumum this configuration file must contain the `site_name` setting. All other settings are optional. +As a minimum this configuration file must contain the `site_name` setting. All other settings are optional. ## Project information @@ -16,7 +16,7 @@ As a miniumum this configuration file must contain the `site_name` setting. All This is a **required setting**, and should be a string that is used as the main title for the project documentation. For example: - site_name: Mashmallow Generator + site_name: Marshmallow Generator When rendering the theme this setting will be passed as the `site_name` context variable. @@ -109,9 +109,9 @@ If you have a lot of project documentation you might choose to use headings to b pages: - Introduction: 'index.md' - User Guide: - - 'Creating a new Mashmallow project': 'user-guide/creating.md' - - 'Mashmallow API guide': 'user-guide/api.md' - - 'Configuring Mashmallow': 'user-guide/configuration.md' + - 'Creating a new Marshmallow project': 'user-guide/creating.md' + - 'Marshmallow API guide': 'user-guide/api.md' + - 'Configuring Marshmallow': 'user-guide/configuration.md' - About: - License: 'about/license.md' @@ -150,16 +150,21 @@ Lets you set the directory where the output HTML and other files are created. T **default**: `'site'` -**Note**: If you are using source code control you will normally want to ensure that your *build output* files are not commited into the repository, and only keep the *source* files under version control. For example, if using `git` you might add the following line to your `.gitignore` file: +!!! note "Note:" + If you are using source code control you will normally want to ensure + that your *build output* files are not committed into the repository, and only + keep the *source* files under version control. For example, if using `git` you + might add the following line to your `.gitignore` file: - site/ + site/ -If you're using another source code control you'll want to check its documentation on how to ignore specific directories. + If you're using another source code control you'll want to check its + documentation on how to ignore specific directories. ### extra_css -Set a list of css files to be included by the theme. +Set a list of CSS files to be included by the theme. **default**: By default `extra_css` will contain a list of all the CSS files found within the `docs_dir`, if none are found it will be `[]` (an empty list). @@ -207,7 +212,7 @@ Determines if a broken link to a page within the documentation is considered a w Determines the address used when running `mkdocs serve`. Setting this allows you to use another port, or allows you to make the service accessible over your local network by using the `0.0.0.0` address. -As with all settings, you can set this from the command line, which can be usful, for example: +As with all settings, you can set this from the command line, which can be useful, for example: mkdocs serve --dev-addr=0.0.0.0:80 # Run on port 80, accessible over the local network. @@ -217,30 +222,62 @@ As with all settings, you can set this from the command line, which can be usful ### markdown_extensions -MkDocs uses the [Python Markdown][pymkd] library to translate Markdown files into HTML. Python Markdown supports a variety of [extensions][pymdk-extensions] that customize how pages are formatted. This setting lets you enable a list of extensions beyond the ones that MkDocs uses by default (`meta`, `toc`, `tables`, and `fenced_code`). +MkDocs uses the [Python Markdown][pymkd] library to translate Markdown files +into HTML. Python Markdown supports a variety of [extensions][pymdk-extensions] +that customize how pages are formatted. This setting lets you enable a list of +extensions beyond the ones that MkDocs uses by default (`meta`, `toc`, `tables`, +and `fenced_code`). For example, to enable the [SmartyPants typography extension][smarty], use: - markdown_extensions: [smartypants] + markdown_extensions: + - smarty -Some extensions provide configuration options of their own. If you would like to set any configuration options, then you can define `markdown_extensions` as a key/value mapping rather than a list. The key must be the name of the extension and the value must be a key/value pair (option name/option value) for the configuration option. +Some extensions provide configuration options of their own. If you would like to +set any configuration options, then you can nest a key/value mapping +(`option_name: option value`) of any options that a given extension supports. +See the documentation for the extension you are using to determine what options +they support. For example, to enable permalinks in the (included) `toc` extension, use: markdown_extensions: - toc: + - toc: permalink: True -Add additonal items for each extension. If you have no configuration options to set for a specific extension, then you may leave that extensions options blank: +Note that a colon (`:`) must follow the extension name (`toc`) and then on a new line +the option name and value must be indented and seperated by a colon. If you would like +to define multipe options for a single extension, each option must be defined on +a seperate line: markdown_extensions: - smartypants: - toc: + - toc: permalink: True + separator: "_" +Add an additional item to the list for each extension. If you have no +configuration options to set for a specific extension, then simply omit options +for that extension: + + markdown_extensions: + - smarty + - toc: + permalink: True + - sane_lists + +!!! note "See Also:" + The Python-Markdown documentation provides a [list of extensions][exts] + which are available out-of-the-box. For a list of configuration options + available for a given extension, see the documentation for that extension. + + You may also install and use various [third party extensions][3rd]. Consult the + documentation provided by those extensions for installation instructions and + available configuration options. **default**: `[]` [pymdk-extensions]: http://pythonhosted.org/Markdown/extensions/index.html [pymkd]: http://pythonhosted.org/Markdown/ -[smarty]: https://pypi.python.org/pypi/mdx_smartypants +[smarty]: https://pythonhosted.org/Markdown/extensions/smarty.html +[exts]:https://pythonhosted.org/Markdown/extensions/index.html +[3rd]: https://github.com/waylan/Python-Markdown/wiki/Third-Party-Extensions \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 930ccd95..e092f9ec 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,9 +5,9 @@ site_description: Project documentation with Markdown. repo_url: https://github.com/mkdocs/mkdocs/ markdown_extensions: - toc: + - toc: permalink: "" - admonition: + - admonition: copyright: Copyright © 2014, Tom Christie. google_analytics: ['UA-27795084-5', 'mkdocs.org'] diff --git a/mkdocs/build.py b/mkdocs/build.py index 73d9a309..70f60e7a 100644 --- a/mkdocs/build.py +++ b/mkdocs/build.py @@ -17,30 +17,17 @@ import mkdocs log = logging.getLogger(__name__) -def convert_markdown(markdown_source, site_navigation=None, extensions=(), strict=False): +def convert_markdown(markdown_source, config, site_navigation=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. + 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. """ - - # Generate the HTML from the markdown source - if isinstance(extensions, dict): - user_extensions = list(extensions.keys()) - extension_configs = dict([(k, v) for k, v in extensions.items() if isinstance(v, dict)]) - else: - user_extensions = list(extensions) - extension_configs = {} - builtin_extensions = ['meta', 'toc', 'tables', 'fenced_code'] - mkdocs_extensions = [RelativePathExtension(site_navigation, strict), ] - extensions = utils.reduce_list(builtin_extensions + mkdocs_extensions + user_extensions) - - html_content, table_of_contents, meta = utils.convert_markdown(markdown_source, extensions, extension_configs) - - return (html_content, table_of_contents, meta) + return utils.convert_markdown( + markdown_source=markdown_source, + extensions=[RelativePathExtension(site_navigation, config['strict'])] + config['markdown_extensions'], + extension_configs=config['mdx_configs'] + ) def get_global_context(nav, config): @@ -182,8 +169,9 @@ def _build_page(page, config, site_navigation, env, dump_json): # Process the markdown text html_content, table_of_contents, meta = convert_markdown( - input_content, site_navigation, - extensions=config['markdown_extensions'], strict=config['strict'] + markdown_source=input_content, + config=config, + site_navigation=site_navigation ) context = get_global_context(site_navigation, config) diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py index c6ac2910..6c47197b 100644 --- a/mkdocs/config/config_options.py +++ b/mkdocs/config/config_options.py @@ -343,3 +343,56 @@ class NumPages(BaseConfigOption): config[key_name] = len(config['pages']) > self.at_lest except TypeError: config[key_name] = False + + +class Private(BaseConfigOption): + """ + Private Config Option + + A config option only for internal use. Raises an error if set by the user. + """ + + def run_validation(self, value): + raise ValidationError('For internal use only.') + + +class MarkdownExtensions(BaseConfigOption): + """ + Markdown Extensions Config Option + + A list of extensions. If a list item contains extension configs, + those are set on the private setting passed to `configkey`. The + `builtins` keyword accepts a list of extensions which cannot be + overriden by the user. However, builtins can be duplicated to define + config options for them if desired. + """ + def __init__(self, builtins=None, configkey='mdx_configs', **kwargs): + super(MarkdownExtensions, self).__init__(**kwargs) + self.builtins = builtins or [] + self.configkey = configkey + self.configdata = {} + + def run_validation(self, value): + if not isinstance(value, (list, tuple)): + raise ValidationError('Invalid Markdown Extensions configuration') + extensions = [] + for item in value: + if isinstance(item, dict): + if len(item) > 1: + raise ValidationError('Invalid Markdown Extensions configuration') + ext, cfg = item.popitem() + extensions.append(ext) + if cfg is None: + continue + if not isinstance(cfg, dict): + raise ValidationError('Invalid config options for Markdown ' + "Extension '{0}'.".format(ext)) + self.configdata[ext] = cfg + elif isinstance(item, six.string_types): + extensions.append(item) + else: + raise ValidationError('Invalid Markdown Extensions configuration') + return utils.reduce_list(self.builtins + extensions) + + def post_validation(self, config, key_name): + config[self.configkey] = self.configdata diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py index 9d7bcd6d..2c2d929f 100644 --- a/mkdocs/config/defaults.py +++ b/mkdocs/config/defaults.py @@ -90,8 +90,12 @@ DEFAULT_SCHEMA = ( ('include_next_prev', config_options.NumPages()), # PyMarkdown extension names. - ('markdown_extensions', config_options.Type( - (list, dict, tuple), default=())), + ('markdown_extensions', config_options.MarkdownExtensions( + builtins=['meta', 'toc', 'tables', 'fenced_code'], + configkey='mdx_configs', default=[])), + + # PyMarkdown Extension Configs. For internal use only. + ('mdx_configs', config_options.Private()), # enabling strict mode causes MkDocs to stop the build when a problem is # encountered rather than display an error. diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py index 470dac46..d474925b 100644 --- a/mkdocs/tests/build_tests.py +++ b/mkdocs/tests/build_tests.py @@ -9,16 +9,26 @@ import unittest from six.moves import zip import mock -from mkdocs import build, nav -from mkdocs.config import base as config_base, defaults as config_defaults +from mkdocs import build, nav, config from mkdocs.exceptions import MarkdownNotFound from mkdocs.tests.base import dedent +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' + conf = config.base.Config(schema=config.defaults.DEFAULT_SCHEMA) + conf.load_dict(cfg) + assert(conf.validate() == ([], [])) + return conf + + class BuildTests(unittest.TestCase): def test_empty_document(self): - html, toc, meta = build.convert_markdown("") + html, toc, meta = build.convert_markdown("", load_config()) self.assertEqual(html, '') self.assertEqual(len(list(toc)), 0) @@ -28,7 +38,6 @@ class BuildTests(unittest.TestCase): """ Ensure that basic Markdown -> HTML and TOC works. """ - html, toc, meta = build.convert_markdown(dedent(""" page_title: custom title @@ -39,7 +48,7 @@ class BuildTests(unittest.TestCase): # Heading 2 And some more text. - """)) + """), load_config()) expected_html = dedent("""

Heading 1

@@ -62,25 +71,25 @@ class BuildTests(unittest.TestCase): 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) + html, toc, meta = build.convert_markdown(md_text, load_config()) self.assertEqual(html.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) + html, toc, meta = build.convert_markdown(md_text, load_config()) self.assertEqual(html.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) + html, toc, meta = build.convert_markdown(md_text, load_config()) self.assertEqual(html.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) + html, toc, meta = build.convert_markdown(md_text, load_config()) self.assertEqual(html.strip(), expected.strip()) def test_convert_internal_media(self): @@ -103,7 +112,7 @@ class BuildTests(unittest.TestCase): 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, site_navigation=site_navigation) + html, _, _ = build.convert_markdown(md_text, load_config(), site_navigation=site_navigation) self.assertEqual(html, template % expected) def test_convert_internal_asbolute_media(self): @@ -126,7 +135,7 @@ class BuildTests(unittest.TestCase): 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, site_navigation=site_navigation) + html, _, _ = build.convert_markdown(md_text, load_config(), site_navigation=site_navigation) self.assertEqual(html, template % expected) def test_dont_convert_code_block_urls(self): @@ -146,11 +155,10 @@ 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, site_navigation=site_navigation) + html, _, _ = build.convert_markdown(markdown, load_config(), site_navigation=site_navigation) self.assertEqual(dedent(html), expected) def test_anchor_only_link(self): - pages = [ 'index.md', 'internal.md', @@ -161,13 +169,13 @@ class BuildTests(unittest.TestCase): for page in site_navigation.walk_pages(): markdown = '[test](#test)' - html, _, _ = build.convert_markdown(markdown, site_navigation=site_navigation) + html, _, _ = build.convert_markdown(markdown, load_config(), site_navigation=site_navigation) self.assertEqual(html, '

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) + html, toc, meta = build.convert_markdown(md_text, load_config()) self.assertEqual(html.strip(), expected.strip()) def test_not_use_directory_urls(self): @@ -177,20 +185,19 @@ class BuildTests(unittest.TestCase): 'internal.md', ] site_navigation = nav.SiteNavigation(pages, use_directory_urls=False) - html, toc, meta = build.convert_markdown(md_text, site_navigation=site_navigation) + html, toc, meta = build.convert_markdown(md_text, load_config(), site_navigation=site_navigation) self.assertEqual(html.strip(), expected.strip()) def test_markdown_table_extension(self): """ Ensure that the table extension is supported. """ - html, toc, meta = build.convert_markdown(dedent(""" First Header | Second Header -------------- | -------------- Content Cell 1 | Content Cell 2 Content Cell 3 | Content Cell 4 - """)) + """), load_config()) expected_html = dedent(""" @@ -219,12 +226,11 @@ class BuildTests(unittest.TestCase): """ Ensure that the fenced code extension is supported. """ - html, toc, meta = build.convert_markdown(dedent(""" ``` print 'foo' ``` - """)) + """), load_config()) expected_html = dedent("""
print 'foo'\n
@@ -241,24 +247,29 @@ class BuildTests(unittest.TestCase): # Check that the plugin is not active when not requested. expected_without_smartstrong = "

foobarbaz

" - html_base, _, _ = build.convert_markdown(md_input) + html_base, _, _ = build.convert_markdown(md_input, load_config()) self.assertEqual(html_base.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, extensions=['smart_strong']) + html_ext, _, _ = build.convert_markdown(md_input, cfg) self.assertEqual(html_ext.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, ['toc']) + html_ext, _, _ = build.convert_markdown(md_input, cfg) self.assertEqual(html_ext.strip(), '

foo

') def test_copying_media(self): - docs_dir = tempfile.mkdtemp() site_dir = tempfile.mkdtemp() try: @@ -281,14 +292,11 @@ class BuildTests(unittest.TestCase): os.mkdir(os.path.join(docs_dir, '.git')) open(os.path.join(docs_dir, '.git/hidden'), 'w').close() - conf = config_base.Config(schema=config_defaults.DEFAULT_SCHEMA) - conf.load_dict({ - 'site_name': 'Example', + cfg = load_config({ 'docs_dir': docs_dir, 'site_dir': site_dir }) - conf.validate() - build.build(conf) + build.build(cfg) # Verify only the markdown (coverted to html) and the image are copied. self.assertTrue(os.path.isfile(os.path.join(site_dir, 'index.html'))) @@ -308,8 +316,8 @@ class BuildTests(unittest.TestCase): site_nav = nav.SiteNavigation(pages) valid = "[test](internal.md)" - build.convert_markdown(valid, site_nav, strict=False) - build.convert_markdown(valid, site_nav, strict=True) + build.convert_markdown(valid, load_config({'strict': False}), site_nav) + build.convert_markdown(valid, load_config({'strict': True}), site_nav) def test_strict_mode_invalid(self): pages = [ @@ -320,24 +328,24 @@ class BuildTests(unittest.TestCase): site_nav = nav.SiteNavigation(pages) invalid = "[test](bad_link.md)" - build.convert_markdown(invalid, site_nav, strict=False) + build.convert_markdown(invalid, load_config({'strict': False}), site_nav) self.assertRaises( MarkdownNotFound, - build.convert_markdown, invalid, site_nav, strict=True) + build.convert_markdown, invalid, load_config({'strict': True}), site_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. """ - markdown_extensions = { - 'toc': {'permalink': True}, - 'meta': None # This gets ignored as it is an invalid config - } + cfg = load_config({ + 'markdown_extensions': [{'toc': {'permalink': True}}] + }) + html, toc, meta = build.convert_markdown(dedent(""" # A Header - """), extensions=markdown_extensions) + """), cfg) expected_html = dedent("""

A Header

@@ -348,17 +356,14 @@ class BuildTests(unittest.TestCase): def test_extra_context(self): # Same as the default schema, but don't verify the docs_dir exists. - config = config_base.Config(schema=config_defaults.DEFAULT_SCHEMA) - config.load_dict({ + cfg = load_config({ 'site_name': "Site", 'extra': { 'a': 1 } }) - self.assertEqual(config.validate(), ([], [])) - - context = build.get_global_context(mock.Mock(), config) + context = build.get_global_context(mock.Mock(), cfg) self.assertEqual(context['config']['extra'], { 'a': 1 diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py index a499ece5..c4a42f94 100644 --- a/mkdocs/tests/config/config_options_tests.py +++ b/mkdocs/tests/config/config_options_tests.py @@ -325,3 +325,170 @@ class NumPagesTest(unittest.TestCase): 'key': True, 'pages': None }, config) + + +class PrivateTest(unittest.TestCase): + + def test_defined(self): + + option = config_options.Private() + self.assertRaises(config_options.ValidationError, + option.validate, 'somevalue') + + +class MarkdownExtensionsTest(unittest.TestCase): + + def test_simple_list(self): + option = config_options.MarkdownExtensions() + config = { + 'markdown_extensions': ['foo', 'bar'] + } + config['markdown_extensions'] = option.validate(config['markdown_extensions']) + option.post_validation(config, 'markdown_extensions') + self.assertEqual({ + 'markdown_extensions': ['foo', 'bar'], + 'mdx_configs': {} + }, config) + + def test_list_dicts(self): + option = config_options.MarkdownExtensions() + config = { + 'markdown_extensions': [ + {'foo': {'foo_option': 'foo value'}}, + {'bar': {'bar_option': 'bar value'}}, + {'baz': None} + ] + } + config['markdown_extensions'] = option.validate(config['markdown_extensions']) + option.post_validation(config, 'markdown_extensions') + self.assertEqual({ + 'markdown_extensions': ['foo', 'bar', 'baz'], + 'mdx_configs': { + 'foo': {'foo_option': 'foo value'}, + 'bar': {'bar_option': 'bar value'} + } + }, config) + + def test_mixed_list(self): + option = config_options.MarkdownExtensions() + config = { + 'markdown_extensions': [ + 'foo', + {'bar': {'bar_option': 'bar value'}} + ] + } + config['markdown_extensions'] = option.validate(config['markdown_extensions']) + option.post_validation(config, 'markdown_extensions') + self.assertEqual({ + 'markdown_extensions': ['foo', 'bar'], + 'mdx_configs': { + 'bar': {'bar_option': 'bar value'} + } + }, config) + + def test_builtins(self): + option = config_options.MarkdownExtensions(builtins=['meta', 'toc']) + config = { + 'markdown_extensions': ['foo', 'bar'] + } + config['markdown_extensions'] = option.validate(config['markdown_extensions']) + option.post_validation(config, 'markdown_extensions') + self.assertEqual({ + 'markdown_extensions': ['meta', 'toc', 'foo', 'bar'], + 'mdx_configs': {} + }, config) + + def test_duplicates(self): + option = config_options.MarkdownExtensions(builtins=['meta', 'toc']) + config = { + 'markdown_extensions': ['meta', 'toc'] + } + config['markdown_extensions'] = option.validate(config['markdown_extensions']) + option.post_validation(config, 'markdown_extensions') + self.assertEqual({ + 'markdown_extensions': ['meta', 'toc'], + 'mdx_configs': {} + }, config) + + def test_builtins_config(self): + option = config_options.MarkdownExtensions(builtins=['meta', 'toc']) + config = { + 'markdown_extensions': [ + {'toc': {'permalink': True}} + ] + } + config['markdown_extensions'] = option.validate(config['markdown_extensions']) + option.post_validation(config, 'markdown_extensions') + self.assertEqual({ + 'markdown_extensions': ['meta', 'toc'], + 'mdx_configs': {'toc': {'permalink': True}} + }, config) + + def test_configkey(self): + option = config_options.MarkdownExtensions(configkey='bar') + config = { + 'markdown_extensions': [ + {'foo': {'foo_option': 'foo value'}} + ] + } + config['markdown_extensions'] = option.validate(config['markdown_extensions']) + option.post_validation(config, 'markdown_extensions') + self.assertEqual({ + 'markdown_extensions': ['foo'], + 'bar': { + 'foo': {'foo_option': 'foo value'} + } + }, config) + + def test_none(self): + option = config_options.MarkdownExtensions(default=[]) + config = { + 'markdown_extensions': None + } + config['markdown_extensions'] = option.validate(config['markdown_extensions']) + option.post_validation(config, 'markdown_extensions') + self.assertEqual({ + 'markdown_extensions': [], + 'mdx_configs': {} + }, config) + + def test_not_list(self): + option = config_options.MarkdownExtensions() + self.assertRaises(config_options.ValidationError, + option.validate, 'not a list') + + def test_invalid_config_option(self): + option = config_options.MarkdownExtensions() + config = { + 'markdown_extensions': [ + {'foo': 'not a dict'} + ] + } + self.assertRaises( + config_options.ValidationError, + option.validate, config['markdown_extensions'] + ) + + def test_invalid_config_item(self): + option = config_options.MarkdownExtensions() + config = { + 'markdown_extensions': [ + ['not a dict'] + ] + } + self.assertRaises( + config_options.ValidationError, + option.validate, config['markdown_extensions'] + ) + + def test_invalid_dict_item(self): + option = config_options.MarkdownExtensions() + config = { + 'markdown_extensions': [ + {'key1': 'value', 'key2': 'too many keys'} + ] + } + self.assertRaises( + config_options.ValidationError, + option.validate, config['markdown_extensions'] + ) diff --git a/mkdocs/utils.py b/mkdocs/utils.py index de7c67a5..82c408fc 100644 --- a/mkdocs/utils.py +++ b/mkdocs/utils.py @@ -279,12 +279,9 @@ def convert_markdown(markdown_source, extensions=None, extension_configs=None): `extensions` is an optional sequence of Python Markdown extensions to add to the default set. """ - extensions = extensions or [] - extension_configs = extension_configs or {} - md = markdown.Markdown( - extensions=extensions, - extension_configs=extension_configs + extensions=extensions or [], + extension_configs=extension_configs or {} ) html_content = md.convert(markdown_source)