diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/markdown/extensions/fenced_code.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/markdown/extensions/fenced_code.py | 193 |
1 files changed, 193 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/markdown/extensions/fenced_code.py b/.venv/lib/python3.12/site-packages/markdown/extensions/fenced_code.py new file mode 100644 index 00000000..bae7330a --- /dev/null +++ b/.venv/lib/python3.12/site-packages/markdown/extensions/fenced_code.py @@ -0,0 +1,193 @@ +# Fenced Code Extension for Python Markdown +# ========================================= + +# This extension adds Fenced Code Blocks to Python-Markdown. + +# See https://Python-Markdown.github.io/extensions/fenced_code_blocks +# for documentation. + +# Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/). + +# All changes Copyright 2008-2014 The Python Markdown Project + +# License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" +This extension adds Fenced Code Blocks to Python-Markdown. + +See the [documentation](https://Python-Markdown.github.io/extensions/fenced_code_blocks) +for details. +""" + +from __future__ import annotations + +from textwrap import dedent +from . import Extension +from ..preprocessors import Preprocessor +from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines +from .attr_list import get_attrs_and_remainder, AttrListExtension +from ..util import parseBoolValue +from ..serializers import _escape_attrib_html +import re +from typing import TYPE_CHECKING, Any, Iterable + +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown + + +class FencedCodeExtension(Extension): + def __init__(self, **kwargs): + self.config = { + 'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"'] + } + """ Default configuration options. """ + super().__init__(**kwargs) + + def extendMarkdown(self, md): + """ Add `FencedBlockPreprocessor` to the Markdown instance. """ + md.registerExtension(self) + + md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25) + + +class FencedBlockPreprocessor(Preprocessor): + """ Find and extract fenced code blocks. """ + + FENCED_BLOCK_RE = re.compile( + dedent(r''' + (?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence + ((\{(?P<attrs>[^\n]*)\})| # (optional {attrs} or + (\.?(?P<lang>[\w#.+-]*)[ ]*)? # optional (.)lang + (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines) + \n # newline (end of opening fence) + (?P<code>.*?)(?<=\n) # the code block + (?P=fence)[ ]*$ # closing fence + '''), + re.MULTILINE | re.DOTALL | re.VERBOSE + ) + + def __init__(self, md: Markdown, config: dict[str, Any]): + super().__init__(md) + self.config = config + self.checked_for_deps = False + self.codehilite_conf: dict[str, Any] = {} + self.use_attr_list = False + # List of options to convert to boolean values + self.bool_options = [ + 'linenums', + 'guess_lang', + 'noclasses', + 'use_pygments' + ] + + def run(self, lines: list[str]) -> list[str]: + """ Match and store Fenced Code Blocks in the `HtmlStash`. """ + + # Check for dependent extensions + if not self.checked_for_deps: + for ext in self.md.registeredExtensions: + if isinstance(ext, CodeHiliteExtension): + self.codehilite_conf = ext.getConfigs() + if isinstance(ext, AttrListExtension): + self.use_attr_list = True + + self.checked_for_deps = True + + text = "\n".join(lines) + index = 0 + while 1: + m = self.FENCED_BLOCK_RE.search(text, index) + if m: + lang, id, classes, config = None, '', [], {} + if m.group('attrs'): + attrs, remainder = get_attrs_and_remainder(m.group('attrs')) + if remainder: # Does not have correctly matching curly braces, so the syntax is invalid. + index = m.end('attrs') # Explicitly skip over this, to prevent an infinite loop. + continue + id, classes, config = self.handle_attrs(attrs) + if len(classes): + lang = classes.pop(0) + else: + if m.group('lang'): + lang = m.group('lang') + if m.group('hl_lines'): + # Support `hl_lines` outside of `attrs` for backward-compatibility + config['hl_lines'] = parse_hl_lines(m.group('hl_lines')) + + # If `config` is not empty, then the `codehighlite` extension + # is enabled, so we call it to highlight the code + if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True): + local_config = self.codehilite_conf.copy() + local_config.update(config) + # Combine classes with `cssclass`. Ensure `cssclass` is at end + # as Pygments appends a suffix under certain circumstances. + # Ignore ID as Pygments does not offer an option to set it. + if classes: + local_config['css_class'] = '{} {}'.format( + ' '.join(classes), + local_config['css_class'] + ) + highliter = CodeHilite( + m.group('code'), + lang=lang, + style=local_config.pop('pygments_style', 'default'), + **local_config + ) + + code = highliter.hilite(shebang=False) + else: + id_attr = lang_attr = class_attr = kv_pairs = '' + if lang: + prefix = self.config.get('lang_prefix', 'language-') + lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"' + if classes: + class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"' + if id: + id_attr = f' id="{_escape_attrib_html(id)}"' + if self.use_attr_list and config and not config.get('use_pygments', False): + # Only assign key/value pairs to code element if `attr_list` extension is enabled, key/value + # pairs were defined on the code block, and the `use_pygments` key was not set to `True`. The + # `use_pygments` key could be either set to `False` or not defined. It is omitted from output. + kv_pairs = ''.join( + f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments' + ) + code = self._escape(m.group('code')) + code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>' + + placeholder = self.md.htmlStash.store(code) + text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}' + # Continue from after the replaced text in the next iteration. + index = m.start() + 1 + len(placeholder) + else: + break + return text.split("\n") + + def handle_attrs(self, attrs: Iterable[tuple[str, str]]) -> tuple[str, list[str], dict[str, Any]]: + """ Return tuple: `(id, [list, of, classes], {configs})` """ + id = '' + classes = [] + configs = {} + for k, v in attrs: + if k == 'id': + id = v + elif k == '.': + classes.append(v) + elif k == 'hl_lines': + configs[k] = parse_hl_lines(v) + elif k in self.bool_options: + configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True) + else: + configs[k] = v + return id, classes, configs + + def _escape(self, txt: str) -> str: + """ basic html escaping """ + txt = txt.replace('&', '&') + txt = txt.replace('<', '<') + txt = txt.replace('>', '>') + txt = txt.replace('"', '"') + return txt + + +def makeExtension(**kwargs): # pragma: no cover + return FencedCodeExtension(**kwargs) |