added support for task lists
This commit is contained in:
parent
7bbb53fc13
commit
818647fbdf
|
@ -382,6 +382,12 @@ class ObsidianLinks(Extension):
|
|||
return el, m.start(0), m.end(0)
|
||||
|
||||
def extendMarkdown(self, md: markdown.Markdown) -> None: # noqa: N802
|
||||
"""
|
||||
Registers the link inline processors with the Markdown parser.
|
||||
|
||||
Args:
|
||||
md (markdown.Markdown): The Markdown parser to register the patterns with.
|
||||
"""
|
||||
md.inlinePatterns.register(ObsidianLinks.ObsidianLinksProc(OBSLINK_PATTERN, md, self),
|
||||
'obsidian_links', PRIO_BASE + 110)
|
||||
md.inlinePatterns.register(ObsidianLinks.GenericLinksProc(GENERICLINK_PATTERN, md, self),
|
||||
|
@ -389,9 +395,10 @@ class ObsidianLinks(Extension):
|
|||
|
||||
|
||||
class ObsidianLists(Extension):
|
||||
"""An extension to process Obsidian lists, including making nested lists."""
|
||||
"""An extension to process Obsidian lists, including making nested lists and task lists."""
|
||||
UL_RE = re.compile(r'^([ ]*)[-+*][ ]+(.*)')
|
||||
OL_RE = re.compile(r'^([ ]*)([1-9][0-9]*)\.[ ]+(.*)')
|
||||
TASK_RE = re.compile(r'^([ ]*)-[ ]\[(.)\][ ]+(.*)')
|
||||
LIST_START = STX + "erbosoft:lstart" + ETX
|
||||
LIST_END = STX + "erbosoft:lend" + ETX
|
||||
|
||||
|
@ -419,11 +426,27 @@ class ObsidianLists(Extension):
|
|||
return '', -1, -1
|
||||
|
||||
class ObsidianListFinder(Preprocessor):
|
||||
"""Preprocessor that finds and marks blocks in the text that contain lists."""
|
||||
def __init__(self, extref: Any) -> None:
|
||||
"""
|
||||
Initialize the ObsidianListFinder.
|
||||
|
||||
Args:
|
||||
extref (ObsidianLists): Reference to the outer object.
|
||||
"""
|
||||
super(ObsidianLists.ObsidianListFinder, self).__init__()
|
||||
self._extref = extref
|
||||
|
||||
def run(self, lines: list[str]) -> list[str]:
|
||||
"""
|
||||
Execute the preprocessor on the Markdown text.
|
||||
|
||||
Args:
|
||||
lines (list[str]): List of lines of text in the input document.
|
||||
|
||||
Returns:
|
||||
list[str]: The list of lines to carry forward with.
|
||||
"""
|
||||
i = 0
|
||||
in_list = False
|
||||
while i < len(lines):
|
||||
|
@ -444,34 +467,43 @@ class ObsidianLists(Extension):
|
|||
class ObsidianListBlock(BlockProcessor):
|
||||
"""The actual block processor that generates lists."""
|
||||
def __init__(self, parser: BlockParser, extref: Any) -> None:
|
||||
"""
|
||||
Initialize the ObsidianListBlock.
|
||||
|
||||
Args:
|
||||
parser (BlockParser): Reference to the block parser.
|
||||
extref (ObsidianLists): Backreference to the outer object.
|
||||
"""
|
||||
super(ObsidianLists.ObsidianListBlock, self).__init__(parser)
|
||||
self._extref = extref
|
||||
|
||||
@staticmethod
|
||||
def _extract(blocks: list[str]) -> list[str]:
|
||||
def _attach_checkbox(listitem: etree.Element, data_line: int, cbox: str, text: str) -> None:
|
||||
"""
|
||||
Extract all lines up to the next blank line, which terminates the list.
|
||||
Attach a checkbox and its associated text to the ``li`` element.
|
||||
|
||||
Args:
|
||||
blocks (list[str]): List of blocks to be parsed.
|
||||
|
||||
Returns:
|
||||
list[str]: The block of lines to be parsed for a list.
|
||||
listitem (etree.Element): The ``li`` element that's being worked on.
|
||||
data_line (int): Data line index for this list item.
|
||||
cbox (str): The checkbox marker character.
|
||||
text (str): The text to include after the checkbox.
|
||||
"""
|
||||
out_lines: list[str] = []
|
||||
while len(blocks) > 0:
|
||||
curblk = blocks.pop(0)
|
||||
cur_lines = curblk.split('\n')
|
||||
while len(cur_lines) > 0:
|
||||
line = cur_lines.pop(0)
|
||||
if line.strip() == '':
|
||||
cur_lines.insert(0, line)
|
||||
blocks.insert(0, '\n'.join(cur_lines))
|
||||
return out_lines
|
||||
out_lines.append(line)
|
||||
return out_lines
|
||||
checkbox = etree.SubElement(listitem, 'input')
|
||||
checkbox.attrib['type'] = 'checkbox'
|
||||
checkbox.attrib['class'] = 'task-list-item-checkbox'
|
||||
checkbox.attrib['aria-label'] = 'Task'
|
||||
checkbox.attrib['data-line'] = str(data_line)
|
||||
checkbox.attrib['data-task'] = cbox
|
||||
if not cbox.isspace():
|
||||
checkbox.attrib['checked'] = 'checked'
|
||||
if cbox == 'x':
|
||||
span = etree.SubElement(listitem, 'span')
|
||||
span.attrib['class'] = 'task-list-item-checked'
|
||||
span.text = text
|
||||
else:
|
||||
checkbox.tail = text
|
||||
|
||||
def _build_element(self, parent: etree.Element, listtype: str, indent: int, lines: list[str]) -> None:
|
||||
def _build_element(self, parent: etree.Element, listtype: str, indent: int, data_line: int, lines: list[str]) -> None:
|
||||
"""
|
||||
Builds a list element and adds it to the specified parent list.
|
||||
|
||||
|
@ -479,9 +511,11 @@ class ObsidianLists(Extension):
|
|||
parent (etree.element): The parent list element to insert "li" items under.
|
||||
listtype (str): The list type, either "ol" or "ul".
|
||||
indent (int): The indent level of the list, which is greater than or equal to 0.
|
||||
data_line (int): The data line index of the element.
|
||||
lines (list[str]): The lines comprising the list element.
|
||||
"""
|
||||
textdata: list[str] = []
|
||||
cbox: str | None = None
|
||||
|
||||
# Parse the header line to get the first bit of text.
|
||||
if listtype == 'ol':
|
||||
|
@ -489,12 +523,24 @@ class ObsidianLists(Extension):
|
|||
assert len(m.group(1)) == indent
|
||||
textdata.append(m.group(3))
|
||||
elif listtype == 'ul':
|
||||
m = self._extref.TASK_RE.match(lines[0])
|
||||
if m:
|
||||
assert len(m.group(1)) == indent
|
||||
cbox = m.group(2)
|
||||
textdata.append(m.group(3))
|
||||
else:
|
||||
m = self._extref.UL_RE.match(lines[0])
|
||||
assert len(m.group(1)) == indent
|
||||
textdata.append(m.group(2))
|
||||
|
||||
# Build the list element
|
||||
listelement = etree.SubElement(parent, 'li')
|
||||
listelement.attrib['data-line'] = str(data_line)
|
||||
if cbox is not None:
|
||||
classname = 'task-list-item'
|
||||
if not cbox.isspace():
|
||||
classname = f"{classname} is-checked"
|
||||
listelement.attrib['class'] = classname
|
||||
my_lines = list(lines)
|
||||
i = 1
|
||||
last_sublist: etree.Element | None = None
|
||||
|
@ -510,6 +556,8 @@ class ObsidianLists(Extension):
|
|||
# flush text data
|
||||
if last_sublist is not None:
|
||||
last_sublist.tail = '\n'.join(textdata)
|
||||
elif cbox is not None:
|
||||
self._attach_checkbox(listelement, data_line, cbox, '\n'.join(textdata))
|
||||
else:
|
||||
listelement.text = '\n'.join(textdata)
|
||||
textdata = []
|
||||
|
@ -521,6 +569,8 @@ class ObsidianLists(Extension):
|
|||
if len(textdata) > 0:
|
||||
if last_sublist is not None:
|
||||
last_sublist.tail = '\n'.join(textdata)
|
||||
elif cbox is not None:
|
||||
self._attach_checkbox(listelement, data_line, cbox, '\n'.join(textdata))
|
||||
else:
|
||||
listelement.text = '\n'.join(textdata)
|
||||
|
||||
|
@ -543,27 +593,34 @@ class ObsidianLists(Extension):
|
|||
list_top = etree.SubElement(parent, listtype)
|
||||
if listtype == 'ol' and start_index > 1:
|
||||
list_top.attrib['start'] = str(start_index)
|
||||
if listtype == 'ul':
|
||||
m = self._extref.TASK_RE.match(lines[0])
|
||||
if m:
|
||||
list_top.attrib['class'] = 'contains-task-list'
|
||||
|
||||
# The start index is set to -1 so, the first time around, we'll pick it up and set the start point.
|
||||
st = -1
|
||||
data_line = 0
|
||||
for i, line in enumerate(lines):
|
||||
new_type, new_indent, _ = self._extref._find_listhead(lines[i])
|
||||
if len(new_type) > 0:
|
||||
if new_indent < indent_level:
|
||||
# if the list head is lesser indented, this is the end of this element and the list
|
||||
if st >= 0:
|
||||
self._build_element(list_top, listtype, indent_level, lines[st:i])
|
||||
self._build_element(list_top, listtype, indent_level, data_line, lines[st:i])
|
||||
data_line += 1
|
||||
return list_top, lines[i:]
|
||||
if new_indent == indent_level:
|
||||
# this is the end of this element
|
||||
if st >= 0:
|
||||
self._build_element(list_top, listtype, indent_level, lines[st:i])
|
||||
self._build_element(list_top, listtype, indent_level, data_line, lines[st:i])
|
||||
data_line += 1
|
||||
if new_type != listtype:
|
||||
# this is also the end of the list
|
||||
return list_top, lines[i:]
|
||||
st = i # start of next element
|
||||
if st >= 0: # end the final element
|
||||
self._build_element(list_top, listtype, indent_level, lines[st:])
|
||||
self._build_element(list_top, listtype, indent_level, data_line, lines[st:])
|
||||
return list_top, []
|
||||
|
||||
def test(self, parent: etree.Element, block: str) -> bool:
|
||||
|
|
|
@ -28,6 +28,9 @@ blockquote {
|
|||
margin-right: 1em;
|
||||
border-left: 2px solid #9873f7;
|
||||
}
|
||||
span.task-list-item-checked {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user