about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/markdown/extensions/footnotes.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/markdown/extensions/footnotes.py')
-rw-r--r--.venv/lib/python3.12/site-packages/markdown/extensions/footnotes.py418
1 files changed, 418 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/markdown/extensions/footnotes.py b/.venv/lib/python3.12/site-packages/markdown/extensions/footnotes.py
new file mode 100644
index 00000000..30c08113
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/markdown/extensions/footnotes.py
@@ -0,0 +1,418 @@
+# Footnotes Extension for Python-Markdown
+# =======================================
+
+# Adds footnote handling to Python-Markdown.
+
+# See https://Python-Markdown.github.io/extensions/footnotes
+# for documentation.
+
+# Copyright The Python Markdown Project
+
+# License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+Adds footnote handling to Python-Markdown.
+
+See the [documentation](https://Python-Markdown.github.io/extensions/footnotes)
+for details.
+"""
+
+from __future__ import annotations
+
+from . import Extension
+from ..blockprocessors import BlockProcessor
+from ..inlinepatterns import InlineProcessor
+from ..treeprocessors import Treeprocessor
+from ..postprocessors import Postprocessor
+from .. import util
+from collections import OrderedDict
+import re
+import copy
+import xml.etree.ElementTree as etree
+
+FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX
+NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX
+RE_REF_ID = re.compile(r'(fnref)(\d+)')
+
+
+class FootnoteExtension(Extension):
+    """ Footnote Extension. """
+
+    def __init__(self, **kwargs):
+        """ Setup configs. """
+
+        self.config = {
+            'PLACE_MARKER': [
+                '///Footnotes Go Here///', 'The text string that marks where the footnotes go'
+            ],
+            'UNIQUE_IDS': [
+                False, 'Avoid name collisions across multiple calls to `reset()`.'
+            ],
+            'BACKLINK_TEXT': [
+                '↩', "The text string that links from the footnote to the reader's place."
+            ],
+            'SUPERSCRIPT_TEXT': [
+                '{}', "The text string that links from the reader's place to the footnote."
+            ],
+            'BACKLINK_TITLE': [
+                'Jump back to footnote %d in the text',
+                'The text string used for the title HTML attribute of the backlink. '
+                '%d will be replaced by the footnote number.'
+            ],
+            'SEPARATOR': [
+                ':', 'Footnote separator.'
+            ]
+        }
+        """ Default configuration options. """
+        super().__init__(**kwargs)
+
+        # In multiple invocations, emit links that don't get tangled.
+        self.unique_prefix = 0
+        self.found_refs: dict[str, int] = {}
+        self.used_refs: set[str] = set()
+
+        self.reset()
+
+    def extendMarkdown(self, md):
+        """ Add pieces to Markdown. """
+        md.registerExtension(self)
+        self.parser = md.parser
+        self.md = md
+        # Insert a `blockprocessor` before `ReferencePreprocessor`
+        md.parser.blockprocessors.register(FootnoteBlockProcessor(self), 'footnote', 17)
+
+        # Insert an inline pattern before `ImageReferencePattern`
+        FOOTNOTE_RE = r'\[\^([^\]]*)\]'  # blah blah [^1] blah
+        md.inlinePatterns.register(FootnoteInlineProcessor(FOOTNOTE_RE, self), 'footnote', 175)
+        # Insert a tree-processor that would actually add the footnote div
+        # This must be before all other tree-processors (i.e., `inline` and
+        # `codehilite`) so they can run on the the contents of the div.
+        md.treeprocessors.register(FootnoteTreeprocessor(self), 'footnote', 50)
+
+        # Insert a tree-processor that will run after inline is done.
+        # In this tree-processor we want to check our duplicate footnote tracker
+        # And add additional `backrefs` to the footnote pointing back to the
+        # duplicated references.
+        md.treeprocessors.register(FootnotePostTreeprocessor(self), 'footnote-duplicate', 15)
+
+        # Insert a postprocessor after amp_substitute processor
+        md.postprocessors.register(FootnotePostprocessor(self), 'footnote', 25)
+
+    def reset(self) -> None:
+        """ Clear footnotes on reset, and prepare for distinct document. """
+        self.footnotes: OrderedDict[str, str] = OrderedDict()
+        self.unique_prefix += 1
+        self.found_refs = {}
+        self.used_refs = set()
+
+    def unique_ref(self, reference: str, found: bool = False) -> str:
+        """ Get a unique reference if there are duplicates. """
+        if not found:
+            return reference
+
+        original_ref = reference
+        while reference in self.used_refs:
+            ref, rest = reference.split(self.get_separator(), 1)
+            m = RE_REF_ID.match(ref)
+            if m:
+                reference = '%s%d%s%s' % (m.group(1), int(m.group(2))+1, self.get_separator(), rest)
+            else:
+                reference = '%s%d%s%s' % (ref, 2, self.get_separator(), rest)
+
+        self.used_refs.add(reference)
+        if original_ref in self.found_refs:
+            self.found_refs[original_ref] += 1
+        else:
+            self.found_refs[original_ref] = 1
+        return reference
+
+    def findFootnotesPlaceholder(
+        self, root: etree.Element
+    ) -> tuple[etree.Element, etree.Element, bool] | None:
+        """ Return ElementTree Element that contains Footnote placeholder. """
+        def finder(element):
+            for child in element:
+                if child.text:
+                    if child.text.find(self.getConfig("PLACE_MARKER")) > -1:
+                        return child, element, True
+                if child.tail:
+                    if child.tail.find(self.getConfig("PLACE_MARKER")) > -1:
+                        return child, element, False
+                child_res = finder(child)
+                if child_res is not None:
+                    return child_res
+            return None
+
+        res = finder(root)
+        return res
+
+    def setFootnote(self, id: str, text: str) -> None:
+        """ Store a footnote for later retrieval. """
+        self.footnotes[id] = text
+
+    def get_separator(self) -> str:
+        """ Get the footnote separator. """
+        return self.getConfig("SEPARATOR")
+
+    def makeFootnoteId(self, id: str) -> str:
+        """ Return footnote link id. """
+        if self.getConfig("UNIQUE_IDS"):
+            return 'fn%s%d-%s' % (self.get_separator(), self.unique_prefix, id)
+        else:
+            return 'fn{}{}'.format(self.get_separator(), id)
+
+    def makeFootnoteRefId(self, id: str, found: bool = False) -> str:
+        """ Return footnote back-link id. """
+        if self.getConfig("UNIQUE_IDS"):
+            return self.unique_ref('fnref%s%d-%s' % (self.get_separator(), self.unique_prefix, id), found)
+        else:
+            return self.unique_ref('fnref{}{}'.format(self.get_separator(), id), found)
+
+    def makeFootnotesDiv(self, root: etree.Element) -> etree.Element | None:
+        """ Return `div` of footnotes as `etree` Element. """
+
+        if not list(self.footnotes.keys()):
+            return None
+
+        div = etree.Element("div")
+        div.set('class', 'footnote')
+        etree.SubElement(div, "hr")
+        ol = etree.SubElement(div, "ol")
+        surrogate_parent = etree.Element("div")
+
+        # Backward compatibility with old '%d' placeholder
+        backlink_title = self.getConfig("BACKLINK_TITLE").replace("%d", "{}")
+
+        for index, id in enumerate(self.footnotes.keys(), start=1):
+            li = etree.SubElement(ol, "li")
+            li.set("id", self.makeFootnoteId(id))
+            # Parse footnote with surrogate parent as `li` cannot be used.
+            # List block handlers have special logic to deal with `li`.
+            # When we are done parsing, we will copy everything over to `li`.
+            self.parser.parseChunk(surrogate_parent, self.footnotes[id])
+            for el in list(surrogate_parent):
+                li.append(el)
+                surrogate_parent.remove(el)
+            backlink = etree.Element("a")
+            backlink.set("href", "#" + self.makeFootnoteRefId(id))
+            backlink.set("class", "footnote-backref")
+            backlink.set(
+                "title",
+                backlink_title.format(index)
+            )
+            backlink.text = FN_BACKLINK_TEXT
+
+            if len(li):
+                node = li[-1]
+                if node.tag == "p":
+                    node.text = node.text + NBSP_PLACEHOLDER
+                    node.append(backlink)
+                else:
+                    p = etree.SubElement(li, "p")
+                    p.append(backlink)
+        return div
+
+
+class FootnoteBlockProcessor(BlockProcessor):
+    """ Find all footnote references and store for later use. """
+
+    RE = re.compile(r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$', re.MULTILINE)
+
+    def __init__(self, footnotes: FootnoteExtension):
+        super().__init__(footnotes.parser)
+        self.footnotes = footnotes
+
+    def test(self, parent: etree.Element, block: str) -> bool:
+        return True
+
+    def run(self, parent: etree.Element, blocks: list[str]) -> bool:
+        """ Find, set, and remove footnote definitions. """
+        block = blocks.pop(0)
+        m = self.RE.search(block)
+        if m:
+            id = m.group(1)
+            fn_blocks = [m.group(2)]
+
+            # Handle rest of block
+            therest = block[m.end():].lstrip('\n')
+            m2 = self.RE.search(therest)
+            if m2:
+                # Another footnote exists in the rest of this block.
+                # Any content before match is continuation of this footnote, which may be lazily indented.
+                before = therest[:m2.start()].rstrip('\n')
+                fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(before)]).lstrip('\n')
+                # Add back to blocks everything from beginning of match forward for next iteration.
+                blocks.insert(0, therest[m2.start():])
+            else:
+                # All remaining lines of block are continuation of this footnote, which may be lazily indented.
+                fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(therest)]).strip('\n')
+
+                # Check for child elements in remaining blocks.
+                fn_blocks.extend(self.detectTabbed(blocks))
+
+            footnote = "\n\n".join(fn_blocks)
+            self.footnotes.setFootnote(id, footnote.rstrip())
+
+            if block[:m.start()].strip():
+                # Add any content before match back to blocks as separate block
+                blocks.insert(0, block[:m.start()].rstrip('\n'))
+            return True
+        # No match. Restore block.
+        blocks.insert(0, block)
+        return False
+
+    def detectTabbed(self, blocks: list[str]) -> list[str]:
+        """ Find indented text and remove indent before further processing.
+
+        Returns:
+            A list of blocks with indentation removed.
+        """
+        fn_blocks = []
+        while blocks:
+            if blocks[0].startswith(' '*4):
+                block = blocks.pop(0)
+                # Check for new footnotes within this block and split at new footnote.
+                m = self.RE.search(block)
+                if m:
+                    # Another footnote exists in this block.
+                    # Any content before match is continuation of this footnote, which may be lazily indented.
+                    before = block[:m.start()].rstrip('\n')
+                    fn_blocks.append(self.detab(before))
+                    # Add back to blocks everything from beginning of match forward for next iteration.
+                    blocks.insert(0, block[m.start():])
+                    # End of this footnote.
+                    break
+                else:
+                    # Entire block is part of this footnote.
+                    fn_blocks.append(self.detab(block))
+            else:
+                # End of this footnote.
+                break
+        return fn_blocks
+
+    def detab(self, block: str) -> str:
+        """ Remove one level of indent from a block.
+
+        Preserve lazily indented blocks by only removing indent from indented lines.
+        """
+        lines = block.split('\n')
+        for i, line in enumerate(lines):
+            if line.startswith(' '*4):
+                lines[i] = line[4:]
+        return '\n'.join(lines)
+
+
+class FootnoteInlineProcessor(InlineProcessor):
+    """ `InlineProcessor` for footnote markers in a document's body text. """
+
+    def __init__(self, pattern: str, footnotes: FootnoteExtension):
+        super().__init__(pattern)
+        self.footnotes = footnotes
+
+    def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
+        id = m.group(1)
+        if id in self.footnotes.footnotes.keys():
+            sup = etree.Element("sup")
+            a = etree.SubElement(sup, "a")
+            sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True))
+            a.set('href', '#' + self.footnotes.makeFootnoteId(id))
+            a.set('class', 'footnote-ref')
+            a.text = self.footnotes.getConfig("SUPERSCRIPT_TEXT").format(
+                list(self.footnotes.footnotes.keys()).index(id) + 1
+            )
+            return sup, m.start(0), m.end(0)
+        else:
+            return None, None, None
+
+
+class FootnotePostTreeprocessor(Treeprocessor):
+    """ Amend footnote div with duplicates. """
+
+    def __init__(self, footnotes: FootnoteExtension):
+        self.footnotes = footnotes
+
+    def add_duplicates(self, li: etree.Element, duplicates: int) -> None:
+        """ Adjust current `li` and add the duplicates: `fnref2`, `fnref3`, etc. """
+        for link in li.iter('a'):
+            # Find the link that needs to be duplicated.
+            if link.attrib.get('class', '') == 'footnote-backref':
+                ref, rest = link.attrib['href'].split(self.footnotes.get_separator(), 1)
+                # Duplicate link the number of times we need to
+                # and point the to the appropriate references.
+                links = []
+                for index in range(2, duplicates + 1):
+                    sib_link = copy.deepcopy(link)
+                    sib_link.attrib['href'] = '%s%d%s%s' % (ref, index, self.footnotes.get_separator(), rest)
+                    links.append(sib_link)
+                    self.offset += 1
+                # Add all the new duplicate links.
+                el = list(li)[-1]
+                for link in links:
+                    el.append(link)
+                break
+
+    def get_num_duplicates(self, li: etree.Element) -> int:
+        """ Get the number of duplicate refs of the footnote. """
+        fn, rest = li.attrib.get('id', '').split(self.footnotes.get_separator(), 1)
+        link_id = '{}ref{}{}'.format(fn, self.footnotes.get_separator(), rest)
+        return self.footnotes.found_refs.get(link_id, 0)
+
+    def handle_duplicates(self, parent: etree.Element) -> None:
+        """ Find duplicate footnotes and format and add the duplicates. """
+        for li in list(parent):
+            # Check number of duplicates footnotes and insert
+            # additional links if needed.
+            count = self.get_num_duplicates(li)
+            if count > 1:
+                self.add_duplicates(li, count)
+
+    def run(self, root: etree.Element) -> None:
+        """ Crawl the footnote div and add missing duplicate footnotes. """
+        self.offset = 0
+        for div in root.iter('div'):
+            if div.attrib.get('class', '') == 'footnote':
+                # Footnotes should be under the first ordered list under
+                # the footnote div.  So once we find it, quit.
+                for ol in div.iter('ol'):
+                    self.handle_duplicates(ol)
+                    break
+
+
+class FootnoteTreeprocessor(Treeprocessor):
+    """ Build and append footnote div to end of document. """
+
+    def __init__(self, footnotes: FootnoteExtension):
+        self.footnotes = footnotes
+
+    def run(self, root: etree.Element) -> None:
+        footnotesDiv = self.footnotes.makeFootnotesDiv(root)
+        if footnotesDiv is not None:
+            result = self.footnotes.findFootnotesPlaceholder(root)
+            if result:
+                child, parent, isText = result
+                ind = list(parent).index(child)
+                if isText:
+                    parent.remove(child)
+                    parent.insert(ind, footnotesDiv)
+                else:
+                    parent.insert(ind + 1, footnotesDiv)
+                    child.tail = None
+            else:
+                root.append(footnotesDiv)
+
+
+class FootnotePostprocessor(Postprocessor):
+    """ Replace placeholders with html entities. """
+    def __init__(self, footnotes: FootnoteExtension):
+        self.footnotes = footnotes
+
+    def run(self, text: str) -> str:
+        text = text.replace(
+            FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT")
+        )
+        return text.replace(NBSP_PLACEHOLDER, " ")
+
+
+def makeExtension(**kwargs):  # pragma: no cover
+    """ Return an instance of the `FootnoteExtension` """
+    return FootnoteExtension(**kwargs)