summaryrefslogtreecommitdiff
path: root/sphinx/directives/code.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/directives/code.py')
-rw-r--r--sphinx/directives/code.py205
1 files changed, 166 insertions, 39 deletions
diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py
index 9bfac5a6..aee32fe3 100644
--- a/sphinx/directives/code.py
+++ b/sphinx/directives/code.py
@@ -9,9 +9,13 @@
import sys
import codecs
+from difflib import unified_diff
from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from docutils.statemachine import ViewList
+
+from six import string_types
from sphinx import addnodes
from sphinx.util import parselinenos
@@ -39,11 +43,39 @@ class Highlight(Directive):
except Exception:
linenothreshold = 10
else:
- linenothreshold = sys.maxint
+ linenothreshold = sys.maxsize
return [addnodes.highlightlang(lang=self.arguments[0].strip(),
linenothreshold=linenothreshold)]
+def dedent_lines(lines, dedent):
+ if not dedent:
+ return lines
+
+ new_lines = []
+ for line in lines:
+ new_line = line[dedent:]
+ if line.endswith('\n') and not new_line:
+ new_line = '\n' # keep CRLF
+ new_lines.append(new_line)
+
+ return new_lines
+
+
+def container_wrapper(directive, literal_node, caption):
+ container_node = nodes.container('', literal_block=True)
+ parsed = nodes.Element()
+ directive.state.nested_parse(ViewList([caption], source=''),
+ directive.content_offset, parsed)
+ caption_node = nodes.caption(parsed[0].rawsource, '',
+ *parsed[0].children)
+ caption_node.source = parsed[0].source
+ caption_node.line = parsed[0].line
+ container_node += caption_node
+ container_node += literal_node
+ return container_node
+
+
class CodeBlock(Directive):
"""
Directive for a code block with special highlighting or line numbering
@@ -56,7 +88,10 @@ class CodeBlock(Directive):
final_argument_whitespace = False
option_spec = {
'linenos': directives.flag,
+ 'dedent': int,
+ 'lineno-start': int,
'emphasize-lines': directives.unchanged_required,
+ 'caption': directives.unchanged_required,
}
def run(self):
@@ -67,18 +102,32 @@ class CodeBlock(Directive):
try:
nlines = len(self.content)
hl_lines = [x+1 for x in parselinenos(linespec, nlines)]
- except ValueError, err:
+ except ValueError as err:
document = self.state.document
return [document.reporter.warning(str(err), line=self.lineno)]
else:
hl_lines = None
+ if 'dedent' in self.options:
+ lines = code.split('\n')
+ lines = dedent_lines(lines, self.options['dedent'])
+ code = '\n'.join(lines)
+
literal = nodes.literal_block(code, code)
literal['language'] = self.arguments[0]
- literal['linenos'] = 'linenos' in self.options
+ literal['linenos'] = 'linenos' in self.options or \
+ 'lineno-start' in self.options
+ extra_args = literal['highlight_args'] = {}
if hl_lines is not None:
- literal['highlight_args'] = {'hl_lines': hl_lines}
+ extra_args['hl_lines'] = hl_lines
+ if 'lineno-start' in self.options:
+ extra_args['linenostart'] = self.options['lineno-start']
set_source_info(self, literal)
+
+ caption = self.options.get('caption')
+ if caption:
+ literal = container_wrapper(self, literal, caption)
+
return [literal]
@@ -94,7 +143,10 @@ class LiteralInclude(Directive):
optional_arguments = 0
final_argument_whitespace = True
option_spec = {
+ 'dedent': int,
'linenos': directives.flag,
+ 'lineno-start': int,
+ 'lineno-match': directives.flag,
'tab-width': int,
'language': directives.unchanged_required,
'encoding': directives.encoding,
@@ -105,8 +157,31 @@ class LiteralInclude(Directive):
'prepend': directives.unchanged_required,
'append': directives.unchanged_required,
'emphasize-lines': directives.unchanged_required,
+ 'caption': directives.unchanged,
+ 'diff': directives.unchanged_required,
}
+ def read_with_encoding(self, filename, document, codec_info, encoding):
+ f = None
+ try:
+ f = codecs.StreamReaderWriter(open(filename, 'rb'), codec_info[2],
+ codec_info[3], 'strict')
+ lines = f.readlines()
+ lines = dedent_lines(lines, self.options.get('dedent'))
+ return lines
+ except (IOError, OSError):
+ return [document.reporter.warning(
+ 'Include file %r not found or reading it failed' % filename,
+ line=self.lineno)]
+ except UnicodeError:
+ return [document.reporter.warning(
+ 'Encoding %r used for reading included file %r seems to '
+ 'be wrong, try giving an :encoding: option' %
+ (encoding, filename))]
+ finally:
+ if f is not None:
+ f.close()
+
def run(self):
document = self.state.document
if not document.settings.file_insertion_enabled:
@@ -120,26 +195,41 @@ class LiteralInclude(Directive):
'Cannot use both "pyobject" and "lines" options',
line=self.lineno)]
- encoding = self.options.get('encoding', env.config.source_encoding)
- codec_info = codecs.lookup(encoding)
- f = None
- try:
- f = codecs.StreamReaderWriter(open(filename, 'rb'),
- codec_info[2], codec_info[3], 'strict')
- lines = f.readlines()
- except (IOError, OSError):
+ if 'lineno-match' in self.options and 'lineno-start' in self.options:
return [document.reporter.warning(
- 'Include file %r not found or reading it failed' % filename,
+ 'Cannot use both "lineno-match" and "lineno-start"',
line=self.lineno)]
- except UnicodeError:
+
+ if 'lineno-match' in self.options and \
+ (set(['append', 'prepend']) & set(self.options.keys())):
return [document.reporter.warning(
- 'Encoding %r used for reading included file %r seems to '
- 'be wrong, try giving an :encoding: option' %
- (encoding, filename))]
- finally:
- if f is not None:
- f.close()
+ 'Cannot use "lineno-match" and "append" or "prepend"',
+ line=self.lineno)]
+ encoding = self.options.get('encoding', env.config.source_encoding)
+ codec_info = codecs.lookup(encoding)
+
+ lines = self.read_with_encoding(filename, document,
+ codec_info, encoding)
+ if not isinstance(lines[0], string_types):
+ return lines
+
+ diffsource = self.options.get('diff')
+ if diffsource is not None:
+ tmp, fulldiffsource = env.relfn2path(diffsource)
+
+ difflines = self.read_with_encoding(fulldiffsource, document,
+ codec_info, encoding)
+ if not isinstance(difflines[0], string_types):
+ return difflines
+ diff = unified_diff(
+ difflines,
+ lines,
+ diffsource,
+ self.arguments[0])
+ lines = list(diff)
+
+ linenostart = self.options.get('lineno-start', 1)
objectname = self.options.get('pyobject')
if objectname is not None:
from sphinx.pycode import ModuleAnalyzer
@@ -150,17 +240,30 @@ class LiteralInclude(Directive):
'Object named %r not found in include file %r' %
(objectname, filename), line=self.lineno)]
else:
- lines = lines[tags[objectname][1]-1 : tags[objectname][2]-1]
+ lines = lines[tags[objectname][1]-1: tags[objectname][2]-1]
+ if 'lineno-match' in self.options:
+ linenostart = tags[objectname][1]
linespec = self.options.get('lines')
- if linespec is not None:
+ if linespec:
try:
linelist = parselinenos(linespec, len(lines))
- except ValueError, err:
+ except ValueError as err:
return [document.reporter.warning(str(err), line=self.lineno)]
- # just ignore nonexisting lines
- nlines = len(lines)
- lines = [lines[i] for i in linelist if i < nlines]
+
+ if 'lineno-match' in self.options:
+ # make sure the line list is not "disjoint".
+ previous = linelist[0]
+ for line_number in linelist[1:]:
+ if line_number == previous + 1:
+ previous = line_number
+ continue
+ return [document.reporter.warning(
+ 'Cannot use "lineno-match" with a disjoint set of '
+ '"lines"', line=self.lineno)]
+ linenostart = linelist[0] + 1
+ # just ignore non-existing lines
+ lines = [lines[i] for i in linelist if i < len(lines)]
if not lines:
return [document.reporter.warning(
'Line spec %r: no lines pulled from include file %r' %
@@ -170,50 +273,74 @@ class LiteralInclude(Directive):
if linespec:
try:
hl_lines = [x+1 for x in parselinenos(linespec, len(lines))]
- except ValueError, err:
+ except ValueError as err:
return [document.reporter.warning(str(err), line=self.lineno)]
else:
hl_lines = None
startafter = self.options.get('start-after')
- endbefore = self.options.get('end-before')
- prepend = self.options.get('prepend')
- append = self.options.get('append')
+ endbefore = self.options.get('end-before')
if startafter is not None or endbefore is not None:
use = not startafter
res = []
- for line in lines:
+ for line_number, line in enumerate(lines):
if not use and startafter and startafter in line:
+ if 'lineno-match' in self.options:
+ linenostart += line_number + 1
use = True
elif use and endbefore and endbefore in line:
- use = False
break
elif use:
res.append(line)
lines = res
+ if 'lineno-match' in self.options:
+ # handle that docutils remove preceding lines which only contains
+ # line separation.
+ for line in lines:
+ # check if line contains anything else than line separation.
+ if line and line.splitlines()[0]:
+ break
+ linenostart += 1
+
+ prepend = self.options.get('prepend')
if prepend:
- lines.insert(0, prepend + '\n')
+ lines.insert(0, prepend + '\n')
+
+ append = self.options.get('append')
if append:
- lines.append(append + '\n')
+ lines.append(append + '\n')
text = ''.join(lines)
if self.options.get('tab-width'):
text = text.expandtabs(self.options['tab-width'])
retnode = nodes.literal_block(text, text, source=filename)
set_source_info(self, retnode)
- if self.options.get('language', ''):
+ if diffsource: # if diff is set, set udiff
+ retnode['language'] = 'udiff'
+ if 'language' in self.options:
retnode['language'] = self.options['language']
- if 'linenos' in self.options:
- retnode['linenos'] = True
+ retnode['linenos'] = 'linenos' in self.options or \
+ 'lineno-start' in self.options or \
+ 'lineno-match' in self.options
+ extra_args = retnode['highlight_args'] = {}
if hl_lines is not None:
- retnode['highlight_args'] = {'hl_lines': hl_lines}
+ extra_args['hl_lines'] = hl_lines
+ extra_args['linenostart'] = linenostart
env.note_dependency(rel_filename)
+
+ caption = self.options.get('caption')
+ if caption is not None:
+ if caption:
+ retnode = container_wrapper(self, retnode, caption)
+ else:
+ retnode = container_wrapper(self, retnode, self.arguments[0])
+
return [retnode]
directives.register_directive('highlight', Highlight)
-directives.register_directive('highlightlang', Highlight) # old
+directives.register_directive('highlightlang', Highlight) # old
directives.register_directive('code-block', CodeBlock)
directives.register_directive('sourcecode', CodeBlock)
directives.register_directive('literalinclude', LiteralInclude)