From a33845f8327fbd3f916005972ee1647a85928868 Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Sun, 11 Aug 2024 00:11:19 -0600 Subject: [PATCH] beginnings of callout support --- src/dragonglass/mparse.py | 68 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/dragonglass/mparse.py b/src/dragonglass/mparse.py index d729ea3..37c2929 100644 --- a/src/dragonglass/mparse.py +++ b/src/dragonglass/mparse.py @@ -13,7 +13,7 @@ from urllib.parse import urlparse import markdown from markdown import Markdown -from markdown.blockprocessors import BlockProcessor, BlockParser +from markdown.blockprocessors import BlockProcessor, BlockParser, BlockQuoteProcessor from markdown.extensions import Extension from markdown.extensions.footnotes import (FootnoteExtension, FootnoteBlockProcessor, FootnoteInlineProcessor, FootnoteTreeprocessor, FootnotePostTreeprocessor, FootnotePostprocessor) @@ -861,6 +861,71 @@ class ObsidianStyleFootnotes(FootnoteExtension): md.postprocessors.register(FootnotePostprocessor(self), 'footnote', 25) +class ObsidianStyleBlockquotes(Extension): + + class ObsidianBlockQuote(BlockQuoteProcessor): + CALLOUT = re.compile(r'^\[!([a-z]+)\]([-+])?(?:[ ]+(.*))?') + + def normal_blockquote(self, parent: etree.Element, block: str) -> None: + sibling = self.lastChild(parent) + if sibling is not None and sibling.tag == "blockquote": + # Previous block was a blockquote so set that as this blocks parent + quote = sibling + else: + # This is a new blockquote. Create a new parent element. + quote = etree.SubElement(parent, 'blockquote') + # Recursively parse block with blockquote as parent. + # change parser state so blockquotes embedded in lists use `p` tags + self.parser.state.set('blockquote') + self.parser.parseChunk(quote, block) + self.parser.state.reset() + + def callout_block(self, parent: etree.Element, lines: list[str]) -> None: + m = self.CALLOUT.match(lines[0]) + callout_type = m.group(1) + # folding = m.group(2) + title = m.group(3) + if not title: + title = callout_type.title() + base_div = etree.SubElement(parent, 'div', {'class': 'callout', 'data-callout': callout_type}) + title_div = etree.SubElement(base_div, 'div', {'class': 'callout-title'}) + # TODO: add title icon here + inner_title_div = etree.SubElement(title_div, 'div', {'class': 'callout-title-inner'}) + inner_title_div.text = title + content_div = etree.SubElement(base_div, 'div', {'class': 'callout-content'}) + lines.pop(0) + first = True + for line in lines: + if first: + first = False + else: + etree.SubElement(content_div, 'br') + self.parser.state.set('list') + self.parser.parseBlocks(content_div, [line]) + self.parser.state.reset() + + def run(self, parent: etree.Element, blocks: list[str]) -> None: + block = blocks.pop(0) + lines: list[str] = [] + callout = False + m = self.RE.search(block) + if m: + before = block[:m.start()] # Lines before blockquote + # Pass lines before blockquote in recursively for parsing first. + self.parser.parseBlocks(parent, [before]) + # Remove `> ` from beginning of each line. + lines = [self.clean(line) for line in block[m.start():].split('\n')] + callout = (self.CALLOUT.match(lines[0]) is not None) + block = '\n'.join(lines) + if callout: + self.callout_block(parent, lines) + else: + self.normal_blockquote(parent, block) + + def extendMarkdown(self, md) -> None: + md.parser.blockprocessors.register(ObsidianStyleBlockquotes.ObsidianBlockQuote(md.parser), 'quote', 20) + + def create_markdown_parser(context: Context) -> markdown.Markdown: """ Creates a Markdown parser with all our extensions loaded. @@ -887,6 +952,7 @@ def create_markdown_parser(context: Context) -> markdown.Markdown: MetaStripper(), ObsidianComments(), ObsidianStyleFootnotes(SUPERSCRIPT_TEXT='[{}]', SEPARATOR='-'), + ObsidianStyleBlockquotes(), ObsidianImages(context), ObsidianLinks(context), ObsidianLists(),