diff options
author | cyfraeviolae <cyfraeviolae> | 2024-04-03 03:10:44 -0400 |
---|---|---|
committer | cyfraeviolae <cyfraeviolae> | 2024-04-03 03:10:44 -0400 |
commit | 6d7ba58f880be618ade07f8ea080fe8c4bf8a896 (patch) | |
tree | b1c931051ffcebd2bd9d61d98d6233ffa289bbce /venv/lib/python3.11/site-packages/rich/syntax.py | |
parent | 4f884c9abc32990b4061a1bb6997b4b37e58ea0b (diff) |
venv
Diffstat (limited to 'venv/lib/python3.11/site-packages/rich/syntax.py')
-rw-r--r-- | venv/lib/python3.11/site-packages/rich/syntax.py | 958 |
1 files changed, 958 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/rich/syntax.py b/venv/lib/python3.11/site-packages/rich/syntax.py new file mode 100644 index 0000000..618e045 --- /dev/null +++ b/venv/lib/python3.11/site-packages/rich/syntax.py @@ -0,0 +1,958 @@ +import os.path +import platform +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 = platform.system() == "Windows" +DEFAULT_THEME = "monokai" + +# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py +# A few modifications were made + +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 # pragma: no cover + + @abstractmethod + def get_background_style(self) -> Style: + """Get the background color.""" + raise NotImplementedError # pragma: no cover + + +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: + # Styles form a hierarchy + # We need to go from most to least specific + # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",) + 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 + + +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: + # More complicated path to only stylize a portion of the code + # This speeds up further operations as there are less spans to process + line_start, line_end = line_range + + def line_tokenize() -> Iterable[Tuple[Any, str]]: + """Split tokens to one per line.""" + assert lexer # required to make MyPy happy - we know lexer is not None at this point + + 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 + + # Skip over tokens until line start + while line_no < _line_start: + try: + _token_type, token = next(tokens) + except StopIteration: + break + yield (token, None) + if token.endswith("\n"): + line_no += 1 + # Generate spans until line end + 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 + ) -> 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]`. + """ + self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end)) + + 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._theme.get_background_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") + # Simple case of just rendering text + 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 = [ + # Let's add outer boundaries at each side of the list: + 0, + # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z": + *[ + 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: + 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_number` is out of range + line_index = line_number - 1 + line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1 + # If `column_index` is out of range: let's silently clamp it: + column_index = min(line_length, column_index) + return newlines_offsets[line_index] + column_index + + +if __name__ == "__main__": # pragma: no cover + 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) |