diff options
Diffstat (limited to 'sphinx/directives/code.py')
-rw-r--r-- | sphinx/directives/code.py | 205 |
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) |