mirror of https://github.com/borgbackup/borg.git
214 lines
6.8 KiB
Python
214 lines
6.8 KiB
Python
import io
|
|
import sys
|
|
|
|
from .helpers import is_terminal
|
|
|
|
|
|
class TextPecker:
|
|
def __init__(self, s):
|
|
self.str = s
|
|
self.i = 0
|
|
|
|
def read(self, n):
|
|
self.i += n
|
|
return self.str[self.i - n:self.i]
|
|
|
|
def peek(self, n):
|
|
if n >= 0:
|
|
return self.str[self.i:self.i + n]
|
|
else:
|
|
return self.str[self.i + n - 1:self.i - 1]
|
|
|
|
def peekline(self):
|
|
out = ''
|
|
i = self.i
|
|
while i < len(self.str) and self.str[i] != '\n':
|
|
out += self.str[i]
|
|
i += 1
|
|
return out
|
|
|
|
def readline(self):
|
|
out = self.peekline()
|
|
self.i += len(out)
|
|
return out
|
|
|
|
|
|
def process_directive(directive, arguments, out, state_hook):
|
|
if directive == 'container' and arguments == 'experimental':
|
|
state_hook('text', '**', out)
|
|
out.write('++ Experimental ++')
|
|
state_hook('**', 'text', out)
|
|
else:
|
|
state_hook('text', '**', out)
|
|
out.write(directive.title())
|
|
out.write(':\n')
|
|
state_hook('**', 'text', out)
|
|
if arguments:
|
|
out.write(arguments)
|
|
out.write('\n')
|
|
|
|
|
|
def rst_to_text(text, state_hook=None, references=None):
|
|
"""
|
|
Convert rST to a more human text form.
|
|
|
|
This is a very loose conversion. No advanced rST features are supported.
|
|
The generated output directly depends on the input (e.g. indentation of
|
|
admonitions).
|
|
"""
|
|
state_hook = state_hook or (lambda old_state, new_state, out: None)
|
|
references = references or {}
|
|
state = 'text'
|
|
inline_mode = 'replace'
|
|
text = TextPecker(text)
|
|
out = io.StringIO()
|
|
|
|
inline_single = ('*', '`')
|
|
|
|
while True:
|
|
char = text.read(1)
|
|
if not char:
|
|
break
|
|
next = text.peek(1) # type: str
|
|
|
|
if state == 'text':
|
|
if char == '\\' and text.peek(1) in inline_single:
|
|
continue
|
|
if text.peek(-1) != '\\':
|
|
if char in inline_single and next != char:
|
|
state_hook(state, char, out)
|
|
state = char
|
|
continue
|
|
if char == next == '*':
|
|
state_hook(state, '**', out)
|
|
state = '**'
|
|
text.read(1)
|
|
continue
|
|
if char == next == '`':
|
|
state_hook(state, '``', out)
|
|
state = '``'
|
|
text.read(1)
|
|
continue
|
|
if text.peek(-1).isspace() and char == ':' and text.peek(5) == 'ref:`':
|
|
# translate reference
|
|
text.read(5)
|
|
ref = ''
|
|
while True:
|
|
char = text.peek(1)
|
|
if char == '`':
|
|
text.read(1)
|
|
break
|
|
if char == '\n':
|
|
text.read(1)
|
|
continue # merge line breaks in :ref:`...\n...`
|
|
ref += text.read(1)
|
|
try:
|
|
out.write(references[ref])
|
|
except KeyError:
|
|
raise ValueError("Undefined reference in Archiver help: %r — please add reference substitution"
|
|
"to 'rst_plain_text_references'" % ref)
|
|
continue
|
|
if char == ':' and text.peek(2) == ':\n': # End of line code block
|
|
text.read(2)
|
|
state_hook(state, 'code-block', out)
|
|
state = 'code-block'
|
|
out.write(':\n')
|
|
continue
|
|
if text.peek(-2) in ('\n\n', '') and char == next == '.':
|
|
text.read(2)
|
|
directive, is_directive, arguments = text.readline().partition('::')
|
|
text.read(1)
|
|
if not is_directive:
|
|
# partition: if the separator is not in the text, the leftmost output is the entire input
|
|
if directive == 'nanorst: inline-fill':
|
|
inline_mode = 'fill'
|
|
elif directive == 'nanorst: inline-replace':
|
|
inline_mode = 'replace'
|
|
continue
|
|
process_directive(directive, arguments.strip(), out, state_hook)
|
|
continue
|
|
if state in inline_single and char == state:
|
|
state_hook(state, 'text', out)
|
|
state = 'text'
|
|
if inline_mode == 'fill':
|
|
out.write(2 * ' ')
|
|
continue
|
|
if state == '``' and char == next == '`':
|
|
state_hook(state, 'text', out)
|
|
state = 'text'
|
|
text.read(1)
|
|
if inline_mode == 'fill':
|
|
out.write(4 * ' ')
|
|
continue
|
|
if state == '**' and char == next == '*':
|
|
state_hook(state, 'text', out)
|
|
state = 'text'
|
|
text.read(1)
|
|
continue
|
|
if state == 'code-block' and char == next == '\n' and text.peek(5)[1:] != ' ':
|
|
# Foo::
|
|
#
|
|
# *stuff* *code* *ignore .. all markup*
|
|
#
|
|
# More arcane stuff
|
|
#
|
|
# Regular text...
|
|
state_hook(state, 'text', out)
|
|
state = 'text'
|
|
out.write(char)
|
|
|
|
assert state == 'text', 'Invalid final state %r (This usually indicates unmatched */**)' % state
|
|
return out.getvalue()
|
|
|
|
|
|
class RstToTextLazy:
|
|
def __init__(self, str, state_hook=None, references=None):
|
|
self.str = str
|
|
self.state_hook = state_hook
|
|
self.references = references
|
|
self._rst = None
|
|
|
|
@property
|
|
def rst(self):
|
|
if self._rst is None:
|
|
self._rst = rst_to_text(self.str, self.state_hook, self.references)
|
|
return self._rst
|
|
|
|
def __getattr__(self, item):
|
|
return getattr(self.rst, item)
|
|
|
|
def __str__(self):
|
|
return self.rst
|
|
|
|
def __add__(self, other):
|
|
return self.rst + other
|
|
|
|
def __iter__(self):
|
|
return iter(self.rst)
|
|
|
|
def __contains__(self, item):
|
|
return item in self.rst
|
|
|
|
|
|
def ansi_escapes(old_state, new_state, out):
|
|
if old_state == 'text' and new_state in ('*', '`', '``'):
|
|
out.write('\033[4m')
|
|
if old_state == 'text' and new_state == '**':
|
|
out.write('\033[1m')
|
|
if old_state in ('*', '`', '``', '**') and new_state == 'text':
|
|
out.write('\033[0m')
|
|
|
|
|
|
def rst_to_terminal(rst, references=None, destination=sys.stdout):
|
|
"""
|
|
Convert *rst* to a lazy string.
|
|
|
|
If *destination* is a file-like object connected to a terminal,
|
|
enrich text with suitable ANSI escapes. Otherwise return plain text.
|
|
"""
|
|
if is_terminal(destination):
|
|
rst_state_hook = ansi_escapes
|
|
else:
|
|
rst_state_hook = None
|
|
return RstToTextLazy(rst, rst_state_hook, references)
|