|
import os.path |
|
import re |
|
import sys |
|
import textwrap |
|
from abc import ABC, abstractmethod |
|
from pathlib import Path |
|
from typing import ( |
|
Any, |
|
Dict, |
|
Iterable, |
|
List, |
|
NamedTuple, |
|
Optional, |
|
Sequence, |
|
Set, |
|
Tuple, |
|
Type, |
|
Union, |
|
) |
|
|
|
from pygments.lexer import Lexer |
|
from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename |
|
from pygments.style import Style as PygmentsStyle |
|
from pygments.styles import get_style_by_name |
|
from pygments.token import ( |
|
Comment, |
|
Error, |
|
Generic, |
|
Keyword, |
|
Name, |
|
Number, |
|
Operator, |
|
String, |
|
Token, |
|
Whitespace, |
|
) |
|
from pygments.util import ClassNotFound |
|
|
|
from rich.containers import Lines |
|
from rich.padding import Padding, PaddingDimensions |
|
|
|
from ._loop import loop_first |
|
from .cells import cell_len |
|
from .color import Color, blend_rgb |
|
from .console import Console, ConsoleOptions, JustifyMethod, RenderResult |
|
from .jupyter import JupyterMixin |
|
from .measure import Measurement |
|
from .segment import Segment, Segments |
|
from .style import Style, StyleType |
|
from .text import Text |
|
|
|
TokenType = Tuple[str, ...] |
|
|
|
WINDOWS = sys.platform == "win32" |
|
DEFAULT_THEME = "monokai" |
|
|
|
|
|
|
|
|
|
ANSI_LIGHT: Dict[TokenType, Style] = { |
|
Token: Style(), |
|
Whitespace: Style(color="white"), |
|
Comment: Style(dim=True), |
|
Comment.Preproc: Style(color="cyan"), |
|
Keyword: Style(color="blue"), |
|
Keyword.Type: Style(color="cyan"), |
|
Operator.Word: Style(color="magenta"), |
|
Name.Builtin: Style(color="cyan"), |
|
Name.Function: Style(color="green"), |
|
Name.Namespace: Style(color="cyan", underline=True), |
|
Name.Class: Style(color="green", underline=True), |
|
Name.Exception: Style(color="cyan"), |
|
Name.Decorator: Style(color="magenta", bold=True), |
|
Name.Variable: Style(color="red"), |
|
Name.Constant: Style(color="red"), |
|
Name.Attribute: Style(color="cyan"), |
|
Name.Tag: Style(color="bright_blue"), |
|
String: Style(color="yellow"), |
|
Number: Style(color="blue"), |
|
Generic.Deleted: Style(color="bright_red"), |
|
Generic.Inserted: Style(color="green"), |
|
Generic.Heading: Style(bold=True), |
|
Generic.Subheading: Style(color="magenta", bold=True), |
|
Generic.Prompt: Style(bold=True), |
|
Generic.Error: Style(color="bright_red"), |
|
Error: Style(color="red", underline=True), |
|
} |
|
|
|
ANSI_DARK: Dict[TokenType, Style] = { |
|
Token: Style(), |
|
Whitespace: Style(color="bright_black"), |
|
Comment: Style(dim=True), |
|
Comment.Preproc: Style(color="bright_cyan"), |
|
Keyword: Style(color="bright_blue"), |
|
Keyword.Type: Style(color="bright_cyan"), |
|
Operator.Word: Style(color="bright_magenta"), |
|
Name.Builtin: Style(color="bright_cyan"), |
|
Name.Function: Style(color="bright_green"), |
|
Name.Namespace: Style(color="bright_cyan", underline=True), |
|
Name.Class: Style(color="bright_green", underline=True), |
|
Name.Exception: Style(color="bright_cyan"), |
|
Name.Decorator: Style(color="bright_magenta", bold=True), |
|
Name.Variable: Style(color="bright_red"), |
|
Name.Constant: Style(color="bright_red"), |
|
Name.Attribute: Style(color="bright_cyan"), |
|
Name.Tag: Style(color="bright_blue"), |
|
String: Style(color="yellow"), |
|
Number: Style(color="bright_blue"), |
|
Generic.Deleted: Style(color="bright_red"), |
|
Generic.Inserted: Style(color="bright_green"), |
|
Generic.Heading: Style(bold=True), |
|
Generic.Subheading: Style(color="bright_magenta", bold=True), |
|
Generic.Prompt: Style(bold=True), |
|
Generic.Error: Style(color="bright_red"), |
|
Error: Style(color="red", underline=True), |
|
} |
|
|
|
RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK} |
|
NUMBERS_COLUMN_DEFAULT_PADDING = 2 |
|
|
|
|
|
class SyntaxTheme(ABC): |
|
"""Base class for a syntax theme.""" |
|
|
|
@abstractmethod |
|
def get_style_for_token(self, token_type: TokenType) -> Style: |
|
"""Get a style for a given Pygments token.""" |
|
raise NotImplementedError |
|
|
|
@abstractmethod |
|
def get_background_style(self) -> Style: |
|
"""Get the background color.""" |
|
raise NotImplementedError |
|
|
|
|
|
class PygmentsSyntaxTheme(SyntaxTheme): |
|
"""Syntax theme that delegates to Pygments theme.""" |
|
|
|
def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None: |
|
self._style_cache: Dict[TokenType, Style] = {} |
|
if isinstance(theme, str): |
|
try: |
|
self._pygments_style_class = get_style_by_name(theme) |
|
except ClassNotFound: |
|
self._pygments_style_class = get_style_by_name("default") |
|
else: |
|
self._pygments_style_class = theme |
|
|
|
self._background_color = self._pygments_style_class.background_color |
|
self._background_style = Style(bgcolor=self._background_color) |
|
|
|
def get_style_for_token(self, token_type: TokenType) -> Style: |
|
"""Get a style from a Pygments class.""" |
|
try: |
|
return self._style_cache[token_type] |
|
except KeyError: |
|
try: |
|
pygments_style = self._pygments_style_class.style_for_token(token_type) |
|
except KeyError: |
|
style = Style.null() |
|
else: |
|
color = pygments_style["color"] |
|
bgcolor = pygments_style["bgcolor"] |
|
style = Style( |
|
color="#" + color if color else "#000000", |
|
bgcolor="#" + bgcolor if bgcolor else self._background_color, |
|
bold=pygments_style["bold"], |
|
italic=pygments_style["italic"], |
|
underline=pygments_style["underline"], |
|
) |
|
self._style_cache[token_type] = style |
|
return style |
|
|
|
def get_background_style(self) -> Style: |
|
return self._background_style |
|
|
|
|
|
class ANSISyntaxTheme(SyntaxTheme): |
|
"""Syntax theme to use standard colors.""" |
|
|
|
def __init__(self, style_map: Dict[TokenType, Style]) -> None: |
|
self.style_map = style_map |
|
self._missing_style = Style.null() |
|
self._background_style = Style.null() |
|
self._style_cache: Dict[TokenType, Style] = {} |
|
|
|
def get_style_for_token(self, token_type: TokenType) -> Style: |
|
"""Look up style in the style map.""" |
|
try: |
|
return self._style_cache[token_type] |
|
except KeyError: |
|
|
|
|
|
|
|
get_style = self.style_map.get |
|
token = tuple(token_type) |
|
style = self._missing_style |
|
while token: |
|
_style = get_style(token) |
|
if _style is not None: |
|
style = _style |
|
break |
|
token = token[:-1] |
|
self._style_cache[token_type] = style |
|
return style |
|
|
|
def get_background_style(self) -> Style: |
|
return self._background_style |
|
|
|
|
|
SyntaxPosition = Tuple[int, int] |
|
|
|
|
|
class _SyntaxHighlightRange(NamedTuple): |
|
""" |
|
A range to highlight in a Syntax object. |
|
`start` and `end` are 2-integers tuples, where the first integer is the line number |
|
(starting from 1) and the second integer is the column index (starting from 0). |
|
""" |
|
|
|
style: StyleType |
|
start: SyntaxPosition |
|
end: SyntaxPosition |
|
style_before: bool = False |
|
|
|
|
|
class Syntax(JupyterMixin): |
|
"""Construct a Syntax object to render syntax highlighted code. |
|
|
|
Args: |
|
code (str): Code to highlight. |
|
lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/) |
|
theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai". |
|
dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False. |
|
line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. |
|
start_line (int, optional): Starting number for line numbers. Defaults to 1. |
|
line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render. |
|
A value of None in the tuple indicates the range is open in that direction. |
|
highlight_lines (Set[int]): A set of line numbers to highlight. |
|
code_width: Width of code to render (not including line numbers), or ``None`` to use all available width. |
|
tab_size (int, optional): Size of tabs. Defaults to 4. |
|
word_wrap (bool, optional): Enable word wrapping. |
|
background_color (str, optional): Optional background color, or None to use theme color. Defaults to None. |
|
indent_guides (bool, optional): Show indent guides. Defaults to False. |
|
padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding). |
|
""" |
|
|
|
_pygments_style_class: Type[PygmentsStyle] |
|
_theme: SyntaxTheme |
|
|
|
@classmethod |
|
def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme: |
|
"""Get a syntax theme instance.""" |
|
if isinstance(name, SyntaxTheme): |
|
return name |
|
theme: SyntaxTheme |
|
if name in RICH_SYNTAX_THEMES: |
|
theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name]) |
|
else: |
|
theme = PygmentsSyntaxTheme(name) |
|
return theme |
|
|
|
def __init__( |
|
self, |
|
code: str, |
|
lexer: Union[Lexer, str], |
|
*, |
|
theme: Union[str, SyntaxTheme] = DEFAULT_THEME, |
|
dedent: bool = False, |
|
line_numbers: bool = False, |
|
start_line: int = 1, |
|
line_range: Optional[Tuple[Optional[int], Optional[int]]] = None, |
|
highlight_lines: Optional[Set[int]] = None, |
|
code_width: Optional[int] = None, |
|
tab_size: int = 4, |
|
word_wrap: bool = False, |
|
background_color: Optional[str] = None, |
|
indent_guides: bool = False, |
|
padding: PaddingDimensions = 0, |
|
) -> None: |
|
self.code = code |
|
self._lexer = lexer |
|
self.dedent = dedent |
|
self.line_numbers = line_numbers |
|
self.start_line = start_line |
|
self.line_range = line_range |
|
self.highlight_lines = highlight_lines or set() |
|
self.code_width = code_width |
|
self.tab_size = tab_size |
|
self.word_wrap = word_wrap |
|
self.background_color = background_color |
|
self.background_style = ( |
|
Style(bgcolor=background_color) if background_color else Style() |
|
) |
|
self.indent_guides = indent_guides |
|
self.padding = padding |
|
|
|
self._theme = self.get_theme(theme) |
|
self._stylized_ranges: List[_SyntaxHighlightRange] = [] |
|
|
|
@classmethod |
|
def from_path( |
|
cls, |
|
path: str, |
|
encoding: str = "utf-8", |
|
lexer: Optional[Union[Lexer, str]] = None, |
|
theme: Union[str, SyntaxTheme] = DEFAULT_THEME, |
|
dedent: bool = False, |
|
line_numbers: bool = False, |
|
line_range: Optional[Tuple[int, int]] = None, |
|
start_line: int = 1, |
|
highlight_lines: Optional[Set[int]] = None, |
|
code_width: Optional[int] = None, |
|
tab_size: int = 4, |
|
word_wrap: bool = False, |
|
background_color: Optional[str] = None, |
|
indent_guides: bool = False, |
|
padding: PaddingDimensions = 0, |
|
) -> "Syntax": |
|
"""Construct a Syntax object from a file. |
|
|
|
Args: |
|
path (str): Path to file to highlight. |
|
encoding (str): Encoding of file. |
|
lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content. |
|
theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs". |
|
dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True. |
|
line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. |
|
start_line (int, optional): Starting number for line numbers. Defaults to 1. |
|
line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render. |
|
highlight_lines (Set[int]): A set of line numbers to highlight. |
|
code_width: Width of code to render (not including line numbers), or ``None`` to use all available width. |
|
tab_size (int, optional): Size of tabs. Defaults to 4. |
|
word_wrap (bool, optional): Enable word wrapping of code. |
|
background_color (str, optional): Optional background color, or None to use theme color. Defaults to None. |
|
indent_guides (bool, optional): Show indent guides. Defaults to False. |
|
padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding). |
|
|
|
Returns: |
|
[Syntax]: A Syntax object that may be printed to the console |
|
""" |
|
code = Path(path).read_text(encoding=encoding) |
|
|
|
if not lexer: |
|
lexer = cls.guess_lexer(path, code=code) |
|
|
|
return cls( |
|
code, |
|
lexer, |
|
theme=theme, |
|
dedent=dedent, |
|
line_numbers=line_numbers, |
|
line_range=line_range, |
|
start_line=start_line, |
|
highlight_lines=highlight_lines, |
|
code_width=code_width, |
|
tab_size=tab_size, |
|
word_wrap=word_wrap, |
|
background_color=background_color, |
|
indent_guides=indent_guides, |
|
padding=padding, |
|
) |
|
|
|
@classmethod |
|
def guess_lexer(cls, path: str, code: Optional[str] = None) -> str: |
|
"""Guess the alias of the Pygments lexer to use based on a path and an optional string of code. |
|
If code is supplied, it will use a combination of the code and the filename to determine the |
|
best lexer to use. For example, if the file is ``index.html`` and the file contains Django |
|
templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no |
|
templating language is used, the "html" lexer will be used. If no string of code |
|
is supplied, the lexer will be chosen based on the file extension.. |
|
|
|
Args: |
|
path (AnyStr): The path to the file containing the code you wish to know the lexer for. |
|
code (str, optional): Optional string of code that will be used as a fallback if no lexer |
|
is found for the supplied path. |
|
|
|
Returns: |
|
str: The name of the Pygments lexer that best matches the supplied path/code. |
|
""" |
|
lexer: Optional[Lexer] = None |
|
lexer_name = "default" |
|
if code: |
|
try: |
|
lexer = guess_lexer_for_filename(path, code) |
|
except ClassNotFound: |
|
pass |
|
|
|
if not lexer: |
|
try: |
|
_, ext = os.path.splitext(path) |
|
if ext: |
|
extension = ext.lstrip(".").lower() |
|
lexer = get_lexer_by_name(extension) |
|
except ClassNotFound: |
|
pass |
|
|
|
if lexer: |
|
if lexer.aliases: |
|
lexer_name = lexer.aliases[0] |
|
else: |
|
lexer_name = lexer.name |
|
|
|
return lexer_name |
|
|
|
def _get_base_style(self) -> Style: |
|
"""Get the base style.""" |
|
default_style = self._theme.get_background_style() + self.background_style |
|
return default_style |
|
|
|
def _get_token_color(self, token_type: TokenType) -> Optional[Color]: |
|
"""Get a color (if any) for the given token. |
|
|
|
Args: |
|
token_type (TokenType): A token type tuple from Pygments. |
|
|
|
Returns: |
|
Optional[Color]: Color from theme, or None for no color. |
|
""" |
|
style = self._theme.get_style_for_token(token_type) |
|
return style.color |
|
|
|
@property |
|
def lexer(self) -> Optional[Lexer]: |
|
"""The lexer for this syntax, or None if no lexer was found. |
|
|
|
Tries to find the lexer by name if a string was passed to the constructor. |
|
""" |
|
|
|
if isinstance(self._lexer, Lexer): |
|
return self._lexer |
|
try: |
|
return get_lexer_by_name( |
|
self._lexer, |
|
stripnl=False, |
|
ensurenl=True, |
|
tabsize=self.tab_size, |
|
) |
|
except ClassNotFound: |
|
return None |
|
|
|
@property |
|
def default_lexer(self) -> Lexer: |
|
"""A Pygments Lexer to use if one is not specified or invalid.""" |
|
return get_lexer_by_name( |
|
"text", |
|
stripnl=False, |
|
ensurenl=True, |
|
tabsize=self.tab_size, |
|
) |
|
|
|
def highlight( |
|
self, |
|
code: str, |
|
line_range: Optional[Tuple[Optional[int], Optional[int]]] = None, |
|
) -> Text: |
|
"""Highlight code and return a Text instance. |
|
|
|
Args: |
|
code (str): Code to highlight. |
|
line_range(Tuple[int, int], optional): Optional line range to highlight. |
|
|
|
Returns: |
|
Text: A text instance containing highlighted syntax. |
|
""" |
|
|
|
base_style = self._get_base_style() |
|
justify: JustifyMethod = ( |
|
"default" if base_style.transparent_background else "left" |
|
) |
|
|
|
text = Text( |
|
justify=justify, |
|
style=base_style, |
|
tab_size=self.tab_size, |
|
no_wrap=not self.word_wrap, |
|
) |
|
_get_theme_style = self._theme.get_style_for_token |
|
|
|
lexer = self.lexer or self.default_lexer |
|
|
|
if lexer is None: |
|
text.append(code) |
|
else: |
|
if line_range: |
|
|
|
|
|
line_start, line_end = line_range |
|
|
|
def line_tokenize() -> Iterable[Tuple[Any, str]]: |
|
"""Split tokens to one per line.""" |
|
assert lexer |
|
|
|
for token_type, token in lexer.get_tokens(code): |
|
while token: |
|
line_token, new_line, token = token.partition("\n") |
|
yield token_type, line_token + new_line |
|
|
|
def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]: |
|
"""Convert tokens to spans.""" |
|
tokens = iter(line_tokenize()) |
|
line_no = 0 |
|
_line_start = line_start - 1 if line_start else 0 |
|
|
|
|
|
while line_no < _line_start: |
|
try: |
|
_token_type, token = next(tokens) |
|
except StopIteration: |
|
break |
|
yield (token, None) |
|
if token.endswith("\n"): |
|
line_no += 1 |
|
|
|
for token_type, token in tokens: |
|
yield (token, _get_theme_style(token_type)) |
|
if token.endswith("\n"): |
|
line_no += 1 |
|
if line_end and line_no >= line_end: |
|
break |
|
|
|
text.append_tokens(tokens_to_spans()) |
|
|
|
else: |
|
text.append_tokens( |
|
(token, _get_theme_style(token_type)) |
|
for token_type, token in lexer.get_tokens(code) |
|
) |
|
if self.background_color is not None: |
|
text.stylize(f"on {self.background_color}") |
|
|
|
if self._stylized_ranges: |
|
self._apply_stylized_ranges(text) |
|
|
|
return text |
|
|
|
def stylize_range( |
|
self, |
|
style: StyleType, |
|
start: SyntaxPosition, |
|
end: SyntaxPosition, |
|
style_before: bool = False, |
|
) -> None: |
|
""" |
|
Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered. |
|
Line numbers are 1-based, while column indexes are 0-based. |
|
|
|
Args: |
|
style (StyleType): The style to apply. |
|
start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`. |
|
end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`. |
|
style_before (bool): Apply the style before any existing styles. |
|
""" |
|
self._stylized_ranges.append( |
|
_SyntaxHighlightRange(style, start, end, style_before) |
|
) |
|
|
|
def _get_line_numbers_color(self, blend: float = 0.3) -> Color: |
|
background_style = self._theme.get_background_style() + self.background_style |
|
background_color = background_style.bgcolor |
|
if background_color is None or background_color.is_system_defined: |
|
return Color.default() |
|
foreground_color = self._get_token_color(Token.Text) |
|
if foreground_color is None or foreground_color.is_system_defined: |
|
return foreground_color or Color.default() |
|
new_color = blend_rgb( |
|
background_color.get_truecolor(), |
|
foreground_color.get_truecolor(), |
|
cross_fade=blend, |
|
) |
|
return Color.from_triplet(new_color) |
|
|
|
@property |
|
def _numbers_column_width(self) -> int: |
|
"""Get the number of characters used to render the numbers column.""" |
|
column_width = 0 |
|
if self.line_numbers: |
|
column_width = ( |
|
len(str(self.start_line + self.code.count("\n"))) |
|
+ NUMBERS_COLUMN_DEFAULT_PADDING |
|
) |
|
return column_width |
|
|
|
def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]: |
|
"""Get background, number, and highlight styles for line numbers.""" |
|
background_style = self._get_base_style() |
|
if background_style.transparent_background: |
|
return Style.null(), Style(dim=True), Style.null() |
|
if console.color_system in ("256", "truecolor"): |
|
number_style = Style.chain( |
|
background_style, |
|
self._theme.get_style_for_token(Token.Text), |
|
Style(color=self._get_line_numbers_color()), |
|
self.background_style, |
|
) |
|
highlight_number_style = Style.chain( |
|
background_style, |
|
self._theme.get_style_for_token(Token.Text), |
|
Style(bold=True, color=self._get_line_numbers_color(0.9)), |
|
self.background_style, |
|
) |
|
else: |
|
number_style = background_style + Style(dim=True) |
|
highlight_number_style = background_style + Style(dim=False) |
|
return background_style, number_style, highlight_number_style |
|
|
|
def __rich_measure__( |
|
self, console: "Console", options: "ConsoleOptions" |
|
) -> "Measurement": |
|
_, right, _, left = Padding.unpack(self.padding) |
|
padding = left + right |
|
if self.code_width is not None: |
|
width = self.code_width + self._numbers_column_width + padding + 1 |
|
return Measurement(self._numbers_column_width, width) |
|
lines = self.code.splitlines() |
|
width = ( |
|
self._numbers_column_width |
|
+ padding |
|
+ (max(cell_len(line) for line in lines) if lines else 0) |
|
) |
|
if self.line_numbers: |
|
width += 1 |
|
return Measurement(self._numbers_column_width, width) |
|
|
|
def __rich_console__( |
|
self, console: Console, options: ConsoleOptions |
|
) -> RenderResult: |
|
segments = Segments(self._get_syntax(console, options)) |
|
if self.padding: |
|
yield Padding(segments, style=self._get_base_style(), pad=self.padding) |
|
else: |
|
yield segments |
|
|
|
def _get_syntax( |
|
self, |
|
console: Console, |
|
options: ConsoleOptions, |
|
) -> Iterable[Segment]: |
|
""" |
|
Get the Segments for the Syntax object, excluding any vertical/horizontal padding |
|
""" |
|
transparent_background = self._get_base_style().transparent_background |
|
code_width = ( |
|
( |
|
(options.max_width - self._numbers_column_width - 1) |
|
if self.line_numbers |
|
else options.max_width |
|
) |
|
if self.code_width is None |
|
else self.code_width |
|
) |
|
|
|
ends_on_nl, processed_code = self._process_code(self.code) |
|
text = self.highlight(processed_code, self.line_range) |
|
|
|
if not self.line_numbers and not self.word_wrap and not self.line_range: |
|
if not ends_on_nl: |
|
text.remove_suffix("\n") |
|
|
|
style = ( |
|
self._get_base_style() |
|
+ self._theme.get_style_for_token(Comment) |
|
+ Style(dim=True) |
|
+ self.background_style |
|
) |
|
if self.indent_guides and not options.ascii_only: |
|
text = text.with_indent_guides(self.tab_size, style=style) |
|
text.overflow = "crop" |
|
if style.transparent_background: |
|
yield from console.render( |
|
text, options=options.update(width=code_width) |
|
) |
|
else: |
|
syntax_lines = console.render_lines( |
|
text, |
|
options.update(width=code_width, height=None, justify="left"), |
|
style=self.background_style, |
|
pad=True, |
|
new_lines=True, |
|
) |
|
for syntax_line in syntax_lines: |
|
yield from syntax_line |
|
return |
|
|
|
start_line, end_line = self.line_range or (None, None) |
|
line_offset = 0 |
|
if start_line: |
|
line_offset = max(0, start_line - 1) |
|
lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl) |
|
if self.line_range: |
|
if line_offset > len(lines): |
|
return |
|
lines = lines[line_offset:end_line] |
|
|
|
if self.indent_guides and not options.ascii_only: |
|
style = ( |
|
self._get_base_style() |
|
+ self._theme.get_style_for_token(Comment) |
|
+ Style(dim=True) |
|
+ self.background_style |
|
) |
|
lines = ( |
|
Text("\n") |
|
.join(lines) |
|
.with_indent_guides(self.tab_size, style=style + Style(italic=False)) |
|
.split("\n", allow_blank=True) |
|
) |
|
|
|
numbers_column_width = self._numbers_column_width |
|
render_options = options.update(width=code_width) |
|
|
|
highlight_line = self.highlight_lines.__contains__ |
|
_Segment = Segment |
|
new_line = _Segment("\n") |
|
|
|
line_pointer = "> " if options.legacy_windows else "β± " |
|
|
|
( |
|
background_style, |
|
number_style, |
|
highlight_number_style, |
|
) = self._get_number_styles(console) |
|
|
|
for line_no, line in enumerate(lines, self.start_line + line_offset): |
|
if self.word_wrap: |
|
wrapped_lines = console.render_lines( |
|
line, |
|
render_options.update(height=None, justify="left"), |
|
style=background_style, |
|
pad=not transparent_background, |
|
) |
|
else: |
|
segments = list(line.render(console, end="")) |
|
if options.no_wrap: |
|
wrapped_lines = [segments] |
|
else: |
|
wrapped_lines = [ |
|
_Segment.adjust_line_length( |
|
segments, |
|
render_options.max_width, |
|
style=background_style, |
|
pad=not transparent_background, |
|
) |
|
] |
|
|
|
if self.line_numbers: |
|
wrapped_line_left_pad = _Segment( |
|
" " * numbers_column_width + " ", background_style |
|
) |
|
for first, wrapped_line in loop_first(wrapped_lines): |
|
if first: |
|
line_column = str(line_no).rjust(numbers_column_width - 2) + " " |
|
if highlight_line(line_no): |
|
yield _Segment(line_pointer, Style(color="red")) |
|
yield _Segment(line_column, highlight_number_style) |
|
else: |
|
yield _Segment(" ", highlight_number_style) |
|
yield _Segment(line_column, number_style) |
|
else: |
|
yield wrapped_line_left_pad |
|
yield from wrapped_line |
|
yield new_line |
|
else: |
|
for wrapped_line in wrapped_lines: |
|
yield from wrapped_line |
|
yield new_line |
|
|
|
def _apply_stylized_ranges(self, text: Text) -> None: |
|
""" |
|
Apply stylized ranges to a text instance, |
|
using the given code to determine the right portion to apply the style to. |
|
|
|
Args: |
|
text (Text): Text instance to apply the style to. |
|
""" |
|
code = text.plain |
|
newlines_offsets = [ |
|
|
|
0, |
|
|
|
*[ |
|
match.start() + 1 |
|
for match in re.finditer("\n", code, flags=re.MULTILINE) |
|
], |
|
len(code) + 1, |
|
] |
|
|
|
for stylized_range in self._stylized_ranges: |
|
start = _get_code_index_for_syntax_position( |
|
newlines_offsets, stylized_range.start |
|
) |
|
end = _get_code_index_for_syntax_position( |
|
newlines_offsets, stylized_range.end |
|
) |
|
if start is not None and end is not None: |
|
if stylized_range.style_before: |
|
text.stylize_before(stylized_range.style, start, end) |
|
else: |
|
text.stylize(stylized_range.style, start, end) |
|
|
|
def _process_code(self, code: str) -> Tuple[bool, str]: |
|
""" |
|
Applies various processing to a raw code string |
|
(normalises it so it always ends with a line return, dedents it if necessary, etc.) |
|
|
|
Args: |
|
code (str): The raw code string to process |
|
|
|
Returns: |
|
Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return, |
|
while the string is the processed code. |
|
""" |
|
ends_on_nl = code.endswith("\n") |
|
processed_code = code if ends_on_nl else code + "\n" |
|
processed_code = ( |
|
textwrap.dedent(processed_code) if self.dedent else processed_code |
|
) |
|
processed_code = processed_code.expandtabs(self.tab_size) |
|
return ends_on_nl, processed_code |
|
|
|
|
|
def _get_code_index_for_syntax_position( |
|
newlines_offsets: Sequence[int], position: SyntaxPosition |
|
) -> Optional[int]: |
|
""" |
|
Returns the index of the code string for the given positions. |
|
|
|
Args: |
|
newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet. |
|
position (SyntaxPosition): The position to search for. |
|
|
|
Returns: |
|
Optional[int]: The index of the code string for this position, or `None` |
|
if the given position's line number is out of range (if it's the column that is out of range |
|
we silently clamp its value so that it reaches the end of the line) |
|
""" |
|
lines_count = len(newlines_offsets) |
|
|
|
line_number, column_index = position |
|
if line_number > lines_count or len(newlines_offsets) < (line_number + 1): |
|
return None |
|
line_index = line_number - 1 |
|
line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1 |
|
|
|
column_index = min(line_length, column_index) |
|
return newlines_offsets[line_index] + column_index |
|
|
|
|
|
if __name__ == "__main__": |
|
import argparse |
|
import sys |
|
|
|
parser = argparse.ArgumentParser( |
|
description="Render syntax to the console with Rich" |
|
) |
|
parser.add_argument( |
|
"path", |
|
metavar="PATH", |
|
help="path to file, or - for stdin", |
|
) |
|
parser.add_argument( |
|
"-c", |
|
"--force-color", |
|
dest="force_color", |
|
action="store_true", |
|
default=None, |
|
help="force color for non-terminals", |
|
) |
|
parser.add_argument( |
|
"-i", |
|
"--indent-guides", |
|
dest="indent_guides", |
|
action="store_true", |
|
default=False, |
|
help="display indent guides", |
|
) |
|
parser.add_argument( |
|
"-l", |
|
"--line-numbers", |
|
dest="line_numbers", |
|
action="store_true", |
|
help="render line numbers", |
|
) |
|
parser.add_argument( |
|
"-w", |
|
"--width", |
|
type=int, |
|
dest="width", |
|
default=None, |
|
help="width of output (default will auto-detect)", |
|
) |
|
parser.add_argument( |
|
"-r", |
|
"--wrap", |
|
dest="word_wrap", |
|
action="store_true", |
|
default=False, |
|
help="word wrap long lines", |
|
) |
|
parser.add_argument( |
|
"-s", |
|
"--soft-wrap", |
|
action="store_true", |
|
dest="soft_wrap", |
|
default=False, |
|
help="enable soft wrapping mode", |
|
) |
|
parser.add_argument( |
|
"-t", "--theme", dest="theme", default="monokai", help="pygments theme" |
|
) |
|
parser.add_argument( |
|
"-b", |
|
"--background-color", |
|
dest="background_color", |
|
default=None, |
|
help="Override background color", |
|
) |
|
parser.add_argument( |
|
"-x", |
|
"--lexer", |
|
default=None, |
|
dest="lexer_name", |
|
help="Lexer name", |
|
) |
|
parser.add_argument( |
|
"-p", "--padding", type=int, default=0, dest="padding", help="Padding" |
|
) |
|
parser.add_argument( |
|
"--highlight-line", |
|
type=int, |
|
default=None, |
|
dest="highlight_line", |
|
help="The line number (not index!) to highlight", |
|
) |
|
args = parser.parse_args() |
|
|
|
from rich.console import Console |
|
|
|
console = Console(force_terminal=args.force_color, width=args.width) |
|
|
|
if args.path == "-": |
|
code = sys.stdin.read() |
|
syntax = Syntax( |
|
code=code, |
|
lexer=args.lexer_name, |
|
line_numbers=args.line_numbers, |
|
word_wrap=args.word_wrap, |
|
theme=args.theme, |
|
background_color=args.background_color, |
|
indent_guides=args.indent_guides, |
|
padding=args.padding, |
|
highlight_lines={args.highlight_line}, |
|
) |
|
else: |
|
syntax = Syntax.from_path( |
|
args.path, |
|
lexer=args.lexer_name, |
|
line_numbers=args.line_numbers, |
|
word_wrap=args.word_wrap, |
|
theme=args.theme, |
|
background_color=args.background_color, |
|
indent_guides=args.indent_guides, |
|
padding=args.padding, |
|
highlight_lines={args.highlight_line}, |
|
) |
|
console.print(syntax, soft_wrap=args.soft_wrap) |
|
|