1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
|
#!/usr/bin/python
from typing import Any, ClassVar
import sphinx.application
from docutils import nodes
from docutils.parsers import rst
from docutils.parsers.rst import directives
from pygments import highlight
from pygments.formatters.html import HtmlFormatter
from pygments.lexers.jvm import ScalaLexer
from pygments.lexers.python import PythonLexer
from pygments.lexers.shell import BashLexer, BatchLexer, PowerShellLexer
from pygments.lexers.special import TextLexer
from sphinx.util import logging
logger = logging.getLogger(__name__)
class PromptCache:
"""The cache of different prompt."""
def __init__(self) -> None:
"""Initialize."""
self.next_index = 1
self.prompts: dict[str, int] = {}
def clear(self, *args: Any) -> None:
"""Clear all cache."""
del args
self.next_index = 1
self.prompts = {}
def register_prompt(self, prompt: str) -> str:
"""Initialize the prompts."""
if prompt in self.prompts:
return ""
index = self.next_index
self.next_index = index + 1
self.prompts[prompt] = index
return f"""span.prompt{index}:before {{
content: "{prompt} ";
}}
"""
def get_prompt_class(self, prompt: str) -> str:
"""Get the CSS class name."""
return f"prompt{self.prompts[prompt]}"
_cache = PromptCache()
PROMPTS = {
"bash": "$",
"batch": r"C:\\>",
"powershell": r"PS C:\\>",
}
LEXERS = {
"bash": BashLexer,
"batch": BatchLexer,
"powershell": PowerShellLexer,
"python": PythonLexer,
"scala": ScalaLexer,
}
class PromptDirective(rst.Directive):
"""The prompt directive."""
optional_arguments = 3
option_spec: ClassVar[dict[str, Any]] = {
"language": directives.unchanged_required,
"prompts": directives.unchanged_required,
"modifiers": directives.unchanged_required,
}
has_content = True
def run(self) -> list[nodes.raw]:
"""Run the directive."""
self.assert_has_content()
arg_count = len(self.arguments)
for idx, option_name in enumerate(("language", "prompts", "modifiers")):
if arg_count > idx:
if self.options.get(option_name):
logger.warning(
"%s is already passed as an option, ignoring the value passed"
" as positional argument and all arguments that come after it.",
option_name,
location=(self.state.document.settings.env.docname, self.lineno),
)
break
self.options[option_name] = self.arguments[idx]
language: str = self.options.get("language") or "text"
prompt: str = self.options.get("prompts") or PROMPTS.get(language, "")
modifiers: list[str] = self.options.get("modifiers", "").split(",")
if "auto" in modifiers:
prompts: list[str] = prompt.split(",")
html = '<div class="highlight-default notranslate"><div class="highlight"><pre>'
styles = ""
if "auto" in modifiers:
for prompt in prompts:
styles += _cache.register_prompt(prompt)
elif prompt is not None:
styles += _cache.register_prompt(prompt)
if styles:
html += '<style type="text/css">\n' + styles + "</style>"
latex = "\\begin{Verbatim}[commandchars=\\\\\\{\\}]"
Lexer = LEXERS.get(language, TextLexer) # noqa: N806 # pylint: disable=invalid-name
statement: list[str] = []
if "auto" in modifiers:
prompt_class = ""
for line in self.content:
latex += "\n" + line
for prompt in prompts:
if line.startswith(prompt):
if len(statement) > 0:
highlighted_line = highlight(
"\n".join(statement),
Lexer(),
HtmlFormatter(nowrap=True),
).strip("\r\n")
html += f'<span class="{prompt_class}">{highlighted_line}</span>\n'
statement = []
line = line[len(prompt) + 1 :].rstrip() # noqa: PLW2901
prompt_class = _cache.get_prompt_class(prompt)
break
statement.append(line)
# Add last prompt
if len(statement) > 0:
highlighted_line = highlight("\n".join(statement), Lexer(), HtmlFormatter(nowrap=True)).strip(
"\r\n",
)
html += f'<span class="{prompt_class}">{highlighted_line}</span>\n'
elif language in ["bash", "python"]:
for line in self.content:
statement.append(line)
highlighted_line = highlight("\n".join(statement), Lexer(), HtmlFormatter(nowrap=True)).strip(
"\r\n",
)
if len(line) == 0 or line[-1] != "\\":
html += f'<span class="{_cache.get_prompt_class(prompt)}">{highlighted_line}</span>\n'
if prompt is not None:
statements = "\n".join(statement)
latex += f"\n{prompt} {statements}"
else:
latex += "\n" + "\n".join(statement)
statement = []
else:
for line in self.content:
highlighted_line = highlight(line, Lexer(), HtmlFormatter(nowrap=True)).strip("\r\n")
html += f'<span class="{_cache.get_prompt_class(prompt)}">{highlighted_line}</span>\n'
if prompt is not None:
latex += f"\n{prompt} {line}"
else:
latex += "\n" + line
html += "</pre></div></div>"
latex += "\n\\end{Verbatim}"
return [
nodes.raw("\n".join(self.content), html, format="html"),
nodes.raw("\n".join(self.content), latex, format="latex"),
]
def setup(app: sphinx.application.Sphinx) -> dict[str, bool]:
"""Register the plugin."""
app.add_directive("prompt", PromptDirective)
app.connect("env-purge-doc", _cache.clear)
return {
"parallel_read_safe": True,
"parallel_write_safe": True,
}
|