Source code for mdit_py_plugins.tasklists

"""Builds task/todo lists out of markdown lists with items starting with [ ] or [x]"""

# Ported by Wolmar Nyberg Åkerström from https://github.com/revin/markdown-it-task-lists
# ISC License
# Copyright (c) 2016, Revin Guillen
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from __future__ import annotations

import re
from uuid import uuid4

from markdown_it import MarkdownIt
from markdown_it.rules_core import StateCore
from markdown_it.token import Token

# Regex string to match a whitespace character, as specified in
# https://github.github.com/gfm/#whitespace-character
# (spec version 0.29-gfm (2019-04-06))
_GFM_WHITESPACE_RE = r"[ \t\n\v\f\r]"


[docs]def tasklists_plugin( md: MarkdownIt, enabled: bool = False, label: bool = False, label_after: bool = False, ) -> None: """Plugin for building task/todo lists out of markdown lists with items starting with [ ] or [x] .. Nothing else For example:: - [ ] An item that needs doing - [x] An item that is complete The rendered HTML checkboxes are disabled; to change this, pass a truthy value into the enabled property of the plugin options. :param enabled: True enables the rendered checkboxes :param label: True wraps the rendered list items in a <label> element for UX purposes, :param label_after: True adds the <label> element after the checkbox. """ disable_checkboxes = not enabled use_label_wrapper = label use_label_after = label_after def fcn(state: StateCore) -> None: tokens = state.tokens for i in range(2, len(tokens) - 1): if is_todo_item(tokens, i): todoify(tokens[i]) tokens[i - 2].attrSet( "class", "task-list-item" + (" enabled" if not disable_checkboxes else ""), ) tokens[parent_token(tokens, i - 2)].attrSet( "class", "contains-task-list" ) md.core.ruler.after("inline", "github-tasklists", fcn) def parent_token(tokens: list[Token], index: int) -> int: target_level = tokens[index].level - 1 for i in range(1, index + 1): if tokens[index - i].level == target_level: return index - i return -1 def is_todo_item(tokens: list[Token], index: int) -> bool: return ( is_inline(tokens[index]) and is_paragraph(tokens[index - 1]) and is_list_item(tokens[index - 2]) and starts_with_todo_markdown(tokens[index]) ) def todoify(token: Token) -> None: assert token.children is not None token.children.insert(0, make_checkbox(token)) token.children[1].content = token.children[1].content[3:] token.content = token.content[3:] if use_label_wrapper: if use_label_after: token.children.pop() # Replaced number generator from original plugin with uuid. checklist_id = f"task-item-{uuid4()}" token.children[0].content = ( token.children[0].content[0:-1] + f' id="{checklist_id}">' ) token.children.append(after_label(token.content, checklist_id)) else: token.children.insert(0, begin_label()) token.children.append(end_label()) def make_checkbox(token: Token) -> Token: checkbox = Token("html_inline", "", 0) disabled_attr = 'disabled="disabled"' if disable_checkboxes else "" if token.content.startswith("[ ] "): checkbox.content = ( '<input class="task-list-item-checkbox" ' f'{disabled_attr} type="checkbox">' ) elif token.content.startswith("[x] ") or token.content.startswith("[X] "): checkbox.content = ( '<input class="task-list-item-checkbox" checked="checked" ' f'{disabled_attr} type="checkbox">' ) return checkbox def begin_label() -> Token: token = Token("html_inline", "", 0) token.content = "<label>" return token def end_label() -> Token: token = Token("html_inline", "", 0) token.content = "</label>" return token def after_label(content: str, checkbox_id: str) -> Token: token = Token("html_inline", "", 0) token.content = ( f'<label class="task-list-item-label" for="{checkbox_id}">{content}</label>' ) token.attrs = {"for": checkbox_id} return token def is_inline(token: Token) -> bool: return token.type == "inline" def is_paragraph(token: Token) -> bool: return token.type == "paragraph_open" def is_list_item(token: Token) -> bool: return token.type == "list_item_open" def starts_with_todo_markdown(token: Token) -> bool: # leading whitespace in a list item is already trimmed off by markdown-it return re.match(rf"\[[ xX]]{_GFM_WHITESPACE_RE}+", token.content) is not None