Source code for gfm.tasklist

# coding: utf-8
# 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>

"""

from __future__ import unicode_literals

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

try:
    _string_type = unicode
except NameError:
    _string_type = str


def _to_list(obj):
    if isinstance(obj, _string_type):
        return [obj]
    if isinstance(obj, tuple):
        return list(obj)
    return 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, *args, **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(TaskListExtension, self).__init__(*args, **kwargs)
[docs] def extendMarkdown(self, md, md_globals): processor = TaskListProcessor(self) md.treeprocessors.add('gfm-tasklist', processor, '_end')