aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/markdown/extensions/footnotes.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/markdown/extensions/footnotes.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
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)