fenced_code.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  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 . import Extension
  12. from ..preprocessors import Preprocessor
  13. from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
  14. import re
  15. class FencedCodeExtension(Extension):
  16. def extendMarkdown(self, md):
  17. """ Add FencedBlockPreprocessor to the Markdown instance. """
  18. md.registerExtension(self)
  19. md.preprocessors.register(FencedBlockPreprocessor(md), 'fenced_code_block', 25)
  20. class FencedBlockPreprocessor(Preprocessor):
  21. FENCED_BLOCK_RE = re.compile(r'''
  22. (?P<fence>^(?:~{3,}|`{3,}))[ ]* # Opening ``` or ~~~
  23. (\{?\.?(?P<lang>[\w#.+-]*))?[ ]* # Optional {, and lang
  24. # Optional highlight lines, single- or double-quote-delimited
  25. (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?[ ]*
  26. }?[ ]*\n # Optional closing }
  27. (?P<code>.*?)(?<=\n)
  28. (?P=fence)[ ]*$''', re.MULTILINE | re.DOTALL | re.VERBOSE)
  29. CODE_WRAP = '<pre><code%s>%s</code></pre>'
  30. LANG_TAG = ' class="%s"'
  31. def __init__(self, md):
  32. super().__init__(md)
  33. self.checked_for_codehilite = False
  34. self.codehilite_conf = {}
  35. def run(self, lines):
  36. """ Match and store Fenced Code Blocks in the HtmlStash. """
  37. # Check for code hilite extension
  38. if not self.checked_for_codehilite:
  39. for ext in self.md.registeredExtensions:
  40. if isinstance(ext, CodeHiliteExtension):
  41. self.codehilite_conf = ext.config
  42. break
  43. self.checked_for_codehilite = True
  44. text = "\n".join(lines)
  45. while 1:
  46. m = self.FENCED_BLOCK_RE.search(text)
  47. if m:
  48. lang = ''
  49. if m.group('lang'):
  50. lang = self.LANG_TAG % m.group('lang')
  51. # If config is not empty, then the codehighlite extension
  52. # is enabled, so we call it to highlight the code
  53. if self.codehilite_conf:
  54. highliter = CodeHilite(
  55. m.group('code'),
  56. linenums=self.codehilite_conf['linenums'][0],
  57. guess_lang=self.codehilite_conf['guess_lang'][0],
  58. css_class=self.codehilite_conf['css_class'][0],
  59. style=self.codehilite_conf['pygments_style'][0],
  60. use_pygments=self.codehilite_conf['use_pygments'][0],
  61. lang=(m.group('lang') or None),
  62. noclasses=self.codehilite_conf['noclasses'][0],
  63. hl_lines=parse_hl_lines(m.group('hl_lines'))
  64. )
  65. code = highliter.hilite()
  66. else:
  67. code = self.CODE_WRAP % (lang,
  68. self._escape(m.group('code')))
  69. placeholder = self.md.htmlStash.store(code)
  70. text = '{}\n{}\n{}'.format(text[:m.start()],
  71. placeholder,
  72. text[m.end():])
  73. else:
  74. break
  75. return text.split("\n")
  76. def _escape(self, txt):
  77. """ basic html escaping """
  78. txt = txt.replace('&', '&amp;')
  79. txt = txt.replace('<', '&lt;')
  80. txt = txt.replace('>', '&gt;')
  81. txt = txt.replace('"', '&quot;')
  82. return txt
  83. def makeExtension(**kwargs): # pragma: no cover
  84. return FencedCodeExtension(**kwargs)