diff options
author | Waylan Limberg <waylan@gmail.com> | 2014-01-07 14:48:07 -0800 |
---|---|---|
committer | Waylan Limberg <waylan@gmail.com> | 2014-01-07 14:48:07 -0800 |
commit | 809195fb900c8e8bd3ff65a2e69de78075224096 (patch) | |
tree | 210eb67b03202906ab375b27d40744365b294fd4 | |
parent | 3e03dd14b3f47f1482e91da155834afc25d6458f (diff) | |
parent | f6b4f7c74dc4215847008cb8a59c03e63b9268e0 (diff) | |
download | markdown-809195fb900c8e8bd3ff65a2e69de78075224096.tar.gz markdown-809195fb900c8e8bd3ff65a2e69de78075224096.tar.bz2 markdown-809195fb900c8e8bd3ff65a2e69de78075224096.zip |
Merge pull request #274 from ajdavis/highlight-lines
Add feature for emphasizing some lines in a code block.
-rw-r--r-- | docs/extensions/code_hilite.txt | 11 | ||||
-rw-r--r-- | docs/extensions/fenced_code_blocks.txt | 25 | ||||
-rw-r--r-- | markdown/extensions/codehilite.py | 38 | ||||
-rw-r--r-- | markdown/extensions/fenced_code.py | 31 | ||||
-rw-r--r-- | tests/test_extensions.py | 84 |
5 files changed, 178 insertions, 11 deletions
diff --git a/docs/extensions/code_hilite.txt b/docs/extensions/code_hilite.txt index ab09742..92f60f8 100644 --- a/docs/extensions/code_hilite.txt +++ b/docs/extensions/code_hilite.txt @@ -91,6 +91,17 @@ block contains and each one has a different result. # Code goes here ... + Certain lines can be selected for emphasis with the colon syntax. By + default, emphasized lines have a yellow background. This is useful to + direct the reader's attention. + + :::python hl_lines="1 3" + # This line is emphasized + # This line isn't + # This line is emphasized + + (`hl_lines` is named for Pygments' "highlighted lines" option.) + * ###When No Language is Defined CodeHilite is completely backward compatible so that if a code block is diff --git a/docs/extensions/fenced_code_blocks.txt b/docs/extensions/fenced_code_blocks.txt index c54c5bd..0148c80 100644 --- a/docs/extensions/fenced_code_blocks.txt +++ b/docs/extensions/fenced_code_blocks.txt @@ -35,6 +35,8 @@ Fenced code blocks can have a blank line as the first and/or last line of a code block and they can also come immediately after a list item without becoming part of the list. +### Language + In addition to PHP Extra's syntax, you can define the language of the code block for use by syntax highlighters etc. The language will be assigned as a class attribute of the ``<code>`` element in the output. Therefore, you should @@ -66,6 +68,29 @@ The above will output: [Github]: http://github.github.com/github-flavored-markdown/ +### Emphasized Lines + +If [Pygments][] is installed, this extension can emphasize certain lines of +code. By default, emphasized lines have a yellow background. This is useful to +direct the reader's attention. The lines can be specified with PHP Extra's +syntax: + + ~~~~{.python hl_lines="1 3"} + # This line is emphasized + # This line isn't + # This line is emphasized + +... or with GitHub's: + + ```python hl_lines="1 3" + # This line is emphasized + # This line isn't + # This line is emphasized + +(`hl_lines` is named for Pygments' "highlighted lines" option.) + +[Pygments]: http://pygments.org/ + Usage ----- diff --git a/markdown/extensions/codehilite.py b/markdown/extensions/codehilite.py index 7a3676b..9f99518 100644 --- a/markdown/extensions/codehilite.py +++ b/markdown/extensions/codehilite.py @@ -31,6 +31,22 @@ try: except ImportError: pygments = False + +def parse_hl_lines(expr): + """Support our syntax for emphasizing certain lines of code. + + expr should be like '1 2' to emphasize lines 1 and 2 of a code block. + Returns a list of ints, the line numbers to emphasize. + """ + if not expr: + return [] + + try: + return map(int, expr.split()) + except ValueError: + return [] + + # ------------------ The Main CodeHilite Class ---------------------- class CodeHilite(object): """ @@ -49,6 +65,8 @@ class CodeHilite(object): * css_class: Set class name of wrapper div ('codehilite' by default). + * hl_lines: (List of integers) Lines to emphasize, 1-indexed. + Low Level Usage: >>> code = CodeHilite() >>> code.src = 'some text' # String or anything with a .readline attr. @@ -59,7 +77,7 @@ class CodeHilite(object): def __init__(self, src=None, linenums=None, guess_lang=True, css_class="codehilite", lang=None, style='default', - noclasses=False, tab_length=4): + noclasses=False, tab_length=4, hl_lines=None): self.src = src self.lang = lang self.linenums = linenums @@ -68,6 +86,7 @@ class CodeHilite(object): self.style = style self.noclasses = noclasses self.tab_length = tab_length + self.hl_lines = hl_lines or [] def hilite(self): """ @@ -83,7 +102,7 @@ class CodeHilite(object): self.src = self.src.strip('\n') if self.lang is None: - self._getLang() + self._parseHeader() if pygments: try: @@ -99,7 +118,8 @@ class CodeHilite(object): formatter = HtmlFormatter(linenos=self.linenums, cssclass=self.css_class, style=self.style, - noclasses=self.noclasses) + noclasses=self.noclasses, + hl_lines=self.hl_lines) return highlight(self.src, lexer, formatter) else: # just escape and build markup usable by JS highlighting libs @@ -118,7 +138,7 @@ class CodeHilite(object): return '<pre class="%s"><code%s>%s</code></pre>\n'% \ (self.css_class, class_str, txt) - def _getLang(self): + def _parseHeader(self): """ Determines language of a code block from shebang line and whether said line should be removed or left in place. If the sheband line contains a @@ -131,6 +151,9 @@ class CodeHilite(object): (e.i.: :::python), line numbering is left in the current state - off by default. + Also parses optional list of highlight lines, like: + + :::python hl_lines="1 3" """ import re @@ -141,9 +164,12 @@ class CodeHilite(object): fl = lines.pop(0) c = re.compile(r''' - (?:(?:^::+)|(?P<shebang>^[#]!)) # Shebang or 2 or more colons. + (?:(?:^::+)|(?P<shebang>^[#]!)) # Shebang or 2 or more colons (?P<path>(?:/\w+)*[/ ])? # Zero or 1 path (?P<lang>[\w+-]*) # The language + \s* # Arbitrary whitespace + # Optional highlight lines, single- or double-quote-delimited + (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))? ''', re.VERBOSE) # search first line for shebang m = c.search(fl) @@ -159,6 +185,8 @@ class CodeHilite(object): if self.linenums is None and m.group('shebang'): # Overridable and Shebang exists - use line numbers self.linenums = True + + self.hl_lines = parse_hl_lines(m.group('hl_lines')) else: # No match lines.insert(0, fl) diff --git a/markdown/extensions/fenced_code.py b/markdown/extensions/fenced_code.py index c5aaa6a..39c6540 100644 --- a/markdown/extensions/fenced_code.py +++ b/markdown/extensions/fenced_code.py @@ -59,6 +59,20 @@ Optionally backticks instead of tildes as per how github's code block markdown i ~~~~~ # these tildes will not close the block </code></pre> +If the codehighlite extension and Pygments are installed, lines can be highlighted: + + >>> text = ''' + ... ```hl_lines="1 3" + ... line 1 + ... line 2 + ... line 3 + ... ```''' + >>> print markdown.markdown(text, extensions=['codehilite', 'fenced_code']) + <pre><code><span class="hilight">line 1</span> + line 2 + <span class="hilight">line 3</span> + </code></pre> + Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/). Project website: <http://packages.python.org/Markdown/extensions/fenced_code_blocks.html> @@ -77,7 +91,7 @@ from __future__ import absolute_import from __future__ import unicode_literals from . import Extension from ..preprocessors import Preprocessor -from .codehilite import CodeHilite, CodeHiliteExtension +from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines import re @@ -93,10 +107,14 @@ class FencedCodeExtension(Extension): class FencedBlockPreprocessor(Preprocessor): - FENCED_BLOCK_RE = re.compile( \ - r'(?P<fence>^(?:~{3,}|`{3,}))[ ]*(\{?\.?(?P<lang>[a-zA-Z0-9_+-]*)\}?)?[ ]*\n(?P<code>.*?)(?<=\n)(?P=fence)[ ]*$', - re.MULTILINE|re.DOTALL - ) + FENCED_BLOCK_RE = re.compile(r''' +(?P<fence>^(?:~{3,}|`{3,}))[ ]* # Opening ``` or ~~~ +(\{?\.?(?P<lang>[a-zA-Z0-9_+-]*))?[ ]* # Optional {, and lang +# Optional highlight lines, single- or double-quote-delimited +(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?[ ]* +}?[ ]*\n # Optional closing } +(?P<code>.*?)(?<=\n) +(?P=fence)[ ]*$''', re.MULTILINE | re.DOTALL | re.VERBOSE) CODE_WRAP = '<pre><code%s>%s</code></pre>' LANG_TAG = ' class="%s"' @@ -135,7 +153,8 @@ class FencedBlockPreprocessor(Preprocessor): css_class=self.codehilite_conf['css_class'][0], style=self.codehilite_conf['pygments_style'][0], lang=(m.group('lang') or None), - noclasses=self.codehilite_conf['noclasses'][0]) + noclasses=self.codehilite_conf['noclasses'][0], + hl_lines=parse_hl_lines(m.group('hl_lines'))) code = highliter.hilite() else: diff --git a/tests/test_extensions.py b/tests/test_extensions.py index add759a..d33feec 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -126,12 +126,37 @@ class TestCodeHilite(unittest.TestCase): '<pre class="codehilite"><code class="language-python"># A Code Comment' '</code></pre>') + def testHighlightLinesWithColon(self): + # Test with hl_lines delimited by single or double quotes. + text0 = '\t:::Python hl_lines="2"\n\t#line 1\n\t#line 2\n\t#line 3' + text1 = "\t:::Python hl_lines='2'\n\t#line 1\n\t#line 2\n\t#line 3" + + for text in (text0, text1): + md = markdown.Markdown(extensions=['codehilite']) + if self.has_pygments: + self.assertEqual(md.convert(text), + '<div class="codehilite"><pre>' + '<span class="c">#line 1</span>\n' + '<span class="hll"><span class="c">#line 2</span>\n</span>' + '<span class="c">#line 3</span>\n' + '</pre></div>') + else: + self.assertEqual(md.convert(text), + '<pre class="codehilite">' + '<code class="language-python">#line 1\n' + '#line 2\n' + '#line 3</code></pre>') class TestFencedCode(unittest.TestCase): """ Test fenced_code extension. """ def setUp(self): self.md = markdown.Markdown(extensions=['fenced_code']) + self.has_pygments = True + try: + import pygments + except ImportError: + self.has_pygments = False def testBasicFence(self): """ Test Fenced Code Blocks. """ @@ -191,6 +216,65 @@ Fenced code block '~~~~~ # these tildes will not close the block\n' '</code></pre>') + def testFencedCodeWithHighlightLines(self): + """ Test Fenced Code with Highlighted Lines. """ + + text = ''' +```hl_lines="1 3" +line 1 +line 2 +line 3 +```''' + md = markdown.Markdown(extensions=[ + 'codehilite(linenums=None,guess_lang=False)', + 'fenced_code']) + + if self.has_pygments: + self.assertEqual(md.convert(text), + '<div class="codehilite"><pre>' + '<span class="hll">line 1\n</span>' + 'line 2\n' + '<span class="hll">line 3\n</span>' + '</pre></div>') + else: + self.assertEqual(md.convert(text), + '<pre class="codehilite"><code>line 1\n' + 'line 2\n' + 'line 3</code></pre>') + + def testFencedLanguageAndHighlightLines(self): + """ Test Fenced Code with Highlighted Lines. """ + + text0 = ''' +```.python hl_lines="1 3" +#line 1 +#line 2 +#line 3 +```''' + text1 = ''' +~~~{.python hl_lines='1 3'} +#line 1 +#line 2 +#line 3 +~~~''' + for text in (text0, text1): + md = markdown.Markdown(extensions=[ + 'codehilite(linenums=None,guess_lang=False)', + 'fenced_code']) + + if self.has_pygments: + self.assertEqual(md.convert(text), + '<div class="codehilite"><pre>' + '<span class="hll"><span class="c">#line 1</span>\n</span>' + '<span class="c">#line 2</span>\n' + '<span class="hll"><span class="c">#line 3</span>\n</span>' + '</pre></div>') + else: + self.assertEqual(md.convert(text), + '<pre class="codehilite"><code class="language-python">#line 1\n' + '#line 2\n' + '#line 3</code></pre>') + class TestHeaderId(unittest.TestCase): """ Test HeaderId Extension. """ |