codekingpro/portable-devtools
114k
1import os.path2import platform3import re4import sys5import textwrap6from abc import ABC, abstractmethod7from pathlib import Path8from typing import (9 Any,10 Dict,11 Iterable,12 List,13 NamedTuple,14 Optional,15 Sequence,16 Set,17 Tuple,18 Type,19 Union,20)21 22from pygments.lexer import Lexer23from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename24from pygments.style import Style as PygmentsStyle25from pygments.styles import get_style_by_name26from pygments.token import (27 Comment,28 Error,29 Generic,30 Keyword,31 Name,32 Number,33 Operator,34 String,35 Token,36 Whitespace,37)38from pygments.util import ClassNotFound39 40from rich.containers import Lines41from rich.padding import Padding, PaddingDimensions42 43from ._loop import loop_first44from .cells import cell_len45from .color import Color, blend_rgb46from .console import Console, ConsoleOptions, JustifyMethod, RenderResult47from .jupyter import JupyterMixin48from .measure import Measurement49from .segment import Segment, Segments50from .style import Style, StyleType51from .text import Text52 53TokenType = Tuple[str, ...]54 55WINDOWS = platform.system() == "Windows"56DEFAULT_THEME = "monokai"57 58# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py59# A few modifications were made60 61ANSI_LIGHT: Dict[TokenType, Style] = {62 Token: Style(),63 Whitespace: Style(color="white"),64 Comment: Style(dim=True),65 Comment.Preproc: Style(color="cyan"),66 Keyword: Style(color="blue"),67 Keyword.Type: Style(color="cyan"),68 Operator.Word: Style(color="magenta"),69 Name.Builtin: Style(color="cyan"),70 Name.Function: Style(color="green"),71 Name.Namespace: Style(color="cyan", underline=True),72 Name.Class: Style(color="green", underline=True),73 Name.Exception: Style(color="cyan"),74 Name.Decorator: Style(color="magenta", bold=True),75 Name.Variable: Style(color="red"),76 Name.Constant: Style(color="red"),77 Name.Attribute: Style(color="cyan"),78 Name.Tag: Style(color="bright_blue"),79 String: Style(color="yellow"),80 Number: Style(color="blue"),81 Generic.Deleted: Style(color="bright_red"),82 Generic.Inserted: Style(color="green"),83 Generic.Heading: Style(bold=True),84 Generic.Subheading: Style(color="magenta", bold=True),85 Generic.Prompt: Style(bold=True),86 Generic.Error: Style(color="bright_red"),87 Error: Style(color="red", underline=True),88}89 90ANSI_DARK: Dict[TokenType, Style] = {91 Token: Style(),92 Whitespace: Style(color="bright_black"),93 Comment: Style(dim=True),94 Comment.Preproc: Style(color="bright_cyan"),95 Keyword: Style(color="bright_blue"),96 Keyword.Type: Style(color="bright_cyan"),97 Operator.Word: Style(color="bright_magenta"),98 Name.Builtin: Style(color="bright_cyan"),99 Name.Function: Style(color="bright_green"),100 Name.Namespace: Style(color="bright_cyan", underline=True),101 Name.Class: Style(color="bright_green", underline=True),102 Name.Exception: Style(color="bright_cyan"),103 Name.Decorator: Style(color="bright_magenta", bold=True),104 Name.Variable: Style(color="bright_red"),105 Name.Constant: Style(color="bright_red"),106 Name.Attribute: Style(color="bright_cyan"),107 Name.Tag: Style(color="bright_blue"),108 String: Style(color="yellow"),109 Number: Style(color="bright_blue"),110 Generic.Deleted: Style(color="bright_red"),111 Generic.Inserted: Style(color="bright_green"),112 Generic.Heading: Style(bold=True),113 Generic.Subheading: Style(color="bright_magenta", bold=True),114 Generic.Prompt: Style(bold=True),115 Generic.Error: Style(color="bright_red"),116 Error: Style(color="red", underline=True),117}118 119RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}120NUMBERS_COLUMN_DEFAULT_PADDING = 2121 122 123class SyntaxTheme(ABC):124 """Base class for a syntax theme."""125 126 @abstractmethod127 def get_style_for_token(self, token_type: TokenType) -> Style:128 """Get a style for a given Pygments token."""129 raise NotImplementedError # pragma: no cover130 131 @abstractmethod132 def get_background_style(self) -> Style:133 """Get the background color."""134 raise NotImplementedError # pragma: no cover135 136 137class PygmentsSyntaxTheme(SyntaxTheme):138 """Syntax theme that delegates to Pygments theme."""139 140 def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None:141 self._style_cache: Dict[TokenType, Style] = {}142 if isinstance(theme, str):143 try:144 self._pygments_style_class = get_style_by_name(theme)145 except ClassNotFound:146 self._pygments_style_class = get_style_by_name("default")147 else:148 self._pygments_style_class = theme149 150 self._background_color = self._pygments_style_class.background_color151 self._background_style = Style(bgcolor=self._background_color)152 153 def get_style_for_token(self, token_type: TokenType) -> Style:154 """Get a style from a Pygments class."""155 try:156 return self._style_cache[token_type]157 except KeyError:158 try:159 pygments_style = self._pygments_style_class.style_for_token(token_type)160 except KeyError:161 style = Style.null()162 else:163 color = pygments_style["color"]164 bgcolor = pygments_style["bgcolor"]165 style = Style(166 color="#" + color if color else "#000000",167 bgcolor="#" + bgcolor if bgcolor else self._background_color,168 bold=pygments_style["bold"],169 italic=pygments_style["italic"],170 underline=pygments_style["underline"],171 )172 self._style_cache[token_type] = style173 return style174 175 def get_background_style(self) -> Style:176 return self._background_style177 178 179class ANSISyntaxTheme(SyntaxTheme):180 """Syntax theme to use standard colors."""181 182 def __init__(self, style_map: Dict[TokenType, Style]) -> None:183 self.style_map = style_map184 self._missing_style = Style.null()185 self._background_style = Style.null()186 self._style_cache: Dict[TokenType, Style] = {}187 188 def get_style_for_token(self, token_type: TokenType) -> Style:189 """Look up style in the style map."""190 try:191 return self._style_cache[token_type]192 except KeyError:193 # Styles form a hierarchy194 # We need to go from most to least specific195 # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",)196 get_style = self.style_map.get197 token = tuple(token_type)198 style = self._missing_style199 while token:200 _style = get_style(token)201 if _style is not None:202 style = _style203 break204 token = token[:-1]205 self._style_cache[token_type] = style206 return style207 208 def get_background_style(self) -> Style:209 return self._background_style210 211 212SyntaxPosition = Tuple[int, int]213 214 215class _SyntaxHighlightRange(NamedTuple):216 """217 A range to highlight in a Syntax object.218 `start` and `end` are 2-integers tuples, where the first integer is the line number219 (starting from 1) and the second integer is the column index (starting from 0).220 """221 222 style: StyleType223 start: SyntaxPosition224 end: SyntaxPosition225 226 227class Syntax(JupyterMixin):228 """Construct a Syntax object to render syntax highlighted code.229 230 Args:231 code (str): Code to highlight.232 lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/)233 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai".234 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False.235 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.236 start_line (int, optional): Starting number for line numbers. Defaults to 1.237 line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render.238 A value of None in the tuple indicates the range is open in that direction.239 highlight_lines (Set[int]): A set of line numbers to highlight.240 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.241 tab_size (int, optional): Size of tabs. Defaults to 4.242 word_wrap (bool, optional): Enable word wrapping.243 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.244 indent_guides (bool, optional): Show indent guides. Defaults to False.245 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).246 """247 248 _pygments_style_class: Type[PygmentsStyle]249 _theme: SyntaxTheme250 251 @classmethod252 def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:253 """Get a syntax theme instance."""254 if isinstance(name, SyntaxTheme):255 return name256 theme: SyntaxTheme257 if name in RICH_SYNTAX_THEMES:258 theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])259 else:260 theme = PygmentsSyntaxTheme(name)261 return theme262 263 def __init__(264 self,265 code: str,266 lexer: Union[Lexer, str],267 *,268 theme: Union[str, SyntaxTheme] = DEFAULT_THEME,269 dedent: bool = False,270 line_numbers: bool = False,271 start_line: int = 1,272 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,273 highlight_lines: Optional[Set[int]] = None,274 code_width: Optional[int] = None,275 tab_size: int = 4,276 word_wrap: bool = False,277 background_color: Optional[str] = None,278 indent_guides: bool = False,279 padding: PaddingDimensions = 0,280 ) -> None:281 self.code = code282 self._lexer = lexer283 self.dedent = dedent284 self.line_numbers = line_numbers285 self.start_line = start_line286 self.line_range = line_range287 self.highlight_lines = highlight_lines or set()288 self.code_width = code_width289 self.tab_size = tab_size290 self.word_wrap = word_wrap291 self.background_color = background_color292 self.background_style = (293 Style(bgcolor=background_color) if background_color else Style()294 )295 self.indent_guides = indent_guides296 self.padding = padding297 298 self._theme = self.get_theme(theme)299 self._stylized_ranges: List[_SyntaxHighlightRange] = []300 301 @classmethod302 def from_path(303 cls,304 path: str,305 encoding: str = "utf-8",306 lexer: Optional[Union[Lexer, str]] = None,307 theme: Union[str, SyntaxTheme] = DEFAULT_THEME,308 dedent: bool = False,309 line_numbers: bool = False,310 line_range: Optional[Tuple[int, int]] = None,311 start_line: int = 1,312 highlight_lines: Optional[Set[int]] = None,313 code_width: Optional[int] = None,314 tab_size: int = 4,315 word_wrap: bool = False,316 background_color: Optional[str] = None,317 indent_guides: bool = False,318 padding: PaddingDimensions = 0,319 ) -> "Syntax":320 """Construct a Syntax object from a file.321 322 Args:323 path (str): Path to file to highlight.324 encoding (str): Encoding of file.325 lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content.326 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs".327 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True.328 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.329 start_line (int, optional): Starting number for line numbers. Defaults to 1.330 line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.331 highlight_lines (Set[int]): A set of line numbers to highlight.332 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.333 tab_size (int, optional): Size of tabs. Defaults to 4.334 word_wrap (bool, optional): Enable word wrapping of code.335 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.336 indent_guides (bool, optional): Show indent guides. Defaults to False.337 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).338 339 Returns:340 [Syntax]: A Syntax object that may be printed to the console341 """342 code = Path(path).read_text(encoding=encoding)343 344 if not lexer:345 lexer = cls.guess_lexer(path, code=code)346 347 return cls(348 code,349 lexer,350 theme=theme,351 dedent=dedent,352 line_numbers=line_numbers,353 line_range=line_range,354 start_line=start_line,355 highlight_lines=highlight_lines,356 code_width=code_width,357 tab_size=tab_size,358 word_wrap=word_wrap,359 background_color=background_color,360 indent_guides=indent_guides,361 padding=padding,362 )363 364 @classmethod365 def guess_lexer(cls, path: str, code: Optional[str] = None) -> str:366 """Guess the alias of the Pygments lexer to use based on a path and an optional string of code.367 If code is supplied, it will use a combination of the code and the filename to determine the368 best lexer to use. For example, if the file is ``index.html`` and the file contains Django369 templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no370 templating language is used, the "html" lexer will be used. If no string of code371 is supplied, the lexer will be chosen based on the file extension..372 373 Args:374 path (AnyStr): The path to the file containing the code you wish to know the lexer for.375 code (str, optional): Optional string of code that will be used as a fallback if no lexer376 is found for the supplied path.377 378 Returns:379 str: The name of the Pygments lexer that best matches the supplied path/code.380 """381 lexer: Optional[Lexer] = None382 lexer_name = "default"383 if code:384 try:385 lexer = guess_lexer_for_filename(path, code)386 except ClassNotFound:387 pass388 389 if not lexer:390 try:391 _, ext = os.path.splitext(path)392 if ext:393 extension = ext.lstrip(".").lower()394 lexer = get_lexer_by_name(extension)395 except ClassNotFound:396 pass397 398 if lexer:399 if lexer.aliases:400 lexer_name = lexer.aliases[0]401 else:402 lexer_name = lexer.name403 404 return lexer_name405 406 def _get_base_style(self) -> Style:407 """Get the base style."""408 default_style = self._theme.get_background_style() + self.background_style409 return default_style410 411 def _get_token_color(self, token_type: TokenType) -> Optional[Color]:412 """Get a color (if any) for the given token.413 414 Args:415 token_type (TokenType): A token type tuple from Pygments.416 417 Returns:418 Optional[Color]: Color from theme, or None for no color.419 """420 style = self._theme.get_style_for_token(token_type)421 return style.color422 423 @property424 def lexer(self) -> Optional[Lexer]:425 """The lexer for this syntax, or None if no lexer was found.426 427 Tries to find the lexer by name if a string was passed to the constructor.428 """429 430 if isinstance(self._lexer, Lexer):431 return self._lexer432 try:433 return get_lexer_by_name(434 self._lexer,435 stripnl=False,436 ensurenl=True,437 tabsize=self.tab_size,438 )439 except ClassNotFound:440 return None441 442 @property443 def default_lexer(self) -> Lexer:444 """A Pygments Lexer to use if one is not specified or invalid."""445 return get_lexer_by_name(446 "text",447 stripnl=False,448 ensurenl=True,449 tabsize=self.tab_size,450 )451 452 def highlight(453 self,454 code: str,455 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,456 ) -> Text:457 """Highlight code and return a Text instance.458 459 Args:460 code (str): Code to highlight.461 line_range(Tuple[int, int], optional): Optional line range to highlight.462 463 Returns:464 Text: A text instance containing highlighted syntax.465 """466 467 base_style = self._get_base_style()468 justify: JustifyMethod = (469 "default" if base_style.transparent_background else "left"470 )471 472 text = Text(473 justify=justify,474 style=base_style,475 tab_size=self.tab_size,476 no_wrap=not self.word_wrap,477 )478 _get_theme_style = self._theme.get_style_for_token479 480 lexer = self.lexer or self.default_lexer481 482 if lexer is None:483 text.append(code)484 else:485 if line_range:486 # More complicated path to only stylize a portion of the code487 # This speeds up further operations as there are less spans to process488 line_start, line_end = line_range489 490 def line_tokenize() -> Iterable[Tuple[Any, str]]:491 """Split tokens to one per line."""492 assert lexer # required to make MyPy happy - we know lexer is not None at this point493 494 for token_type, token in lexer.get_tokens(code):495 while token:496 line_token, new_line, token = token.partition("\n")497 yield token_type, line_token + new_line498 499 def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:500 """Convert tokens to spans."""501 tokens = iter(line_tokenize())502 line_no = 0503 _line_start = line_start - 1 if line_start else 0504 505 # Skip over tokens until line start506 while line_no < _line_start:507 try:508 _token_type, token = next(tokens)509 except StopIteration:510 break511 yield (token, None)512 if token.endswith("\n"):513 line_no += 1514 # Generate spans until line end515 for token_type, token in tokens:516 yield (token, _get_theme_style(token_type))517 if token.endswith("\n"):518 line_no += 1519 if line_end and line_no >= line_end:520 break521 522 text.append_tokens(tokens_to_spans())523 524 else:525 text.append_tokens(526 (token, _get_theme_style(token_type))527 for token_type, token in lexer.get_tokens(code)528 )529 if self.background_color is not None:530 text.stylize(f"on {self.background_color}")531 532 if self._stylized_ranges:533 self._apply_stylized_ranges(text)534 535 return text536 537 def stylize_range(538 self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition539 ) -> None:540 """541 Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.542 Line numbers are 1-based, while column indexes are 0-based.543 544 Args:545 style (StyleType): The style to apply.546 start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.547 end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.548 """549 self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end))550 551 def _get_line_numbers_color(self, blend: float = 0.3) -> Color:552 background_style = self._theme.get_background_style() + self.background_style553 background_color = background_style.bgcolor554 if background_color is None or background_color.is_system_defined:555 return Color.default()556 foreground_color = self._get_token_color(Token.Text)557 if foreground_color is None or foreground_color.is_system_defined:558 return foreground_color or Color.default()559 new_color = blend_rgb(560 background_color.get_truecolor(),561 foreground_color.get_truecolor(),562 cross_fade=blend,563 )564 return Color.from_triplet(new_color)565 566 @property567 def _numbers_column_width(self) -> int:568 """Get the number of characters used to render the numbers column."""569 column_width = 0570 if self.line_numbers:571 column_width = (572 len(str(self.start_line + self.code.count("\n")))573 + NUMBERS_COLUMN_DEFAULT_PADDING574 )575 return column_width576 577 def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:578 """Get background, number, and highlight styles for line numbers."""579 background_style = self._get_base_style()580 if background_style.transparent_background:581 return Style.null(), Style(dim=True), Style.null()582 if console.color_system in ("256", "truecolor"):583 number_style = Style.chain(584 background_style,585 self._theme.get_style_for_token(Token.Text),586 Style(color=self._get_line_numbers_color()),587 self.background_style,588 )589 highlight_number_style = Style.chain(590 background_style,591 self._theme.get_style_for_token(Token.Text),592 Style(bold=True, color=self._get_line_numbers_color(0.9)),593 self.background_style,594 )595 else:596 number_style = background_style + Style(dim=True)597 highlight_number_style = background_style + Style(dim=False)598 return background_style, number_style, highlight_number_style599 600 def __rich_measure__(601 self, console: "Console", options: "ConsoleOptions"602 ) -> "Measurement":603 _, right, _, left = Padding.unpack(self.padding)604 padding = left + right605 if self.code_width is not None:606 width = self.code_width + self._numbers_column_width + padding + 1607 return Measurement(self._numbers_column_width, width)608 lines = self.code.splitlines()609 width = (610 self._numbers_column_width611 + padding612 + (max(cell_len(line) for line in lines) if lines else 0)613 )614 if self.line_numbers:615 width += 1616 return Measurement(self._numbers_column_width, width)617 618 def __rich_console__(619 self, console: Console, options: ConsoleOptions620 ) -> RenderResult:621 segments = Segments(self._get_syntax(console, options))622 if self.padding:623 yield Padding(624 segments, style=self._theme.get_background_style(), pad=self.padding625 )626 else:627 yield segments628 629 def _get_syntax(630 self,631 console: Console,632 options: ConsoleOptions,633 ) -> Iterable[Segment]:634 """635 Get the Segments for the Syntax object, excluding any vertical/horizontal padding636 """637 transparent_background = self._get_base_style().transparent_background638 code_width = (639 (640 (options.max_width - self._numbers_column_width - 1)641 if self.line_numbers642 else options.max_width643 )644 if self.code_width is None645 else self.code_width646 )647 648 ends_on_nl, processed_code = self._process_code(self.code)649 text = self.highlight(processed_code, self.line_range)650 651 if not self.line_numbers and not self.word_wrap and not self.line_range:652 if not ends_on_nl:653 text.remove_suffix("\n")654 # Simple case of just rendering text655 style = (656 self._get_base_style()657 + self._theme.get_style_for_token(Comment)658 + Style(dim=True)659 + self.background_style660 )661 if self.indent_guides and not options.ascii_only:662 text = text.with_indent_guides(self.tab_size, style=style)663 text.overflow = "crop"664 if style.transparent_background:665 yield from console.render(666 text, options=options.update(width=code_width)667 )668 else:669 syntax_lines = console.render_lines(670 text,671 options.update(width=code_width, height=None, justify="left"),672 style=self.background_style,673 pad=True,674 new_lines=True,675 )676 for syntax_line in syntax_lines:677 yield from syntax_line678 return679 680 start_line, end_line = self.line_range or (None, None)681 line_offset = 0682 if start_line:683 line_offset = max(0, start_line - 1)684 lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl)685 if self.line_range:686 if line_offset > len(lines):687 return688 lines = lines[line_offset:end_line]689 690 if self.indent_guides and not options.ascii_only:691 style = (692 self._get_base_style()693 + self._theme.get_style_for_token(Comment)694 + Style(dim=True)695 + self.background_style696 )697 lines = (698 Text("\n")699 .join(lines)700 .with_indent_guides(self.tab_size, style=style + Style(italic=False))701 .split("\n", allow_blank=True)702 )703 704 numbers_column_width = self._numbers_column_width705 render_options = options.update(width=code_width)706 707 highlight_line = self.highlight_lines.__contains__708 _Segment = Segment709 new_line = _Segment("\n")710 711 line_pointer = "> " if options.legacy_windows else "❱ "712 713 (714 background_style,715 number_style,716 highlight_number_style,717 ) = self._get_number_styles(console)718 719 for line_no, line in enumerate(lines, self.start_line + line_offset):720 if self.word_wrap:721 wrapped_lines = console.render_lines(722 line,723 render_options.update(height=None, justify="left"),724 style=background_style,725 pad=not transparent_background,726 )727 else:728 segments = list(line.render(console, end=""))729 if options.no_wrap:730 wrapped_lines = [segments]731 else:732 wrapped_lines = [733 _Segment.adjust_line_length(734 segments,735 render_options.max_width,736 style=background_style,737 pad=not transparent_background,738 )739 ]740 741 if self.line_numbers:742 wrapped_line_left_pad = _Segment(743 " " * numbers_column_width + " ", background_style744 )745 for first, wrapped_line in loop_first(wrapped_lines):746 if first:747 line_column = str(line_no).rjust(numbers_column_width - 2) + " "748 if highlight_line(line_no):749 yield _Segment(line_pointer, Style(color="red"))750 yield _Segment(line_column, highlight_number_style)751 else:752 yield _Segment(" ", highlight_number_style)753 yield _Segment(line_column, number_style)754 else:755 yield wrapped_line_left_pad756 yield from wrapped_line757 yield new_line758 else:759 for wrapped_line in wrapped_lines:760 yield from wrapped_line761 yield new_line762 763 def _apply_stylized_ranges(self, text: Text) -> None:764 """765 Apply stylized ranges to a text instance,766 using the given code to determine the right portion to apply the style to.767 768 Args:769 text (Text): Text instance to apply the style to.770 """771 code = text.plain772 newlines_offsets = [773 # Let's add outer boundaries at each side of the list:774 0,775 # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z":776 *[777 match.start() + 1778 for match in re.finditer("\n", code, flags=re.MULTILINE)779 ],780 len(code) + 1,781 ]782 783 for stylized_range in self._stylized_ranges:784 start = _get_code_index_for_syntax_position(785 newlines_offsets, stylized_range.start786 )787 end = _get_code_index_for_syntax_position(788 newlines_offsets, stylized_range.end789 )790 if start is not None and end is not None:791 text.stylize(stylized_range.style, start, end)792 793 def _process_code(self, code: str) -> Tuple[bool, str]:794 """795 Applies various processing to a raw code string796 (normalises it so it always ends with a line return, dedents it if necessary, etc.)797 798 Args:799 code (str): The raw code string to process800 801 Returns:802 Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return,803 while the string is the processed code.804 """805 ends_on_nl = code.endswith("\n")806 processed_code = code if ends_on_nl else code + "\n"807 processed_code = (808 textwrap.dedent(processed_code) if self.dedent else processed_code809 )810 processed_code = processed_code.expandtabs(self.tab_size)811 return ends_on_nl, processed_code812 813 814def _get_code_index_for_syntax_position(815 newlines_offsets: Sequence[int], position: SyntaxPosition816) -> Optional[int]:817 """818 Returns the index of the code string for the given positions.819 820 Args:821 newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet.822 position (SyntaxPosition): The position to search for.823 824 Returns:825 Optional[int]: The index of the code string for this position, or `None`826 if the given position's line number is out of range (if it's the column that is out of range827 we silently clamp its value so that it reaches the end of the line)828 """829 lines_count = len(newlines_offsets)830 831 line_number, column_index = position832 if line_number > lines_count or len(newlines_offsets) < (line_number + 1):833 return None # `line_number` is out of range834 line_index = line_number - 1835 line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1836 # If `column_index` is out of range: let's silently clamp it:837 column_index = min(line_length, column_index)838 return newlines_offsets[line_index] + column_index839 840 841if __name__ == "__main__": # pragma: no cover842 import argparse843 import sys844 845 parser = argparse.ArgumentParser(846 description="Render syntax to the console with Rich"847 )848 parser.add_argument(849 "path",850 metavar="PATH",851 help="path to file, or - for stdin",852 )853 parser.add_argument(854 "-c",855 "--force-color",856 dest="force_color",857 action="store_true",858 default=None,859 help="force color for non-terminals",860 )861 parser.add_argument(862 "-i",863 "--indent-guides",864 dest="indent_guides",865 action="store_true",866 default=False,867 help="display indent guides",868 )869 parser.add_argument(870 "-l",871 "--line-numbers",872 dest="line_numbers",873 action="store_true",874 help="render line numbers",875 )876 parser.add_argument(877 "-w",878 "--width",879 type=int,880 dest="width",881 default=None,882 help="width of output (default will auto-detect)",883 )884 parser.add_argument(885 "-r",886 "--wrap",887 dest="word_wrap",888 action="store_true",889 default=False,890 help="word wrap long lines",891 )892 parser.add_argument(893 "-s",894 "--soft-wrap",895 action="store_true",896 dest="soft_wrap",897 default=False,898 help="enable soft wrapping mode",899 )900 parser.add_argument(901 "-t", "--theme", dest="theme", default="monokai", help="pygments theme"902 )903 parser.add_argument(904 "-b",905 "--background-color",906 dest="background_color",907 default=None,908 help="Override background color",909 )910 parser.add_argument(911 "-x",912 "--lexer",913 default=None,914 dest="lexer_name",915 help="Lexer name",916 )917 parser.add_argument(918 "-p", "--padding", type=int, default=0, dest="padding", help="Padding"919 )920 parser.add_argument(921 "--highlight-line",922 type=int,923 default=None,924 dest="highlight_line",925 help="The line number (not index!) to highlight",926 )927 args = parser.parse_args()928 929 from rich.console import Console930 931 console = Console(force_terminal=args.force_color, width=args.width)932 933 if args.path == "-":934 code = sys.stdin.read()935 syntax = Syntax(936 code=code,937 lexer=args.lexer_name,938 line_numbers=args.line_numbers,939 word_wrap=args.word_wrap,940 theme=args.theme,941 background_color=args.background_color,942 indent_guides=args.indent_guides,943 padding=args.padding,944 highlight_lines={args.highlight_line},945 )946 else:947 syntax = Syntax.from_path(948 args.path,949 lexer=args.lexer_name,950 line_numbers=args.line_numbers,951 word_wrap=args.word_wrap,952 theme=args.theme,953 background_color=args.background_color,954 indent_guides=args.indent_guides,955 padding=args.padding,956 highlight_lines={args.highlight_line},957 )958 console.print(syntax, soft_wrap=args.soft_wrap)959 