Find and extract fenced code blocks.
| 59 | |
| 60 | |
| 61 | class FencedBlockPreprocessor(Preprocessor): |
| 62 | """ Find and extract fenced code blocks. """ |
| 63 | |
| 64 | FENCED_BLOCK_RE = re.compile( |
| 65 | dedent(r''' |
| 66 | (?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence |
| 67 | ((\{(?P<attrs>[^\n]*)\})| # (optional {attrs} or |
| 68 | (\.?(?P<lang>[\w#.+-]*)[ ]*)? # optional (.)lang |
| 69 | (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines) |
| 70 | \n # newline (end of opening fence) |
| 71 | (?P<code>.*?)(?<=\n) # the code block |
| 72 | (?P=fence)[ ]*$ # closing fence |
| 73 | '''), |
| 74 | re.MULTILINE | re.DOTALL | re.VERBOSE |
| 75 | ) |
| 76 | |
| 77 | def __init__(self, md: Markdown, config: dict[str, Any]): |
| 78 | super().__init__(md) |
| 79 | self.config = config |
| 80 | self.checked_for_deps = False |
| 81 | self.codehilite_conf: dict[str, Any] = {} |
| 82 | self.use_attr_list = False |
| 83 | # List of options to convert to boolean values |
| 84 | self.bool_options = [ |
| 85 | 'linenums', |
| 86 | 'guess_lang', |
| 87 | 'noclasses', |
| 88 | 'use_pygments' |
| 89 | ] |
| 90 | |
| 91 | def run(self, lines: list[str]) -> list[str]: |
| 92 | """ Match and store Fenced Code Blocks in the `HtmlStash`. """ |
| 93 | |
| 94 | # Check for dependent extensions |
| 95 | if not self.checked_for_deps: |
| 96 | for ext in self.md.registeredExtensions: |
| 97 | if isinstance(ext, CodeHiliteExtension): |
| 98 | self.codehilite_conf = ext.getConfigs() |
| 99 | if isinstance(ext, AttrListExtension): |
| 100 | self.use_attr_list = True |
| 101 | |
| 102 | self.checked_for_deps = True |
| 103 | |
| 104 | text = "\n".join(lines) |
| 105 | index = 0 |
| 106 | while 1: |
| 107 | m = self.FENCED_BLOCK_RE.search(text, index) |
| 108 | if m: |
| 109 | lang, id, classes, config = None, '', [], {} |
| 110 | if m.group('attrs'): |
| 111 | attrs, remainder = get_attrs_and_remainder(m.group('attrs')) |
| 112 | if remainder: # Does not have correctly matching curly braces, so the syntax is invalid. |
| 113 | index = m.end('attrs') # Explicitly skip over this, to prevent an infinite loop. |
| 114 | continue |
| 115 | id, classes, config = self.handle_attrs(attrs) |
| 116 | if len(classes): |
| 117 | lang = classes.pop(0) |
| 118 | else: |