Repository: mkdocs/mkdocs

Issue:
smartypants extension error
I'm new to mkdocs and have recently installed it.  I'm running on Windows and am using Python 3.4.

I was walking though some of the mkdocs documentation and wanted to try to use smartypants.  Initially I found out I had to install the extension itself which wasn't mentioned in the mkdocs documentation (might be helpful to add that).  After installing the extension with pip I tried to use it using all 3 of the below configuration formats..

```
markdown_extensions: [smartypants]
```
```
markdown_extensions: 
       - smartypants
```
```
markdown_extensions:
          smartypants:
```


Either way, when I try to run mkdocs serve I get the below error

```
ERROR - Error building page index.md
[E 150512 11:21:27 build:221] Error building page index.md
Traceback (most recent call last):
  File "C:\tools\python\Scripts\mkdocs-script.py", line 9, in <module>
    load_entry_point('mkdocs==0.12.2', 'console_scripts', 'mkdocs')()
  File "C:\tools\python\lib\site-packages\mkdocs\main.py", line 77, in run_main
    main(cmd, args=sys.argv[2:], options=dict(opts))
  File "C:\tools\python\lib\site-packages\mkdocs\main.py", line 49, in main
    serve(config, options=options)
  File "C:\tools\python\lib\site-packages\mkdocs\serve.py", line 22, in serve
    builder()
  File "C:\tools\python\lib\site-packages\mkdocs\serve.py", line 19, in builder
    build(config, live_server=True)
  File "C:\tools\python\lib\site-packages\mkdocs\build.py", line 252, in build
    build_pages(config)
  File "C:\tools\python\lib\site-packages\mkdocs\build.py", line 219, in build_pages
    _build_page(page, config, site_navigation, env, dump_json)
  File "C:\tools\python\lib\site-packages\mkdocs\build.py", line 173, in _build_page
    extensions=config['markdown_extensions'], strict=config['strict']
  File "C:\tools\python\lib\site-packages\mkdocs\build.py", line 36, in convert_markdown
    extensions=extensions
  File "C:\tools\python\lib\site-packages\markdown\__init__.py", line 159, in __init__
    configs=kwargs.get('extension_configs', {}))
  File "C:\tools\python\lib\site-packages\markdown\__init__.py", line 185, in registerExtensions
    ext = self.build_extension(ext, configs.get(ext, {}))
  File "C:\tools\python\lib\site-packages\markdown\__init__.py", line 291, in build_extension
    return module.makeExtension(**configs)
  File "C:\tools\python\lib\site-packages\mdx_smartypants\core.py", line 52, in makeExtension
    return SmartypantsExt(configs=configs)
  File "C:\tools\python\lib\site-packages\mdx_smartypants\core.py", line 41, in __init__
    self.configs = dict(configs)
TypeError: 'NoneType' object is not iterable
```

Discussion:
landscape-bot: [![Code Health](https://landscape.io/badge/165865/landscape.svg?style=flat)](https://landscape.io/diff/153156)
Code quality remained the same when pulling **[d3fa1bd](https://github.com/waylan/mkdocs/commit/d3fa1bd74f3d126c31e03d9330922b265354fa15) on waylan:519** into **[667fc66](https://github.com/mkdocs/mkdocs/commit/667fc663a2d9f6b3c7fb5c26777161a4ad42368f) on mkdocs:master**.
d0ugal: This looks great! I should be able to review it today.
landscape-bot: [![Code Health](https://landscape.io/badge/166893/landscape.svg?style=flat)](https://landscape.io/diff/154259)
Code quality remained the same when pulling **[06a74af](https://github.com/waylan/mkdocs/commit/06a74afdc8bfe120398344b021738e305ca246d3) on waylan:519** into **[93a181a](https://github.com/mkdocs/mkdocs/commit/93a181a208af0440f44e7f51081b9df9d9f30568) on mkdocs:master**.
d0ugal: This is lovely, do you plan to do any more updates? I'm happy to merge as it is. Tested it and it works great.
waylan: The only other thing I would consider doing at this point is removing the OrderedDict stuff for the config as this removes the need for it. I don't think it would hurt to leave it in, but removing it removes one more thing to maintain long-term. @facelessuser have any thoughts?

Regardless, that could happen latter. Feel free to merge any time.
facelessuser: Remove it, unless ordered dicts will be desired somewhere in the future.  It hurts nothing, but I only added it because, at the time, it was decided not to do the extension config this way.  But now we are, so the original need is gone.

Patch:
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
+
+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:
+        - toc:
             permalink: True
+            separator: "_"
 
-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:
+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:
-        smartypants:
-        toc:
+        - 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 &copy; 2014, <a href="https://twitter.com/_tomchristie">Tom Christie</a>.
 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/base.py b/mkdocs/config/base.py
index 870d00b7..53ffbad1 100644
--- a/mkdocs/config/base.py
+++ b/mkdocs/config/base.py
@@ -1,9 +1,9 @@
 import six
 import logging
 import os
+import yaml
 
 from mkdocs import exceptions
-from mkdocs import utils
 from mkdocs.config import config_options, defaults
 
 log = logging.getLogger('mkdocs.config')
@@ -82,7 +82,7 @@ class Config(six.moves.UserDict):
         self.data.update(patch)
 
     def load_file(self, config_file):
-        return self.load_dict(utils.yaml_load(config_file))
+        return self.load_dict(yaml.load(config_file))
 
 
 def _open_config_file(config_file):
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/utils.py b/mkdocs/utils.py
index de7c67a5..7ff7f07e 100644
--- a/mkdocs/utils.py
+++ b/mkdocs/utils.py
@@ -12,30 +12,8 @@ import shutil
 
 import markdown
 import six
-import yaml
 
 from mkdocs import toc
-from mkdocs.legacy import OrderedDict
-
-
-def yaml_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict):
-    """
-    Make all YAML dictionaries load as ordered Dicts.
-    http://stackoverflow.com/a/21912744/3609487
-    """
-    class OrderedLoader(Loader):
-        pass
-
-    def construct_mapping(loader, node):
-        loader.flatten_mapping(node)
-        return object_pairs_hook(loader.construct_pairs(node))
-
-    OrderedLoader.add_constructor(
-        yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
-        construct_mapping
-    )
-
-    return yaml.load(stream, OrderedLoader)
 
 
 def reduce_list(data_set):
@@ -279,12 +257,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)
 


Test patch:
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("""
             <h1 id="heading-1">Heading 1</h1>
@@ -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 = '<p>An <a href="internal/">internal link</a> to another document.</p>'
-        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 = '<p><a href="first/">First link</a> <a href="second/">second link</a>.</p>'
-        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 = '<p>An <a href="../internal/">internal link</a> to another document.</p>'
-        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 = '<p>An <a href="internal/#section1.1">internal link</a> to another document.</p>'
-        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    <a href="index.md">My example link</a>\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, '<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)
+        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("""
         <table>
@@ -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("""
         <pre><code>print 'foo'\n</code></pre>
@@ -241,24 +247,29 @@ class BuildTests(unittest.TestCase):
 
         # 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)
+        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 = "<p>foo__bar__baz</p>"
-        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(), '<p>foo</p>')
 
     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("""
         <h1 id="a-header">A Header<a class="headerlink" href="#a-header" title="Permanent link">&para;</a></h1>
@@ -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/tests/utils_tests.py b/mkdocs/tests/utils_tests.py
index 41d066e6..44af6799 100644
--- a/mkdocs/tests/utils_tests.py
+++ b/mkdocs/tests/utils_tests.py
@@ -5,7 +5,6 @@ import os
 import unittest
 
 from mkdocs import nav, utils
-from mkdocs.tests.base import dedent
 
 
 class UtilsTests(unittest.TestCase):
@@ -70,32 +69,6 @@ class UtilsTests(unittest.TestCase):
             urls = utils.create_media_urls(site_navigation, [path])
             self.assertEqual(urls[0], expected_result)
 
-    def test_yaml_load(self):
-        try:
-            from collections import OrderedDict
-        except ImportError:
-            # Don't test if can't import OrderdDict
-            # Exception can be removed when Py26 support is removed
-            return
-
-        yaml_text = dedent('''
-            test:
-                key1: 1
-                key2: 2
-                key3: 3
-                key4: 4
-                key5: 5
-                key5: 6
-                key3: 7
-        ''')
-
-        self.assertEqual(
-            utils.yaml_load(yaml_text),
-            OrderedDict([('test', OrderedDict([('key1', 1), ('key2', 2),
-                                               ('key3', 7), ('key4', 4),
-                                               ('key5', 6)]))])
-        )
-
     def test_reduce_list(self):
         self.assertEqual(
             utils.reduce_list([1, 2, 3, 4, 5, 5, 2, 4, 6, 7, 8]),