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