From cd7324333a995eeb62a3e6eacdb3b179c6256133 Mon Sep 17 00:00:00 2001
From: Waylan Limberg
Line 1
Line 2
Text with double__underscore__words.
' ->>> markdown.markdown('__Strong__ still works.', \ - extensions=['markdown.extensions.smart_strong']) +>>> markdown.markdown('__Strong__ still works.', extensions=['smart_strong']) u'Strong still works.
' ->>> markdown.markdown('__this__works__too__.', \ - extensions=['markdown.extensions.smart_strong']) +>>> markdown.markdown('__this__works__too__.', extensions=['smart_strong']) u'this__works__too.
' ``` Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.smart_strong` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `smart_strong` as +the name of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/smarty.md b/docs/extensions/smarty.md index 9353d9b..109d78c 100644 --- a/docs/extensions/smarty.md +++ b/docs/extensions/smarty.md @@ -27,7 +27,7 @@ the German language: ```python extension_configs = { - 'markdown.extensions.smarty': { + 'smarty': { 'substitutions': { 'left-single-quote': '‚', # sb is not a typo! 'right-single-quote': '‘', @@ -53,8 +53,8 @@ extension_configs = { Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.smarty` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `smarty` as the +name of the extension. See the [Library Reference](../reference.md#extensions) for information about configuring extensions. diff --git a/docs/extensions/tables.md b/docs/extensions/tables.md index 2bea470..59693f0 100644 --- a/docs/extensions/tables.md +++ b/docs/extensions/tables.md @@ -52,7 +52,7 @@ will be rendered as: Usage ----- -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.tables` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `tables` as the +name of the extension. This extension does not accept any special configuration options. diff --git a/docs/extensions/toc.md b/docs/extensions/toc.md index a1c583f..e358132 100644 --- a/docs/extensions/toc.md +++ b/docs/extensions/toc.md @@ -65,7 +65,7 @@ This allows one to insert the Table of Contents elsewhere in their page template. For example: ```pycon ->>> md = markdown.Markdown(extensions=['markdown.extensions.toc']) +>>> md = markdown.Markdown(extensions=['toc']) >>> html = md.convert(text) >>> page = render_some_template(context={'body': html, 'toc': md.toc}) ``` @@ -73,8 +73,8 @@ template. For example: Usage ----- -See [Extensions](index.md) for general extension usage, specify `markdown.extensions.toc` -as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `toc` as the name +of the extension. See the [Library Reference](../reference.md#extensions) for information about configuring extensions. diff --git a/docs/extensions/wikilinks.md b/docs/extensions/wikilinks.md index a46265a..dd82a96 100644 --- a/docs/extensions/wikilinks.md +++ b/docs/extensions/wikilinks.md @@ -46,8 +46,8 @@ becomes ## Usage -See [Extensions](index.md) for general extension usage, specify -`markdown.extensions.wikilinks` as the name of the extension. +See [Extensions](index.md) for general extension usage. Use `wikilinks` as the +name of the extension. See the [Library Reference](../reference.md#extensions) for information about configuring extensions. diff --git a/docs/reference.md b/docs/reference.md index 55fb501..1d73439 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -67,7 +67,7 @@ __extensions__{: #extensions } of extension names. :::python - extensions=[MyExtension(), 'path.to.my.ext'] + extensions=[MyExtClass(), 'myext', 'path.to.my.ext:MyExtClass'] !!! note The preferred method is to pass in an instance of an extension. Strings @@ -81,37 +81,48 @@ __extensions__{: #extensions } :::python from markdown.extensions import Extension - class MyExtension(Extension): + class MyExtClass(Extension): # define your extension here... - markdown.markdown(text, extensions=[MyExtension(option='value')]) + markdown.markdown(text, extensions=[MyExtClass(option='value')]) - If an extension name is provided as a string, the extension must be - importable as a python module on your PYTHONPATH. Python's dot notation is - required. Therefore, to import the 'extra' extension, one would do - `extensions=['markdown.extensions.extra']` + If an extension name is provided as a string, the string must either be the + registered entry point of any installed extension or the importable path + using Python's dot notation. - Additionally, a Class may be specified in the name. The class must be at the - end of the name and be separated by a colon from the module. + See the documentation specific to an extension for the string name assigned + to an extension as an entry point. Simply include the defined name as + a string in the list of extensions. For example, if an extension has the + name `myext` assigned to it and the extension is properly installed, then + do the following: + + :::python + markdown.markdown(text, extensions=['myext']) + + If an extension does not have a registered entry point, Python's dot + notation may be used instead. The extension must be installed as a + Python module on your PYTHONPATH. Generally, a class should be specified in + the name. The class must be at the end of the name and be separated by a + colon from the module. Therefore, if you were to import the class like this: :::python - from path.to.module import SomeExtensionClass + from path.to.module import MyExtClass - Then the named extension would comprise this string: + Then load the extension as follows: :::python - "path.to.module:SomeExtensionClass" + markdown.markdown(text, extensions=['path.to.module:MyExtClass']) - !!! note - You should only need to specify the class name if more than one extension - is defined within the same module. The extensions that come with - Python-Markdown do *not* need to have the class name specified. However, - doing so will not effect the behavior of the parser. + If only one extension is defined within a module and the module includes a + `makeExtension` function which returns an instance of the extension, then + the class name is not necessary. For example, in that case one could do + `extensions=['path.to.module']`. Check the documentation for a specific + extension to determine if it supports this feature. - When loading an extension by name (as a string), you may pass in - configuration settings to the extension using the + When loading an extension by name (as a string), you can only pass in + configuration settings to the extension by using the [`extension_configs`](#extension_configs) keyword. !!! seealso "See Also" @@ -144,6 +155,14 @@ __extension_configs__{: #extension_configs } } } + When specifying the extension name, be sure to use the exact same + string as is used in the [extensions](#extensions) keyword to load the + extension. Otherwise, the configuration settings will not be applied to + the extension. In other words, you cannot use the entry point in on + place and Python dot notation in the other. While both may be valid for + a given extension, they will not be recognized as being the same + extension by Markdown. + See the documentation specific to the extension you are using for help in specifying configuration settings for that extension. diff --git a/markdown/core.py b/markdown/core.py index 7d9d839..ce5c333 100644 --- a/markdown/core.py +++ b/markdown/core.py @@ -2,9 +2,9 @@ from __future__ import absolute_import from __future__ import unicode_literals import codecs import sys -import warnings import logging import importlib +import pkg_resources from . import util from .preprocessors import build_preprocessors from .blockprocessors import build_block_parser @@ -49,10 +49,11 @@ class Markdown(object): Keyword arguments: * extensions: A list of extensions. - If they are of type string, the module mdx_name.py will be loaded. - If they are a subclass of markdown.Extension, they will be used - as-is. - * extension_configs: Configuration settingis for extensions. + If an item is an instance of a subclass of `markdown.extension.Extension`, the instance will be used + as-is. If an item is of type string, first an entry point will be loaded. If that fails, the string is + assumed to use Python dot notation (`path.to.module:ClassName`) to load a markdown.Extension subclass. If + no class is specified, then a `makeExtension` function is called within the specified module. + * extension_configs: Configuration settings for extensions. * output_format: Format of output. Supported formats are: * "xhtml1": Outputs XHTML 1.x. Default. * "xhtml5": Outputs XHTML style tags of HTML 5 @@ -108,8 +109,8 @@ class Markdown(object): Keyword arguments: * extensions: A list of extensions, which can either - be strings or objects. See the docstring on Markdown. - * configs: A dictionary mapping module names to config options. + be strings or objects. + * configs: A dictionary mapping extension names to config options. """ for ext in extensions: @@ -130,72 +131,37 @@ class Markdown(object): def build_extension(self, ext_name, configs): """ - Build extension by name, then return the module. + Build extension from a string name, then return an instance. - """ + First attempt to load an entry point. The string name must be registered as an entry point in the + `markdown.extensions` group which points to a subclass of the `markdown.extensions.Extension` class. If + multiple distributions have registered the same name, the first one found by `pkg_resources.iter_entry_points` + is returned. + If no entry point is found, assume dot notation (`path.to.module:ClassName`). Load the specified class and + return an instance. If no class is specified, import the module and call a `makeExtension` function and return + the Extension instance returned by that function. + """ configs = dict(configs) + entry_points = [ep for ep in pkg_resources.iter_entry_points('markdown.extensions', ext_name)] + if entry_points: + ext = entry_points[0].load() + return ext(**configs) + # Get class name (if provided): `path.to.module:ClassName` ext_name, class_name = ext_name.split(':', 1) \ if ':' in ext_name else (ext_name, '') - # Try loading the extension first from one place, then another try: - # Assume string uses dot syntax (`path.to.some.module`) module = importlib.import_module(ext_name) logger.debug( 'Successfuly imported extension module "%s".' % ext_name ) - # For backward compat (until deprecation) - # check that this is an extension. - if ('.' not in ext_name and not (hasattr(module, 'makeExtension') or - (class_name and hasattr(module, class_name)))): - # We have a name conflict - # eg: extensions=['tables'] and PyTables is installed - raise ImportError - except ImportError: - # Preppend `markdown.extensions.` to name - module_name = '.'.join(['markdown.extensions', ext_name]) - try: - module = importlib.import_module(module_name) - logger.debug( - 'Successfuly imported extension module "%s".' % - module_name - ) - warnings.warn('Using short names for Markdown\'s builtin ' - 'extensions is deprecated. Use the ' - 'full path to the extension with Python\'s dot ' - 'notation (eg: "%s" instead of "%s"). The ' - 'current behavior will raise an error in version ' - '2.7. See the Release Notes for ' - 'Python-Markdown version 2.6 for more info.' % - (module_name, ext_name), - DeprecationWarning) - except ImportError: - # Preppend `mdx_` to name - module_name_old_style = '_'.join(['mdx', ext_name]) - try: - module = importlib.import_module(module_name_old_style) - logger.debug( - 'Successfuly imported extension module "%s".' % - module_name_old_style) - warnings.warn('Markdown\'s behavior of prepending "mdx_" ' - 'to an extension name is deprecated. ' - 'Use the full path to the ' - 'extension with Python\'s dot notation ' - '(eg: "%s" instead of "%s"). The current ' - 'behavior will raise an error in version 2.7. ' - 'See the Release Notes for Python-Markdown ' - 'version 2.6 for more info.' % - (module_name_old_style, ext_name), - DeprecationWarning) - except ImportError as e: - message = "Failed loading extension '%s' from '%s', '%s' " \ - "or '%s'" % (ext_name, ext_name, module_name, - module_name_old_style) - e.args = (message,) + e.args[1:] - raise + except ImportError as e: + message = 'Failed loading extension "%s".' % ext_name + e.args = (message,) + e.args[1:] + raise if class_name: # Load given class name from module. diff --git a/markdown/extensions/extra.py b/markdown/extensions/extra.py index f59e09e..cea18ed 100644 --- a/markdown/extensions/extra.py +++ b/markdown/extensions/extra.py @@ -37,13 +37,13 @@ from .. import util import re extensions = [ - 'markdown.extensions.smart_strong', - 'markdown.extensions.fenced_code', - 'markdown.extensions.footnotes', - 'markdown.extensions.attr_list', - 'markdown.extensions.def_list', - 'markdown.extensions.tables', - 'markdown.extensions.abbr' + 'smart_strong', + 'fenced_code', + 'footnotes', + 'attr_list', + 'def_list', + 'tables', + 'abbr' ] diff --git a/setup.py b/setup.py index 6f0a1e7..73c7ab3 100755 --- a/setup.py +++ b/setup.py @@ -59,6 +59,25 @@ setup( entry_points={ 'console_scripts': [ '%s = markdown.__main__:run' % SCRIPT_NAME, + ], + # Register the built in extensions + 'markdown.extensions': [ + 'abbr = markdown.extensions.abbr:AbbrExtension', + 'admonition = markdown.extensions.admonition:AdmonitionExtension', + 'attr_list = markdown.extensions.attr_list:AttrListExtension', + 'codehilite = markdown.extensions.codehilite:CodeHiliteExtension', + 'def_list = markdown.extensions.def_list:DefListExtension', + 'extra = markdown.extensions.extra:ExtraExtension', + 'fenced_code = markdown.extensions.fenced_code:FencedCodeExtension', + 'footnotes = markdown.extensions.footnotes:FootnoteExtension', + 'meta = markdown.extensions.meta:MetaExtension', + 'nl2br = markdown.extensions.nl2br:Nl2BrExtension', + 'sane_lists = markdown.extensions.sane_lists:SaneListExtension', + 'smart_strong = markdown.extensions.smart_strong:SmartEmphasisExtension', + 'smarty = markdown.extensions.smarty:SmartyExtension', + 'tables = markdown.extensions.tables:TableExtension', + 'toc = markdown.extensions.toc:TocExtension', + 'wikilinks = markdown.extensions.wikilinks:WikiLinkExtension', ] }, classifiers=[ diff --git a/tests/test_apis.py b/tests/test_apis.py index 6a1829b..42e7496 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -11,7 +11,6 @@ from __future__ import unicode_literals import unittest import sys import os -import types import markdown import warnings from markdown.__main__ import parse_options @@ -46,11 +45,15 @@ class TestMarkdownBasics(unittest.TestCase): from markdown.extensions.footnotes import FootnoteExtension markdown.Markdown(extensions=[FootnoteExtension()]) - def testNamedExtension(self): + def testEntryPointExtension(self): + """ Test Extension loading with an entry point. """ + markdown.Markdown(extensions=['footnotes']) + + def testDotNotationExtension(self): """ Test Extension loading with Name (`path.to.module`). """ markdown.Markdown(extensions=['markdown.extensions.footnotes']) - def TestNamedExtensionWithClass(self): + def TestDotNotationExtensionWithClass(self): """ Test Extension loading with class name (`path.to.module:Class`). """ markdown.Markdown(extensions=['markdown.extensions.footnotes:FootnoteExtension']) @@ -343,46 +346,6 @@ class TestErrors(unittest.TestCase): markdown.Markdown, extensions=[markdown.extensions.Extension()] ) - def testMdxExtention(self): - """ Test that prepending mdx_ raises a DeprecationWarning. """ - _create_fake_extension(name='fake', use_old_style=True) - self.assertRaises( - DeprecationWarning, - markdown.Markdown, extensions=['fake'] - ) - - def testShortNameExtention(self): - """ Test that using a short name raises a DeprecationWarning. """ - self.assertRaises( - DeprecationWarning, - markdown.Markdown, extensions=['footnotes'] - ) - - -def _create_fake_extension(name, has_factory_func=True, is_wrong_type=False, use_old_style=False): - """ Create a fake extension module for testing. """ - if use_old_style: - mod_name = '_'.join(['mdx', name]) - else: - mod_name = name - if not PY3: - # mod_name must be bytes in Python 2.x - mod_name = bytes(mod_name) - ext_mod = types.ModuleType(mod_name) - - def makeExtension(*args, **kwargs): - if is_wrong_type: - return object - else: - return markdown.extensions.Extension(*args, **kwargs) - - if has_factory_func: - ext_mod.makeExtension = makeExtension - # Warning: this brute forces the extenson module onto the system. Either - # this needs to be specificly overriden or a new python session needs to - # be started to get rid of this. This should be ok in a testing context. - sys.modules[mod_name] = ext_mod - class testETreeComments(unittest.TestCase): """ diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 5dee0fd..5a04e64 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -72,7 +72,7 @@ class TestAbbr(unittest.TestCase): """ Test abbr extension. """ def setUp(self): - self.md = markdown.Markdown(extensions=['markdown.extensions.abbr']) + self.md = markdown.Markdown(extensions=['abbr']) def testSimpleAbbr(self): """ Test Abbreviations. """ @@ -109,7 +109,7 @@ class TestCodeHilite(TestCaseWithAssertStartsWith): def testBasicCodeHilite(self): text = '\t# A Code Comment' - md = markdown.Markdown(extensions=['markdown.extensions.codehilite']) + md = markdown.Markdown(extensions=['codehilite']) if self.has_pygments: # Pygments can use random lexer here as we did not specify the language self.assertStartsWith('', md.convert(text)) @@ -205,7 +205,7 @@ class TestCodeHilite(TestCaseWithAssertStartsWith): text1 = "\t:::Python hl_lines='1'\n\t#line 1\n\t#line 2\n\t#line 3" for text in (text0, text1): - md = markdown.Markdown(extensions=['markdown.extensions.codehilite']) + md = markdown.Markdown(extensions=['codehilite']) if self.has_pygments: self.assertStartsWith( '' in md.convert(text)) @@ -392,7 +392,7 @@ class TestMetaData(unittest.TestCase): """ Test MetaData extension. """ def setUp(self): - self.md = markdown.Markdown(extensions=['markdown.extensions.meta']) + self.md = markdown.Markdown(extensions=['meta']) def testBasicMetaData(self): """ Test basic metadata. """ @@ -459,7 +459,7 @@ class TestWikiLinks(unittest.TestCase): """ Test Wikilinks Extension. """ def setUp(self): - self.md = markdown.Markdown(extensions=['markdown.extensions.wikilinks']) + self.md = markdown.Markdown(extensions=['wikilinks']) self.text = "Some text with a [[WikiLink]]." def testBasicWikilinks(self): @@ -500,9 +500,9 @@ class TestWikiLinks(unittest.TestCase): """ Test Complex Settings. """ md = markdown.Markdown( - extensions=['markdown.extensions.wikilinks'], + extensions=['wikilinks'], extension_configs={ - 'markdown.extensions.wikilinks': [ + 'wikilinks': [ ('base_url', 'http://example.com/'), ('end_url', '.html'), ('html_class', '') @@ -524,7 +524,7 @@ wiki_end_url: .html wiki_html_class: Some text with a [[WikiLink]].""" - md = markdown.Markdown(extensions=['markdown.extensions.meta', 'markdown.extensions.wikilinks']) + md = markdown.Markdown(extensions=['meta', 'wikilinks']) self.assertEqual( md.convert(text), '
Some text with a ' @@ -557,7 +557,7 @@ class TestAdmonition(unittest.TestCase): """ Test Admonition Extension. """ def setUp(self): - self.md = markdown.Markdown(extensions=['markdown.extensions.admonition']) + self.md = markdown.Markdown(extensions=['admonition']) def testRE(self): RE = self.md.parser.blockprocessors['admonition'].RE @@ -574,7 +574,7 @@ class TestTOC(TestCaseWithAssertStartsWith): """ Test TOC Extension. """ def setUp(self): - self.md = markdown.Markdown(extensions=['markdown.extensions.toc']) + self.md = markdown.Markdown(extensions=['toc']) def testMarker(self): """ Test TOC with a Marker. """ @@ -818,7 +818,7 @@ class TestTOC(TestCaseWithAssertStartsWith): def testWithAttrList(self): """ Test TOC with attr_list Extension. """ - md = markdown.Markdown(extensions=['markdown.extensions.toc', 'markdown.extensions.attr_list']) + md = markdown.Markdown(extensions=['toc', 'attr_list']) text = '# Header 1\n\n## Header 2 { #foo }' self.assertEqual( md.convert(text), @@ -849,7 +849,7 @@ class TestTOC(TestCaseWithAssertStartsWith): class TestSmarty(unittest.TestCase): def setUp(self): config = { - 'markdown.extensions.smarty': [ + 'smarty': [ ('smart_angled_quotes', True), ('substitutions', { 'ndash': '\u2013', @@ -865,7 +865,7 @@ class TestSmarty(unittest.TestCase): ] } self.md = markdown.Markdown( - extensions=['markdown.extensions.smarty'], + extensions=['smarty'], extension_configs=config ) @@ -885,8 +885,8 @@ class TestFootnotes(unittest.TestCase): def testBacklinkText(self): md = markdown.Markdown( - extensions=['markdown.extensions.footnotes'], - extension_configs={'markdown.extensions.footnotes': {'BACKLINK_TEXT': 'back'}} + extensions=['footnotes'], + extension_configs={'footnotes': {'BACKLINK_TEXT': 'back'}} ) text = 'paragraph[^1]\n\n[^1]: A Footnote' self.assertEqual( diff --git a/tests/test_legacy.py b/tests/test_legacy.py index ddc54bb..7fca02a 100644 --- a/tests/test_legacy.py +++ b/tests/test_legacy.py @@ -123,80 +123,67 @@ class TestExtensions(LegacyTestCase): location = os.path.join(parent_test_dir, 'extensions') exclude = ['codehilite'] - attr_list = Kwargs( - extensions=[ - 'markdown.extensions.attr_list', - 'markdown.extensions.def_list', - 'markdown.extensions.smarty' - ] - ) + attr_list = Kwargs(extensions=['attr_list', 'def_list', 'smarty']) - codehilite = Kwargs(extensions=['markdown.extensions.codehilite']) + codehilite = Kwargs(extensions=['codehilite']) - toc = Kwargs(extensions=['markdown.extensions.toc']) + toc = Kwargs(extensions=['toc']) - toc_invalid = Kwargs(extensions=['markdown.extensions.toc']) + toc_invalid = Kwargs(extensions=['toc']) - toc_out_of_order = Kwargs(extensions=['markdown.extensions.toc']) + toc_out_of_order = Kwargs(extensions=['toc']) toc_nested = Kwargs( - extensions=['markdown.extensions.toc'], - extension_configs={'markdown.extensions.toc': {'permalink': True}} + extensions=['toc'], + extension_configs={'toc': {'permalink': True}} ) toc_nested2 = Kwargs( - extensions=['markdown.extensions.toc'], - extension_configs={'markdown.extensions.toc': {'permalink': "[link]"}} + extensions=['toc'], + extension_configs={'toc': {'permalink': "[link]"}} ) - toc_nested_list = Kwargs(extensions=['markdown.extensions.toc']) + toc_nested_list = Kwargs(extensions=['toc']) - wikilinks = Kwargs(extensions=['markdown.extensions.wikilinks']) + wikilinks = Kwargs(extensions=['wikilinks']) - fenced_code = Kwargs(extensions=['markdown.extensions.fenced_code']) + fenced_code = Kwargs(extensions=['fenced_code']) - github_flavored = Kwargs(extensions=['markdown.extensions.fenced_code']) + github_flavored = Kwargs(extensions=['fenced_code']) - sane_lists = Kwargs(extensions=['markdown.extensions.sane_lists']) + sane_lists = Kwargs(extensions=['sane_lists']) - nl2br_w_attr_list = Kwargs( - extensions=[ - 'markdown.extensions.nl2br', - 'markdown.extensions.attr_list' - ] - ) + nl2br_w_attr_list = Kwargs(extensions=['nl2br', 'attr_list']) - admonition = Kwargs(extensions=['markdown.extensions.admonition']) + admonition = Kwargs(extensions=['admonition']) smarty = Kwargs( - extensions=['markdown.extensions.smarty'], - extension_configs={'markdown.extensions.smarty': {'smart_angled_quotes': True}} + extensions=['smarty'], + extension_configs={'smarty': {'smart_angled_quotes': True}} ) class TestExtensionsExtra(LegacyTestCase): location = os.path.join(parent_test_dir, 'extensions/extra') - default_kwargs = Kwargs(extensions=['markdown.extensions.extra']) + default_kwargs = Kwargs(extensions=['extra']) - loose_def_list = Kwargs(extensions=['markdown.extensions.def_list']) + loose_def_list = Kwargs(extensions=['def_list']) - simple_def_lists = Kwargs(extensions=['markdown.extensions.def_list']) + simple_def_lists = Kwargs(extensions=['def_list']) - abbr = Kwargs(extensions=['markdown.extensions.abbr']) + abbr = Kwargs(extensions=['abbr']) - footnotes = Kwargs(extensions=['markdown.extensions.footnotes']) + footnotes = Kwargs(extensions=['footnotes']) - tables = Kwargs(extensions=['markdown.extensions.tables']) + tables = Kwargs(extensions=['tables']) - tables_and_attr_list = Kwargs( - extensions=['markdown.extensions.tables', 'markdown.extensions.attr_list'] - ) + tables_and_attr_list = Kwargs(extensions=['tables', 'attr_list']) extra_config = Kwargs( - extensions=['markdown.extensions.extra'], + extensions=['extra'], extension_configs={ - 'markdown.extensions.extra': { - 'markdown.extensions.footnotes': { + 'extra': { + 'footnotes': { 'PLACE_MARKER': '~~~placemarker~~~' } } -- cgit v1.2.3