Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions snakemd/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,9 @@ class MDList(Block):
(i.e., :code:`- [x]`)
- set to :code:`Iterable[bool]` to render the checked
status of the top-level list elements directly

.. deprecated:: 2.4
Use :class:`snakemd.Checklist` template instead
"""

def __init__(
Expand All @@ -859,7 +862,7 @@ def __init__(
checked if checked is None or isinstance(checked, bool) else list(checked)
)
self._space = ""
if isinstance(self._checked, list) and self._top_level_count() != len(
if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len(
self._checked
):
raise ValueError(
Expand Down Expand Up @@ -896,7 +899,7 @@ def __str__(self) -> str:
i = 1
for item in self._items:
if isinstance(item, MDList):
item._space = self._space + " " * self._get_indent_size(i)
item._space = self._space + " " * self._get_indent_size(self._ordered, i)
output.append(str(item))
else:
# Create the start of the row based on `order` parameter
Expand Down Expand Up @@ -964,34 +967,40 @@ def _process_items(items) -> list[Block]:
processed.append(item)
return processed

def _top_level_count(self) -> int:
@staticmethod
def _top_level_count(items) -> int:
"""
Given that MDList can accept a variety of blocks,
we need to know how many items in the provided list
are top-level elements (i.e., not nested list elements).
We use this number to throw errors if this count does
not match up with the checklist count.

:param items:
a list of items
:return:
a count of top-level elements
"""
count = 0
for item in self._items:
for item in items:
if not isinstance(item, MDList):
count += 1
return count

def _get_indent_size(self, item_index: int = -1) -> int:
@staticmethod
def _get_indent_size(ordered: bool, item_index: int = -1) -> int:
"""
Returns the number of spaces that any sublists should be indented.

:param bool ordered:
the boolean value indicating if a list is ordered
:param int item_index:
the index of the item to check (only used for ordered lists);
defaults to -1
:return:
the number of spaces
"""
if not self._ordered:
if not ordered:
return 2
# Ordered items vary in length, so we adjust the result based on the index
return 2 + len(str(item_index))
Expand Down
139 changes: 136 additions & 3 deletions snakemd/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,148 @@ class Kind(Enum):
WARNING = auto()
CAUTION = auto()

def __init__(self, kind: Kind, message: str | Iterable[str | Inline | Block]) -> None:
def __init__(
self,
kind: Kind,
message: str | Iterable[str | Inline | Block]
) -> None:
super().__init__()
self._kind = kind
self._message = message
self._alert = Quote([f"[!{self._kind.name}]", self._message])

def __str__(self) -> str:
return str(Quote([f"[!{self._kind.name}]", self._message]))
"""
Renders self as a markdown ready string. See
:class:`snakemd.Quote` for more details.

:return:
the Alert as a markdown string
"""
return str(self._alert)

def __repr__(self) -> str:
"""
Renders self as an unambiguous string for development.
See :class:`snakemd.Quote` for more details.

:return:
the Alert as a development string
"""
return repr(self._alert)


class Checklist(Template):
"""
Checklist is an MDList extension to provide support
for Markdown checklists, which are a Markdown
extension. Previously, this feature was baked
directly into MDList. However, because checklists
are not a vanilla Markdown feature, they were
moved here.

.. versionadded:: 2.4
Included for user convenience

:raises ValueError:
when the checked argument is an Iterable[bool] that does not
match the number of top-level elements in the list
:param Iterable[str | Inline | Block] items:
a "list" of objects to be rendered as a list
:param bool | Iterable[bool] checked:
the checked state of the list

- defaults to :code:`False` which renders a series of unchecked
boxes (i.e., :code:`- [ ]`)
- set to :code:`True` to render a series of checked boxes
(i.e., :code:`- [x]`)
- set to :code:`Iterable[bool]` to render the checked
status of the top-level list elements directly
"""

def __init__(
self,
items: Iterable[str | Inline | Block],
checked: bool | Iterable[bool] = False
) -> None:
super().__init__()
self._items: list[Block] = MDList._process_items(items)
self._checked: bool | list[bool] = (
checked if checked is None or isinstance(
checked, bool) else list(checked)
)
self._space = ""
if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len(
self._checked
):
raise ValueError(
"Number of top-level elements in checklist does not "
"match number of booleans supplied by checked parameter: "
f"{self._checked}"
)

def __str__(self):
"""
Renders the checklist as a markdown string. Checklists
function very similarly to unorded lists, but require
additional information about the status of each task
(i.e., whether it is checked or not).

.. code-block:: markdown

- [ ] Do reading
- [X] Do writing

:return:
the list as a markdown string
"""
output = []
i = 1
for item in self._items:
if isinstance(item, (Checklist, MDList)):
item._space = self._space + " " * 2
output.append(str(item))
else:
row = f"{self._space}-"

if isinstance(self._checked, bool):
checked_str = "X" if self._checked else " "
row = f"{row} [{checked_str}] {item}"
else:
checked_str = "X" if self._checked[i - 1] else " "
row = f"{row} [{checked_str}] {item}"

output.append(row)
i += 1

checklist = "\n".join(output)
logger.debug("Rendered checklist: %r", checklist)
return checklist

def __repr__(self) -> str:
return f"Alerts(kind={self._kind!r},message={self._message!r})"
"""
Renders self as an unambiguous string for development.
In this case, it displays in the style of a dataclass,
where instance variables are listed with their
values. Unlike many of the other templates, Checklists
aren't a direct wrapper of MDList, and therefore cannot
be represented as MDList alone.

.. doctest:: checklist

>>> checklist = Checklist(["Do Homework"], True)
>>> repr(checklist)
"Checklist(items=[Paragraph(...)], checked=True)"

:return:
the Checklist object as a development string
"""
return (
f"Checklist("
f"items={self._items!r}, "
f"checked={self._checked!r}"
f")"
)


class CSVTable(Template):
Expand Down
26 changes: 26 additions & 0 deletions tests/templates/test_checklist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from snakemd.elements import MDList
from snakemd.templates import Checklist

def test_checklist_one_item_true():
checklist = Checklist(["Write code"], True)
assert str(checklist) == "- [X] Write code"

def test_checklist_one_item_false():
checklist = Checklist(["Write code"], False)
assert str(checklist) == "- [ ] Write code"

def test_checklist_one_item_explicit():
checklist = Checklist(["Write code"], [False])
assert str(checklist) == "- [ ] Write code"

def test_checklist_many_items_true():
checklist = Checklist(["Write code", "Do Laundry"], True)
assert str(checklist) == "- [X] Write code\n- [X] Do Laundry"

def test_checklist_many_items_nested_true():
checklist = Checklist(["Write code", Checklist(["Implement TODO"], True), "Do Laundry"], True)
assert str(checklist) == "- [X] Write code\n - [X] Implement TODO\n- [X] Do Laundry"

def test_checklist_many_items_nested_mdlist_true():
checklist = Checklist(["Write code", MDList(["Implement TODO"]), "Do Laundry"], True)
assert str(checklist) == "- [X] Write code\n - Implement TODO\n- [X] Do Laundry"