Repository: mkdocs/mkdocs

Issue:
Support Markdown Extension Configs
Mkdocs uses Python-Markdown and offers support for users to specify extensions supported by Python-Markdown. However, currently mkdocs provides no way (sort-of; see below) to pass config settings into the extensions specified. For example, I would often like to use the [TOC][1] Extension's `permalink` feature to add the paragraph symbol (¶ or `&para;`) to the end of each header (`h1-6`) as a self-link. A very handy feature in documentation (in fact, it's used by Python-Markdown's docs, which I've linked to). Another use may be to customize how [syntax highlighting][2] works. Or non-english users may want to override the default substitutions used by [SmartyPants][3]. etc, etc...

Sure, mkdocs could hard-code some specific settings for things that make sense. Granted, not all extensions configs make sense in the context of mkdocs. But do you really want to be fielding bug reports for years to come asking for some additional config setting? And don't forget about [third party extensions][9]. Some of them (like a few of the math extensions) make a lot of sense for some users of mkdocs. But you couldn't possibly expect to provide good support for them without offering a holistic approach.

Interestingly, as mkdocs' config file is YAML and Python-Markdown's [`extension_configs`][4] keyword accepts a dictionary of dictionaries, it is pretty easy to replicate in the config file. Of note is the recent addition to Python-Markdown's CLI of support for a [YAML config file][5] for this very thing. One difference I would suggest is that, unlike the CLI feature, there should be no need to also provide a list of extensions separate from the extension configs.  I would expect the implementation to be something like this:

```python
if isinstance(config['markdown_extensions'], dict):
    extensions = config['markdown_extensions'].keys()
    extensions_configs = config['markdown_extensions']
else:
    # backward compat with old list type
    extensions = config['markdown_extensions']
    extensions_configs = {}

# then later in the code...
md = markdown.Markdown(
    extensions=extensions,
    extensions_configs = extensions_configs
)
html_content = md.convert(markdown_source)
```

Note that support for the existing list of extensions is still supported. The new behavior only happens when the config setting is of the dict type. And the new format in the config file would look something like this:

```yaml
markdown_extensions:
    markdown.extensions.toc:
        permalink: True
    markdown.extensions.codehilite:
        linenums: True
    markdown.extensions.smarty:
        smart_angled_quotes: True,
        substitutions:
            left-single-quote: '&sbquo;', 
            right-single-quote: '&lsquo;',
            left-double-quote: '&bdquo;',
            right-double-quote: '&ldquo;'
```

A few notes about this proposal:

1. Actually mkdocs does provide a method of sorts for passing in config settings which involves including the settings right in the name of the extension (see the [old docs][7]). However, that behavior is [deprecated][6] in Python-Markdown 2.6 (released yesterday) and was "pending deprecation" in 2.5. Even in older versions, it is very limited as no spaces are allowed and only string types could be used (booleans were tricky -- usually a 0 or 1 and then special code in the extension to call `int()` on the string). 

2. In previous versions of Python-Markdown (prior to version 2.5), the `extensions_configs` keyword expected a list of tuples for each extension rather than a dictionary (again see the [old docs][8]). That being the case, the earliest version of Python-Markdown this would work with is 2.5.

3. As of today, mkdocs only supports Python-Markdown version 2.4 as that was the last version to support Python 2.6 (see #165). However, dropping support for Python 2.6 appears to be earmarked for mkdoc 1.0.0 (0.12.0 is currently in development with 1.0.0 being the next milestone).

Given the above, I expect that this feature would be added to 1.0 at the earliest (although I understand if the desire is to not add any new features until after 1.0). Given that fact, I don't see much point in actually creating a pull request just yet (with tests and docs -- the code changes are pretty simple -- see above) as I'm not really interested in continually rebasing it until you guys are ready to accept it. That said, if the idea is accepted, I'll happily do the work when the time comes. If, on the other hand, you guys are not interested in adding support for this, I'll probably look for another project which fits my needs better (not having this is a non-starter for me).

[1]: https://pythonhosted.org/Markdown/extensions/toc.html#usage
[2]: https://pythonhosted.org/Markdown/extensions/code_hilite.html#usage
[3]: https://pythonhosted.org/Markdown/extensions/smarty.html
[4]: https://pythonhosted.org/Markdown/reference.html#extension_configs
[5]: https://pythonhosted.org/Markdown/cli.html#using-extensions
[6]: https://pythonhosted.org/Markdown/release-2.6.html#extension-configuration-as-part-of-extension-name-deprecated
[7]: https://github.com/waylan/Python-Markdown/blob/e4c13788f1c6f6f204ca7c471b25246f6c156832/docs/reference.txt#L91
[8]: https://github.com/waylan/Python-Markdown/blob/e4c13788f1c6f6f204ca7c471b25246f6c156832/docs/reference.txt#L107
[9]: https://github.com/waylan/Python-Markdown/wiki/Third-Party-Extensions

Discussion:

Patch:
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index fbd5a287..bb0ba228 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -200,6 +200,22 @@ For example, to enable the [SmartyPants typography extension][smarty], use:
 
     markdown_extensions: [smartypants]
 
+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.
+
+For example, to enable permalinks in the (included) `toc` extension, use:
+
+    markdown_extensions:
+        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:
+
+    markdown_extensions:
+        smartypants:
+        toc:
+            permalink: True
+
+
 **default**: `[]`
 
 [pymdk-extensions]: http://pythonhosted.org/Markdown/extensions/index.html
diff --git a/mkdocs/build.py b/mkdocs/build.py
index bda72820..cd29d3c3 100644
--- a/mkdocs/build.py
+++ b/mkdocs/build.py
@@ -28,11 +28,18 @@ def convert_markdown(markdown_source, site_navigation=None, extensions=(), stric
     """
 
     # 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 = builtin_extensions + mkdocs_extensions + list(extensions)
+    extensions = set(builtin_extensions + mkdocs_extensions + user_extensions)
     md = markdown.Markdown(
-        extensions=extensions
+        extensions=extensions,
+        extension_configs=extension_configs
     )
     html_content = md.convert(markdown_source)
 


Test patch:
diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py
index eaadf753..8c0808d5 100644
--- a/mkdocs/tests/build_tests.py
+++ b/mkdocs/tests/build_tests.py
@@ -320,3 +320,22 @@ class BuildTests(unittest.TestCase):
         self.assertRaises(
             MarkdownNotFound,
             build.convert_markdown, invalid, site_nav, strict=True)
+
+    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
+        }
+        html, toc, meta = build.convert_markdown(dedent("""
+        # A Header
+        """), extensions=markdown_extensions)
+
+        expected_html = dedent("""
+        <h1 id="a-header">A Header<a class="headerlink" href="#a-header" title="Permanent link">&para;</a></h1>
+        """)
+
+        self.assertEqual(html.strip(), expected_html)