codehilite.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. # markdown is released under the BSD license
  2. # Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later)
  3. # Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
  4. # Copyright 2004 Manfred Stienstra (the original version)
  5. #
  6. # All rights reserved.
  7. #
  8. # Redistribution and use in source and binary forms, with or without
  9. # modification, are permitted provided that the following conditions are met:
  10. #
  11. # * Redistributions of source code must retain the above copyright
  12. # notice, this list of conditions and the following disclaimer.
  13. # * Redistributions in binary form must reproduce the above copyright
  14. # notice, this list of conditions and the following disclaimer in the
  15. # documentation and/or other materials provided with the distribution.
  16. # * Neither the name of the <organization> nor the
  17. # names of its contributors may be used to endorse or promote products
  18. # derived from this software without specific prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY
  21. # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  22. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  23. # DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT
  24. # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  25. # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  26. # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  27. # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  28. # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  29. # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  30. # POSSIBILITY OF SUCH DAMAGE.
  31. """
  32. CodeHilite Extension for Python-Markdown
  33. ========================================
  34. Adds code/syntax highlighting to standard Python-Markdown code blocks.
  35. Copyright 2006-2008 [Waylan Limberg](http://achinghead.com/).
  36. Project website: <http://packages.python.org/Markdown/extensions/code_hilite.html>
  37. Contact: markdown@freewisdom.org
  38. License: BSD (see ../LICENSE.md for details)
  39. Dependencies:
  40. * [Python 2.3+](http://python.org/)
  41. * [Markdown 2.0+](http://packages.python.org/Markdown/)
  42. * [Pygments](http://pygments.org/)
  43. """
  44. from __future__ import absolute_import
  45. from __future__ import unicode_literals
  46. from . import Extension
  47. from ..treeprocessors import Treeprocessor
  48. import warnings
  49. try:
  50. from pygments import highlight
  51. from pygments.lexers import get_lexer_by_name, guess_lexer, TextLexer
  52. from pygments.formatters import HtmlFormatter
  53. pygments = True
  54. except ImportError:
  55. pygments = False
  56. # ------------------ The Main CodeHilite Class ----------------------
  57. class CodeHilite(object):
  58. """
  59. Determine language of source code, and pass it into the pygments hilighter.
  60. Basic Usage:
  61. >>> code = CodeHilite(src = 'some text')
  62. >>> html = code.hilite()
  63. * src: Source string or any object with a .readline attribute.
  64. * linenums: (Boolean) Set line numbering to 'on' (True), 'off' (False) or 'auto'(None).
  65. Set to 'auto' by default.
  66. * guess_lang: (Boolean) Turn language auto-detection 'on' or 'off' (on by default).
  67. * css_class: Set class name of wrapper div ('codehilite' by default).
  68. Low Level Usage:
  69. >>> code = CodeHilite()
  70. >>> code.src = 'some text' # String or anything with a .readline attr.
  71. >>> code.linenos = True # True or False; Turns line numbering on or of.
  72. >>> html = code.hilite()
  73. """
  74. def __init__(self, src=None, linenums=None, guess_lang=True,
  75. css_class="codehilite", lang=None, style='default',
  76. noclasses=False, tab_length=4):
  77. self.src = src
  78. self.lang = lang
  79. self.linenums = linenums
  80. self.guess_lang = guess_lang
  81. self.css_class = css_class
  82. self.style = style
  83. self.noclasses = noclasses
  84. self.tab_length = tab_length
  85. def hilite(self):
  86. """
  87. Pass code to the [Pygments](http://pygments.pocoo.org/) highliter with
  88. optional line numbers. The output should then be styled with css to
  89. your liking. No styles are applied by default - only styling hooks
  90. (i.e.: <span class="k">).
  91. returns : A string of html.
  92. """
  93. self.src = self.src.strip('\n')
  94. if self.lang is None:
  95. self._getLang()
  96. if pygments:
  97. try:
  98. lexer = get_lexer_by_name(self.lang)
  99. except ValueError:
  100. try:
  101. if self.guess_lang:
  102. lexer = guess_lexer(self.src)
  103. else:
  104. lexer = TextLexer()
  105. except ValueError:
  106. lexer = TextLexer()
  107. formatter = HtmlFormatter(linenos=self.linenums,
  108. cssclass=self.css_class,
  109. style=self.style,
  110. noclasses=self.noclasses)
  111. return highlight(self.src, lexer, formatter)
  112. else:
  113. # just escape and build markup usable by JS highlighting libs
  114. txt = self.src.replace('&', '&amp;')
  115. txt = txt.replace('<', '&lt;')
  116. txt = txt.replace('>', '&gt;')
  117. txt = txt.replace('"', '&quot;')
  118. classes = []
  119. if self.lang:
  120. classes.append('language-%s' % self.lang)
  121. if self.linenums:
  122. classes.append('linenums')
  123. class_str = ''
  124. if classes:
  125. class_str = ' class="%s"' % ' '.join(classes)
  126. return '<pre class="%s"><code%s>%s</code></pre>\n'% \
  127. (self.css_class, class_str, txt)
  128. def _getLang(self):
  129. """
  130. Determines language of a code block from shebang line and whether said
  131. line should be removed or left in place. If the sheband line contains a
  132. path (even a single /) then it is assumed to be a real shebang line and
  133. left alone. However, if no path is given (e.i.: #!python or :::python)
  134. then it is assumed to be a mock shebang for language identifitation of a
  135. code fragment and removed from the code block prior to processing for
  136. code highlighting. When a mock shebang (e.i: #!python) is found, line
  137. numbering is turned on. When colons are found in place of a shebang
  138. (e.i.: :::python), line numbering is left in the current state - off
  139. by default.
  140. """
  141. import re
  142. #split text into lines
  143. lines = self.src.split("\n")
  144. #pull first line to examine
  145. fl = lines.pop(0)
  146. c = re.compile(r'''
  147. (?:(?:^::+)|(?P<shebang>^[#]!)) # Shebang or 2 or more colons.
  148. (?P<path>(?:/\w+)*[/ ])? # Zero or 1 path
  149. (?P<lang>[\w+-]*) # The language
  150. ''', re.VERBOSE)
  151. # search first line for shebang
  152. m = c.search(fl)
  153. if m:
  154. # we have a match
  155. try:
  156. self.lang = m.group('lang').lower()
  157. except IndexError:
  158. self.lang = None
  159. if m.group('path'):
  160. # path exists - restore first line
  161. lines.insert(0, fl)
  162. if self.linenums is None and m.group('shebang'):
  163. # Overridable and Shebang exists - use line numbers
  164. self.linenums = True
  165. else:
  166. # No match
  167. lines.insert(0, fl)
  168. self.src = "\n".join(lines).strip("\n")
  169. # ------------------ The Markdown Extension -------------------------------
  170. class HiliteTreeprocessor(Treeprocessor):
  171. """ Hilight source code in code blocks. """
  172. def run(self, root):
  173. """ Find code blocks and store in htmlStash. """
  174. blocks = root.getiterator('pre')
  175. for block in blocks:
  176. children = block.getchildren()
  177. if len(children) == 1 and children[0].tag == 'code':
  178. code = CodeHilite(children[0].text,
  179. linenums=self.config['linenums'],
  180. guess_lang=self.config['guess_lang'],
  181. css_class=self.config['css_class'],
  182. style=self.config['pygments_style'],
  183. noclasses=self.config['noclasses'],
  184. tab_length=self.markdown.tab_length)
  185. placeholder = self.markdown.htmlStash.store(code.hilite(),
  186. safe=True)
  187. # Clear codeblock in etree instance
  188. block.clear()
  189. # Change to p element which will later
  190. # be removed when inserting raw html
  191. block.tag = 'p'
  192. block.text = placeholder
  193. class CodeHiliteExtension(Extension):
  194. """ Add source code hilighting to markdown codeblocks. """
  195. def __init__(self, configs):
  196. # define default configs
  197. self.config = {
  198. 'linenums': [None, "Use lines numbers. True=yes, False=no, None=auto"],
  199. 'force_linenos' : [False, "Depreciated! Use 'linenums' instead. Force line numbers - Default: False"],
  200. 'guess_lang' : [True, "Automatic language detection - Default: True"],
  201. 'css_class' : ["codehilite",
  202. "Set class name for wrapper <div> - Default: codehilite"],
  203. 'pygments_style' : ['default', 'Pygments HTML Formatter Style (Colorscheme) - Default: default'],
  204. 'noclasses': [False, 'Use inline styles instead of CSS classes - Default false']
  205. }
  206. # Override defaults with user settings
  207. for key, value in configs:
  208. # convert strings to booleans
  209. if value == 'True': value = True
  210. if value == 'False': value = False
  211. if value == 'None': value = None
  212. if key == 'force_linenos':
  213. warnings.warn('The "force_linenos" config setting'
  214. ' to the CodeHilite extension is deprecrecated.'
  215. ' Use "linenums" instead.', PendingDeprecationWarning)
  216. if value:
  217. # Carry 'force_linenos' over to new 'linenos'.
  218. self.setConfig('linenums', True)
  219. self.setConfig(key, value)
  220. def extendMarkdown(self, md, md_globals):
  221. """ Add HilitePostprocessor to Markdown instance. """
  222. hiliter = HiliteTreeprocessor(md)
  223. hiliter.config = self.getConfigs()
  224. md.treeprocessors.add("hilite", hiliter, "<inline")
  225. md.registerExtension(self)
  226. def makeExtension(configs={}):
  227. return CodeHiliteExtension(configs=configs)