641 lines
29 KiB
Python
641 lines
29 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
reStructuredText parser for rich
|
|
|
|
Initial few lines gotten from: https://github.com/willmcgugan/rich/discussions/1263#discussioncomment-808898
|
|
There are a lot of improvements are added by me
|
|
"""
|
|
from io import StringIO
|
|
from html.parser import HTMLParser
|
|
from typing import Optional, Union
|
|
|
|
# Imports from docutils package for the parsing
|
|
import docutils.io
|
|
import docutils.nodes
|
|
import docutils.parsers.rst
|
|
import docutils.utils
|
|
|
|
# Imports from the rich package for the printing
|
|
import rich
|
|
from rich import box
|
|
from rich.align import Align
|
|
from rich.console import Console, ConsoleOptions, RenderResult, NewLine
|
|
from rich.jupyter import JupyterMixin
|
|
from rich.panel import Panel
|
|
from rich.style import Style
|
|
from rich.syntax import Syntax, SyntaxTheme
|
|
from rich.text import Text
|
|
from rich.table import Table
|
|
from rich.traceback import install
|
|
from rich.rule import Rule
|
|
|
|
from pygments.lexers import guess_lexer
|
|
from pygments.util import ClassNotFound
|
|
|
|
__all__ = ("RST", "ReStructuredText", "reStructuredText", "RestructuredText")
|
|
__author__ = "Arian Mollik Wasi (aka. Wasi Master)"
|
|
__version__ = "1.3.1"
|
|
|
|
install()
|
|
|
|
|
|
class MLStripper(HTMLParser):
|
|
"""Utility class to strip out html for raw html source"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.reset()
|
|
self.strict = False
|
|
self.convert_charrefs = True
|
|
self.text = StringIO()
|
|
|
|
def handle_data(self, d):
|
|
self.text.write(d)
|
|
|
|
def get_data(self):
|
|
return self.text.getvalue()
|
|
|
|
|
|
def strip_tags(html):
|
|
s = MLStripper()
|
|
s.feed(html)
|
|
return s.get_data()
|
|
|
|
|
|
# pylama:ignore=D,C0116
|
|
class RSTVisitor(docutils.nodes.SparseNodeVisitor):
|
|
"""A visitor that produces rich renderables"""
|
|
|
|
def __init__(
|
|
self,
|
|
document: docutils.nodes.document,
|
|
console: Console,
|
|
code_theme: Union[str, SyntaxTheme] = "monokai",
|
|
guess_lexer: Optional[bool] = True,
|
|
default_lexer: Optional[str] = "python",
|
|
) -> None:
|
|
super().__init__(document)
|
|
self.console = console
|
|
self.code_theme = code_theme
|
|
self.renderables = []
|
|
self.supercript = str.maketrans(
|
|
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ=+-*/×÷",
|
|
"¹²³⁴⁵⁶⁷⁸⁹⁰ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖᑫʳˢᵗᵘᵛʷˣʸᶻᴬᴮᶜᴰᴱᶠᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾQᴿˢᵀᵁⱽᵂˣʸᶻ⁼⁺⁻*/×÷",
|
|
)
|
|
self.subscript = str.maketrans(
|
|
"1234567890abcdefghijklmnopqrstuvwxyz=+-*/×÷", "₁₂₃₄₅₆₇₈₉₀abcdₑfgₕᵢⱼₖₗₘₙₒₚqᵣₛₜᵤᵥwₓyz₌₊₋*/×÷"
|
|
)
|
|
self.errors = []
|
|
self.footer = []
|
|
self.guess_lexer = guess_lexer
|
|
self.default_lexer = default_lexer
|
|
self.refname_to_renderable = {}
|
|
|
|
def _find_lexer(self, node):
|
|
lexer = (
|
|
node["classes"][1] if len(node.get("classes")) >= 2 else (node["format"] if node.get("format") else None)
|
|
)
|
|
if lexer is None and self.guess_lexer:
|
|
try:
|
|
lexer = guess_lexer(node.astext())
|
|
except ClassNotFound:
|
|
lexer = self.default_lexer
|
|
else:
|
|
lexer = lexer.aliases[0] if lexer.aliases else self.default_lexer
|
|
if lexer == "text":
|
|
return self.default_lexer
|
|
return lexer
|
|
elif lexer is None and not self.guess_lexer:
|
|
lexer = self.default_lexer
|
|
return lexer
|
|
return lexer
|
|
|
|
def visit_reference(self, node):
|
|
refuri = node.attributes.get("refuri")
|
|
style = self.console.get_style("restructuredtext.reference", default="blue underline on default")
|
|
if refuri:
|
|
style = style.update_link(refuri)
|
|
renderable = Text(node.astext().replace("\n", " "), style=style, end="")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
renderable.end = " "
|
|
start = len(self.renderables[-1])
|
|
self.renderables[-1].append_text(renderable)
|
|
else:
|
|
start = 0
|
|
self.renderables.append(renderable)
|
|
end = len(self.renderables[-1])
|
|
|
|
if not refuri:
|
|
# We'll get the URL reference later in visit_target.
|
|
refname = node.attributes.get("refname")
|
|
if refname:
|
|
self.refname_to_renderable[refname] = (self.renderables[-1], start, end)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_target(self, node):
|
|
uri = node.get("refuri")
|
|
if uri:
|
|
for name in node["names"]:
|
|
try:
|
|
renderable, start, end = self.refname_to_renderable[name]
|
|
except KeyError:
|
|
continue
|
|
style = renderable.get_style_at_offset(self.console, start)
|
|
style = style.update_link(uri)
|
|
renderable.stylize(style, start, end)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_paragraph(self, node):
|
|
if hasattr(node, "parent") and isinstance(node.parent, docutils.nodes.system_message):
|
|
self.visit_system_message(node.parent)
|
|
|
|
def depart_paragraph(self, node): # pylint: disable=unused-argument
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
if len(self.renderables[-1].end) == 0:
|
|
self.renderables[-1].append("\n\n")
|
|
|
|
def visit_title(self, node):
|
|
style = self.console.get_style("restructuredtext.title", default="bold")
|
|
self.renderables.append(Panel(Align(node.astext(), "center"), box=box.DOUBLE, style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_Text(self, node):
|
|
style = self.console.get_style("restructuredtext.text", default="default on default not underline")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].append_text(Text(node.astext().replace("\n", " "), style=style, end=" "))
|
|
return
|
|
self.renderables.append(Text(node.astext().replace("\n", " "), end="", style=style))
|
|
|
|
def visit_comment(self, node):
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_admonition(self, node):
|
|
style = self.console.get_style("restructuredtext.admonition", default="bold white")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="Admonition: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_attention(self, node):
|
|
style = self.console.get_style("restructuredtext.attention", default="bold black on yellow")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="Attention: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_caution(self, node):
|
|
style = self.console.get_style("restructuredtext.caution", default="red")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="Caution: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_danger(self, node):
|
|
style = self.console.get_style("restructuredtext.danger", default="bold white on red")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="DANGER: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_error(self, node):
|
|
style = self.console.get_style("restructuredtext.error", default="bold red")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="ERROR: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_hint(self, node):
|
|
style = self.console.get_style("restructuredtext.hint", default="yellow")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="Hint: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_important(self, node):
|
|
style = self.console.get_style("restructuredtext.important", default="bold blue")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="IMPORTANT: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_note(self, node):
|
|
style = self.console.get_style("restructuredtext.note", default="bold white")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="Note: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_tip(self, node):
|
|
style = self.console.get_style("restructuredtext.tip", default="bold green")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="Tip: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_warning(self, node):
|
|
style = self.console.get_style("restructuredtext.warning", default="bold yellow")
|
|
self.renderables.append(Panel(node.astext().replace("\n", " "), title="Warning: ", style=style, border_style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_subscript(self, node):
|
|
style = self.console.get_style("restructuredtext.subscript", default="none")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].append_text(Text(node.astext().translate(self.subscript), style=style, end=" "))
|
|
raise docutils.nodes.SkipChildren()
|
|
self.renderables.append(Text(node.astext().translate(self.subscript), end="", style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_superscript(self, node):
|
|
style = self.console.get_style("restructuredtext.superscript", default="none")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].append_text(Text(node.astext().translate(self.supercript), style=style, end=" "))
|
|
raise docutils.nodes.SkipChildren()
|
|
self.renderables.append(Text(node.astext().translate(self.supercript), end="", style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_emphasis(self, node):
|
|
style = self.console.get_style("restructuredtext.emphasis", default="italic")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].append_text(Text(node.astext().replace("\n", " "), style=style, end=" "))
|
|
raise docutils.nodes.SkipChildren()
|
|
self.renderables.append(Text(node.astext().replace("\n", " "), style=style, end=""))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_strong(self, node):
|
|
style = self.console.get_style("restructuredtext.strong", default="bold")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].append_text(Text(node.astext().replace("\n", " "), style=style, end=" "))
|
|
raise docutils.nodes.SkipChildren()
|
|
self.renderables.append(Text(node.astext().replace("\n", " "), style=style, end=""))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_image(self, node):
|
|
alt, target = None, None
|
|
if ":target:" in node.rawsource:
|
|
target = node.rawsource.split(":target:")[-1].strip()
|
|
if ":alt:" in node.rawsource:
|
|
alt = node.rawsource.split(":alt:")[-1].strip()
|
|
self.renderables.append(
|
|
Text("🌆 ")
|
|
+ Text(
|
|
node.get("alt", alt or "Image"),
|
|
style=Style(
|
|
link=node.get("target", target or "Image") or node.get("uri"),
|
|
color="#6088ff",
|
|
),
|
|
)
|
|
)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_bullet_list(self, node):
|
|
# Currently as it stands, this isn't gonna work with more than 3 nested levels
|
|
# TODO: I need to figure out some way to handle nested lists recursively
|
|
marker_style = self.console.get_style("restructuredtext.bullet_list_marker", default="bold yellow")
|
|
text_style = self.console.get_style("restructuredtext.bullet_list_text", default="none")
|
|
for list_item in node.children:
|
|
nested_list = [i for i in list_item.children if isinstance(i, docutils.nodes.bullet_list)]
|
|
if nested_list:
|
|
for list_item in list_item.children:
|
|
self.renderables.append(Text(" ", end="") + Text(" ∘ ", end="", style=marker_style))
|
|
self.renderables.append(Text(list_item.astext().replace("\n", " "), style=text_style))
|
|
if isinstance(list_item, docutils.nodes.bullet_list):
|
|
for list_item in list_item.children:
|
|
self.renderables.append(Text(" ", end="") + Text(" ▪ ", end="", style=marker_style))
|
|
self.renderables.append(Text(list_item.astext().replace("\n", " "), style=text_style))
|
|
self.renderables.append(Text(" • ", end="", style=marker_style))
|
|
self.renderables.append(Text(list_item.astext().replace("\n", " "), style=text_style))
|
|
self.renderables.append(NewLine())
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_enumerated_list(self, node):
|
|
marker_style = self.console.get_style("restructuredtext.enumerated_list_marker", default="bold yellow")
|
|
text_style = self.console.get_style("restructuredtext.enumerated_text", default="none")
|
|
for i, list_item in enumerate(node.children, 1):
|
|
self.renderables.append(Text(f" {i}", end=" ", style=marker_style))
|
|
self.renderables.append(Text(list_item.astext().replace("\n", " "), style=text_style))
|
|
|
|
self.renderables.append(NewLine())
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_literal(self, node):
|
|
style = self.console.get_style("restructuredtext.inline_codeblock", default="grey78 on grey7")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].append_text(Text(node.astext().replace("\n", " "), style=style, end=" "))
|
|
raise docutils.nodes.SkipChildren()
|
|
self.renderables.append(Text(node.astext().replace("\n", " "), style=style, end=""))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_literal_block(self, node):
|
|
style = self.console.get_style("restructuredtext.literal_block_border", default="grey58")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].rstrip()
|
|
self.renderables[-1].append_text(Text("\n"))
|
|
lexer = self._find_lexer(node)
|
|
self.renderables.append(
|
|
Panel(Syntax(node.astext(), lexer, theme=self.code_theme), border_style=style, box=box.SQUARE, title=lexer)
|
|
)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_system_message(self, node):
|
|
self.errors.append(
|
|
Panel(
|
|
self.console.render_str(node.astext()),
|
|
title=f"System Message: {node.attributes.get('type', '?')}/{node.attributes.get('level', '?')} ({node.attributes.get('source', '?')}, line {node.attributes.get('line', '?')});",
|
|
border_style={None: "none", "INFO": "bold cyan", "WARNING": "bold yellow", "ERROR": "bold red"}[
|
|
node.attributes.get("type")
|
|
],
|
|
),
|
|
)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_field(self, node):
|
|
field_name_style = self.console.get_style("restructuredtext.field_name", default="bold")
|
|
field_value_style = self.console.get_style("restructuredtext.field_value", default="none")
|
|
previous_table = None
|
|
if isinstance(self.renderables[-1], Table):
|
|
possible_table = self.renderables[-1]
|
|
if (possible_table.columns[0].header == "Field Name") and (possible_table.columns[1].header == "Field Value"):
|
|
table = possible_table
|
|
previous_table = True
|
|
else:
|
|
table = Table("Field Name", "Field Value", show_lines=True)
|
|
previous_table = False
|
|
else:
|
|
previous_table = False
|
|
if previous_table is False:
|
|
table = Table("Field Name", "Field Value", show_lines=True)
|
|
table.add_row(Text(node.children[0].astext(), style=field_name_style), Text(node.children[1].astext(), style=field_value_style))
|
|
self.renderables.append(table)
|
|
else:
|
|
table.add_row(Text(node.children[0].astext(), style=field_name_style), Text(node.children[1].astext(), style=field_value_style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_definition_list(self, node):
|
|
term_style = self.console.get_style("restructuredtext.term_style", default="none")
|
|
classifier_style = self.console.get_style("restructuredtext.classifier_style", default="cyan")
|
|
definitions_style = self.console.get_style("restructuredtext.definitions_style", default="none")
|
|
for child in node.children:
|
|
try:
|
|
term, classifier, definitions = child.children
|
|
except ValueError:
|
|
term, classifier = child.children[0], child.children[1]
|
|
if len(child.children) > 2:
|
|
for children in child.children[2:]:
|
|
if isinstance(children, docutils.nodes.bullet_list):
|
|
self.visit_bullet_list(children)
|
|
elif isinstance(children, docutils.nodes.literal_block):
|
|
self.visit_literal_block(children)
|
|
elif isinstance(children, docutils.nodes.literal):
|
|
self.visit_literal(children)
|
|
elif isinstance(children, docutils.nodes.block_quote):
|
|
self.visit_block_quote(children)
|
|
else:
|
|
|
|
self.renderables.append(
|
|
Text.from_markup(f"[{classifier_style}]{term.astext()}[/{classifier_style}]")
|
|
+ Text("\n ", end="")
|
|
+ Text(classifier.astext().replace("\n", " "), style=definitions_style)
|
|
+ Text("\n ", end="")
|
|
)
|
|
else:
|
|
self.renderables.append(
|
|
Text(" ")
|
|
+ Text(term.astext(), style=term_style, end="")
|
|
+ Text(" : ", end="")
|
|
+ Text(classifier.astext(), style=classifier_style)
|
|
+ Text("\n ", end="")
|
|
+ Text(definitions.astext().replace("\n", " "), style=definitions_style)
|
|
+ Text("\n", end="")
|
|
)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_option_list(self, node):
|
|
option_string_style = self.console.get_style("restructuredtext.option_string", default="none")
|
|
option_argument_style = self.console.get_style("restructuredtext.option_argument", default="none")
|
|
option_child_text_separator_style = self.console.get_style(
|
|
"restructuredtext.option_child_text_separator", default="none"
|
|
)
|
|
option_description_style = self.console.get_style("restructuredtext.option_description", default="none")
|
|
for option_list_item in node.children:
|
|
option_group, description = option_list_item.children
|
|
# option_group.child_text_separator.join(map(lambda x: x.astext(), option_group.children)))
|
|
option_text = Text(end="")
|
|
for option in option_group.children:
|
|
try:
|
|
option_string, option_argument = option.children
|
|
except ValueError:
|
|
option_string, option_argument = option.children[0], None
|
|
option_text += (
|
|
Text(option_string.astext(), style=option_string_style)
|
|
+ (Text(option_argument.astext(), style=option_argument_style) if option_argument else Text())
|
|
+ (
|
|
Text(option_group.child_text_separator, style=option_child_text_separator_style)
|
|
if len(option_group.children) > 1
|
|
else ""
|
|
)
|
|
)
|
|
if description:
|
|
option_text += Text("\n ")
|
|
option_text += Text(description.astext(), style=option_description_style)
|
|
self.renderables.append(option_text + Text("\n"))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_doctest_block(self, node):
|
|
style = self.console.get_style("restructuredtext.literal_block_border", default="grey58")
|
|
self.renderables.append(
|
|
Panel(
|
|
Syntax(node.astext(), "pycon", theme=self.code_theme),
|
|
border_style=style,
|
|
box=box.SQUARE,
|
|
)
|
|
)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_block_quote(self, node):
|
|
text_style = self.console.get_style("restructuredtext.blockquote_text", default="white")
|
|
marker_style = self.console.get_style(
|
|
"restructuredtext.blockquote_attribution_marker", default="bright_magenta"
|
|
)
|
|
author_style = self.console.get_style("restructuredtext.blockquote_attribution_text", default="grey89")
|
|
try:
|
|
paragraph, attribution = node.children
|
|
except ValueError:
|
|
paragraph = node.children[0]
|
|
self.renderables.append(
|
|
Text(" ")
|
|
+ Text(paragraph.astext().replace('\n', ' '), style=text_style)
|
|
+ Text("\n\n")
|
|
)
|
|
else:
|
|
self.renderables.append(
|
|
Text("▌ ", style=marker_style)
|
|
+ Text(paragraph.astext(), style=text_style)
|
|
+ Text("\n")
|
|
+ Text(" - " + attribution.astext(), style=author_style)
|
|
)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_line_block(self, node):
|
|
for line in node.children:
|
|
self.renderables.append(Text(line.astext()))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_sidebar(self, node):
|
|
if len(node.children) > 2:
|
|
title, subtitle, paragraph = node.children
|
|
else:
|
|
title, subtitle, paragraph = node.children[0], "", node.children[1]
|
|
|
|
self.renderables.append(
|
|
Panel(paragraph.astext(), title=title.astext(), subtitle=subtitle.astext(), expand=False)
|
|
)
|
|
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_transition(self, node):
|
|
style = self.console.get_style("restructuredtext.hr", default="yellow")
|
|
self.renderables.append(Rule(style=style))
|
|
|
|
def visit_rubric(self, node):
|
|
self.visit_title(node)
|
|
|
|
def visit_math_block(self, node):
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].append(Text(node.astext(), end=" "))
|
|
raise docutils.nodes.SkipChildren()
|
|
self.renderables.append(Text(node.astext()))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_citation(self, node):
|
|
border_style = self.console.get_style("restructuredtext.citation_border", default="grey74")
|
|
self.renderables.append(Panel(node.astext(), title="citation", border_style=border_style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_citation_reference(self, node):
|
|
style = self.console.get_style("restructuredtext.citation_reference", default="grey74")
|
|
if self.renderables and isinstance(self.renderables[-1], Text):
|
|
self.renderables[-1].append(node.astext().replace("\n", " "), style=style)
|
|
raise docutils.nodes.SkipChildren()
|
|
self.renderables.append(Text(node.astext().replace("\n", " "), style=style, end=""))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_header(self, node):
|
|
style = self.console.get_style("restructuredtext.caption", default="bold")
|
|
self.renderables.insert(0, Panel(Align(node.astext(), "center"), title="caption", box=box.DOUBLE, style=style))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_footer(self, node):
|
|
self.footer.append(Align(node.astext(), "center"))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_footnote(self, node):
|
|
self.footer.append(Align(node.astext(), "center"))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_generated(self, node):
|
|
self.footer.append(Align(node.astext(), "center"))
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_pendings(self, node):
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
def visit_problematic(self, node):
|
|
self.errors.append(
|
|
Panel(
|
|
Syntax(node.astext(), lexer="rst", theme=self.code_theme),
|
|
title=f"System Message: Problematic Element",
|
|
border_style="bold red",
|
|
),
|
|
)
|
|
|
|
def visit_raw(self, node):
|
|
style = self.console.get_style("restructuredtext.literal_block_border", default="grey58")
|
|
lexer = self._find_lexer(node)
|
|
text = node.astext()
|
|
title = "raw " + ("stripped raw html" if lexer == "html" else lexer)
|
|
|
|
if lexer == "html":
|
|
text = strip_tags(text)
|
|
lexer = guess_lexer(text).aliases[0] if self.guess_lexer else self.default_lexer
|
|
|
|
self.renderables.append(
|
|
Panel(Syntax(text, lexer, theme=self.code_theme), border_style=style, box=box.SQUARE, title=title)
|
|
)
|
|
raise docutils.nodes.SkipChildren()
|
|
|
|
|
|
class RestructuredText(JupyterMixin):
|
|
"""A reStructuredText renderable for rich.
|
|
|
|
Parameters
|
|
----------
|
|
markup : str
|
|
A string containing reStructuredText markup.
|
|
code_theme : Optional[Union[str, SyntaxTheme]]
|
|
Pygments theme for code blocks. Defaults to "monokai".
|
|
show_errors : Optional[bool]
|
|
Whether to show system_messages aka errors and warnings.
|
|
guess_lexer : Optional[bool]
|
|
Whether to guess lexers for code blocks without specified language.
|
|
default_lexer : Optional[str]
|
|
Which lexer to use if no lexer is guessed or found. Defaults to "python"
|
|
filename : Optional[str]
|
|
A file name to use for error messages, useful for debugging purposes. Defaults to "<rst-document>"
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
markup: str,
|
|
code_theme: Optional[Union[str, SyntaxTheme]] = "monokai",
|
|
show_errors: Optional[bool] = True,
|
|
guess_lexer: Optional[bool] = False,
|
|
default_lexer: Optional[str] = "python",
|
|
filename: Optional[str] = "<rst-document>"
|
|
) -> None:
|
|
self.markup = markup
|
|
self.code_theme = code_theme
|
|
self.log_errors = show_errors
|
|
self.guess_lexer = guess_lexer
|
|
self.default_lexer = default_lexer
|
|
self.filename = filename
|
|
|
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
# Parse the `markup` into a RST `document`.
|
|
|
|
# Docutils version compatability; from https://stackoverflow.com/a/75996218
|
|
if hasattr(docutils.frontend, 'get_default_settings'):
|
|
# docutils >= 0.18
|
|
settings = docutils.frontend.get_default_settings(docutils.parsers.rst.Parser)
|
|
else:
|
|
# docutils < 0.18
|
|
settings = docutils.frontend.OptionParser(components=(docutils.parsers.rst.Parser,)).get_default_values()
|
|
settings.report_level = 69
|
|
source = docutils.io.StringInput(self.markup)
|
|
document = docutils.utils.new_document(self.filename, settings)
|
|
rst_parser = docutils.parsers.rst.Parser()
|
|
rst_parser.parse(source.read(), document)
|
|
|
|
# Render the RST `document` using Rich.
|
|
visitor = RSTVisitor(
|
|
document,
|
|
console=console,
|
|
code_theme=self.code_theme,
|
|
guess_lexer=self.guess_lexer,
|
|
default_lexer=self.default_lexer,
|
|
)
|
|
document.walkabout(visitor)
|
|
|
|
# Strip all trailing newlines and newline-like rich objects
|
|
while visitor.renderables:
|
|
if isinstance(visitor.renderables[-1], Text):
|
|
visitor.renderables[-1].rstrip()
|
|
visitor.renderables[-1].end = "\n"
|
|
if visitor.renderables[-1]: # The Text object still contains data.
|
|
break
|
|
else:
|
|
visitor.renderables.pop()
|
|
elif isinstance(visitor.renderables[-1], NewLine):
|
|
visitor.renderables.pop()
|
|
else:
|
|
break
|
|
|
|
for renderable in visitor.renderables:
|
|
yield from console.render(renderable, options)
|
|
if self.log_errors and visitor.errors:
|
|
for error in visitor.errors:
|
|
yield from console.render(error, options)
|
|
style = console.get_style("restructuredtext.footer", default="none")
|
|
border_style = console.get_style("restructuredtext.footer_border", default="grey74")
|
|
footer_text = ""
|
|
for element in visitor.footer:
|
|
footer_text = element
|
|
if footer_text:
|
|
yield from console.render(
|
|
Panel(footer_text, title="Footer", box=box.SQUARE, border_style=border_style, style=style)
|
|
)
|
|
|
|
|
|
RST = reST = ReStructuredText = reStructuredText = RestructuredText
|