Skip to content

Commit 0569524

Browse files
authored
Added Checklist to Templates (#178)
* Added deprecation text and made change to private method * Started toying with Checklist object * Added an initial iteration of the string method * Got a prototype working * Got nested checklists working * Nesting seems to work as well * Cleaned up docs * Added missing docs * Cleaned up trailing whitespace * Removed python 3.10 feature
1 parent 54f9928 commit 0569524

File tree

3 files changed

+177
-9
lines changed

3 files changed

+177
-9
lines changed

snakemd/elements.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,9 @@ class MDList(Block):
845845
(i.e., :code:`- [x]`)
846846
- set to :code:`Iterable[bool]` to render the checked
847847
status of the top-level list elements directly
848+
849+
.. deprecated:: 2.4
850+
Use :class:`snakemd.Checklist` template instead
848851
"""
849852

850853
def __init__(
@@ -859,7 +862,7 @@ def __init__(
859862
checked if checked is None or isinstance(checked, bool) else list(checked)
860863
)
861864
self._space = ""
862-
if isinstance(self._checked, list) and self._top_level_count() != len(
865+
if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len(
863866
self._checked
864867
):
865868
raise ValueError(
@@ -896,7 +899,7 @@ def __str__(self) -> str:
896899
i = 1
897900
for item in self._items:
898901
if isinstance(item, MDList):
899-
item._space = self._space + " " * self._get_indent_size(i)
902+
item._space = self._space + " " * self._get_indent_size(self._ordered, i)
900903
output.append(str(item))
901904
else:
902905
# Create the start of the row based on `order` parameter
@@ -964,34 +967,40 @@ def _process_items(items) -> list[Block]:
964967
processed.append(item)
965968
return processed
966969

967-
def _top_level_count(self) -> int:
970+
@staticmethod
971+
def _top_level_count(items) -> int:
968972
"""
969973
Given that MDList can accept a variety of blocks,
970974
we need to know how many items in the provided list
971975
are top-level elements (i.e., not nested list elements).
972976
We use this number to throw errors if this count does
973977
not match up with the checklist count.
974978
979+
:param items:
980+
a list of items
975981
:return:
976982
a count of top-level elements
977983
"""
978984
count = 0
979-
for item in self._items:
985+
for item in items:
980986
if not isinstance(item, MDList):
981987
count += 1
982988
return count
983989

984-
def _get_indent_size(self, item_index: int = -1) -> int:
990+
@staticmethod
991+
def _get_indent_size(ordered: bool, item_index: int = -1) -> int:
985992
"""
986993
Returns the number of spaces that any sublists should be indented.
987994
995+
:param bool ordered:
996+
the boolean value indicating if a list is ordered
988997
:param int item_index:
989998
the index of the item to check (only used for ordered lists);
990999
defaults to -1
9911000
:return:
9921001
the number of spaces
9931002
"""
994-
if not self._ordered:
1003+
if not ordered:
9951004
return 2
9961005
# Ordered items vary in length, so we adjust the result based on the index
9971006
return 2 + len(str(item_index))

snakemd/templates.py

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,148 @@ class Kind(Enum):
8383
WARNING = auto()
8484
CAUTION = auto()
8585

86-
def __init__(self, kind: Kind, message: str | Iterable[str | Inline | Block]) -> None:
86+
def __init__(
87+
self,
88+
kind: Kind,
89+
message: str | Iterable[str | Inline | Block]
90+
) -> None:
91+
super().__init__()
8792
self._kind = kind
8893
self._message = message
94+
self._alert = Quote([f"[!{self._kind.name}]", self._message])
8995

9096
def __str__(self) -> str:
91-
return str(Quote([f"[!{self._kind.name}]", self._message]))
97+
"""
98+
Renders self as a markdown ready string. See
99+
:class:`snakemd.Quote` for more details.
100+
101+
:return:
102+
the Alert as a markdown string
103+
"""
104+
return str(self._alert)
105+
106+
def __repr__(self) -> str:
107+
"""
108+
Renders self as an unambiguous string for development.
109+
See :class:`snakemd.Quote` for more details.
110+
111+
:return:
112+
the Alert as a development string
113+
"""
114+
return repr(self._alert)
115+
116+
117+
class Checklist(Template):
118+
"""
119+
Checklist is an MDList extension to provide support
120+
for Markdown checklists, which are a Markdown
121+
extension. Previously, this feature was baked
122+
directly into MDList. However, because checklists
123+
are not a vanilla Markdown feature, they were
124+
moved here.
125+
126+
.. versionadded:: 2.4
127+
Included for user convenience
128+
129+
:raises ValueError:
130+
when the checked argument is an Iterable[bool] that does not
131+
match the number of top-level elements in the list
132+
:param Iterable[str | Inline | Block] items:
133+
a "list" of objects to be rendered as a list
134+
:param bool | Iterable[bool] checked:
135+
the checked state of the list
136+
137+
- defaults to :code:`False` which renders a series of unchecked
138+
boxes (i.e., :code:`- [ ]`)
139+
- set to :code:`True` to render a series of checked boxes
140+
(i.e., :code:`- [x]`)
141+
- set to :code:`Iterable[bool]` to render the checked
142+
status of the top-level list elements directly
143+
"""
144+
145+
def __init__(
146+
self,
147+
items: Iterable[str | Inline | Block],
148+
checked: bool | Iterable[bool] = False
149+
) -> None:
150+
super().__init__()
151+
self._items: list[Block] = MDList._process_items(items)
152+
self._checked: bool | list[bool] = (
153+
checked if checked is None or isinstance(
154+
checked, bool) else list(checked)
155+
)
156+
self._space = ""
157+
if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len(
158+
self._checked
159+
):
160+
raise ValueError(
161+
"Number of top-level elements in checklist does not "
162+
"match number of booleans supplied by checked parameter: "
163+
f"{self._checked}"
164+
)
165+
166+
def __str__(self):
167+
"""
168+
Renders the checklist as a markdown string. Checklists
169+
function very similarly to unorded lists, but require
170+
additional information about the status of each task
171+
(i.e., whether it is checked or not).
172+
173+
.. code-block:: markdown
174+
175+
- [ ] Do reading
176+
- [X] Do writing
177+
178+
:return:
179+
the list as a markdown string
180+
"""
181+
output = []
182+
i = 1
183+
for item in self._items:
184+
if isinstance(item, (Checklist, MDList)):
185+
item._space = self._space + " " * 2
186+
output.append(str(item))
187+
else:
188+
row = f"{self._space}-"
189+
190+
if isinstance(self._checked, bool):
191+
checked_str = "X" if self._checked else " "
192+
row = f"{row} [{checked_str}] {item}"
193+
else:
194+
checked_str = "X" if self._checked[i - 1] else " "
195+
row = f"{row} [{checked_str}] {item}"
196+
197+
output.append(row)
198+
i += 1
199+
200+
checklist = "\n".join(output)
201+
logger.debug("Rendered checklist: %r", checklist)
202+
return checklist
92203

93204
def __repr__(self) -> str:
94-
return f"Alerts(kind={self._kind!r},message={self._message!r})"
205+
"""
206+
Renders self as an unambiguous string for development.
207+
In this case, it displays in the style of a dataclass,
208+
where instance variables are listed with their
209+
values. Unlike many of the other templates, Checklists
210+
aren't a direct wrapper of MDList, and therefore cannot
211+
be represented as MDList alone.
212+
213+
.. doctest:: checklist
214+
215+
>>> checklist = Checklist(["Do Homework"], True)
216+
>>> repr(checklist)
217+
"Checklist(items=[Paragraph(...)], checked=True)"
218+
219+
:return:
220+
the Checklist object as a development string
221+
"""
222+
return (
223+
f"Checklist("
224+
f"items={self._items!r}, "
225+
f"checked={self._checked!r}"
226+
f")"
227+
)
95228

96229

97230
class CSVTable(Template):

tests/templates/test_checklist.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from snakemd.elements import MDList
2+
from snakemd.templates import Checklist
3+
4+
def test_checklist_one_item_true():
5+
checklist = Checklist(["Write code"], True)
6+
assert str(checklist) == "- [X] Write code"
7+
8+
def test_checklist_one_item_false():
9+
checklist = Checklist(["Write code"], False)
10+
assert str(checklist) == "- [ ] Write code"
11+
12+
def test_checklist_one_item_explicit():
13+
checklist = Checklist(["Write code"], [False])
14+
assert str(checklist) == "- [ ] Write code"
15+
16+
def test_checklist_many_items_true():
17+
checklist = Checklist(["Write code", "Do Laundry"], True)
18+
assert str(checklist) == "- [X] Write code\n- [X] Do Laundry"
19+
20+
def test_checklist_many_items_nested_true():
21+
checklist = Checklist(["Write code", Checklist(["Implement TODO"], True), "Do Laundry"], True)
22+
assert str(checklist) == "- [X] Write code\n - [X] Implement TODO\n- [X] Do Laundry"
23+
24+
def test_checklist_many_items_nested_mdlist_true():
25+
checklist = Checklist(["Write code", MDList(["Implement TODO"]), "Do Laundry"], True)
26+
assert str(checklist) == "- [X] Write code\n - Implement TODO\n- [X] Do Laundry"

0 commit comments

Comments
 (0)