Source code for gfm.tasklist

# Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

"""
:mod:`gfm.tasklist` -- Task list support
========================================

The :mod:`gfm.tasklist` module provides GitHub-like support for task lists.
Those are normal lists with a checkbox-like syntax at the beginning of items
that will be converted to actual checkbox inputs. Nested lists are supported.

Example syntax::

   - [x] milk
   - [ ] eggs
   - [x] chocolate
   - [ ] if possible:
       1. [ ] solve world peace
       2. [ ] solve world hunger

.. NOTE::
   GitHub has support for updating the Markdown source text by toggling the
   checkbox (by clicking on it). This is not supported by this extension, as it
   requires server-side processing that is out of scope of a Markdown parser.

Available configuration options
-------------------------------

================== ============== ======== ===========
Name               Type           Default  Description
================== ============== ======== ===========
``unordered``      bool           ``True`` Set to ``False`` to disable parsing of unordered lists.
``ordered``        bool           ``True`` Set to ``False`` to disable parsing of ordered lists.
``max_depth``      integer        ∞        Set to a positive integer to stop parsing nested task
                                           lists that are deeper than this limit.
``list_attrs``     dict, callable ``{}``   Attributes to be added to the ``<ul>`` or ``<ol>`` element
                                           containing the items.
``item_attrs``     dict, callable ``{}``   Attributes to be added to the ``<li>`` element containing
                                           the checkbox. See `Item attributes`_.
``checkbox_attrs`` dict, callable ``{}``   Attributes to be added to the checkbox element.
                                           See `Checkbox attributes`_.
================== ============== ======== ===========

List attributes
***************

If option ``list_attrs`` is a *dict*, the key-value pairs will be applied to
the ``<ul>`` (resp. ``<ol>``) unordered (resp. ordered) list element, that is
the parent element of the ``<li>`` elements.

.. warning::

   These attributes are applied to all nesting levels of lists, that is,
   to both the root lists and their potential sub-lists, recursively.

   You can control this behavior by using a *callable* instead (see below).

If option ``list_attrs`` is a *callable*, it should be a function that respects
the following prototype::

   def function(list, depth: int) -> dict:

where:

- ``list`` is the ``<ul>`` or ``<ol>`` element;
- ``depth`` is the depth of this list relative to its root list (root lists have
  a depth of 1).

The returned *dict* items will be applied as HTML attributes to the list
element.

.. note::

   Thanks to this feature, you could apply attributes to root lists only.
   Take this code sample::

      import markdown
      from gfm import TaskListExtension

      def list_attr_cb(list, depth):
          if depth == 1:
              return {'class': 'tasklist'}
          return {}

      tl_ext = TaskListExtension(list_attrs=list_attr_cb)

      print(markdown.markdown(\"""
      - [x] some thing
      - [ ] some other
          - [ ] sub thing
          - [ ] sub other
      \""", extensions=[tl_ext]))

   In this example, only the root list will have the ``tasklist`` class
   attribute, not the one containing “sub” items.


Item attributes
***************

If option ``item_attrs`` is a *dict*, the key-value pairs will be applied to
the ``<li>`` element as its HTML attributes.

Example::

   item_attrs={'class': 'list-item'}

will result in::

   <li class="list-item">...</li>

If option ``item_attrs`` is a *callable*, it should be a function that
respects the following prototype::

   def function(parent, element, checkbox) -> dict:

where:

- ``parent`` is the ``<li>`` parent element;
- ``element`` is the ``<li>`` element;
- ``checkbox`` is the generated ``<input type="checkbox">`` element.

The returned *dict* items will be applied as HTML attributes to the ``<li>``
element containing the checkbox.

Checkbox attributes
*******************

If option ``checkbox_attrs`` is a *dict*, the key-value pairs will be applied to
the ``<input type="checkbox">`` element as its HTML attributes.

Example::

   checkbox_attrs={'class': 'list-cb'}

will result in::

   <li><input type="checkbox" class="list-cb"> ...</li>

If option ``checkbox_attrs`` is a *callable*, it should be a function that
respects the following prototype::

   def function(parent, element) -> dict:

where:

- ``parent`` is the ``<li>`` parent element;
- ``element`` is the ``<li>`` element.

The returned *dict* items will be applied as HTML attributes to the checkbox
element.

Typical usage
-------------

.. testcode::

   import markdown
   from gfm import TaskListExtension

   print(markdown.markdown(\"""
   - [x] milk
   - [ ] eggs
   - [x] chocolate
   - not a checkbox
   \""", extensions=[TaskListExtension()]))

.. testoutput::

   <ul>
   <li><input checked="checked" disabled="disabled" type="checkbox" /> milk</li>
   <li><input disabled="disabled" type="checkbox" /> eggs</li>
   <li><input checked="checked" disabled="disabled" type="checkbox" /> chocolate</li>
   <li>not a checkbox</li>
   </ul>

"""

import markdown
from functools import reduce
from markdown.treeprocessors import Treeprocessor
from markdown.util import etree


def _to_list(obj):
    if isinstance(obj, str):
        return [obj]
    return list(obj)


[docs]class TaskListProcessor(Treeprocessor): def __init__(self, ext): super(TaskListProcessor, self).__init__() self.ext = ext
[docs] def run(self, root): ordered = self.ext.getConfig("ordered") unordered = self.ext.getConfig("unordered") if not ordered and not unordered: return root checked_pattern = _to_list(self.ext.getConfig("checked")) unchecked_pattern = _to_list(self.ext.getConfig("unchecked")) if not checked_pattern and not unchecked_pattern: return root prefix_length = reduce( max, (len(e) for e in checked_pattern + unchecked_pattern) ) item_attrs = self.ext.getConfig("item_attrs") list_attrs = self.ext.getConfig("list_attrs") base_cb_attrs = self.ext.getConfig("checkbox_attrs") max_depth = self.ext.getConfig("max_depth") if not max_depth: max_depth = float("inf") lists = set() stack = [(root, None, 0)] while stack: el, parent, depth = stack.pop() if ( parent and el.tag == "li" and (parent.tag == "ul" and unordered or parent.tag == "ol" and ordered) ): depth += 1 text = (el.text or "").lstrip() lower_text = text[:prefix_length].lower() found = False checked = False for p in checked_pattern: if lower_text.startswith(p): found = True checked = True text = text[len(p) :] break else: for p in unchecked_pattern: if lower_text.startswith(p): found = True text = text[len(p) :] break if found: # Add root <ol> or <ul> element to the list set if list_attrs: lists.add((parent, depth)) # Checkbox attributes attrs = {"type": "checkbox", "disabled": "disabled"} if checked: attrs["checked"] = "checked" # Give user a chance to update checkbox attributes attrs.update( base_cb_attrs(parent, el) if callable(base_cb_attrs) else base_cb_attrs ) checkbox = etree.Element("input", attrs) checkbox.tail = text # Prepend checkbox to <li> el.text = "" el.insert(0, checkbox) # Give user a chance to update <li> attributes for k, v in ( item_attrs(parent, el, checkbox) if callable(item_attrs) else item_attrs ).items(): el.set(k, v) if depth < max_depth: for child in el: stack.append((child, el, depth)) for list, depth in lists: for k, v in ( list_attrs(list, depth) if callable(list_attrs) else list_attrs ).items(): list.set(k, v) return root
[docs]class TaskListExtension(markdown.Extension): """ An extension that supports GitHub task lists. Both ordered and unordered lists are supported and can be separately enabled. Nested lists are supported. Example:: - [x] milk - [ ] eggs - [x] chocolate - [ ] if possible: 1. [ ] solve world peace 2. [ ] solve world hunger """ def __init__(self, **kwargs): self.config = { "ordered": [True, "Enable parsing of ordered lists"], "unordered": [True, "Enable parsing of unordered lists"], "checked": ["[x]", "The checked state pattern"], "unchecked": ["[ ]", "The unchecked state pattern"], "max_depth": [0, "Maximum list nesting depth (None for " "unlimited)"], "list_attrs": [ {}, "Additional attribute dict (or callable) to " "add to the <ul> or <ol> element", ], "item_attrs": [ {}, "Additional attribute dict (or callable) to " "add to the <li> element", ], "checkbox_attrs": [ {}, "Additional attribute dict (or callable) " "to add to the checkbox <input> element", ], } super().__init__(**kwargs)
[docs] def extendMarkdown(self, md): md.treeprocessors.register(TaskListProcessor(self), "gfm-tasklist", 100)