mirror of https://github.com/borgbackup/borg.git
216 lines
6.8 KiB
Python
216 lines
6.8 KiB
Python
import io
|
|
import sys
|
|
|
|
from . 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)
|