The blog.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

183 lines
7.3 KiB

  1. """
  2. Fenced Code Extension for Python Markdown
  3. =========================================
  4. This extension adds Fenced Code Blocks to Python-Markdown.
  5. See <https://Python-Markdown.github.io/extensions/fenced_code_blocks>
  6. for documentation.
  7. Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
  8. All changes Copyright 2008-2014 The Python Markdown Project
  9. License: [BSD](https://opensource.org/licenses/bsd-license.php)
  10. """
  11. from textwrap import dedent
  12. from . import Extension
  13. from ..preprocessors import Preprocessor
  14. from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
  15. from .attr_list import get_attrs, AttrListExtension
  16. from ..util import parseBoolValue
  17. import re
  18. class FencedCodeExtension(Extension):
  19. def __init__(self, **kwargs):
  20. self.config = {
  21. 'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"']
  22. }
  23. super().__init__(**kwargs)
  24. def extendMarkdown(self, md):
  25. """ Add FencedBlockPreprocessor to the Markdown instance. """
  26. md.registerExtension(self)
  27. md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25)
  28. class FencedBlockPreprocessor(Preprocessor):
  29. FENCED_BLOCK_RE = re.compile(
  30. dedent(r'''
  31. (?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence
  32. ((\{(?P<attrs>[^\}\n]*)\})?| # (optional {attrs} or
  33. (\.?(?P<lang>[\w#.+-]*))?[ ]* # optional (.)lang
  34. (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?) # optional hl_lines)
  35. [ ]*\n # newline (end of opening fence)
  36. (?P<code>.*?)(?<=\n) # the code block
  37. (?P=fence)[ ]*$ # closing fence
  38. '''),
  39. re.MULTILINE | re.DOTALL | re.VERBOSE
  40. )
  41. def __init__(self, md, config):
  42. super().__init__(md)
  43. self.config = config
  44. self.checked_for_deps = False
  45. self.codehilite_conf = {}
  46. self.use_attr_list = False
  47. # List of options to convert to bool values
  48. self.bool_options = [
  49. 'linenums',
  50. 'guess_lang',
  51. 'noclasses',
  52. 'use_pygments'
  53. ]
  54. def run(self, lines):
  55. """ Match and store Fenced Code Blocks in the HtmlStash. """
  56. # Check for dependent extensions
  57. if not self.checked_for_deps:
  58. for ext in self.md.registeredExtensions:
  59. if isinstance(ext, CodeHiliteExtension):
  60. self.codehilite_conf = ext.getConfigs()
  61. if isinstance(ext, AttrListExtension):
  62. self.use_attr_list = True
  63. self.checked_for_deps = True
  64. text = "\n".join(lines)
  65. while 1:
  66. m = self.FENCED_BLOCK_RE.search(text)
  67. if m:
  68. lang, id, classes, config = None, '', [], {}
  69. if m.group('attrs'):
  70. id, classes, config = self.handle_attrs(get_attrs(m.group('attrs')))
  71. if len(classes):
  72. lang = classes.pop(0)
  73. else:
  74. if m.group('lang'):
  75. lang = m.group('lang')
  76. if m.group('hl_lines'):
  77. # Support hl_lines outside of attrs for backward-compatibility
  78. config['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
  79. # If config is not empty, then the codehighlite extension
  80. # is enabled, so we call it to highlight the code
  81. if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True):
  82. local_config = self.codehilite_conf.copy()
  83. local_config.update(config)
  84. # Combine classes with cssclass. Ensure cssclass is at end
  85. # as pygments appends a suffix under certain circumstances.
  86. # Ignore ID as Pygments does not offer an option to set it.
  87. if classes:
  88. local_config['css_class'] = '{} {}'.format(
  89. ' '.join(classes),
  90. local_config['css_class']
  91. )
  92. highliter = CodeHilite(
  93. m.group('code'),
  94. lang=lang,
  95. style=local_config.pop('pygments_style', 'default'),
  96. **local_config
  97. )
  98. code = highliter.hilite()
  99. else:
  100. id_attr = lang_attr = class_attr = kv_pairs = ''
  101. if lang:
  102. lang_attr = ' class="{}{}"'.format(self.config.get('lang_prefix', 'language-'), lang)
  103. if classes:
  104. class_attr = ' class="{}"'.format(' '.join(classes))
  105. if id:
  106. id_attr = ' id="{}"'.format(id)
  107. if self.use_attr_list and config and not config.get('use_pygments', False):
  108. # Only assign key/value pairs to code element if attr_list ext is enabled, key/value pairs
  109. # were defined on the code block, and the `use_pygments` key was not set to True. The
  110. # `use_pygments` key could be either set to False or not defined. It is omitted from output.
  111. kv_pairs = ' ' + ' '.join(
  112. '{k}="{v}"'.format(k=k, v=v) for k, v in config.items() if k != 'use_pygments'
  113. )
  114. codeblk = ''.join('<code{lang}{kv}>{line}</code>'.format(
  115. kv=kv_pairs,
  116. lang=lang_attr,
  117. line=self._escape(line)
  118. ) for line in m.group('code').rstrip().split('\n'))
  119. code = '<pre{id}{cls}>{codeblk}</pre>'.format(
  120. id=id_attr,
  121. codeblk=codeblk,
  122. cls=class_attr,
  123. )
  124. placeholder = self.md.htmlStash.store(code)
  125. text = '{}\n{}\n{}'.format(text[:m.start()],
  126. placeholder,
  127. text[m.end():])
  128. else:
  129. break
  130. return text.split("\n")
  131. def handle_attrs(self, attrs):
  132. """ Return tuple: (id, [list, of, classes], {configs}) """
  133. id = ''
  134. classes = []
  135. configs = {}
  136. for k, v in attrs:
  137. if k == 'id':
  138. id = v
  139. elif k == '.':
  140. classes.append(v)
  141. elif k == 'hl_lines':
  142. configs[k] = parse_hl_lines(v)
  143. elif k in self.bool_options:
  144. configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True)
  145. else:
  146. configs[k] = v
  147. return id, classes, configs
  148. def _escape(self, txt):
  149. """ basic html escaping """
  150. txt = txt.replace('&', '&amp;')
  151. txt = txt.replace('<', '&lt;')
  152. txt = txt.replace('>', '&gt;')
  153. txt = txt.replace('"', '&quot;')
  154. return txt
  155. def makeExtension(**kwargs): # pragma: no cover
  156. return FencedCodeExtension(**kwargs)