1# Abbreviation Extension for Python-Markdown
2# ==========================================
3
4# This extension adds abbreviation handling to Python-Markdown.
5
6# See https://Python-Markdown.github.io/extensions/abbreviations
7# for documentation.
8
9# Original code Copyright 2007-2008 [Waylan Limberg](https://github.com/waylan)
10# and [Seemant Kulleen](http://www.kulleen.org/)
11
12# All changes Copyright 2008-2014 The Python Markdown Project
13
14# License: [BSD](https://opensource.org/licenses/bsd-license.php)
15
16"""
17This extension adds abbreviation handling to Python-Markdown.
18
19See the [documentation](https://Python-Markdown.github.io/extensions/abbreviations)
20for details.
21"""
22
23from __future__ import annotations
24
25from . import Extension
26from ..blockprocessors import BlockProcessor
27from ..inlinepatterns import InlineProcessor
28from ..treeprocessors import Treeprocessor
29from ..util import AtomicString, deprecated
30from typing import TYPE_CHECKING
31import re
32import xml.etree.ElementTree as etree
33
34if TYPE_CHECKING: # pragma: no cover
35 from .. import Markdown
36 from ..blockparser import BlockParser
37
38
39class AbbrExtension(Extension):
40 """ Abbreviation Extension for Python-Markdown. """
41
42 def __init__(self, **kwargs):
43 """ Initiate Extension and set up configs. """
44 self.config = {
45 'glossary': [
46 {},
47 'A dictionary where the `key` is the abbreviation and the `value` is the definition.'
48 "Default: `{}`"
49 ],
50 }
51 """ Default configuration options. """
52 super().__init__(**kwargs)
53 self.abbrs = {}
54 self.glossary = {}
55
56 def reset(self):
57 """ Clear all previously defined abbreviations. """
58 self.abbrs.clear()
59 if (self.glossary):
60 self.abbrs.update(self.glossary)
61
62 def reset_glossary(self):
63 """ Clear all abbreviations from the glossary. """
64 self.glossary.clear()
65
66 def load_glossary(self, dictionary: dict[str, str]):
67 """Adds `dictionary` to our glossary. Any abbreviations that already exist will be overwritten."""
68 if dictionary:
69 self.glossary = {**dictionary, **self.glossary}
70
71 def extendMarkdown(self, md):
72 """
73 Register the processors.
74
75 | Class Instance | Registry | Name | Priority |
76 | ------------------------------------------------------------------- | ---------------------------------------------------------------- | ------ | :------: |
77 | [`AbbrTreeprocessor`][markdown.extensions.abbr.AbbrTreeprocessor] | [`treeprocessors`][markdown.treeprocessors.build_treeprocessors] | `abbr` | `7` |
78 | [`AbbrBlockprocessor`][markdown.extensions.abbr.AbbrBlockprocessor] | [`blockprocessors`][markdown.blockprocessors.build_block_parser] | `abbr` | `16` |
79
80 """
81 # flake8: noqa: E501 75-78
82 if (self.config['glossary'][0]):
83 self.load_glossary(self.config['glossary'][0])
84 self.abbrs.update(self.glossary)
85 md.registerExtension(self)
86 md.treeprocessors.register(AbbrTreeprocessor(md, self.abbrs), 'abbr', 7)
87 md.parser.blockprocessors.register(AbbrBlockprocessor(md.parser, self.abbrs), 'abbr', 16)
88
89
90class AbbrTreeprocessor(Treeprocessor):
91 """ Replace abbreviation text with `<abbr>` elements. """
92
93 def __init__(self, md: Markdown | None = None, abbrs: dict | None = None):
94 self.abbrs: dict = abbrs if abbrs is not None else {}
95 self.RE: re.RegexObject | None = None
96 super().__init__(md)
97
98 def create_element(self, title: str, text: str, tail: str) -> etree.Element:
99 ''' Create an `abbr` element. '''
100 abbr = etree.Element('abbr', {'title': title})
101 abbr.text = AtomicString(text)
102 abbr.tail = tail
103 return abbr
104
105 def iter_element(self, el: etree.Element, parent: etree.Element | None = None) -> None:
106 ''' Recursively iterate over elements, run regex on text and wrap matches in `abbr` tags. '''
107 for child in reversed(el):
108 self.iter_element(child, el)
109 if text := el.text:
110 if not isinstance(text, AtomicString):
111 for m in reversed(list(self.RE.finditer(text))):
112 if self.abbrs[m.group(0)]:
113 abbr = self.create_element(self.abbrs[m.group(0)], m.group(0), text[m.end():])
114 el.insert(0, abbr)
115 text = text[:m.start()]
116 el.text = text
117 if parent is not None and el.tail:
118 tail = el.tail
119 index = list(parent).index(el) + 1
120 if not isinstance(tail, AtomicString):
121 for m in reversed(list(self.RE.finditer(tail))):
122 abbr = self.create_element(self.abbrs[m.group(0)], m.group(0), tail[m.end():])
123 parent.insert(index, abbr)
124 tail = tail[:m.start()]
125 el.tail = tail
126
127 def run(self, root: etree.Element) -> etree.Element | None:
128 ''' Step through tree to find known abbreviations. '''
129 if not self.abbrs:
130 # No abbreviations defined. Skip running processor.
131 return
132 # Build and compile regex
133 abbr_list = list(self.abbrs.keys())
134 abbr_list.sort(key=len, reverse=True)
135 self.RE = re.compile(f"\\b(?:{ '|'.join(re.escape(key) for key in abbr_list) })\\b")
136 # Step through tree and modify on matches
137 self.iter_element(root)
138
139
140class AbbrBlockprocessor(BlockProcessor):
141 """ Parse text for abbreviation references. """
142
143 RE = re.compile(r'^[*]\[(?P<abbr>[^\\]*?)\][ ]?:[ ]*\n?[ ]*(?P<title>.*)$', re.MULTILINE)
144
145 def __init__(self, parser: BlockParser, abbrs: dict):
146 self.abbrs: dict = abbrs
147 super().__init__(parser)
148
149 def test(self, parent: etree.Element, block: str) -> bool:
150 return True
151
152 def run(self, parent: etree.Element, blocks: list[str]) -> bool:
153 """
154 Find and remove all abbreviation references from the text.
155 Each reference is added to the abbreviation collection.
156
157 """
158 block = blocks.pop(0)
159 m = self.RE.search(block)
160 if m:
161 abbr = m.group('abbr').strip()
162 title = m.group('title').strip()
163 if title and abbr:
164 if title == "''" or title == '""':
165 self.abbrs.pop(abbr)
166 else:
167 self.abbrs[abbr] = title
168 if block[m.end():].strip():
169 # Add any content after match back to blocks as separate block
170 blocks.insert(0, block[m.end():].lstrip('\n'))
171 if block[:m.start()].strip():
172 # Add any content before match back to blocks as separate block
173 blocks.insert(0, block[:m.start()].rstrip('\n'))
174 return True
175 # No match. Restore block.
176 blocks.insert(0, block)
177 return False
178
179
180AbbrPreprocessor = deprecated("This class has been renamed to `AbbrBlockprocessor`.")(AbbrBlockprocessor)
181
182
183@deprecated("This class will be removed in the future; use `AbbrTreeprocessor` instead.")
184class AbbrInlineProcessor(InlineProcessor):
185 """ Abbreviation inline pattern. """
186
187 def __init__(self, pattern: str, title: str):
188 super().__init__(pattern)
189 self.title = title
190
191 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
192 abbr = etree.Element('abbr')
193 abbr.text = AtomicString(m.group('abbr'))
194 abbr.set('title', self.title)
195 return abbr, m.start(0), m.end(0)
196
197
198def makeExtension(**kwargs): # pragma: no cover
199 return AbbrExtension(**kwargs)