diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 38facc551..131847cc6 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -43,6 +43,7 @@ Fixed * Use ``simple_tag`` for assignment tag :url-issue:`791` (Raffaele Salmaso) * Direct invocation of ``pytest`` fixed (removing ``runtests.py``) :url-issue:`781` (Branko Majic) * Line breaks in help texts for macros :url-issue:`851` (Mathias Dannesbo) + * Table of contents now has a header by default, and several built-in django-wiki extensions can be configured using ``WIKI_MARKDOWN_KWARGS`` :url-issue:`881` (Mathias Rav) Deprecated/Removed ~~~~~~~~~~~~~~~~~~ diff --git a/src/wiki/conf/settings.py b/src/wiki/conf/settings.py index 69c6a97af..228816c25 100644 --- a/src/wiki/conf/settings.py +++ b/src/wiki/conf/settings.py @@ -26,13 +26,28 @@ 'WIKI_MARKDOWN_SANITIZE_HTML', True) -#: Arguments for the Markdown instance, for instance a list of extensions to -#: use. -#: See: https://pythonhosted.org/Markdown/extensions/index.html +#: Arguments for the Markdown instance, as a dictionary. The "extensions" key +#: should be a list of extra extensions to use besides the built-in django-wiki +#: extensions, and the "extension_configs" should be a dictionary, specifying +#: the keyword-arguments to pass to each extension. #: -#: To set a custom title for TOC's:: +#: For a list of extensions officially supported by Python-Markdown, see: +#: https://python-markdown.github.io/extensions/ #: -#: WIKI_MARKDOWN_KWARGS = {'extension_configs': {'toc': _('Contents of this article')}} +#: To set a custom title for table of contents, specify the following in your +#: Django project settings:: +#: +#: WIKI_MARKDOWN_KWARGS = { +#: 'extension_configs': { +#: 'wiki.plugins.macros.mdx.toc': {'title': 'Contents of this article'}, +#: }, +#: } +#: +#: Besides the extensions enabled by the "extensions" key, the following +#: built-in django-wiki extensions can be configured with "extension_configs": +#: "wiki.core.markdown.mdx.codehilite", "wiki.core.markdown.mdx.previewlinks", +#: "wiki.core.markdown.mdx.responsivetable", "wiki.plugins.macros.mdx.macro", +#: "wiki.plugins.macros.mdx.toc", "wiki.plugins.macros.mdx.wikilinks". MARKDOWN_KWARGS = { 'extensions': [ 'markdown.extensions.footnotes', @@ -46,8 +61,8 @@ 'markdown.extensions.sane_lists', ], 'extension_configs': { - 'toc': { - 'title': _('Table of Contents')}}, + 'wiki.plugins.macros.mdx.toc': {'title': _('Contents')}, + }, } MARKDOWN_KWARGS.update(getattr(django_settings, 'WIKI_MARKDOWN_KWARGS', {})) diff --git a/src/wiki/core/markdown/__init__.py b/src/wiki/core/markdown/__init__.py index 32c7fca3a..a6201382f 100644 --- a/src/wiki/core/markdown/__init__.py +++ b/src/wiki/core/markdown/__init__.py @@ -1,9 +1,6 @@ import bleach import markdown from wiki.conf import settings -from wiki.core.markdown.mdx.codehilite import WikiCodeHiliteExtension -from wiki.core.markdown.mdx.previewlinks import PreviewLinksExtension -from wiki.core.markdown.mdx.responsivetable import ResponsiveTableExtension from wiki.core.plugins import registry as plugin_registry @@ -20,9 +17,9 @@ def __init__(self, article, preview=False, user=None, *args, **kwargs): def core_extensions(self): """List of core extensions found in the mdx folder""" return [ - PreviewLinksExtension(), - ResponsiveTableExtension(), - WikiCodeHiliteExtension(), + 'wiki.core.markdown.mdx.codehilite', + 'wiki.core.markdown.mdx.previewlinks', + 'wiki.core.markdown.mdx.responsivetable', ] def get_markdown_extensions(self): diff --git a/src/wiki/core/markdown/mdx/codehilite.py b/src/wiki/core/markdown/mdx/codehilite.py index 85c5f81ba..b8c2ef6f0 100644 --- a/src/wiki/core/markdown/mdx/codehilite.py +++ b/src/wiki/core/markdown/mdx/codehilite.py @@ -117,3 +117,8 @@ def extendMarkdown(self, md, md_globals): ">normalize_whitespace") md.registerExtension(self) + + +def makeExtension(*args, **kwargs): + """Return an instance of the extension.""" + return WikiCodeHiliteExtension(*args, **kwargs) diff --git a/src/wiki/core/markdown/mdx/previewlinks.py b/src/wiki/core/markdown/mdx/previewlinks.py index 242bf8d50..f679af3da 100644 --- a/src/wiki/core/markdown/mdx/previewlinks.py +++ b/src/wiki/core/markdown/mdx/previewlinks.py @@ -19,3 +19,8 @@ def run(self, root): if not a.get('href').startswith('#'): a.set('target', '_blank') return root + + +def makeExtension(*args, **kwargs): + """Return an instance of the extension.""" + return PreviewLinksExtension(*args, **kwargs) diff --git a/src/wiki/core/markdown/mdx/responsivetable.py b/src/wiki/core/markdown/mdx/responsivetable.py index 25d68be09..d0e0e03a6 100644 --- a/src/wiki/core/markdown/mdx/responsivetable.py +++ b/src/wiki/core/markdown/mdx/responsivetable.py @@ -37,3 +37,8 @@ def move_children(self, element1, element2): def convert_to_wrapper(self, element): element.tag = 'div' element.set('class', 'table-responsive') + + +def makeExtension(*args, **kwargs): + """Return an instance of the extension.""" + return ResponsiveTableExtension(*args, **kwargs) diff --git a/src/wiki/plugins/macros/mdx/macro.py b/src/wiki/plugins/macros/mdx/macro.py index 4030d4e58..e5adb630f 100644 --- a/src/wiki/plugins/macros/mdx/macro.py +++ b/src/wiki/plugins/macros/mdx/macro.py @@ -99,3 +99,8 @@ def wikilink(self): 'Insert a link to another wiki page with a short notation.'), example_code='[[WikiLink]]', args={}) + + +def makeExtension(*args, **kwargs): + """Return an instance of the extension.""" + return MacroExtension(*args, **kwargs) diff --git a/src/wiki/plugins/macros/mdx/toc.py b/src/wiki/plugins/macros/mdx/toc.py index b36efdb13..5a4e9c5b0 100644 --- a/src/wiki/plugins/macros/mdx/toc.py +++ b/src/wiki/plugins/macros/mdx/toc.py @@ -1,30 +1,6 @@ -""" -Table of Contents Extension for Python-Markdown -* * * - -(c) 2008 [Jack Miller](http://codezen.org) - -Dependencies: -* [Markdown 2.1+](http://packages.python.org/Markdown/) - -Pull request to include the below code in Python-Markdown: -https://github.com/waylan/Python-Markdown/pull/191 - -Until it's released, we have a copy here. - -/benjaoming - - -UPDATE PR WAS MERGED FOR MARKDOWN 2.3 - -SO WE AN JUST DEPEND ON THAT! - - -""" import re -import unicodedata -import markdown +from markdown.extensions.toc import TocTreeprocessor, TocExtension, slugify from markdown.util import etree from wiki.plugins.macros import settings @@ -33,279 +9,31 @@ IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$') -def slugify(value, separator): - """ Slugify a string, to make it URL friendly. """ - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') - value = re.sub('[^\w\s-]', '', value.decode('ascii')).strip().lower() - return re.sub('[%s\s]+' % separator, separator, value) - - -def itertext(elem): - """ Loop through all children and return text only. - - Reimplements method of same name added to ElementTree in Python 2.7 - - """ - if elem.text: - yield elem.text - for e in elem: - yield from itertext(e) - if e.tail: - yield e.tail - - -def unique(elem_id, ids): - """ Ensure id is unique in set of ids. Append '_1', '_2'... if not """ - while elem_id in ids: - m = IDCOUNT_RE.match(elem_id) - if m: - elem_id = '%s_%d' % (m.group(1), int(m.group(2)) + 1) - else: - elem_id = '%s_%d' % (elem_id, 1) - ids.add(elem_id) - return HEADER_ID_PREFIX + elem_id - - -def order_toc_list(toc_list): - """Given an unsorted list with errors and skips, return a nested one. - [{'level': 1}, {'level': 2}] - => - [{'level': 1, 'children': [{'level': 2, 'children': []}]}] - - A wrong list is also converted: - [{'level': 2}, {'level': 1}] - => - [{'level': 2, 'children': []}, {'level': 1, 'children': []}] - """ - - def build_correct(remaining_list, prev_elements=[{'level': 1000}]): - - if not remaining_list: - return [], [] - - current = remaining_list.pop(0) - if 'children' not in current: - current['children'] = [] - - if not prev_elements: - # This happens for instance with [8, 1, 1], ie. when some - # header level is outside a scope. We treat it as a - # top-level - next_elements, children = build_correct(remaining_list, [current]) - current['children'].append(children) - return [current] + next_elements, [] - - prev_element = prev_elements.pop() - children = [] - next_elements = [] - # Is current part of the child list or next list? - if current['level'] > prev_element['level']: - # print "%d is a child of %d" % (current['level'], - # prev_element['level']) - prev_elements.append(prev_element) - prev_elements.append(current) - prev_element['children'].append(current) - next_elements2, children2 = build_correct( - remaining_list, prev_elements) - children += children2 - next_elements += next_elements2 - else: - # print "%d is ancestor of %d" % (current['level'], - # prev_element['level']) - if not prev_elements: - # print "No previous elements, so appending to the next set" - next_elements.append(current) - prev_elements = [current] - next_elements2, children2 = build_correct( - remaining_list, prev_elements) - current['children'].extend(children2) - else: - # print "Previous elements, comparing to those first" - remaining_list.insert(0, current) - next_elements2, children2 = build_correct( - remaining_list, prev_elements) - children.extend(children2) - next_elements += next_elements2 - - return next_elements, children - - flattened_list, __ = build_correct(toc_list) - return flattened_list - - -class TocTreeprocessor(markdown.treeprocessors.Treeprocessor): - - # Iterator wrapper to get parent and child all at once - def iterparent(self, root): - for parent in root.getiterator(): - for child in parent: - yield parent, child - - def add_anchor(self, c, elem_id): # @ReservedAssignment - if self.use_anchors: - anchor = etree.Element("a") - anchor.text = c.text - anchor.attrib["href"] = "#" + elem_id - anchor.attrib["class"] = "toclink" - c.text = "" - for elem in c.getchildren(): - anchor.append(elem) - c.remove(elem) - c.append(anchor) - - def build_toc_etree(self, div, toc_list): - - def build_etree_ul(toc_list, parent): - ul = etree.SubElement(parent, "ul") - for item in toc_list: - # List item link, to be inserted into the toc div - li = etree.SubElement(ul, "li") - link = etree.SubElement(li, "a") - link.text = item.get('name', '') - link.attrib["href"] = '#' + item.get('id', '') - if item['children']: - build_etree_ul(item['children'], li) - return ul - - return build_etree_ul(toc_list, div) - - def run(self, doc): # noqa - - div = etree.Element("div") - div.attrib["class"] = "toc" - header_rgx = re.compile("[Hh][123456]") - - self.use_anchors = self.config["anchorlink"] in [ - 1, - '1', - True, - 'True', - 'true'] - - # Get a list of id attributes - used_ids = set() - for c in doc.getiterator(): - if "id" in c.attrib: - used_ids.add(c.attrib["id"]) - - toc_list = [] - marker_found = False - for (p, c) in self.iterparent(doc): - text = ''.join(itertext(c)).strip() - if not text: - continue - - # To keep the output from screwing up the - # validation by putting a
inside of a

- # we actually replace the

in its entirety. - # We do not allow the marker inside a header as that - # would causes an enless loop of placing a new TOC - # inside previously generated TOC. - if c.text and c.text.strip() == self.config["marker"] and \ - not header_rgx.match(c.tag) and c.tag not in ['pre', 'code']: - for i in range(len(p)): - if p[i] == c: - p[i] = div - break - marker_found = True - - if header_rgx.match(c.tag): - - # Do not override pre-existing ids - if "id" not in c.attrib: - elem_id = unique( - self.config["slugify"]( - text, - '-'), - used_ids) - c.attrib["id"] = elem_id - else: - elem_id = c.attrib["id"] - - tag_level = int(c.tag[-1]) - - toc_list.append({ - 'level': tag_level, - 'id': elem_id, - 'name': c.text - }) - - self.add_anchor(c, elem_id) - - if marker_found: - toc_list_nested = order_toc_list(toc_list) - self.build_toc_etree(div, toc_list_nested) - # serialize and attach to markdown instance. - prettify = self.markdown.treeprocessors.get('prettify') - if prettify: - prettify.run(div) - toc = self.markdown.serializer(div) - for pp in self.markdown.postprocessors.values(): - toc = pp.run(toc) - self.markdown.toc = toc - - -class TocExtension(markdown.Extension): - - TreeProcessorClass = TocTreeprocessor - - def __init__(self, configs=[]): - self.config = { - "marker": [ - "[TOC]", "Text to find and replace with Table of Contents -" - "Defaults to \"[TOC]\""], "slugify": [ - slugify, "Function to generate anchors based on header text-" - "Defaults to the headerid ext's slugify function."], "title": [ - None, "Title to insert into TOC

- " - "Defaults to None"], "anchorlink": [ - 0, "1 if header should be a self link" - "Defaults to 0"]} - - for key, value in configs: - self.setConfig(key, value) - - def extendMarkdown(self, md, md_globals): - tocext = self.TreeProcessorClass(md) - tocext.config = self.getConfigs() - # Headerid ext is set to '>inline'. With this set to 'attr_list") - - -def makeExtension(configs={}): - return TocExtension(configs=configs) +def wiki_slugify(*args, **kwargs): + return HEADER_ID_PREFIX + slugify(*args, **kwargs) class WikiTreeProcessorClass(TocTreeprocessor): - def build_toc_etree(self, div, toc_list): - # Add title to the div - if self.config["title"]: - header = etree.SubElement(div, "span") - header.attrib["class"] = "toctitle" - header.text = self.config["title"] - - def build_etree_ul(toc_list, parent): - ul = etree.SubElement(parent, "ul") - for item in toc_list: - # List item link, to be inserted into the toc div - li = etree.SubElement(ul, "li") - link = etree.SubElement(li, "a") - link.text = item.get('name', '') - link.attrib["href"] = '#' + item.get('id', '') - if item['children']: - build_etree_ul(item['children'], li) - return ul - - return build_etree_ul(toc_list, div) + def run(self, doc): + # Necessary because self.title is set to a LazyObject via gettext_lazy + if self.title: + self.title = str(self.title) + super().run(doc) class WikiTocExtension(TocExtension): TreeProcessorClass = WikiTreeProcessorClass + def __init__(self, **kwargs): + kwargs.setdefault('slugify', wiki_slugify) + super().__init__(**kwargs) + def extendMarkdown(self, md, md_globals): if 'toc' in settings.METHODS: TocExtension.extendMarkdown(self, md, md_globals) + + +def makeExtension(*args, **kwargs): + """Return an instance of the extension.""" + return WikiTocExtension(*args, **kwargs) diff --git a/src/wiki/plugins/macros/mdx/wikilinks.py b/src/wiki/plugins/macros/mdx/wikilinks.py index 4379fe1a7..6ca051567 100644 --- a/src/wiki/plugins/macros/mdx/wikilinks.py +++ b/src/wiki/plugins/macros/mdx/wikilinks.py @@ -5,7 +5,7 @@ import markdown from django.urls import reverse -from markdown.extensions import wikilinks +from markdown.extensions import Extension, wikilinks def build_url(label, base, end, md): @@ -22,9 +22,9 @@ def build_url(label, base, end, md): return '%s%s%s' % (base, clean_label, end) -class WikiLinkExtension(wikilinks.WikiLinkExtension): +class WikiLinkExtension(Extension): - def __init__(self, configs={}): + def __init__(self, **kwargs): # set extension defaults self.config = { 'base_url': ['', 'String to append to beginning or URL.'], @@ -32,10 +32,7 @@ def __init__(self, configs={}): 'html_class': ['wiki_wikilink', 'CSS hook. Leave blank for none.'], 'build_url': [build_url, 'Callable formats URL from label.'], } - - # Override defaults with user settings - for key, value in configs: - self.setConfig(key, value) + super().__init__(**kwargs) def extendMarkdown(self, md, md_globals): self.md = md @@ -62,3 +59,8 @@ def handleMatch(self, m): else: a = '' return a + + +def makeExtension(*args, **kwargs): + """Return an instance of the extension.""" + return WikiLinkExtension(*args, **kwargs) diff --git a/src/wiki/plugins/macros/wiki_plugin.py b/src/wiki/plugins/macros/wiki_plugin.py index f34c468c9..8e15d2174 100644 --- a/src/wiki/plugins/macros/wiki_plugin.py +++ b/src/wiki/plugins/macros/wiki_plugin.py @@ -2,9 +2,6 @@ from wiki.core.plugins import registry from wiki.core.plugins.base import BasePlugin from wiki.plugins.macros import settings -from wiki.plugins.macros.mdx.macro import MacroExtension -from wiki.plugins.macros.mdx.toc import WikiTocExtension -from wiki.plugins.macros.mdx.wikilinks import WikiLinkExtension class MacroPlugin(BasePlugin): @@ -18,9 +15,10 @@ class MacroPlugin(BasePlugin): 'get_form_kwargs': (lambda a: {})} markdown_extensions = [ - WikiLinkExtension(), - MacroExtension(), - WikiTocExtension()] + 'wiki.plugins.macros.mdx.macro', + 'wiki.plugins.macros.mdx.toc', + 'wiki.plugins.macros.mdx.wikilinks', + ] registry.register(MacroPlugin)